@com55/node-red-ai-access 0.2.0-beta.1 → 0.2.0-beta.3

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/plugin.cjs CHANGED
@@ -1,12 +1,13 @@
1
- "use strict";var cd=Object.create;var an=Object.defineProperty;var ud=Object.getOwnPropertyDescriptor;var pd=Object.getOwnPropertyNames;var fd=Object.getPrototypeOf,md=Object.prototype.hasOwnProperty;var a=(e,t)=>an(e,"name",{value:t,configurable:!0});var j=(e,t)=>()=>(e&&(t=e(e=0)),t);var rs=(e,t)=>{for(var n in t)an(e,n,{get:t[n],enumerable:!0})},os=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of pd(t))!md.call(e,o)&&o!==n&&an(e,o,{get:()=>t[o],enumerable:!(r=ud(t,o))||r.enumerable});return e};var dn=(e,t,n)=>(n=e!=null?cd(fd(e)):{},os(t||!e||!e.__esModule?an(n,"default",{value:e,enumerable:!0}):n,e)),hd=e=>os(an({},"__esModule",{value:!0}),e);function De(e,t,n){let r=n.pretty?`${JSON.stringify(e,null,2)}
1
+ "use strict";var kd=Object.create;var dn=Object.defineProperty;var Ad=Object.getOwnPropertyDescriptor;var xd=Object.getOwnPropertyNames;var Sd=Object.getPrototypeOf,Td=Object.prototype.hasOwnProperty;var a=(e,t)=>dn(e,"name",{value:t,configurable:!0});var U=(e,t)=>()=>(e&&(t=e(e=0)),t);var ds=(e,t)=>{for(var n in t)dn(e,n,{get:t[n],enumerable:!0})},ls=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of xd(t))!Td.call(e,o)&&o!==n&&dn(e,o,{get:()=>t[o],enumerable:!(r=Ad(t,o))||r.enumerable});return e};var ln=(e,t,n)=>(n=e!=null?kd(Sd(e)):{},ls(t||!e||!e.__esModule?dn(n,"default",{value:e,enumerable:!0}):n,e)),Pd=e=>ls(dn({},"__esModule",{value:!0}),e);function Be(e,t,n){let r=n.pretty?`${JSON.stringify(e,null,2)}
2
2
  `:`${JSON.stringify(e)}
3
- `,o=Buffer.from(r,"utf8");if(o.byteLength>t)throw new J(n.kind,o.byteLength,t);return{bytes:o}}function is(e,t){let n=(0,ln.resolve)(t),r=(0,ln.resolve)(e);if(r!==n&&!r.startsWith(n+ln.sep))throw new Error("Path escapes .ai-access storage root");return r}function xt(e,t,n){let r=is(t,e),o=`${r}.${process.pid}.tmp`;(0,Oe.writeFileSync)(o,n.bytes,{mode:Dn}),(0,Oe.renameSync)(o,r),(0,Oe.chmodSync)(r,Dn)}function as(e,t,n){let r=is(t,e);(0,Oe.appendFileSync)(r,n.bytes,{mode:Dn});try{(0,Oe.chmodSync)(r,Dn)}catch{}}var Oe,ln,Xr,Dn,$,ss,J,q=j(()=>{"use strict";Oe=require("fs"),ln=require("path"),Xr=448,Dn=384,$=15e5,ss=8192,J=class extends Error{constructor(n,r,o){super(`${n} size ${r} bytes exceeds limit of ${o} bytes`);this.kind=n;this.size=r;this.maxBytes=o;this.name="StoragePayloadTooLargeError"}static{a(this,"StoragePayloadTooLargeError")}code="payload_too_large";statusCode=413};a(De,"encodeTrustedJson");a(is,"assertUnderAiAccess");a(xt,"writeTrustedAtomic");a(as,"appendTrusted")});function un(e){if(typeof e!="string"||e.length===0||e.length>2048)return;let t;try{t=new URL(e)}catch{return}if(!(t.protocol!=="http:"&&t.protocol!=="https:")&&!(t.username||t.password||t.search||t.hash)&&!(t.pathname!=="/"||/^[a-z][a-z0-9+.-]*:\/\/[^/]*\//i.test(e)))return t.origin.toLowerCase()}function vd(e,t){let n=e.getHeader("Vary"),r=String(n??"").split(",").map(o=>o.trim()).filter(Boolean);r.some(o=>o.toLowerCase()===t.toLowerCase())||r.push(t),e.setHeader("Vary",r.join(", "))}function ls(e){return(t,n,r)=>{if(!gd.test(t.path)){r();return}for(let c of n.getHeaderNames())c.startsWith("access-control-")&&n.removeHeader(c);let o=n.setHeader.bind(n),s=!1;n.setHeader=((c,u)=>!s&&/^access-control-/i.test(c)?n:o(c,u));let i=un(t.get("origin")),d=i===void 0?void 0:e().find(c=>c===i),l=d!==void 0;if(vd(n,"Origin"),!l){if(t.method==="OPTIONS"&&t.get("access-control-request-method")){n.status(204).end();return}r();return}if(s=!0,n.setHeader("Access-Control-Allow-Origin",d),n.setHeader("Access-Control-Expose-Headers",bd),t.method==="OPTIONS"&&t.get("access-control-request-method")){n.setHeader("Access-Control-Allow-Methods",yd),n.setHeader("Access-Control-Allow-Headers",wd),n.setHeader("Access-Control-Max-Age",String(600)),n.status(204).end();return}s=!1,r()}}function cs(e,t){let n=e.parent,r=n&&typeof n.use=="function"?n:e;r.use(t);let o=r,i=(o._router??o.router)?.stack;if(!Array.isArray(i)||i.length<2)return;let d=i.pop(),l=0;for(;l<i.length;){let c=i[l].name;if(c!=="query"&&c!=="expressInit")break;l+=1}i.splice(l,0,d)}var gd,yd,wd,bd,Un=j(()=>{"use strict";gd=/^\/mcp\/(?:a(?:\/|$)|api(?:\/|$)|rpc\/?$)/,yd="GET, POST, DELETE, OPTIONS",wd="Authorization, Content-Type, Accept, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID",bd="Mcp-Session-Id, WWW-Authenticate, Retry-After, RateLimit, RateLimit-Policy, RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset";a(un,"canonicalOrigin");a(vd,"appendVary");a(ls,"aiCorsMiddleware");a(cs,"installAiCorsMiddleware")});function Ad(e){return e==="nodes"||e==="flows"||e==="full"}function Bn(e,t){if(!e||typeof e!="object"||Array.isArray(e))return{};let n=e,r={};if(typeof n.enabled=="boolean"&&(r.enabled=n.enabled),typeof n.allowWithoutAdminAuth=="boolean"&&(r.allowWithoutAdminAuth=n.allowWithoutAdminAuth),typeof n.defaultLinkTtlSeconds=="number"&&Number.isFinite(n.defaultLinkTtlSeconds)&&n.defaultLinkTtlSeconds>0&&(r.defaultLinkTtlSeconds=Math.floor(n.defaultLinkTtlSeconds)),Ad(n.defaultDeploymentType)&&(r.defaultDeploymentType=n.defaultDeploymentType),typeof n.maxActiveLinks=="number"&&Number.isFinite(n.maxActiveLinks)&&n.maxActiveLinks>0&&(r.maxActiveLinks=Math.floor(n.maxActiveLinks)),typeof n.publicBaseUrl=="string")try{r.publicBaseUrl=_t(n.publicBaseUrl)}catch{t?.(`AI Access: invalid publicBaseUrl value ${JSON.stringify(n.publicBaseUrl)} was dropped`)}if(Array.isArray(n.corsAllowedOrigins)){let o=new Set;for(let s of n.corsAllowedOrigins){let i=un(s);i?o.add(i):t?.(`AI Access: invalid CORS origin value ${JSON.stringify(s)} was dropped`)}r.corsAllowedOrigins=[...o]}for(let o of["rateLimitWindowMs","rateLimitMaxPerIp","rateLimitMaxPerToken"]){let s=n[o],i=o==="rateLimitWindowMs"?100:1,d=o==="rateLimitWindowMs"?36e5:1e6;typeof s=="number"&&Number.isInteger(s)&&s>=i&&s<=d&&(r[o]=s)}if(n.oauth&&typeof n.oauth=="object"&&!Array.isArray(n.oauth)){let o=n.oauth;r.oauth={},typeof o.enabled=="boolean"&&(r.oauth.enabled=o.enabled);for(let i of["accessTokenTtlSec","refreshIdleTtlSec","dcrMaxClients"]){let d=o[i];typeof d=="number"&&Number.isInteger(d)&&d>0&&d<=31536e3&&(r.oauth[i]=d)}let s=o.refreshMaxLifetimeSec;typeof s=="number"&&Number.isInteger(s)&&s>=0&&s<=31536e4&&(r.oauth.refreshMaxLifetimeSec=s)}return r}function xd(e,t){if(!(0,fn.existsSync)(e))return{};try{let n=(0,fn.readFileSync)(e,"utf8");return Bn(JSON.parse(n),t)}catch(n){return t?.(`AI Access: config.json is corrupt or unreadable; defaults are used (${String(n)})`),{}}}function qn(e,t){let n={},r={...pn};for(let s of Object.keys(pn).filter(i=>i!=="oauth"))n[s]=e[s]!==void 0?"settings.js":t[s]!==void 0?"disk":"default",Object.assign(r,{[s]:e[s]??t[s]??pn[s]});let o={...r,sources:n};return e.oauth!==void 0||t.oauth!==void 0?(o.oauth={...St,...t.oauth??{},...e.oauth??{}},n.oauth=e.oauth!==void 0?"settings.js":"disk"):(o.oauth={...St},n.oauth="default"),o}function Zr(e,t){let n=Bn(e??{},t);for(let o of Object.keys(pn).filter(s=>s!=="oauth"))e&&Object.hasOwn(e,o)&&n[o]===void 0&&Object.assign(n,{[o]:pn[o]});let r=e?.oauth;if(r&&typeof r=="object"&&!Array.isArray(r)){let o=n.oauth??={};for(let s of Object.keys(St))Object.hasOwn(r,s)&&o[s]===void 0&&(t?.(`AI Access: settings.js aiAccess.oauth.${s} is invalid; using the default (${String(St[s])})`),Object.assign(o,{[s]:St[s]}))}return n}var fn,Qr,kd,St,pn,Tt,Wn=j(()=>{"use strict";fn=require("fs"),Qr=require("path");ae();Un();q();kd=16384,St={enabled:!1,accessTokenTtlSec:3600,refreshIdleTtlSec:2592e3,refreshMaxLifetimeSec:7776e3,dcrMaxClients:20},pn={enabled:!0,allowWithoutAdminAuth:!1,defaultLinkTtlSeconds:14400,maxActiveLinks:10,defaultDeploymentType:"nodes",publicBaseUrl:"",corsAllowedOrigins:[],rateLimitWindowMs:6e4,rateLimitMaxPerIp:600,rateLimitMaxPerToken:300,oauth:{...St}};a(Ad,"isDeploymentType");a(Bn,"sanitizePartial");a(xd,"readDiskFile");a(qn,"mergeAiAccessSettings");a(Zr,"settingsJsPartial");Tt=class{constructor(t,n,r){this.paths=t;this.filePath=(0,Qr.join)(t.root,"config.json"),this.settingsJs=Zr(n,r),this.disk=xd((0,fn.existsSync)(this.filePath)?this.filePath:(0,Qr.join)(t.root,"settings.json"),r),this.cached=qn(this.settingsJs,this.disk)}static{a(this,"SettingsStore")}filePath;disk;settingsJs;cached;getEffective(){return this.cached}getDisk(){return{...this.disk}}update(t){let n=Bn(t);for(let r of Object.keys(this.settingsJs))delete n[r];return this.persist(n),this.disk=n,this.cached=qn(this.settingsJs,this.disk),this.cached}patch(t){let n=Bn(t);return this.update({...this.disk,...n,...n.oauth?{oauth:{...this.disk.oauth,...n.oauth}}:{}})}persist(t){let n=De(t,kd,{kind:"settings",pretty:!0});xt(this.paths.root,this.filePath,n)}}});function Gn(e){eo=e}function V(e){return e.adminAuth!=null&&e.adminAuth!==!1}function ge(e){return eo?eo.getEffective():qn(Zr(e.aiAccess),{})}function ye(e){let t=ge(e);return t.enabled?V(e)?!0:t.allowWithoutAdminAuth:!1}var eo,_e=j(()=>{"use strict";Wn();eo=null;a(Gn,"bindSettingsStore");a(V,"isAdminAuthEnabled");a(ge,"resolveAiAccessSettings");a(ye,"areAiFeaturesEnabled")});function ne(e){return!e||e==="/"?"/":e.endsWith("/")?e.slice(0,-1):e}function de(e){let t=ne(e);return t==="/"?"/mcp/api/v1":`${t}/mcp/api/v1`}function je(e){let t=ne(e);return t==="/"?"/mcp/rpc":`${t}/mcp/rpc`}function Sd(e){let t=e.authInfo?.scope;if(t)return Array.isArray(t)?t:[t];let n=e.user?.permissions;return n?Array.isArray(n)?n:[n]:[]}function Pt(e){if(e.auth)return e.auth.scopes.includes("propose")?"propose":e.auth.scopes.includes("read")?"read":null;let t=Sd(e);return t.includes("*")?"*":t.includes("read")?"read":null}function Td(e,t){return!t||!e?{view:!0,manageLinks:!1,approveProposals:!1}:e==="*"?{view:!0,manageLinks:!0,approveProposals:!0}:{view:!0,manageLinks:!1,approveProposals:!1}}function us(e,t={error:"Authentication required",code:"auth_required"},n){let r=e.end.bind(e),o=!0;return e.end=((...s)=>o&&e.statusCode===401&&!e.headersSent?(o=!1,e.removeHeader("Content-Length"),e.setHeader("Content-Type","application/json; charset=utf-8"),n&&e.setHeader("WWW-Authenticate",n),r(JSON.stringify(t))):r(...s)),()=>{o=!1,e.end=r}}function we(e,t){let n="read";return(r,o,s)=>{if(!V(e.settings)){s();return}let i=t==="api"?us(o):null;e.auth.needsPermission(n)(r,o,d=>{if(i?.(),d){s(d);return}if(!r.user){if(t==="page"){o.redirect(ne(e.settings.httpAdminRoot));return}o.status(401).json({error:"Authentication required",code:"auth_required"});return}if(!Pt(r)){o.status(401).json({error:"Insufficient permissions",code:"insufficient_permissions"});return}s()})}}function zn(e,t){let n=V(e.settings),r=ge(e.settings),o=ye(e.settings),s=null;return n?s=Pt(t):o&&(s="*"),{adminAuthEnabled:n,aiFeaturesEnabled:o,allowWithoutAdminAuth:r.allowWithoutAdminAuth,permission:s,capabilities:Td(s,o)}}function Rt(e){return(t,n,r)=>{if(!ye(e.settings)){n.status(403).json({error:"AI Access features are disabled. Enable adminAuth, or set aiAccess.allowWithoutAdminAuth to true for local-only use.",code:"ai_features_disabled"});return}r()}}function mn(e){return(t,n,r)=>{if(!V(e.settings)){if(ye(e.settings)){r();return}n.status(403).json({error:"AI Access features are disabled",code:"ai_features_disabled"});return}if(Pt(t)!=="*"){n.status(403).json({error:"Full admin permission (*) is required for this action",code:"insufficient_permissions"});return}r()}}function _d(e){return e.startsWith("aia_")}function Pd(e){return _d(e)?!0:e.length<24}function Rd(e){if(e.length<24||e.startsWith("aia_")&&e.length<40)return"The token may be truncated \u2014 ask the user for the full access-link token."}function ps(e,t,n,r){if(!V(e.settings)){r();return}let o={error:le,code:Re},s=us(n,o,K);e.auth.needsPermission("read")(t,n,i=>{if(s(),i){r(i);return}if(!n.headersSent){if(!t.user||!Pt(t)){n.setHeader("WWW-Authenticate",K),n.status(401).json(o);return}r()}})}function fs(e,t,n,r,o){let s=e.headers.authorization;if(typeof s!="string"||!/^Bearer\s+\S+/i.test(s.trim()))return"continue";let d=/^Bearer\s+(\S+)$/i.exec(s.trim())?.[1];if(!d)return"continue";let l=n(d);if(l.status==="active")return l.link.kind!==r?(yn(t,r,l.link.kind),"wrong_kind"):(e.accessLink=l.link,"ok");if(l.status==="expired")return gn(e,t,o,l.expiresAt),"invalid";if(Pd(d)){t.setHeader("WWW-Authenticate",K);let c=Rd(d);return t.status(401).json({error:et,code:hn,...c?{hint:c}:{}}),"invalid"}return"continue"}function ms(e,t,n="rest"){return(r,o,s)=>{if(!ye(e.settings)){o.status(403).json({error:"AI Access features are disabled. Enable adminAuth, or set aiAccess.allowWithoutAdminAuth to true for local-only use.",code:"ai_features_disabled"});return}let i=fs(r,o,t,n,e.settings.httpAdminRoot);if(i==="ok"){s();return}i==="invalid"||i==="wrong_kind"||ps(e,r,o,s)}}function hs(e,t,n="rest"){return(r,o,s)=>{let i=fs(r,o,t,n,e.settings.httpAdminRoot);if(i==="ok"){s();return}i==="invalid"||i==="wrong_kind"||ps(e,r,o,s)}}var Pe=j(()=>{"use strict";tt();_e();a(ne,"normalizeHttpAdminRoot");a(de,"mcpApiBase");a(je,"mcpRpcPath");a(Sd,"scopesFromRequest");a(Pt,"resolvePermission");a(Td,"capabilitiesFor");a(us,"interceptNeedsPermissionUnauthorized");a(we,"requireAdminSession");a(zn,"describeAccess");a(Rt,"requireAiFeatures");a(mn,"requireStarPermission");a(_d,"looksLikeAccessLinkToken");a(Pd,"looksLikeFailedAccessLinkAttempt");a(Rd,"truncatedTokenHint");a(ps,"continueAsAdminSessionOrAccessLinkMissing");a(fs,"tryAccessLinkBearer");a(ms,"requireAdminReadOrAccessLink");a(hs,"requireAdminSessionOrAccessLink")});function Ld(e,t){let n=ne(e);return`${n==="/"?"/mcp/a":`${n}/mcp/a`}/${t}`}function Cd(e){return!!e.app.get("trust proxy")}function gs(e,t){let n=e.get(t);if(typeof n!="string"||n.length===0)return;let r=n.split(",")[0]?.trim();return r&&r.length>0?r:void 0}function Ed(e){if(typeof e!="string"||e.trim().length===0)return;let t;try{t=new URL(e.trim())}catch{return}if(!(t.protocol!=="http:"&&t.protocol!=="https:"))return`${t.origin}${t.pathname.replace(/\/+$/,"")}`}function nt(e){try{let n=new URL(`http://${e.includes(":")&&!e.startsWith("[")?`[${e}]`:e}`).hostname.replace(/^\[|\]$/g,"");if(!(0,to.isIP)(n))return;if(n.startsWith("::ffff:")){let r=n.slice(7);if((0,to.isIP)(r)===4)return r;let o=r.split(":");if(o.length===2){let s=parseInt(o[0],16)*65536+parseInt(o[1],16);return[24,16,8,0].map(i=>s>>>i&255).join(".")}}return n}catch{return}}function _t(e){if(e==="")return"";let t="publicBaseUrl must be an HTTP(S) origin with no path, query, fragment, or credentials";if(typeof e!="string")throw new Error(t);let n;try{n=new URL(e.trim())}catch{throw new Error(t)}if(n.protocol!=="https:"&&n.protocol!=="http:"||!/^https?:\/\/[^/?#]+\/?$/i.test(e.trim())||n.pathname!=="/"||n.search||n.hash||n.username||n.password||e.includes("?")||e.includes("#"))throw new Error(t);return n.origin}function Jn(e){no=Ed(e)}function ws(e){if(typeof e!="string"||e.length===0||e.length>260)return!1;let t,n,r=/^(\[[^\]]*\])(?::(\d+))?$/.exec(e);if(r){if(t=r[1],n=r[2],!Fd.test(t))return!1;try{new URL(`http://${t}/`)}catch{return!1}}else{let o=/^([^:]+)(?::(\d+))?$/.exec(e);if(!o||(t=o[1],n=o[2],t.length>253)||!Nd.test(t)&&!Id.test(t))return!1}if(n!==void 0){if(!/^\d{1,5}$/.test(n))return!1;let o=Number(n);if(o<1||o>65535)return!1}return!0}function vs(e){let t=e?.trim().toLowerCase();return t==="http"||t==="https"?t:void 0}function Od(e){let t=e.socket;return t?.encrypted?"https":t?"http":vs(e.protocol)??"http"}function ro(e){if(no)return no;let t=Cd(e),n=t?gs(e,"x-forwarded-host"):void 0,r=e.get("host")?.trim(),o=ws(n)?n:ws(r)?r:void 0;if(!o)return;let s=(t?vs(gs(e,"x-forwarded-proto")):void 0)??Od(e);try{return new URL(`${s}://${o}`).origin}catch{return}}function wn(e,t){let n=ro(e);return n?`${n}${t}`:t}function oo(e,t,n){return wn(e,Ld(t,n))}function so(e){return e.replace(/\/mcp\/a\/[^/?#]+/gi,"/mcp/a/<redacted>")}var to,Lt,bs,no,ys,Id,Nd,Fd,ae=j(()=>{"use strict";to=require("net");Pe();Lt=8192,bs=1800*1e3;a(Ld,"mcpAccessLinkPath");a(Cd,"isTrustProxyEnabled");a(gs,"firstForwardedValue");a(Ed,"normalizePublicBaseUrl");a(nt,"canonicalizeIp");a(_t,"validateSettingsPublicBaseUrl");a(Jn,"bindPublicBaseUrl");ys="[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?",Id=new RegExp(`^${ys}(?:\\.${ys})*$`),Nd=/^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/,Fd=/^\[[0-9A-Fa-f:.]+\]$/;a(ws,"isValidHostHeader");a(vs,"validScheme");a(Od,"connectionScheme");a(ro,"requestPublicOrigin");a(wn,"absolutizePath");a(oo,"buildAccessLinkUrl");a(so,"redactAccessLinkTokenInPath")});function Ud(e){if(!e||e.trim().length===0)return{kind:"missing"};let t=e.trim();if(/^Basic\s+/i.test(t))return{kind:"basic"};let n=/^Bearer\s+(\S+)$/i.exec(t);return n?.[1]?{kind:"bearer",token:n[1]}:{kind:"other"}}function io(e,t,n,r){e.setHeader("WWW-Authenticate",K),e.status(401).json({error:t,code:n,...r?{hint:r}:{}})}function Bd(e){let t=e??"";for(;t.endsWith("/");)t=t.slice(0,-1);return`${t}/mcp#links`}function qd(e,t,n,r){let o=wn(e,Bd(t));return{error:Md,code:Dd,...n?{expiredAt:n}:{},accessLinksUrl:o,hint:r??`Ask the user to create a new access link in Node-RED on the /mcp page \u2192 Access links (${o}).`}}function gn(e,t,n,r,o){t.setHeader("WWW-Authenticate",K),t.status(401).json(qd(e,n,r,o))}function Wd(e){if(e.length<24||e.startsWith("aia_")&&e.length<40)return"The token may be truncated \u2014 ask the user for the full access-link token."}function Vn(e){return e==="get_link"?"Please open Node-RED \u2192 /mcp \u2192 Access links and create a get_link access URL, then send me that /mcp/a/<token> link.":`Please open Node-RED \u2192 /mcp \u2192 Access links and create a kind=${e} token, then send me the Bearer token (not a get_link path URL).`}function zd(e,t){let n=Gd;return e==="get_link"?`This URL requires a get_link token. This token is kind=${t} \u2014 ${n} for a get_link access URL (/mcp/a/<token>/\u2026). What to tell the user: ${Vn("get_link")}`:e==="rest"?`This REST endpoint requires a rest token. This token is kind=${t} \u2014 ${n}. What to tell the user: ${Vn("rest")}`:`This MCP endpoint requires an mcp token. This token is kind=${t} \u2014 ${n}. What to tell the user: ${Vn("mcp")}`}function yn(e,t,n){e.setHeader("WWW-Authenticate",K),e.status(403).json({error:`Access link kind mismatch: expected ${t}, got ${n}`,code:Hd,expectedKind:t,actualKind:n,hint:zd(t,n),tellUser:Vn(t)})}function Ct(e,t,n){return(r,o,s)=>{if(!ye(e.settings)){o.status(403).json({error:"AI Access features are disabled. Enable adminAuth, or set aiAccess.allowWithoutAdminAuth to true for local-only use.",code:"ai_features_disabled"});return}let i=Ud(r.headers.authorization);if(i.kind==="basic"){io(o,$d,jd);return}if(i.kind==="missing"||i.kind==="other"){io(o,le,Re);return}let d=t.validateResult(i.token);if(d.status==="expired"){gn(r,o,e.settings.httpAdminRoot,d.expiresAt);return}if(d.status!=="active"){io(o,et,hn,Wd(i.token));return}if(d.link.kind!==n){yn(o,n,d.link.kind);return}r.accessLink=d.link,s()}}var et,Md,le,$d,K,Re,hn,Dd,jd,Hd,Gd,tt=j(()=>{"use strict";ae();_e();et="Access link is invalid, revoked, or no longer available (it may have expired, or Node-RED may have restarted). Ask the user for a new access link.",Md="Access link token has expired. Ask the user for a new access link.",le="Access link Bearer token is missing. Ask the user for a new access link and send Authorization: Bearer <token>.",$d="Authorization must use the Bearer scheme (Authorization: Bearer <access-link-token>), not Basic. Ask the user for a new access link.",K='Bearer realm="node-red-ai-access", charset="UTF-8"',Re="access_link_missing",hn="access_link_invalid",Dd="token_expired",jd="bearer_scheme_required",Hd="wrong_token_kind";a(Ud,"parseAuthorizationHeader");a(io,"sendUnauthorized");a(Bd,"accessLinksPagePath");a(qd,"tokenExpiredBody");a(gn,"sendTokenExpired");a(Wd,"truncatedBearerHint");Gd="ask the owner: Node-RED /mcp \u2192 Access links \u2192 create kind=rest/mcp (or get_link)";a(Vn,"whatToTellUserWrongTokenKind");a(zd,"wrongTokenKindHint");a(yn,"sendWrongTokenKind");a(Ct,"requireAccessLink")});var W,B,be,Le,Xn,ve=j(()=>{"use strict";W=class extends Error{static{a(this,"FlowRevisionConflictError")}code="version_mismatch";statusCode=409;expectedRev;currentRev;context;constructor(t,n,r="updateFlow"){super(`Flow revision conflict in ${r}: expectedRev '${t}' does not match current rev '${n}'. The flows changed since you last read them \u2014 re-read flows, re-apply your change, and retry.`),this.name="FlowRevisionConflictError",this.expectedRev=t,this.currentRev=n,this.context=r}},B=class extends Error{static{a(this,"FlowNotFoundError")}code="flow_not_found";statusCode=404;flowId;constructor(t){super(`Flow '${t}' was not found`),this.name="FlowNotFoundError",this.flowId=t}},be=class extends Error{static{a(this,"AdapterUnavailableError")}code="adapter_unavailable";statusCode=502;constructor(t){super(t),this.name="AdapterUnavailableError"}},Le=class extends Error{static{a(this,"PluginMisconfiguredError")}code="plugin_misconfigured";statusCode=502;constructor(t){super(t),this.name="PluginMisconfiguredError"}},Xn="Node-RED AI Access could not use the in-process flow runtime and the HTTP Admin API fallback returned Unauthorized. The plugin is misconfigured \u2014 tell the admin to ensure the plugin loads inside Node-RED so RED.runtime.flows is available (do not rely on unauthenticated localhost HTTP when adminAuth is enabled)."});function Ce(e,t){if(!e||!Jd.test(e)||e.includes(".."))throw new Error(`Invalid ${t}`);return e}function Yn(e){if(!e||!Vd.test(e)||e.includes(".."))throw new Error("Invalid proposal id");return e}function ao(e,t){let n=(0,rt.basename)(t);if(!n||n!==t||n.includes(".."))throw new Error("Invalid file name");let r=(0,rt.resolve)(e),o=(0,rt.resolve)(r,n);if(o!==r&&!o.startsWith(r+rt.sep))throw new Error("Path escapes storage directory");return o}function ks(e,t=1e5){if(typeof e=="string")return e.replace(/\0/g,"").slice(0,t)}function Et(e){let t=e&&typeof e=="object"&&!Array.isArray(e)?e:{},n={};for(let o of Kd){if(!(o in t))continue;let s=t[o];if(o==="disabled"&&typeof s=="boolean"){n[o]=s;continue}if(o==="env"&&Array.isArray(s)){let d=JSON.stringify(s);d.length<=5e5&&(n[o]=JSON.parse(d));continue}let i=ks(s,1e4);i!==void 0&&(n[o]=i)}for(let o of["nodes","configs"]){let s=t[o];if(!Array.isArray(s))continue;let i=JSON.stringify(s),d=Buffer.byteLength(i,"utf8");if(d>$)throw new J(`flow.${o}`,d,$);n[o]=JSON.parse(i)}let r=Buffer.byteLength(JSON.stringify(n),"utf8");if(r>$)throw new J("flow",r,$);return n}function As(e){let t={};for(let n of Xd){if(!(n in e))continue;let r=e[n];if(Yd.has(n)){if(typeof r=="number"&&Number.isFinite(r))t[n]=r;else if(typeof r=="string"&&r.trim()!==""){let s=Number(r);Number.isFinite(s)&&(t[n]=s)}continue}let o=ks(r,2e3);o!==void 0&&(t[n]=o)}return t}var rt,Jd,Vd,Kd,Xd,Yd,Qn=j(()=>{"use strict";rt=require("path");q();Jd=/^[A-Za-z0-9_.-]+$/,Vd=/^prop-[A-Za-z0-9_-]+$/,Kd=["id","type","label","disabled","info","env"],Xd=["proposalId","flowId","expectedRev","resultRev","reason","code","currentRev","title","displayName","flowLabel","linkId","label","expiresAt","createdAt","ttlSeconds","kind"],Yd=new Set(["ttlSeconds"]);a(Ce,"assertSafeSegment");a(Yn,"assertSafeProposalId");a(ao,"resolveUnderDir");a(ks,"sanitizeString");a(Et,"sanitizeFlowTab");a(As,"sanitizeAuditDetails")});function lo(e,t){return`oauth-${(0,er.createHash)("sha256").update(`${e}\0${t}`).digest("hex")}`}function ot(e){return e.kind==="oauth"?lo(e.clientId,e.user):e.linkId}function Qd(e){return e==="pending"||e==="approved"||e==="rejected"||e==="failed"||e==="withdrawn"||e==="expired"||e==="superseded"}function xs(e){if(!Array.isArray(e)||e.length===0)return;let t=[];for(let n of e.slice(0,Zd)){if(!n||typeof n!="object"||Array.isArray(n))continue;let r=n;if(typeof r.code!="string"||!r.code||typeof r.error!="string"||!r.error)continue;let o={code:r.code.replace(/\0/g,"").slice(0,80),error:r.error.replace(/\0/g,"").slice(0,500)};typeof r.nodeId=="string"&&r.nodeId&&(o.nodeId=r.nodeId.replace(/\0/g,"").slice(0,200)),typeof r.type=="string"&&r.type&&(o.type=r.type.replace(/\0/g,"").slice(0,200)),typeof r.property=="string"&&r.property&&(o.property=r.property.replace(/\0/g,"").slice(0,200)),typeof r.configType=="string"&&r.configType&&(o.configType=r.configType.replace(/\0/g,"").slice(0,200)),Array.isArray(r.candidates)&&(o.candidates=r.candidates.filter(s=>typeof s=="string"&&s.length>0).slice(0,20).map(s=>s.replace(/\0/g,"").slice(0,200))),t.push(o)}return t.length>0?t:void 0}var er,He,Ss,re,ee,It,bn,Zd,Zn,Ue=j(()=>{"use strict";er=require("crypto"),He=require("fs"),Ss=require("path");Qn();q();a(lo,"oauthOwnerId");a(ot,"proposalOwnerId");re=class extends Error{static{a(this,"ProposalNotFoundError")}code="proposal_not_found";statusCode=404;constructor(t){super(`Proposal '${t}' was not found`),this.name="ProposalNotFoundError"}},ee=class extends Error{static{a(this,"ProposalNotPendingError")}code="proposal_not_pending";statusCode=409;constructor(t,n){super(`Proposal '${t}' is '${n}', not pending`),this.name="ProposalNotPendingError"}},It=class extends Error{static{a(this,"ProposalNotOwnedError")}code="proposal_not_owned";statusCode=403;constructor(t){super(`Proposal '${t}' was not created by this access link`),this.name="ProposalNotOwnedError"}},bn=class extends Error{static{a(this,"ProposalConcurrentApproveError")}code="proposal_approve_in_progress";statusCode=409;constructor(t){super(`Proposal '${t}' is already being approved`),this.name="ProposalConcurrentApproveError"}};a(Qd,"isProposalStatus");Zd=50;a(xs,"sanitizeProposalWarnings");Zn=class{constructor(t,n={}){this.paths=t;this.now=n.now??(()=>Date.now()),this.randomId=n.randomId??(()=>(0,er.randomBytes)(12).toString("hex")),this.maxProposalBytes=n.maxProposalBytes??$}static{a(this,"ProposalStore")}inFlight=new Set;now;randomId;maxProposalBytes;fileFor(t){let n;try{n=Yn(t)}catch{throw new re(t)}return ao(this.paths.proposals,`${n}.json`)}toDiskRecord(t){let n=Yn(t.id),r=Ce(String(t.expectedRev).slice(0,200),"expectedRev"),o=Ce(String(t.payload.flowId).slice(0,200),"flowId"),s=Et(t.payload.flow),i=t.baseFlow!==void 0?Et(t.baseFlow):void 0,d=t.createdBy.kind==="oauth"?{kind:"oauth",clientId:Ce(String(t.createdBy.clientId).slice(0,200),"clientId"),user:String(t.createdBy.user).replace(/\0/g,"").slice(0,200)}:{kind:"access-link",linkId:Ce(String(t.createdBy.linkId).slice(0,200),"linkId"),...typeof t.createdBy.label=="string"?{label:t.createdBy.label.replace(/\0/g,"").slice(0,200)}:{}},l={id:n,type:"replace_flow",status:Qd(t.status)?t.status:"pending",expectedRev:r,payload:{flowId:o,flow:s},createdBy:d,createdAt:String(t.createdAt).slice(0,64),updatedAt:String(t.updatedAt).slice(0,64)};i!==void 0&&(l.baseFlow=i),typeof t.title=="string"&&t.title.length>0&&(l.title=t.title.replace(/\0/g,"").slice(0,80).trim(),l.title||delete l.title);let c=xs(t.warnings);if(c&&(l.warnings=c),t.decidedBy)if(t.decidedBy.kind==="access-link"){let u={kind:"access-link",linkId:Ce(String(t.decidedBy.linkId).slice(0,200),"linkId")};typeof t.decidedBy.label=="string"&&(u.label=t.decidedBy.label.replace(/\0/g,"").slice(0,200)),l.decidedBy=u}else{let u={kind:"admin"};typeof t.decidedBy.username=="string"&&(u.username=t.decidedBy.username.replace(/\0/g,"").slice(0,200)),l.decidedBy=u}return typeof t.reason=="string"&&(l.reason=t.reason.replace(/\0/g,"").slice(0,2e3)),typeof t.resultRev=="string"&&(l.resultRev=Ce(t.resultRev.slice(0,200),"resultRev")),l}writeAtomic(t,n){let r=this.fileFor(t),o=this.toDiskRecord(n),s=De(o,this.maxProposalBytes,{kind:"proposal",pretty:!0});xt(this.paths.root,r,s)}create(t){let n=new Date(this.now()).toISOString(),r=Yn(`prop-${this.randomId()}`),o={id:r,type:"replace_flow",status:"pending",expectedRev:t.expectedRev,payload:{flowId:t.payload.flowId,flow:t.payload.flow},baseFlow:t.baseFlow,title:t.title,createdBy:t.createdBy,createdAt:n,updatedAt:n},s=xs(t.warnings);return s&&(o.warnings=s),this.writeAtomic(r,o),this.get(r)}get(t){let n=this.fileFor(t);if(!(0,He.existsSync)(n))throw new re(t);return JSON.parse((0,He.readFileSync)(n,"utf8"))}list(t){if(!(0,He.existsSync)(this.paths.proposals))return[];let n=(0,He.readdirSync)(this.paths.proposals).filter(o=>o.endsWith(".json")),r=[];for(let o of n){let s=(0,Ss.basename)(o);if(!(s!==o||s.includes("..")))try{let i=JSON.parse((0,He.readFileSync)(ao(this.paths.proposals,s),"utf8"));if(t?.linkId&&ot(i.createdBy)!==t.linkId||t?.status&&i.status!==t.status||t?.flowId&&i.payload.flowId!==t.flowId)continue;r.push(i)}catch{}}return r.sort((o,s)=>o.createdAt<s.createdAt?1:-1)}beginApprove(t){if(this.inFlight.has(t))throw new bn(t);let n=this.get(t);if(n.status!=="pending")throw new ee(t,n.status);return this.inFlight.add(t),n}releaseLock(t){this.inFlight.delete(t)}markApproved(t,n,r){let o=this.get(t);return o.status="approved",o.decidedBy=n,o.resultRev=r,o.updatedAt=new Date(this.now()).toISOString(),this.writeAtomic(t,o),this.get(t)}markRejected(t,n,r){let o=this.get(t);return o.status="rejected",n&&(o.decidedBy=n),o.reason=r,o.updatedAt=new Date(this.now()).toISOString(),this.writeAtomic(t,o),this.get(t)}markFailed(t,n,r){let o=this.get(t);return o.status="failed",n&&(o.decidedBy=n),o.reason=r,o.updatedAt=new Date(this.now()).toISOString(),this.writeAtomic(t,o),this.get(t)}markWithdrawn(t,n,r){let o=this.get(t);if(o.status!=="pending")throw new ee(t,o.status);return o.status="withdrawn",o.decidedBy=n,o.reason=r,o.updatedAt=new Date(this.now()).toISOString(),this.writeAtomic(t,o),this.get(t)}markClosed(t,n,r,o){let s=this.get(t);if(s.status!=="pending")throw new ee(t,s.status);return s.status=n,o&&(s.decidedBy=o),s.reason=r,s.updatedAt=new Date(this.now()).toISOString(),this.writeAtomic(t,s),this.get(t)}}});function Ts(e,t){if(!e||typeof e!="object"||Array.isArray(e))throw new Error(`${t} must be a node object`);let n=e;if(typeof n.id!="string"||n.id.length===0)throw new Error(`${t} needs a non-empty string id`);if(typeof n.type!="string"||n.type.length===0)throw new Error(`${t} '${n.id}' needs a non-empty string type`);return n}function tr(e,t,n){let r=e.find(d=>d.id===t);if(!r||r.type!=="tab"&&r.type!=="subflow")throw new B(t);let o={...r,id:t,type:r.type};delete o.nodes,delete o.configs,delete o.subflows,n.label!==void 0&&(o.label=n.label),n.disabled!==void 0&&(o.disabled=n.disabled),n.info!==void 0&&(o.info=n.info),n.env!==void 0&&(o.env=n.env);let s=[];for(let[d,l]of(n.nodes??[]).entries())s.push({...Ts(l,`flowData.nodes[${d}]`),z:t});for(let[d,l]of(n.configs??[]).entries())s.push({...Ts(l,`flowData.configs[${d}]`),z:t});return[...e.filter(d=>d.id!==t&&d.z!==t),o,...s]}function vn(e,t){if(!e||typeof e!="object"||Array.isArray(e))throw new Error(`Expected ${t} to be an object with rev and flows`);let n=e;if(typeof n.rev!="string"||n.rev.length===0)throw new Error(`Expected ${t}.rev to be a non-empty string`);if(!Array.isArray(n.flows))throw new Error(`Expected ${t}.flows to be an array`);return{rev:n.rev,flows:n.flows}}function Nt(e,t){if(Array.isArray(t)){let d=t.find(u=>u&&typeof u=="object"&&u.id===e&&(u.type==="tab"||u.type==="subflow"));if(!d)throw new B(e);let l=t.filter(u=>u&&typeof u=="object"&&u.id!==e&&u.z===e),c={id:e,type:d.type,nodes:l};return typeof d.label=="string"&&(c.label=d.label),typeof d.disabled=="boolean"&&(c.disabled=d.disabled),typeof d.info=="string"&&(c.info=d.info),c}if(!t||typeof t!="object")throw new B(e);let n=t,r=typeof n.id=="string"&&n.id.length>0?n.id:e,o=typeof n.type=="string"&&n.type.length>0?n.type:"tab",s=Array.isArray(n.nodes)?n.nodes:[],i={...n,id:r,type:o,nodes:s};return Array.isArray(n.configs)&&(i.configs=n.configs),i}var nr=j(()=>{"use strict";ve();a(Ts,"asFlowNode");a(tr,"replaceFlowTabInFlatFlows");a(vn,"assertFlowsSnapshot");a(Nt,"normalizeFlowTab")});function fo(e){return e?el.has(e)?!0:e.startsWith("subflow:"):!1}function Be(e,t="node.type"){if(e==null||e==="")throw new b(`${t} is required`,"missing_node_type");if(typeof e!="string")throw new b(`${t} must be a non-empty string (got ${typeof e})`,"invalid_node_type",{details:{type:e,got:typeof e}});if(!e.trim())throw new b(`${t} is required`,"missing_node_type");return e}function al(e,t){if(typeof e!="string"||typeof t!="string")return Number.MAX_SAFE_INTEGER;let n=e.toLowerCase(),r=t.toLowerCase();if(n===r)return 0;let o=n.length,s=r.length;if(o===0)return s;if(s===0)return o;let i=new Array(s+1),d=new Array(s+1);for(let l=0;l<=s;l++)i[l]=l;for(let l=1;l<=o;l++){d[0]=l;let c=n.charAt(l-1);for(let u=1;u<=s;u++){let p=c===r.charAt(u-1)?0:1;d[u]=Math.min((d[u-1]??0)+1,(i[u]??0)+1,(i[u-1]??0)+p)}for(let u=0;u<=s;u++)i[u]=d[u]??0}return i[s]??s}function dl(e,t){let n=e.toLowerCase(),r=t.toLowerCase(),o=Math.min(n.length,r.length),s=0;for(;s<o&&n.charAt(s)===r.charAt(s);)s+=1;return s}function cl(e){return Math.max(2,Math.floor(e.length/3))}function mo(e,t,n=5){if(typeof e!="string"||!e)return[];let r=cl(e);return t.filter(s=>typeof s=="string"&&s!==e&&!ll.has(s)&&!s.startsWith("_")&&!s.startsWith("subflow:")).map(s=>({c:s,dist:al(e,s),prefix:dl(e,s)})).filter(s=>s.dist<=r).sort((s,i)=>s.dist-i.dist||i.prefix-s.prefix||s.c.localeCompare(i.c)).slice(0,n).map(s=>s.c)}function ul(e){return e.endsWith("/")?e.slice(0,-1):e}function Is(){if(typeof require=="function")return require;let e=(0,eval)("import.meta.url");return(0,Es.createRequire)(e)}function pl(){try{let t=Is()("@node-red/runtime/lib/settings");return typeof t.available=="function"&&t.available()===!0}catch{return!1}}function Ns(e){let t=e.types,n=Array.isArray(t)?t.filter(i=>typeof i=="string"&&i.length>0):[],r="";if(typeof e.module=="string"&&e.module)r=e.module;else if(typeof e.name=="string"&&e.name)r=e.name;else if(typeof e.id=="string"){let i=String(e.id).split("/")[0];r=i&&i.length>0?i:String(e.id)}let o=typeof e.id=="string"&&e.id?e.id:n.length===1&&r?`${r}/${n[0]}`:r;if(!o||n.length===0)return null;let s={id:o,module:r.length>0?r:o,types:n};return typeof e.version=="string"&&(s.version=e.version),typeof e.enabled=="boolean"&&(s.enabled=e.enabled),typeof e.name=="string"&&(s.name=e.name),s}function ho(e=il,t={}){let n=t.module??"node-red",r=t.version??"4.0.0",o=[...e],s=t.configs??{};return{listNodeSets(){return Promise.resolve([{id:n,module:n,version:r,types:o,enabled:!0,name:n}])},getNodeConfigHtml(i){if(s[i])return Promise.resolve(s[i]??null);let d=i.includes("/")?i.slice(i.lastIndexOf("/")+1):i;return s[d]?Promise.resolve(s[d]??null):Promise.resolve(null)}}}function Fs(e={}){if(pl())try{let n=Is()("@node-red/runtime");if(n.nodes&&typeof n.nodes.getNodeList=="function")return new po(n.nodes)}catch{}if(e.baseUrl)return new uo({baseUrl:e.baseUrl,...e.accessToken?{accessToken:e.accessToken}:{},...e.fetch?{fetch:e.fetch}:{}});throw new Error("No usable NodeTypesAdapter: runtime.nodes unavailable and no HTTP baseUrl")}async function go(e){let t=await e.listNodeSets(),n=new Map;for(let r of t)for(let o of r.types){if(n.has(o))continue;let s={type:o,module:r.module,setId:r.id};typeof r.enabled=="boolean"&&(s.enabled=r.enabled),r.version&&(s.version=r.version),n.set(o,s)}return{byType:n,allNames:[...n.keys()]}}function fl(e,t){let n=t.toLowerCase();return[e.type,e.module,e.category??"",e.description??""].join(`
4
- `).toLowerCase().includes(n)}async function or(e,t={}){let n=t.limit??tl,{byType:r}=await go(e),o=[...r.values()].sort((u,p)=>u.type.localeCompare(p.type)),s=t.q?.trim();s&&(o=o.filter(u=>fl(u,s)));let i=o.length,d=i>n,c={types:o.slice(0,n),total:i,truncated:d};return s&&(c.q=s),c}function ml(e,t){let n=`data-help-name="${t}"`,r=`data-help-name='${t}'`,o=e.toLowerCase(),s=0;for(;s<e.length;){let i=e.indexOf(n,s),d=e.indexOf(r,s),l=-1;if(i>=0&&(d<0||i<d)?l=i:d>=0&&(l=d),l<0)return null;let c=o.lastIndexOf("<script",l);if(c<0||c<s){s=l+1;continue}let u=e.indexOf(">",l);if(u<0)return null;let p=o.indexOf("</script",u+1);if(p<0)return null;let f=p+8;for(;f<e.length&&/\s/.test(e.charAt(f));)f+=1;if(e.charAt(f)!==">"){s=p+1;continue}return e.slice(u+1,p)}return null}function Ps(e,t){let n=`<${t}`,r=`</${t}`,o="",s=0,i=e.toLowerCase();for(;s<e.length;){let d=i.indexOf(n,s);if(d<0){o+=e.slice(s);break}o+=e.slice(s,d);let l=e.indexOf(">",d);if(l<0)break;let c=i.indexOf(r,l+1);if(c<0)break;let u=c+r.length;for(;u<e.length&&/\s/.test(e.charAt(u));)u+=1;e.charAt(u)===">"?(s=u+1,o+=" "):s=c+r.length}return o}function hl(e,t=2e3){let n=Ps(e,"script");return n=Ps(n,"style"),n=n.replace(/<[^>]+>/g," ").replace(/&nbsp;/gi," ").replace(/[#*_`]+/g," ").replace(/\s+/g," ").trim(),n.length>t&&(n=`${n.slice(0,t-1)}\u2026`),n}function gl(e,t){try{let n=/RED\.nodes\.registerType\s*\(\s*['"]([^'"]+)['"]\s*,\s*\{/.exec(e);if(!n?.[1])return null;let r=n[1];if(t&&r!==t){let o=/RED\.nodes\.registerType\s*\(\s*['"]([^'"]+)['"]\s*,\s*\{/g,s,i=null,d=-1;for(;(s=o.exec(e))!==null;)if(s[1]===t){i=s[1],d=s.index;break}return!i||d<0?null:Rs(e,d,i)}return Rs(e,n.index,r)}catch{return null}}function Rs(e,t,n){let r=e.slice(t),o=r.indexOf("{");if(o<0)return{type:n};let s=kn(r.slice(o));if(!s)return{type:n};let i=Ls(s,"category"),d=Ls(s,"color"),l=Cs(s,"inputs"),c=Cs(s,"outputs"),u=wl(s),p=[];if(u){for(let g of Object.values(u))if(g&&typeof g=="object"&&!Array.isArray(g)){let v=g.type;typeof v=="string"&&v.length>0&&p.push(v)}}let f,m=ml(e,n);m&&(f=hl(m));let h={type:n};return i&&(h.category=i),d&&(h.color=d),l!==void 0&&(h.inputs=l),c!==void 0&&(h.outputs=c),u&&(h.defaults=u),p.length>0&&(h.requiredConfigNodeTypes=[...new Set(p)]),f&&(h.help=f),h}function yl(e){let t=e.trim();if(!t)return{};if(!/\n(?=#{1,3}\s*(inputs?|outputs?)\b)/i.test(`
5
- ${t}`))return{text:t,summary:t};let r=t.split(/\n(?=#{1,3}\s+)/),o="",s,i;for(let l of r){let c=l.trim();if(!c)continue;let u=/^(#{1,3})\s*(inputs?|outputs?)\b[\s:]*\n?([\s\S]*)$/i.exec(c);if(u){let p=(u[2]??"").toLowerCase(),f=(u[3]??"").trim();p.startsWith("input")?s=f||c:i=f||c}else o||(o=c.replace(/^#{1,3}\s*/,"").trim())}let d={};return o&&(d.summary=o),s&&(d.inputs=s),i&&(d.outputs=i),!d.summary&&!d.inputs&&!d.outputs&&(d.text=t),d}function kn(e){if(!e.startsWith("{"))return null;let t=0,n=null,r=!1;for(let o=0;o<e.length;o++){let s=e[o];if(n){if(r){r=!1;continue}if(s==="\\"){r=!0;continue}s===n&&(n=null);continue}if(s==='"'||s==="'"||s==="`"){n=s;continue}if(s==="{")t+=1;else if(s==="}"&&(t-=1,t===0))return e.slice(0,o+1)}return null}function Ls(e,t){let n=new RegExp(`\\b${t}\\s*:\\s*['"]([^'"]+)['"]`);return e.match(n)?.[1]}function Cs(e,t){let n=new RegExp(`\\b${t}\\s*:\\s*(-?\\d+(?:\\.\\d+)?)`),r=e.match(n);if(!r?.[1])return;let o=Number(r[1]);return Number.isFinite(o)?o:void 0}function wl(e){let t=e.search(/\bdefaults\s*:/);if(t<0)return;let n=e.slice(t),r=n.indexOf("{");if(r<0)return;let o=kn(n.slice(r));if(!o)return;let s={},i=/([a-zA-Z_$][\w$]*)\s*:\s*\{/g,d;for(;(d=i.exec(o))!==null;){let l=d[1];if(!l||l==="defaults")continue;let c=kn(o.slice(d.index+d[0].length-1));if(!c)continue;let u={},p=/\btype\s*:\s*['"]([^'"]+)['"]/.exec(c);p?.[1]&&(u.type=p[1]),/\brequired\s*:\s*true\b/.test(c)&&(u.required=!0);let f=/\bvalue\s*:\s*/.exec(c);if(f){let m=c.slice(f.index+f[0].length).trimStart();u.value=kl(m)}s[l]=u}return Object.keys(s).length>0?s:void 0}function bl(e,t){let n=e.charAt(t);if(n==="u"&&/^[0-9a-fA-F]{4}/.test(e.slice(t+1,t+5)))return{char:String.fromCharCode(Number.parseInt(e.slice(t+1,t+5),16)),next:t+5};if(n==="x"&&/^[0-9a-fA-F]{2}/.test(e.slice(t+1,t+3)))return{char:String.fromCharCode(Number.parseInt(e.slice(t+1,t+3),16)),next:t+3};let r;switch(n){case"n":r=`
6
- `;break;case"r":r="\r";break;case"t":r=" ";break;case"b":r="\b";break;case"f":r="\f";break;case"v":r="\v";break;case"0":r="\0";break;case"\\":r="\\";break;case"'":r="'";break;case'"':r='"';break;case"/":r="/";break;default:r=n;break}return{char:r,next:t+1}}function vl(e){return e==="_DEFAULT_"?"":e}function rr(e){let t=e.trimStart();if(t.startsWith("'")||t.startsWith('"')){let r=t[0],o=1,s="";for(;o<t.length;){let i=t[o];if(i==="\\"&&o+1<t.length){let d=bl(t,o+1);s+=d.char,o=d.next;continue}if(i===r)break;s+=i,o+=1}return vl(s)}if(t.startsWith("[")){let r=Os(t);if(!r)return;let o=r.slice(1,-1).trim();if(!o)return[];let s=[],i=0;for(;i<o.length;){for(;i<o.length&&/[\s,]/.test(o.charAt(i));)i+=1;if(i>=o.length)break;let d=o.slice(i),l=rr(d);s.push(l);let c=co(d);i+=c>0?c:1}return s}if(t.startsWith("{")){let r=kn(t);if(!r)return;let o=r.slice(1,-1);if(!o.trim())return{};let s={},i=0;for(;i<o.length;){for(;i<o.length&&/[\s,]/.test(o.charAt(i));)i+=1;if(i>=o.length)break;let d;if(o[i]==="'"||o[i]==='"'){let c=rr(o.slice(i));d=typeof c=="string"?c:"",i+=co(o.slice(i))}else{let c=/^[a-zA-Z_$][\w$]*/.exec(o.slice(i));if(!c)break;d=c[0],i+=d.length}for(;i<o.length&&/\s/.test(o.charAt(i));)i+=1;if(o[i]!==":")break;for(i+=1;i<o.length&&/\s/.test(o.charAt(i));)i+=1;let l=o.slice(i);s[d]=rr(l),i+=co(l)}return s}let n=/^-?\d+(?:\.\d+)?/.exec(t);if(n)return Number(n[0]);if(t.startsWith("true"))return!0;if(t.startsWith("false"))return!1;if(t.startsWith("null"))return null}function co(e){let t=e.trimStart(),n=e.length-t.length;if(t.startsWith("'")||t.startsWith('"')){let o=t[0],s=1;for(;s<t.length;){if(t[s]==="\\"&&s+1<t.length){s+=2;continue}if(t[s]===o)return n+s+1;s+=1}return n+t.length}if(t.startsWith("[")){let o=Os(t);return n+(o?.length??1)}if(t.startsWith("{")){let o=kn(t);return n+(o?.length??1)}let r=/^(?:-?\d+(?:\.\d+)?|true|false|null)/.exec(t);return r?n+r[0].length:n+1}function Os(e){if(!e.startsWith("["))return null;let t=0,n=null,r=!1;for(let o=0;o<e.length;o++){let s=e[o];if(n){if(r){r=!1;continue}if(s==="\\"){r=!0;continue}s===n&&(n=null);continue}if(s==="'"||s==='"'){n=s;continue}if(s==="[")t+=1;else if(s==="]"&&(t-=1,t===0))return e.slice(0,o+1)}return null}function kl(e){return rr(e)}function Al(e,t){for(let n of e)if(n.types.includes(t)){let r=n.id.includes("/")||n.types.length===1?n.id:`${n.module}/${t}`;return{set:n,setId:r}}return null}function yo(e,t){let n=e.toLowerCase();return t.includes(e)?e:t.find(r=>r.toLowerCase()===n)}async function st(e,t,n={}){if(!t||typeof t!="string")throw new b("Missing node type","missing_node_type");let r=n.includeHelp!==!1;if(fo(t)){let l={type:t,module:"node-red",category:"config",inputs:0,outputs:0,defaults:null};return r&&(l.help={summary:"Built-in structural Node-RED type (not a palette node)."}),l}let o=await e.listNodeSets(),s=o.flatMap(l=>l.types),i=Al(o,t);if(!i){let l=yo(t,s);throw l&&l!==t?new b(`Node type '${t}' was not found \u2014 did you mean '${l}'? Types are case-sensitive.`,"node_type_not_found",{statusCode:404,details:{type:t,didYouMean:l,caseSensitive:!0,nearest:[l]}}):new b(`Node type '${t}' was not found`,"node_type_not_found",{statusCode:404,details:{type:t,nearest:mo(t,s,5)}})}let d={type:t,module:i.set.module,setId:i.setId,inputs:1,outputs:1,defaults:null};if(i.set.version&&(d.version=i.set.version),typeof i.set.enabled=="boolean"&&(d.enabled=i.set.enabled),typeof e.getNodeConfigHtml=="function")try{let l=await e.getNodeConfigHtml(i.setId)??await e.getNodeConfigHtml(`${i.set.module}/${t}`)??await e.getNodeConfigHtml(t);if(l){let c=gl(l,t);c&&(c.category&&(d.category=c.category),c.color&&(d.color=c.color),d.inputs=c.inputs??d.inputs,d.outputs=c.outputs??d.outputs,c.defaults&&Object.keys(c.defaults).length>0?d.defaults=c.defaults:d.defaults=null,c.requiredConfigNodeTypes&&(d.requiredConfigNodeTypes=c.requiredConfigNodeTypes),r&&c.help&&(d.help=yl(c.help)))}}catch{}return d}async function Ms(e,t){let{allNames:n}=await go(e),r=new Set(n),o=new Set,s=a(l=>{let c=Be(l,"node.type");if(o.has(c)||fo(c)){o.add(c);return}if(o.add(c),r.has(c))return;let u=yo(c,n);throw u&&u!==c?new b(`Unknown node type '${c}' \u2014 did you mean '${u}'? Types are case-sensitive.`,"unknown_node_type",{details:{type:c,didYouMean:u,caseSensitive:!0,nearest:[u]}}):new b(`Unknown node type '${c}'. Call node-types / palette search; ask the user to install via Manage palette, then recheck.`,"unknown_node_type",{details:{type:c,nearest:mo(c,n,5)}})},"checkType"),i=a(l=>{if(!l||typeof l!="object"||Array.isArray(l))return;let c=l;typeof c.id=="string"&&c.id.length>0&&s(c.type)},"visitNodeOrConfig");a(l=>{if(!l||typeof l!="object"||Array.isArray(l))return;let c=l;if(c.type!==void 0&&s(c.type),Array.isArray(c.nodes))for(let u of c.nodes)i(u);if(Array.isArray(c.configs))for(let u of c.configs)i(u)},"visitTab")(t)}async function $s(e,t){let n=Be(t,"node.type");if(fo(n))return;let{allNames:r}=await go(e);if(r.includes(n))return;let o=yo(n,r);throw o&&o!==n?new b(`Unknown node type '${n}' \u2014 did you mean '${o}'? Types are case-sensitive.`,"unknown_node_type",{details:{type:n,didYouMean:o,caseSensitive:!0,nearest:[o]}}):new b(`Unknown node type '${n}'. Call node-types / palette search; ask the user to install via Manage palette, then recheck.`,"unknown_node_type",{details:{type:n,nearest:mo(n,r,5)}})}async function xl(e,t){let n=new Headers({Accept:"application/json"});t&&n.set("If-None-Match",t);let r=await fetch(e,{headers:n}),o=r.headers.get("etag")??void 0;if(r.status===304)return{status:304,...o?{etag:o}:{}};if(!r.ok)return{status:r.status,body:null,...o?{etag:o}:{}};let s=await r.json();return{status:r.status,body:s,...o?{etag:o}:{}}}async function Sl(e,t){let n=Date.now();if(ke&&n-ke.fetchedAt<sl)return ke.body;let r=ke?.etag,o=await e(t,r);if(o.status===304&&ke)return ke={...ke,fetchedAt:n,...o.etag?{etag:o.etag}:{}},ke.body;if(!o.body||!Array.isArray(o.body.modules)){if(ke)return ke.body;throw new b("Node-RED catalogue unavailable","catalogue_unavailable",{statusCode:502})}return ke={fetchedAt:n,body:o.body,...o.etag?{etag:o.etag}:{}},o.body}async function sr(e,t,n={}){let r=t.trim();if(!r)throw new b("q query parameter is required","missing_query");let o=n.limit??nl,s=n.catalogueUrl??ol,i=n.catalogueFetcher??xl,d=await Sl(i,s),l=Array.isArray(d.modules)?d.modules:[],c=await e.listNodeSets(),u=new Set(c.flatMap(g=>{let v=[g.module,g.id];return g.id.includes("/")&&v.push(g.id.split("/")[0]??g.module),v.map(A=>A.toLowerCase())})),p=r.toLowerCase(),f=[];for(let g of l){let v=g.id.toLowerCase(),A=(g.keywords??[]).map(te=>String(te).toLowerCase()),E=(g.description??"").toLowerCase(),D=Tl(p,v,A,E);if(D<=0)continue;let O={module:g.id,installed:u.has(v)};typeof g.description=="string"&&(O.description=g.description),typeof g.version=="string"&&(O.version=g.version),typeof g.updated_at=="string"&&(O.updated=g.updated_at),typeof g.downloads=="number"&&(O.downloads=g.downloads),typeof g.url=="string"&&(O.link=g.url),f.push({item:O,score:D})}f.sort((g,v)=>v.score-g.score||(v.item.downloads??0)-(g.item.downloads??0)||g.item.module.localeCompare(v.item.module));let m=f.length,h=m>o;return{q:r,modules:f.slice(0,o).map(g=>g.item),total:m,truncated:h,hint:rl}}function Tl(e,t,n,r){if(!e)return 0;if(t===e||t===`node-red-contrib-${e}`||t===`node-red-node-${e}`)return 1e4;let o=0;t.startsWith(`node-red-contrib-${e}`)||t.startsWith(`node-red-node-${e}`)?o+=500:t.startsWith(e)?o+=80:t.includes(e)&&(o+=60);for(let s of n)s===e?o+=50:s.startsWith(e)||e.startsWith(s)?o+=35:s.includes(e)&&(o+=25);return r.includes(e)&&(o+=10),o}var Es,el,tl,nl,rl,ol,sl,il,ll,uo,_s,po,ke,it=j(()=>{"use strict";Es=require("module");G();el=new Set(["tab","subflow","group","junction"]),tl=100,nl=30,rl="Recommend only \u2014 ask the user to install via Manage palette, then recheck node-types. This API never installs. Core Node-RED nodes (module node-red) are not listed in the public catalogue; use node-types for installed core types.",ol="https://catalogue.nodered.org/catalogue.json",sl=3600*1e3,il=["inject","debug","function","template","comment","change","switch","delay","trigger","link in","link out","link call","mqtt in","mqtt out","http in","http response","http request","websocket in","websocket out","tcp in","tcp out","udp in","udp out","catch","status","complete","join","split","sort","batch","csv","html","json","xml","yaml","file","file in","watch","exec","range","rbe"];a(fo,"isStructuralNodeType");a(Be,"requireNodeTypeName");a(al,"editDistance");a(dl,"sharedPrefixLength");ll=new Set(["unknown","tab","subflow","group"]);a(cl,"nearestDistanceCutoff");a(mo,"nearestNodeTypes");a(ul,"trimTrailingSlash");a(Is,"createPluginRequire");a(pl,"isRuntimeSettingsAvailable");a(Ns,"normalizeNodeSet");a(ho,"createStaticNodeTypesAdapter");uo=class{static{a(this,"HttpNodeTypesAdapter")}baseUrl;accessToken;fetchImpl;constructor(t){this.baseUrl=ul(t.baseUrl),this.accessToken=t.accessToken,this.fetchImpl=t.fetch??globalThis.fetch.bind(globalThis)}headers(t){let n=new Headers;return n.set("Accept",t),this.accessToken&&n.set("Authorization",`Bearer ${this.accessToken}`),n}async listNodeSets(){let t=await this.fetchImpl(`${this.baseUrl}/nodes`,{headers:this.headers("application/json")});if(!t.ok)throw new Error(`Admin API GET /nodes failed (${t.status})`);let n=await t.json();if(!Array.isArray(n))throw new Error("Admin API GET /nodes returned non-array JSON");let r=[];for(let o of n){if(!o||typeof o!="object"||Array.isArray(o))continue;let s=Ns(o);s&&r.push(s)}return r}async getNodeConfigHtml(t,n){let r=t.split("/").map(i=>encodeURIComponent(i)).join("/"),o=n?`?lang=${encodeURIComponent(n)}`:"",s=await this.fetchImpl(`${this.baseUrl}/nodes/${r}${o}`,{headers:this.headers("text/html")});return s.status===404||!s.ok?null:s.text()}},_s={user:null,req:null},po=class{constructor(t){this.nodes=t}static{a(this,"RedNodeTypesAdapter")}async listNodeSets(){if(typeof this.nodes.getNodeList!="function")return[];let t,n=this.nodes.getNodeList;try{t=await Promise.resolve(n(_s))}catch{try{t=await Promise.resolve(n())}catch{return[]}}if(!Array.isArray(t))return[];let r=[];for(let o of t){if(!o||typeof o!="object"||Array.isArray(o))continue;let s=Ns(o);s&&r.push(s)}return r}async getNodeConfigHtml(t,n){if(typeof this.nodes.getNodeConfig!="function")return null;try{let r=await Promise.resolve(this.nodes.getNodeConfig({id:t,lang:n??"en-US",..._s}));if(typeof r=="string"&&r.length>0)return r}catch{}try{let r=await Promise.resolve(this.nodes.getNodeConfig(t,n));return typeof r=="string"&&r.length>0?r:null}catch{return null}}};a(Fs,"createNodeTypesAdapter");a(go,"flattenInstalledTypes");a(fl,"matchesQuery");a(or,"listNodeTypes");a(ml,"extractHelpScriptBody");a(Ps,"removeHtmlBlocks");a(hl,"stripHelpText");a(gl,"parseRegisterTypeFromHtml");a(Rs,"parseRegisterTypeAt");a(yl,"splitHelpSections");a(kn,"extractBalancedObject");a(Ls,"matchStringProp");a(Cs,"matchNumberProp");a(wl,"parseDefaultsBlock");a(bl,"unescapeJsStringEscape");a(vl,"normalizeDefaultValue");a(rr,"parseJsLiteral");a(co,"measureJsLiteralLength");a(Os,"extractBalancedArray");a(kl,"parseLooseValue");a(Al,"findSetForType");a(yo,"findCaseInsensitiveType");a(st,"getNodeTypeDetail");a(Ms,"assertKnownNodeTypes");a($s,"assertKnownNodeType");ke=null;a(xl,"defaultCatalogueFetcher");a(Sl,"loadCatalogue");a(sr,"searchPalette");a(Tl,"paletteMatchScore")});function qe(e){if(e.length===0)return;let t=e[0];if(!t)return;let n=e.map(s=>{let i={...s};return delete i.statusCode,i});if(e.length>1)throw new b(`${e.length} validation errors`,"validation_failed",{statusCode:400,details:{errors:n,errorCount:e.length}});let r={errors:n};for(let[s,i]of Object.entries(t))s==="code"||s==="error"||s==="errors"||s==="statusCode"||i!==void 0&&(r[s]=i);let o=typeof t.statusCode=="number"?t.statusCode:400;throw new b(t.error,t.code,{statusCode:o,details:r})}function wo(e,t="payload.flow"){let n=new Map;for(let o of e)for(let s of o){if(!s||typeof s!="object"||Array.isArray(s))continue;let i=s.id;typeof i=="string"&&i.length>0&&n.set(i,(n.get(i)??0)+1)}let r=[];for(let[o,s]of n)s>1&&r.push({code:"duplicate_node_id",error:`Node id '${o}' appears ${s} times in ${t}; node ids must be unique`,nodeId:o,count:s});return r}function _l(e){let t=typeof e.label=="string"&&e.label?` '${e.label}'`:"";return e.type==="tab"?`tab${t||` '${e.id}'`}`:e.type==="subflow"?`subflow${t||` '${e.id}'`}`:typeof e.z=="string"&&e.z?`${e.type} node on '${e.z}'`:`global ${e.type} config node`}function js(e,t,n){let r=new Map;for(let i of n)!i||typeof i.id!="string"||i.id===t||i.z===t||r.set(i.id,i);let o=[],s=new Set;for(let i of[e.nodes,e.configs])if(Array.isArray(i))for(let d of i){let l=d&&typeof d=="object"?d.id:void 0;if(typeof l!="string"||!l||s.has(l))continue;if(s.add(l),l===t){o.push({code:"duplicate_node_id",error:`Node id '${l}' equals the tab id; node ids must be unique across all flows`,nodeId:l});continue}let c=r.get(l);c&&o.push({code:"duplicate_node_id",error:`Node id '${l}' is already used by ${_l(c)}; node ids must be unique across all flows`,nodeId:l})}return o}function Ds(e){let t=new Map;for(let n of[e.nodes,e.configs])if(Array.isArray(n))for(let r of n){if(!r||typeof r!="object"||Array.isArray(r))continue;let o=r;typeof o.id=="string"&&t.set(o.id,o)}return t}function Rl(e,t){let n=new Set([...Object.keys(e),...Object.keys(t)]);for(let r of n)if(!Pl.has(r)&&JSON.stringify(e[r])!==JSON.stringify(t[r]))return!0;return!1}function Ll(e,t){let n=Ds(t);if(!e)return new Set(n.keys());let r=Ds(e),o=new Set;for(let[s,i]of n){let d=r.get(s);(!d||Rl(d,i))&&o.add(s)}return o}function Hs(e,t){let n=[],r=e;typeof r.id!="string"||r.id.length===0?n.push({code:"missing_flow_id",error:"payload.flow.id is required and must match payload.flowId"}):r.id!==t&&n.push({code:"invalid_flow_id",error:`payload.flow.id ('${r.id}') must match payload.flowId ('${t}')`}),"nodes"in r?Array.isArray(r.nodes)||n.push({code:"invalid_nodes",error:"payload.flow.nodes must be an array"}):n.push({code:"invalid_nodes",error:"payload.flow.nodes must be an array (use [] to clear the tab intentionally)"}),"configs"in r&&r.configs!==void 0&&!Array.isArray(r.configs)&&n.push({code:"invalid_nodes",error:"payload.flow.configs must be an array"});let o=[];Array.isArray(r.nodes)&&o.push({key:"nodes",list:r.nodes}),Array.isArray(r.configs)&&o.push({key:"configs",list:r.configs}),n.push(...wo(o.map(s=>s.list)));for(let{key:s,list:i}of o)for(let d=0;d<i.length;d+=1){let l=i[d];if(!l||typeof l!="object"||Array.isArray(l)){n.push({code:"invalid_node",error:`payload.flow.${s}[${d}] must be an object`});continue}let c=l;(typeof c.id!="string"||c.id.length===0)&&n.push({code:"missing_node_id",error:`payload.flow.${s}[${d}] needs a non-empty string id`});try{Be(c.type,"node.type")}catch(u){u instanceof b&&n.push({code:u.code,error:u.message,...typeof c.id=="string"?{nodeId:c.id}:{}})}"wires"in c&&Ft(c.wires,n,typeof c.id=="string"?c.id:void 0)}return n}function Ft(e,t,n){if(!Array.isArray(e))return t.push({code:"invalid_wires",error:"wires must be an array of output ports (each a list of node ids)",...n?{nodeId:n}:{}}),!1;for(let r of e){if(!Array.isArray(r))return t.push({code:"invalid_wires",error:"wires must be an array of output ports (each a list of node ids)",...n?{nodeId:n}:{}}),!1;for(let o of r)if(typeof o!="string"||o.length===0)return t.push({code:"invalid_wires",error:"wires targets must be non-empty node id strings",...n?{nodeId:n}:{}}),!1}return!0}async function Cl(e,t){if(typeof e.outputs=="number"&&Number.isFinite(e.outputs)&&e.outputs>=0)return Math.trunc(e.outputs);let n=typeof e.type=="string"?e.type:"";if(!(!t||!n))try{return(await st(t,n,{includeHelp:!1})).outputs}catch{return}}function El(e,t,n){let r=typeof e.type=="string"?e.type:"node";if(!(n.length<=t))return t===0?{code:"invalid_wires",error:`${r} has 0 outputs; wires must be []`,nodeId:e.id,type:r}:{code:"invalid_wires",error:`${r} has ${t} outputs; wires has ${n.length} port arrays (max ${t})`,nodeId:e.id,type:r}}function Il(e){return Array.isArray(e)&&e.some(t=>typeof t=="string"&&t.length>0)}function Nl(e,t){return t<=0?!1:!("wires"in e)||e.wires===void 0?!0:Array.isArray(e.wires)?!e.wires.some(Il):!1}async function bo(e,t,n={}){let r=[],o=[],s=new Set(e.map(d=>d.id)),i=n.softWarningNodeIds;for(let d of e){let l=await Cl(d,t),c=typeof d.type=="string"?d.type:"node",u=!i||i.has(d.id);if("wires"in d&&d.wires!==void 0){if(!Ft(d.wires,r,d.id))continue;let p=d.wires;if(l!==void 0){let f=El(d,l,p);f?(f.cause="excess_ports",r.push(f)):u&&p.length<l&&o.push({code:"incomplete_wires",error:`${c} '${d.id}' declares ${l} outputs but wires has only ${p.length} port(s)`,nodeId:d.id,type:c})}for(let f of p)for(let m of f){let h=String(m);s.has(h)||r.push({code:"invalid_wires",error:`Wire target '${h}' on node '${d.id}' was not found in this flow (add the node in the same patch or use an existing id)`,nodeId:d.id,cause:"missing_target",targetId:h})}}u&&l!==void 0&&Nl(d,l)&&o.push({code:"unwired_node",error:`Node '${d.id}' (${c}) has ${l} output(s) but is not wired to anything yet`,nodeId:d.id,type:c})}return{errors:r,warnings:o}}function vo(e,t){let n=[];for(let r of e)r.z===void 0||r.z===""||typeof r.z=="string"&&r.z!==t&&n.push({code:"z_mismatch",error:`Node '${r.id}' has z='${r.z}' but flow id is '${t}'`,nodeId:r.id});return n}function Fl(e,t){return e.filter(n=>n.type===t).map(n=>n.id)}async function ko(e,t,n){let r=[],o=[...t];for(let s of e){let i;try{i=Be(s.type,"node.type")}catch{continue}let d;try{d=await st(n,i,{includeHelp:!1})}catch{continue}let l=d.defaults;if(!l||typeof l!="object")continue;let c=new Set(d.requiredConfigNodeTypes??[]);for(let[u,p]of Object.entries(l)){if(!p||typeof p!="object"||Array.isArray(p))continue;let f=p.type;if(typeof f!="string"||!f)continue;let h=p.required===!0||c.has(f),g=s[u],v=Fl(o,f);if(typeof g!="string"||g.length===0){h&&r.push({code:"missing_config_node",error:`Node '${s.id}' (${i}) has no ${f} on '${u}' yet \u2014 configure before deploy if needed`,nodeId:s.id,type:i,property:u,configType:f,candidates:v});continue}o.find(E=>E.id===g)?.type!==f&&r.push({code:"missing_config_node",error:`Node '${s.id}' property '${u}' does not resolve to a ${f} config yet`,nodeId:s.id,type:i,property:u,configType:f,candidates:v})}}return r}async function Us(e,t={}){let n=t.flowId??(typeof e.id=="string"?e.id:""),r=Array.isArray(e.nodes)?e.nodes:[],o=Array.isArray(e.configs)?e.configs:[],s=[],i=[];n&&s.push(...vo(r,n));let d=t.baseFlow?Ll(t.baseFlow,e):void 0,l=await bo(r,t.nodeTypes,{...d?{softWarningNodeIds:d}:{}});if(s.push(...l.errors),i.push(...l.warnings),t.nodeTypes){let c=await ko(r,o,t.nodeTypes);d?i.push(...c.filter(u=>!u.nodeId||d.has(u.nodeId))):i.push(...c)}return{errors:s,warnings:i}}var Pl,Ao=j(()=>{"use strict";G();it();a(qe,"throwValidationIssues");a(wo,"collectDuplicateNodeIdIssues");a(_l,"describeLiveNode");a(js,"collectCrossFlowDuplicateIssues");Pl=new Set(["x","y","w","h","g","id","z"]);a(Ds,"collectNodesMap");a(Rl,"nodeContentChanged");a(Ll,"changedNodeIds");a(Hs,"collectFullFlowShapeIssues");a(Ft,"assertValidWiresShape");a(Cl,"resolveNodeOutputCount");a(El,"wireCountIssue");a(Il,"portHasTargets");a(Nl,"nodeIsUnwired");a(bo,"collectWireValidation");a(vo,"collectZMismatchIssues");a(Fl,"configCandidates");a(ko,"collectConfigWarnings");a(Us,"validateFlowTab")});function We(e,t="node"){return typeof e=="string"&&e.trim()?e.trim():t}function ir(e){let t=new Map;for(let n of e)n.type==="tab"&&typeof n.id=="string"&&typeof n.label=="string"&&n.label.trim()&&t.set(n.id,n.label.trim());return t}function qs(e,t){e[Bs]=t}function $l(e){if(!e)return;let t=e[Bs];return t instanceof Map?t:void 0}function Dl(e){return typeof e!="string"||!e.trim()?new Set:new Set(e.split(",").map(t=>t.trim()).filter(Boolean))}function jl(e){if(e===!0||e===1)return!0;if(typeof e=="string"){let t=e.trim().toLowerCase();return t==="1"||t==="true"||t==="yes"}return!1}function oe(e){let t=Dl(e.include);return jl(e.include_payload)&&(t.add("payload"),t.add("baseFlow")),t}function U(e,t){let n={req:e};t!==void 0&&(n.httpAdminRoot=t);let r=$l(e);return r&&(n.liveFlowLabels=r),n}function Ot(e){return typeof e.name=="string"&&e.name.trim()?e.name.trim():typeof e.label=="string"&&e.label.trim()?e.label.trim():""}function An(e){return`"${e.replace(/"/g,"'")}"`}function Mt(e){let t=new Map;for(let n of e.nodes??[])t.set(n.id,n);for(let n of e.configs??[])t.set(n.id,n);return t}function Hl(e,t){let n=new Set(["x","y","w","h","g","id","z"]),r=new Set([...Object.keys(e),...Object.keys(t)]);for(let o of r)if(!n.has(o)&&JSON.stringify(e[o])!==JSON.stringify(t[o]))return!0;return!1}function Ul(e,t){let n=Ot(e),r=Ot(t);if(!n||!r||n===r)return!1;let o=new Set(["x","y","w","h","g","id","z","name","label"]),s=new Set([...Object.keys(e),...Object.keys(t)]);for(let i of s)if(!o.has(i)&&JSON.stringify(e[i])!==JSON.stringify(t[i]))return!1;return!0}function xo(e,t){let n=[],r=[],o=[];for(let[s,i]of t)e.has(s)||n.push(i);for(let[s,i]of e)t.has(s)||r.push(i);for(let[s,i]of t){let d=e.get(s);d&&Hl(d,i)&&o.push({before:d,after:i})}return{added:n,removed:r,modified:o}}function ar(e){return e===1?"node":"nodes"}function Bl(e){return e.length===1&&e[0]?`adds 1 ${We(e[0].type)}`:`adds ${e.length} ${ar(e.length)}`}function ql(e){return`deletes ${e} ${ar(e)}`}function Wl(e){return`edits ${e} ${ar(e)}`}function Gl(e){let t=[];e.removed>0&&t.push(ql(e.removed));let n=e.added??[],r=e.addedCount??n.length;r>0&&t.push(n.length===r?Bl(n):`adds ${r} ${ar(r)}`),(e.modified??0)>0&&t.push(Wl(e.modified??0));let o=t.length?t.join(", "):"replaces nodes",s=e.flowLabel?.trim();return s?`Replace ${s}: ${o}`:`Replace flow: ${o}`}function zl(e){let{added:t,removed:n,modified:r}=e;if(t.length+n.length+r.length!==1)return;if(t[0]){let u=t[0],p=We(u.type),f=Ot(u);return f?`Add ${p} node ${An(f)}`:`Add ${p} node`}if(n[0]){let u=n[0],p=We(u.type),f=Ot(u);return f?`Remove ${p} node ${An(f)}`:`Remove ${p} node`}let s=r[0];if(!s)return;let i=We(s.after.type,We(s.before.type)),d=Ot(s.before),l=Ot(s.after);if(Ul(s.before,s.after)&&d&&l)return`Rename ${i} node ${An(d)} \u2192 ${An(l)}`;let c=l||d;return c?`Edit ${i} ${An(c)}`:`Edit ${i}`}function So(e,t){return typeof e?.label=="string"&&e.label.trim()?e.label.trim():typeof t?.label=="string"&&t.label.trim()?t.label.trim():""}function dr(e,t){if(typeof e.flowLabel=="string"&&e.flowLabel.trim())return e.flowLabel.trim();let n=So(e.payload?.flow,e.baseFlow);if(n)return n;let r=e.payload?.flowId;return r&&t?.has(r)?t.get(r)??"":""}function Ws(e,t){let n=t.trim(),r=e.trim();if(!n||!r||r.includes(n))return r;let o=r.lastIndexOf(" in ");if(o>=0){let s=r.slice(o+4);if(s.length>0&&n.startsWith(s))return r}return`${r} in ${n}`}function To(e,t){let n=xo(Mt(e??{}),Mt(t)),r=n.added.length+n.removed.length+n.modified.length,o=So(t,e);if(r===0)return o?`Update ${o}`:"Update flow";if(n.removed.length>0)return Gl({...o?{flowLabel:o}:{},removed:n.removed.length,added:n.added,modified:n.modified.length});if(r>Ml)return o?`${r} changes in ${o}`:`${r} changes`;let s=zl(n)??(o?`Update ${o}`:"Update flow");return Ws(s,o)}function Jl(e,t){let n=(e??"").trim();return!!(!n||n.includes("[object Object]")||/^Update(\s|$)/i.test(n)||t>0&&!/^Replace\b/i.test(n))}function _o(e,t){return`${e} \xB7 ${Kl(t)}`}function Vl(e){let t=e>=0?"+":"-",n=Math.abs(e),r=String(Math.floor(n/60)).padStart(2,"0"),o=String(n%60).padStart(2,"0");return`${t}${r}:${o}`}function Kl(e,t=new Date(e)){let n=Number.isNaN(t.getTime())?new Date(e):t;if(Number.isNaN(n.getTime()))return e.slice(0,16);let r=n.getDate(),s=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"][n.getMonth()]??"Jan",i=String(n.getHours()).padStart(2,"0"),d=String(n.getMinutes()).padStart(2,"0"),l=Vl(-n.getTimezoneOffset());return`${r} ${s} ${i}:${d} ${l}`}function lr(e){if(typeof e!="string")return;let t=e.replace(/\0/g,"").replace(/[\u0001-\u001F\u007F]/g," ").replace(/\s+/g," ").trim().slice(0,Ol).trim();return t.length>0?t:void 0}function Xl(e,t){let n=ne(t);return`${n==="/"?"/mcp":`${n}/mcp`}#proposal=${encodeURIComponent(e)}`}function Po(e,t={}){let n=dr(e,t.liveFlowLabels),r=lr(e.title),o=To(e.baseFlow,n&&!So(e.payload.flow,e.baseFlow)?{...e.payload.flow,label:n}:e.payload.flow),s=xo(Mt(e.baseFlow??{}),Mt(e.payload.flow)),i=Jl(r,s.removed.length)?o:r??o;return Ws(i,n)}function Yl(e){return typeof e.storedTitle=="string"&&e.storedTitle?e.storedTitle:lr(e.title)}function se(e,t={}){let n=Po(e,t),r=dr(e,t.liveFlowLabels),o=Yl(e)??n,s=Xl(e.id,t.httpAdminRoot),i=t.req?wn(t.req,s):s;return{...e,title:n,storedTitle:o,flowLabel:r,displayName:_o(n,e.createdAt),reviewUrl:i,reviewPath:s}}function Ql(e){let t=xo(Mt(e.baseFlow??{}),Mt(e.payload.flow)),n=[];return t.added.length>0&&n.push(t.added.length===1?"Adds 1 node":`Adds ${t.added.length} nodes`),t.removed.length>0&&n.push(t.removed.length===1?"Deletes 1 node":`Deletes ${t.removed.length} nodes`),t.modified.length>0&&n.push(t.modified.length===1?"Edits 1 node":`Edits ${t.modified.length} nodes`),n.length===0?"No node changes":n.map((r,o)=>o===0?r:r.charAt(0).toLowerCase()+r.slice(1)).join(", ")}function Zl(e){if(e.kind==="oauth")return{kind:"oauth",clientId:e.clientId,user:e.user};let t={kind:e.kind,linkId:e.linkId};return typeof e.label=="string"&&e.label.length>0&&(t.label=e.label),t}function ce(e,t={}){let n=se(e,t),r=t.include??new Set,o={id:n.id,status:n.status,type:n.type,title:n.title,storedTitle:n.storedTitle,displayName:n.displayName,flowLabel:n.flowLabel,reviewUrl:n.reviewUrl,reviewPath:n.reviewPath,createdAt:n.createdAt,updatedAt:n.updatedAt,expectedRev:n.expectedRev,createdBy:Zl(n.createdBy),changeSummary:Ql(e)};return r.has("payload")&&(o.payload=n.payload),r.has("baseFlow")&&n.baseFlow!==void 0&&(o.baseFlow=n.baseFlow),n.warnings&&n.warnings.length>0&&(o.warnings=n.warnings),n.baseRevStale!==void 0&&(o.baseRevStale=n.baseRevStale),n.decidedBy!==void 0&&(o.decidedBy=n.decidedBy),n.reason!==void 0&&(o.reason=n.reason),n.resultRev!==void 0&&(o.resultRev=n.resultRev),o}function Gs(e,t={}){return e.map(n=>ce(n,t))}var Ol,Ml,Bs,Ae=j(()=>{"use strict";ae();Pe();Ol=80,Ml=1;a(We,"nodeTypeText");Bs=Symbol.for("ai-access.liveFlowLabels");a(ir,"liveFlowLabelsFromFlows");a(qs,"setRequestFlowLabels");a($l,"getRequestFlowLabels");a(Dl,"parseIncludeQuery");a(jl,"isTruthyFlag");a(oe,"resolveIncludeFromQuery");a(U,"presentOptionsFor");a(Ot,"nodeLabel");a(An,"quoteName");a(Mt,"collectNodes");a(Hl,"nodeContentChanged");a(Ul,"isRenameOnly");a(xo,"classifyFlowChanges");a(ar,"pluralNodes");a(Bl,"describeAddedClause");a(ql,"describeRemovedClause");a(Wl,"describeModifiedClause");a(Gl,"buildReplaceFlowTitle");a(zl,"describeOneChange");a(So,"flowLabelHint");a(dr,"resolveFlowLabel");a(Ws,"ensureTitleIncludesFlowName");a(To,"autoGenerateProposalTitle");a(Jl,"shouldRefreshDisplayTitle");a(_o,"buildProposalDisplayName");a(Vl,"formatUtcOffset");a(Kl,"formatProposalSubmittedAt");a(lr,"sanitizeProposalTitle");a(Xl,"buildProposalReviewPath");a(Po,"resolveProposalTitle");a(Yl,"storedTitleOf");a(se,"presentProposal");a(Ql,"buildChangeSummary");a(Zl,"presentCreatedByForApi");a(ce,"presentProposalForApi");a(Gs,"presentProposalsForApi")});function zs(e){let t=new Map;for(let n of e.nodes??[])t.set(n.id,n);for(let n of e.configs??[])t.set(n.id,n);return t}function tc(e,t){let n=new Set([...Object.keys(e),...Object.keys(t)]),r=[];for(let o of n)ec.has(o)||JSON.stringify(e[o])!==JSON.stringify(t[o])&&r.push(o);return r.sort()}function Js(e){let t=zs(e.baseFlow??{}),n=zs(e.payload.flow),r=new Map;for(let[o,s]of n){let i=t.get(o);if(!i){r.set(o,new Set(["*"]));continue}let d=tc(i,s);d.length>0&&r.set(o,new Set(d))}for(let[o]of t)n.has(o)||r.set(o,new Set(["*"]));return r}function nc(e,t){let n=[],r=[];for(let[o,s]of e){let i=t.get(o);if(!i)continue;n.push(o);let d=[];for(let c of s){if(c==="*"){d.push(...[...i].filter(u=>u!=="*")),i.has("*")&&d.push("*");continue}(i.has(c)||i.has("*"))&&d.push(c)}let l=[...new Set(d)].sort();l.length>0&&r.push({nodeId:o,fields:l})}return n.sort(),{nodeIds:n,fields:r}}function z(e,t,n={}){let r=0;for(let o of e.list())if(o.status==="pending"&&!(n.flowId&&o.payload.flowId!==n.flowId)&&o.expectedRev!==t)try{e.markClosed(o.id,"expired",rc),r+=1}catch{}return r}function Vs(e,t,n,r){let o=`Superseded by approved proposal ${n} (resultRev ${r}). Re-read the flow and resubmit if still needed.`,s=0;for(let i of e.list())if(!(i.id===n||i.status!=="pending"||i.payload.flowId!==t))try{e.markClosed(i.id,"superseded",o),s+=1}catch{}return s}function Ee(e,t,n){let r=Js(t),o=[];for(let s of e.list()){if(s.id===t.id||s.status!=="pending"||s.payload.flowId!==t.payload.flowId)continue;let i=Po(s),d=n?ot(s.createdBy)===n:ot(s.createdBy)===ot(t.createdBy),l=nc(r,Js(s)),c=s.expectedRev===t.expectedRev,u=l.nodeIds.length>0;if(!c&&!u)continue;let p={id:s.id,title:i,displayName:_o(i,s.createdAt),mine:d};l.nodeIds.length>0&&(p.overlappingNodeIds=l.nodeIds),l.fields.length>0&&(p.overlappingFields=l.fields),o.push(p)}return{conflictingProposals:o}}function Ie(e){let t={};return e.conflictingProposals.length>0&&(t.conflictingProposals=e.conflictingProposals,t.warning="Other pending proposals touch the same flow \u2014 approve order may conflict (like a git merge)."),t}function X(e){return e>0?{autoClosedCount:e}:{}}var ec,rc,Ge=j(()=>{"use strict";Ue();Ae();ec=new Set(["x","y","w","h","g","id","z"]);a(zs,"collectNodes");a(tc,"changedFieldKeys");a(Js,"computeProposalTouches");a(nc,"overlapBetween");rc="Proposal expired: the live flow revision moved past expectedRev. Re-read the flow and resubmit.";a(z,"expireStalePendingProposals");a(Vs,"supersedePendingOnFlow");a(Ee,"findConflictingProposals");a(Ie,"conflictWarningFields");a(X,"autoClosedFields")});var ni={};rs(ni,{applyFlowNodeOps:()=>at,applyReplaceTextToValue:()=>ti,buildPatchedFlowTab:()=>xn,collectSetProps:()=>Ro,decodeBase64UrlJson:()=>Sn,findTextOccurrences:()=>ei,interpretPatch:()=>Tn});function Zs(e){return e===null?"null":Array.isArray(e)?"array":typeof e}function oc(e){if(typeof e!="object"||e===null||Array.isArray(e))throw new b(`add op needs node as an object (got ${Zs(e)})`,"invalid_node")}async function xn(e,t,n){return at(e,t,n.map(r=>({op:"set",nodeId:r.nodeId,set:r.set})))}function ei(e,t){if(!t)return[];let n=[],r=0;for(;r<=e.length;){let o=e.indexOf(t,r);if(o<0)break;n.push(o),r=o+t.length}return n}function ti(e,t,n,r,o){if(Qs.has(t))throw new b(`Cannot change protected node property '${t}' via replace_text`,"protected_property");if(typeof n!="string"||n.length===0)throw new b("find must be a non-empty string","missing_find");if(typeof r!="string")throw new b("replace must be a string","invalid_replace");if(e===void 0)throw new b(`Field '${t}' was not found on the node`,"field_not_found");if(typeof e!="string")throw new b(`Field '${t}' is not a string (got ${e===null?"null":typeof e})`,"field_not_string");let s=ei(e,n);if(s.length===0)throw new b(`Text not found in field '${t}'`,"text_not_found",{details:{matchCount:0}});let i=o;if(i===void 0){if(s.length>1)throw new b(`Found ${s.length} matches in '${t}'; pass occurrence (1-based) to pick one`,"ambiguous_match",{statusCode:409,details:{matchCount:s.length}});i=1}if(!Number.isInteger(i)||i<1||i>s.length)throw new b(`occurrence must be an integer from 1 to ${s.length}`,"invalid_occurrence",{details:{matchCount:s.length}});let d=s[i-1];if(d===void 0)throw new b(`occurrence must be an integer from 1 to ${s.length}`,"invalid_occurrence",{details:{matchCount:s.length}});return e.slice(0,d)+r+e.slice(d+n.length)}function sc(e){let t=[];for(let n of e)if(n.op==="add"){let r=n.node;if(typeof r!="object"||r===null||Array.isArray(r)){t.push({code:"invalid_node",error:`add op needs node as an object (got ${Zs(r)})`});continue}let o=r;(!o.id||typeof o.id!="string")&&t.push({code:"missing_node_id",error:"add op needs node.id"});try{Be(o.type,"node.type")}catch(s){s instanceof b&&t.push({code:s.code,error:s.message,nodeId:o.id})}"wires"in o&&Ft(o.wires,t,typeof o.id=="string"?o.id:void 0)}else n.op==="set"&&typeof n.set=="object"&&"wires"in n.set&&Ft(n.set.wires,t,n.nodeId);return t}function ic(e){let t=new Set;for(let n of e)n.op==="add"&&typeof n.node.id=="string"?t.add(n.node.id):n.op!=="add"&&typeof n.nodeId=="string"&&t.add(n.nodeId);return t}async function at(e,t,n,r={}){if(!t)throw new b("flow id is required","missing_flow_id");if(!n.length)throw new b("At least one node edit is required","missing_edits");let o=sc(n);o.push(...wo([n.flatMap(h=>h.op==="add"?[h.node]:[])],"the batch (add ops)")),qe(o);let s=await e.getFlow(t);if(r.expectedRev!==void 0&&s.rev!==r.expectedRev){let h=Array.isArray(s.flow.nodes)?[...s.flow.nodes]:[];return{expectedRev:s.rev,flow:{...s.flow,id:s.flow.id,type:s.flow.type??"tab",nodes:h}}}let i=Array.isArray(s.flow.nodes)?[...s.flow.nodes]:[],d=Array.isArray(s.flow.configs)?[...s.flow.configs]:[],l={...s.flow,id:s.flow.id,type:s.flow.type??"tab",nodes:i,configs:d},c=i.map(h=>({...h})),u=[];for(let h of n)try{h.op==="add"&&r.nodeTypes&&await $s(r.nodeTypes,h.node.type),Xs(c,t,h)}catch(g){if(g instanceof b)u.push({code:g.code,error:g.message,...g.statusCode!==400?{statusCode:g.statusCode}:{},...g.details??{}});else throw g}u.push(...vo(c,t));let p=ic(n),f=await bo(c,r.nodeTypes,{softWarningNodeIds:p});u.push(...f.errors),qe(u);for(let h of n)Xs(i,t,h);let m=[...f.warnings];if(r.nodeTypes){let h=await ko(i,d,r.nodeTypes);m.push(...h.filter(g=>!g.nodeId||p.has(g.nodeId)))}return{expectedRev:s.rev,flow:l,...m.length>0?{warnings:m}:{}}}function Ks(e,t,n){if(!t||typeof t!="string")throw new b("Each edit needs a node id","missing_node_id");let r=e.findIndex(o=>o.id===t);if(r<0||!e[r])throw new b(`Node '${t}' was not found in flow '${n}'`,"node_not_found",{statusCode:404});return r}function Xs(e,t,n){if(n.op==="add"){let i=n.node;if(!i.id||typeof i.id!="string")throw new b("add op needs node.id","missing_node_id");if(Be(i.type,"node.type"),e.some(d=>d.id===i.id))throw new b(`Node '${i.id}' already exists in flow '${t}'`,"node_already_exists");e.push({...i,id:i.id,type:i.type,z:typeof i.z=="string"&&i.z?i.z:t});return}if(n.op==="remove"){let i=Ks(e,n.nodeId,t);e.splice(i,1);return}let r=Ks(e,n.nodeId,t),o=e[r];if(!o)throw new b(`Node '${n.nodeId}' was not found in flow '${t}'`,"node_not_found",{statusCode:404});if(n.op==="replace_text"){if(!(n.field in o))throw new b(`Field '${n.field}' was not found on the node`,"field_not_found");let i=ti(o[n.field],n.field,n.find,n.replace,n.occurrence);e[r]={...o,id:o.id,type:o.type,[n.field]:i};return}let s={...o,id:o.id,type:o.type};for(let[i,d]of Object.entries(n.set)){if(Qs.has(i))throw new b(`Cannot change protected node property '${i}' via GET propose`,"protected_property");if(i==="wires"){let l=[];Ft(d,l,n.nodeId),qe(l)}s[i]=d}e[r]=s}function Ys(e){if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean"||typeof e=="bigint")return String(e)}function Ro(e){let t={},n=e.set;if(n&&typeof n=="object"&&!Array.isArray(n))for(let[r,o]of Object.entries(n)){let s=Ys(o);s!==void 0&&(t[r]=s)}for(let[r,o]of Object.entries(e)){if(!r.startsWith("set.")||r.length<=4)continue;let s=r.slice(4);if(typeof o=="string")t[s]=o;else if(Array.isArray(o)&&typeof o[0]=="string")t[s]=o[0];else{let i=Ys(o);i!==void 0&&(t[s]=i)}}return t}function Sn(e){let t;try{t=Buffer.from(e,"base64url").toString("utf8")}catch{throw new b("patch must be base64url-encoded JSON","invalid_patch")}try{return JSON.parse(t)}catch{throw new b("patch JSON is malformed","invalid_patch_json")}}function ac(e,t){if(typeof e.flowId=="string"&&e.flowId)return e.flowId;let n=e.flow;if(n&&typeof n=="object"&&!Array.isArray(n)){let r=n.id;if(typeof r=="string"&&r)return r}return t}function Lo(e){return typeof e.id=="string"&&e.id?e.id:typeof e.nodeId=="string"&&e.nodeId?e.nodeId:typeof e.node=="string"&&e.node?e.node:""}function dc(e){if(e==null||e==="")return;let t;if(typeof e=="number")t=e;else if(typeof e=="string")t=Number(e);else if(typeof e=="boolean"||typeof e=="bigint")t=Number(e);else throw new b("occurrence must be a positive integer","invalid_occurrence");if(!Number.isFinite(t))throw new b("occurrence must be a positive integer","invalid_occurrence");return Math.trunc(t)}function lc(e){let t=e.node;if(t!==void 0)return oc(t),{op:"add",node:t};let n={...e};return delete n.op,{op:"add",node:n}}function cc(e){let t=Lo(e);if(!t)throw new b("replace_text op needs node id","missing_node_id");let n=typeof e.field=="string"?e.field:"";if(!n)throw new b("replace_text op needs field","missing_field");if(typeof e.find!="string")throw new b("replace_text op needs find string","missing_find");if(typeof e.replace!="string")throw new b("replace_text op needs replace string","invalid_replace");let r=dc(e.occurrence),o={op:"replace_text",nodeId:t,field:n,find:e.find,replace:e.replace};return r!==void 0&&(o.occurrence=r),o}function uc(e){let t=Lo(e);if(!t)throw new b("Each patch.nodes entry needs id","missing_node_id");let n=e.set,r={};if(n&&typeof n=="object"&&!Array.isArray(n))Object.assign(r,n);else for(let[o,s]of Object.entries(e))o==="id"||o==="nodeId"||o==="node"||o==="set"||o==="op"||(r[o]=s);return{op:"set",nodeId:t,set:r}}function pc(e){let t=typeof e.op=="string"?e.op.toLowerCase():"set";if(t==="add")return lc(e);if(t==="remove"){let n=Lo(e);if(!n)throw new b("remove op needs id","missing_node_id");return{op:"remove",nodeId:n}}if(t==="replace_text")return cc(e);if(t!=="set")throw new b("nodes[].op must be 'add', 'remove', 'set', or 'replace_text'","invalid_patch");return uc(e)}function Tn(e,t){if(!e||typeof e!="object"||Array.isArray(e))throw new b("patch must be a JSON object","invalid_patch");let n=e;if(n.type==="replace_flow"||n.flow&&typeof n.flow=="object"){let r=ac(n,t);if(!r)throw new b("patch.replace_flow needs flowId","missing_flow_id");return{kind:"replace_flow",flowId:r,flow:n.flow}}if(Array.isArray(n.nodes)){let r=typeof n.flowId=="string"?n.flowId:t;if(!r)throw new b("patch.nodes needs flow id","missing_flow_id");let o=[];for(let i of n.nodes){if(!i||typeof i!="object"||Array.isArray(i))throw new b("Each patch.nodes entry must be an object","invalid_patch");o.push(pc(i))}let s=o.filter(i=>i.op==="set").map(i=>({nodeId:i.nodeId,set:i.set}));return{kind:"nodes",flowId:r,ops:o,edits:s}}throw new b("patch must include nodes[] edits or type/flow for replace_flow","invalid_patch")}var Qs,cr=j(()=>{"use strict";G();Ao();it();Qs=new Set(["id","type","z"]);a(Zs,"describeNonObjectNode");a(oc,"assertAddNodeObject");a(xn,"buildPatchedFlowTab");a(ei,"findTextOccurrences");a(ti,"applyReplaceTextToValue");a(sc,"collectOpShapeIssues");a(ic,"touchedNodeIdsFromOps");a(at,"applyFlowNodeOps");a(Ks,"requireNodeIndex");a(Xs,"applyOneNodeOp");a(Ys,"stringifyQueryValue");a(Ro,"collectSetProps");a(Sn,"decodeBase64UrlJson");a(ac,"resolveReplaceFlowId");a(Lo,"resolveNodeId");a(dc,"parseOccurrence");a(lc,"parseAddOp");a(cc,"parseReplaceTextOp");a(uc,"parseSetOp");a(pc,"parseNodeOp");a(Tn,"interpretPatch")});function $t(e,t){return ot(e.createdBy)===t.id}function Co(e,t){return!t||t.length===0||t.includes(e)}function fc(e){return typeof e=="string"&&oi.has(e)}function jt(e,t){let n=mc[e],r=e!=="status"&&Array.isArray(t);return new b(r?`Invalid ${e} filter: pass it at most once (multiple values are not supported). Allowed: ${n}`:`Invalid ${e} filter. Allowed: ${n}`,"invalid_filter",{details:{parameter:e,allowed:n,hint:`Pass ${e} as ${n}, or omit it.`}})}function ur(e,t){if(Array.isArray(t))throw jt(e,t);return t}function hc(e){return typeof e=="number"?Number.isInteger(e)?e:null:typeof e=="string"&&/^-?\d+$/.test(e.trim())?Number(e.trim()):null}function Eo(e){return e==null||e===""}function gc(e){if(!Eo(e)){if(e==="true"||e==="1"||e===!0)return!0;if(e==="false"||e==="0"||e===!1)return!1;throw jt("mine",e)}}function Io(e){if(Eo(e))return;let t=(Array.isArray(e)?e:[e]).flatMap(r=>typeof r=="string"?r.split(","):[r]),n=[];for(let r of t){let o=typeof r=="string"?r.trim():r;if(o!==""){if(!fc(o))throw jt("status",e);n.includes(o)||n.push(o)}}return n.length>0?n:void 0}function ri(e,t,n,r){if(Eo(t))return;let o=hc(t);if(o===null||o<n||o>r)throw jt(e,t);return o}function pr(e){let t={},n=gc(ur("mine",e.mine));n!==void 0&&(t.mine=n);let r=Io(e.status);r&&(t.status=r);let o=ur("flowId",e.flowId??e.flow_id??e.flow);typeof o=="string"&&o.length>0&&(t.flowId=o);let s=ri("limit",ur("limit",e.limit),1,Dt);s!==void 0&&(t.limit=s);let i=ri("offset",ur("offset",e.offset),0,Number.MAX_SAFE_INTEGER);return i!==void 0&&(t.offset=i),t}function dt(e){let t=e.offset+e.returned,n=t<e.total;return{total:e.total,offset:e.offset,...e.limit!==null?{limit:e.limit}:{},hasMore:n,...n?{nextOffset:t}:{}}}function yc(e,t){let n=e.length,r=t.offset!==void 0&&t.offset>0?t.offset:0;if(t.limit===void 0)return{items:r>0?e.slice(r):e,total:n,limit:null,offset:r};let o=Math.min(Math.max(1,t.limit),Dt);return{items:e.slice(r,r+o),total:n,limit:o,offset:r}}function Ht(e,t,n={},r={}){let o=r.mine===!0?t.id:void 0,s={};o&&(s.linkId=o),r.flowId&&(s.flowId=r.flowId);let d=e.list(s).filter(c=>r.mine!==!1||!$t(c,t)).filter(c=>Co(c.status,r.status)).map(c=>({...se(c,n),mine:$t(c,t)})),l=yc(d,r);return{proposals:l.items,total:l.total,limit:l.limit,offset:l.offset}}function Me(e,t,n,r={}){if(!n)throw new b("Missing proposal id","missing_proposal_id");let o=e.get(n);return{...se(o,r),mine:$t(o,t)}}function wc(e){let t={kind:"access-link",linkId:e.id};return typeof e.label=="string"&&e.label&&(t.label=e.label),t}function Ut(e,t,n,r,o,s={}){if(!r)throw new b("Missing proposal id","missing_proposal_id");let i;try{i=e.get(r)}catch(f){throw f instanceof re,f}if(!$t(i,n))throw new It(r);if(i.status!=="pending")throw new ee(r,i.status);let d=typeof o=="string"&&o.trim()?o.replace(/\0/g,"").trim().slice(0,2e3):"Withdrawn by access link",l=wc(n),c=e.markWithdrawn(r,l,d),u=se(c,s),p=u.flowLabel||void 0;return t.append("proposal.withdrawn",l,{proposalId:c.id,flowId:c.payload.flowId,title:u.title,displayName:u.displayName,reason:d,...p?{flowLabel:p}:{}}),{...u,mine:!0}}function bc(e,t,n,r={}){let o;try{o=e.get(t.id)}catch{return{}}if(r.liveRev){z(e,r.liveRev,{flowId:o.payload.flowId});try{o=e.get(t.id)}catch{return{}}}return o.status!=="pending"?{}:Ie(Ee(e,o,n))}function ue(e,t,n,r={},o={}){let s="mine"in n?n:{...se(n,r),mine:$t(n,t)},i=0;if(o.liveRev)try{let c=e.get(s.id).payload.flowId;i=z(e,o.liveRev,{flowId:c});let u=e.get(s.id);s={...se(u,r),mine:$t(u,t)}}catch{}let d=ce(s,r),l={...d,mine:s.mine,...X(i)};return o.includeConflicts!==!1&&d.status==="pending"&&Object.assign(l,bc(e,d,t.id)),l}async function pe(e){let t=await e.getFlows();return{rev:t.rev,flows:t.flows}}async function xe(e,t){if(!t)throw new b("Missing flow id","missing_flow_id");let n=await e.getFlow(t);return{rev:n.rev,flow:Nt(t,n.flow)}}function vc(e){if(typeof e.expectedRev=="string"&&e.expectedRev.length>0)return e.expectedRev;if(typeof e.expected_rev=="string"&&e.expected_rev.length>0)return e.expected_rev}function si(e,t){if("nodes"in e&&e.nodes!==void 0&&!Array.isArray(e.nodes))throw new b("payload.nodes must be an array of node ops","invalid_nodes");if(t!==void 0&&!Array.isArray(t))throw new b("nodes must be an array of node ops","invalid_nodes")}async function kc(e,t,n,r,o){let{applyFlowNodeOps:s,interpretPatch:i}=await Promise.resolve().then(()=>(cr(),ni));si(n,r);let d=Array.isArray(n.nodes)?n.nodes:Array.isArray(r)?r:null;if(!d)throw new b("payload.flow is required","missing_flow");let l=n.flowId,c=typeof l=="string"?l:"";if(!c)throw new b("payload.flowId is required","missing_flow_id");let u=i({nodes:d,flowId:c},c);if(u.kind!=="nodes")throw new b("payload.nodes must be node ops","invalid_patch");let p=await s(e,u.flowId,u.ops,{expectedRev:t,...o?{nodeTypes:o}:{}});if(p.expectedRev!==t)throw new W(t,p.expectedRev,"submit_proposal");return{flowId:u.flowId,flow:p.flow}}async function Ac(e,t,n,r,o){let s=n.flowId,i=n.flow,d=!i||typeof i!="object"||Array.isArray(i);if(d&&si(n,r),d&&(Array.isArray(n.nodes)||Array.isArray(r))){let l=await kc(e,t,n,r,o);s=l.flowId,i=l.flow}if(typeof s!="string"||s.length===0)throw new b("payload.flowId is required","missing_flow_id");if(!i||typeof i!="object"||Array.isArray(i))throw new b("payload.flow is required","missing_flow");return{flowId:s,flow:i}}function xc(e,t){try{return{safeExpectedRev:Ce(e.slice(0,200),"expectedRev"),safeFlowId:Ce(t.slice(0,200),"flowId")}}catch{throw new b("expectedRev and payload.flowId must be safe path segments","invalid_path_segment")}}async function ze(e,t,n,r,o,s={},i){let d=r.type,l=vc(r),c=r.payload;if(d!=="replace_flow")throw new b("type must be 'replace_flow'","invalid_proposal_type");if(!l)throw new b("expectedRev is required (top-level field next to type and payload)","missing_expected_rev");if(!c||typeof c!="object"||Array.isArray(c))throw new b("payload must be an object","invalid_payload");let p=await Ac(o,l,c,r.nodes,i),{safeExpectedRev:f,safeFlowId:m}=xc(l,p.flowId),h=p.flow,g=Buffer.byteLength(JSON.stringify(c),"utf8");if(g>$)throw new J("proposal payload",g,$);qe(Hs(h,m));let A=Et(h),E=await o.getFlows();if(E.rev!==f)throw new W(f,E.rev,"submit_proposal");qe(js(A,m,E.flows));let D=await o.getFlow(m);if(D.rev!==f)throw new W(f,D.rev,"submit_proposal");let O=Et(D.flow);i&&await Ms(i,A);let{errors:te,warnings:me}=await Us(A,{flowId:m,baseFlow:O,...i?{nodeTypes:i}:{}});qe(te);let y={kind:"access-link",linkId:n.id};n.label!==void 0&&(y.label=n.label);let w=n.oauthClientId&&n.oauthUser?{kind:"oauth",clientId:n.oauthClientId,user:n.oauthUser}:y,k=lr(r.title)??To(O,A),T=e.create({type:"replace_flow",expectedRev:f,payload:{flowId:m,flow:A},baseFlow:O,title:k,createdBy:w,...me.length>0?{warnings:me}:{}}),_=se(T,s),C=_.flowLabel||void 0;return t.append("proposal.created",y,{proposalId:T.id,flowId:m,expectedRev:f,title:_.title,displayName:_.displayName,...C?{flowLabel:C}:{},...me.length>0?{warningCount:me.length}:{}}),_}function N(e,t="rest"){return _n(e,t)}var Dt,oi,mc,b,G=j(()=>{"use strict";ve();nr();Ue();Qn();q();Ne();Ao();it();Ge();Ae();a($t,"isMineProposal");Dt=200,oi=new Set(["pending","approved","rejected","failed","withdrawn","expired","superseded"]);a(Co,"matchesStatusFilter");a(fc,"isProposalStatus");mc={status:[...oi].join(", "),limit:`integer 1\u2013${Dt}`,offset:"integer \u2265 0",mine:"true, false, 1, 0",flowId:"one flow tab id"};a(jt,"invalidFilterError");a(ur,"singleQueryValue");a(hc,"parseIntegerFilter");a(Eo,"isUnset");a(gc,"parseMineFilter");a(Io,"parseStatusFilter");a(ri,"parseRangeFilter");a(pr,"parseListProposalsQuery");a(dt,"listPageFields");a(yc,"applyListPagination");a(Ht,"listAccessibleProposals");a(Me,"getAccessibleProposal");a(wc,"accessLinkActor");a(Ut,"withdrawMyProposal");a(bc,"conflictsForProposal");a(ue,"presentAccessibleProposalForApi");b=class extends Error{static{a(this,"AiOpsValidationError")}statusCode;code;details;constructor(t,n="validation_error",r={}){super(t),this.name="AiOpsValidationError",this.code=n,this.statusCode=r.statusCode??400,r.details&&(this.details=r.details)}};a(pe,"listFlows");a(xe,"getFlow");a(vc,"resolveExpectedRev");a(si,"assertNodesArrayOrAbsent");a(kc,"materializePayloadFromNodes");a(Ac,"resolveReplaceFlowPayload");a(xc,"assertSafeRevAndFlowId");a(ze,"submitReplaceFlowProposal");a(N,"aiOpsErrorToHttp")});function No(e){return e==="get_link"?86400:2592e3}function Fo(e,t){return e==="get_link"?`get_link ttlSeconds must be \u2264 ${t} (24 hours)`:`ttlSeconds must be \u2264 ${t} (30 days) for ${e} tokens`}var lt,Pn=j(()=>{"use strict";a(No,"maxTtlSecondsForKind");a(Fo,"ttlCapErrorMessage");lt="ttlSeconds must be a positive number of seconds within the cap for the token kind: get_link \u2264 86400 (24h); rest and mcp \u2264 2592000 (30 days)."});function S(e,t,n,r,o=Wt,s="error"){return{code:e,status:t,meaning:n,hint:r,channels:o,kind:s}}function Rn(e){return Tc.get(e)?.hint}function ii(e){return e.replace(/\\/g,"\\\\").replace(/\|/g,"\\|")}function ai(){let e=a(s=>`| \`${s.code}\` | ${s.status} | ${ii(s.meaning)} | ${ii(s.hint)} | ${s.channels.join(", ")} |`,"row"),t=["| Code | HTTP | Meaning | Next step | Channels |","| --- | --- | --- | --- | --- |"],n=fr.filter(s=>s.kind==="error"),r=fr.filter(s=>s.kind==="warning"),o=fr.filter(s=>s.kind==="status");return[...t,...n.map(e),"","**Warnings** (in `warnings[]` on a successful propose; never blocking):","",...t,...r.map(e),"","**Auto-closed proposal statuses.** Pending proposals are closed automatically: `expired` when the live flow rev moves past their `expectedRev`, `superseded` when another proposal on the same flow is approved. Submit, get, and list responses include `autoClosedCount` when that request auto-closed proposals (omitted when zero).","",...t,...o.map(e)].join(`
7
- `)}var Wt,Bt,Oo,Sc,qt,fr,Tc,mr=j(()=>{"use strict";Pn();Wt=["GET","REST","MCP"],Bt=["GET"],Oo=["REST","MCP"],Sc=["GET","REST"],qt=["admin"];a(S,"e");fr=[S("access_link_missing",401,"No Bearer / path token","Ask the user for a new access link of the right kind"),S("bearer_scheme_required",401,"Auth used Basic (or non-Bearer)","Send `Authorization: Bearer <token>`",Oo),S("token_expired",401,"Known token past `expiresAt` (body includes `expiredAt` and `accessLinksUrl`)","Ask the user to create a new link on the `/mcp` page \u2192 Access links (`accessLinksUrl`)"),S("access_link_invalid",401,"Unknown, revoked, or cleared after restart","Ask the user for a new access link"),S("wrong_token_kind",403,"Token kind does not match this surface","Mint the matching kind on `/mcp` \u2192 Access links; show `tellUser` to the human"),S("ai_features_disabled",403,"Plugin AI features off","Owner enables AI Access / `adminAuth`"),S("auth_required",401,"Admin session required","Sign in to the Node-RED editor",qt),S("insufficient_permissions",403,"Admin lacks required scope","Need `*` for link mint / approve",qt),S("invalid_link_kind",400,"Unknown `kind` on create","Use `get_link`, `rest`, or `mcp`",qt),S("invalid_ttl",400,"`ttlSeconds` is zero, negative, not a number, or over the cap for the kind",lt,qt),S("access_link_limit",409,"Too many active links","Revoke an existing link first",qt),S("access_link_not_found",404,"Unknown link id on revoke","List links and use an existing id",qt),S("link_create_not_supported",403,"Attempted mint under a GET link","Admin mints via `POST /mcp/api/v1/links` only",Bt),S("rate_limited",429,"Per-IP or per-token limit","Wait `retryAfter` / `Retry-After`"),S("payload_too_large",413,"Body / proposal over ~1.5MB","Shrink content"),S("invalid_json",400,"Malformed JSON body","Fix Content-Type / body",Oo),S("unsupported_media_type",415,"POST body is not `application/json`","Send `Content-Type: application/json`",Oo),S("url_too_long",414,"GET URL over soft cap (~8KB)","Use chunked drafts or REST/MCP",Bt),S("not_found",404,"Unknown path or resource","Re-read docs / discovery"),S("missing_query",400,"Required search `q` missing","Pass `?q=`",Sc),S("invalid_filter",400,"Invalid list filter (`status`, `limit`, `offset`, `mine`); body names `parameter` and `allowed`","Use an allowed value: status one of the proposal statuses; limit integer 1\u2013200; offset integer \u2265 0; mine true|false"),S("invalid_arguments",400,"MCP tool arguments failed schema validation; body names `parameter`","Fix the named argument to match the tool input schema (see tools/list)",["MCP"]),S("invalid_diff_format",400,"Unknown `format` on proposal diff","Use `format=summary` (default) or `format=full`"),S("confirm_required",400,"GET withdraw without `confirm=1`","Repeat the request with `confirm=1`",Bt),S("missing_expected_rev",400,"Proposal submitted without `expectedRev` / `rev`","Read the flow and pass its `rev`"),S("version_mismatch",409,"Stale `rev` / `expectedRev`","Re-read flows and resubmit"),S("proposal_not_found",404,"Unknown proposal id","List proposals and use an existing id"),S("proposal_not_pending",409,"Action needs a pending proposal","Only pending can withdraw / decide"),S("proposal_not_owned",403,"Withdraw of another token\u2019s proposal","Only the creating token can withdraw"),S("validation_failed",400,"Several validation errors at once (message `N validation errors`)","Fix every entry in errors[] and resubmit."),S("duplicate_node_id",400,"The same node id appears more than once in a full tab or batch","Give every node a unique id"),S("missing_node_id",400,"Node / op without an id","Include a non-empty string id"),S("missing_node_type",400,"Node without a `type`","Set `type` to an installed palette type name"),S("invalid_node_type",400,"`type` is not a string","Use the palette type name exactly"),S("invalid_nodes",400,"`flow.nodes` / `flow.configs` not an array","Send arrays (use `[]` to clear)"),S("invalid_node",400,"Node entry is not an object","Send `{ id, type, \u2026 }` objects"),S("invalid_wires",400,"Bad wires shape, missing target, or more ports than outputs","Wire only to existing node ids / ports"),S("z_mismatch",400,"Node `z` differs from the flow id","Set `z` to the tab id or omit it"),S("node_already_exists",400,"`add` op for an id already in the flow","Use `set` or a new id"),S("protected_property",400,"Tried to change id / type / z","Omit id, type, and z from edits"),S("node_not_found",404,"Unknown node (single error only; several errors \u2192 `validation_failed`)","Re-read the flow"),S("field_not_found",404,"Unknown field on a node","Re-read the node"),S("text_not_found",400,"`replace_text` find had no match","Re-read field; find is exact"),S("ambiguous_match",409,"`replace_text` matched more than once","Pass `occurrence` (1-based) or unique find"),S("unknown_node_type",400,"Proposal uses a type that is not installed","List node-types / palette search; ask user to install"),S("node_type_not_found",404,"Unknown type on node-types detail","List types or search palette"),S("chunk_conflict",409,"Same draft seq re-sent with different data","Re-send identical data or start a new draft",Bt),S("missing_chunks",400,"Draft submit with gaps in seq","Upload every seq from 0 through max",Bt),S("draft_hash_mismatch",400,"Draft sha256 does not match assembled bytes","Send the hex sha256 of the assembled UTF-8",Bt),S("missing_config_node",201,"Warning: node has no (or an unresolved) config node yet","Configure before deploy if needed; see `candidates`",Wt,"warning"),S("unwired_node",201,"Warning: node has outputs but no wires","Wire it, or ignore while scaffolding",Wt,"warning"),S("incomplete_wires",201,"Warning: fewer wire ports than outputs","Add ports if outputs should connect",Wt,"warning"),S("expired",200,"Status: pending proposal auto-closed because the live flow rev moved past `expectedRev`","Re-read the flow and submit a new proposal",Wt,"status"),S("superseded",200,"Status: pending proposal auto-closed because another proposal on the same flow was approved","Re-read the flow and submit a new proposal if still needed",Wt,"status"),S("upstream_error",502,"Node-RED runtime/flows error","Retry; check Node-RED logs"),S("internal_error",500,"Unexpected server error","Retry; check Node-RED logs")],Tc=new Map(fr.map(e=>[e.code,e]));a(Rn,"registryHint");a(ii,"cell");a(ai,"renderGetLinkErrorTable")});function ie(e,t,n={}){return Mo(e,t,n)}function _c(e,t){return t==="excess_ports"?e==="mcp"?"This node has more wire ports than its type declares \u2014 remove extra ports or raise the node outputs, then retry.":e==="rest"?"This node has more wire ports than its type declares \u2014 trim wires[] or raise outputs, then retry.":"This node has more wire ports than its type declares \u2014 trim wires or raise outputs, then retry.":t==="missing_target"?e==="mcp"?"Wire targets must be node ids in this flow \u2014 add the node in the same patch or use get_flow ids.":e==="rest"?"Wire targets must be node ids in this flow \u2014 add the node in the same patch or use GET /flows/{id} ids.":"wires must list existing node ids (or add those nodes in the same patch).":e==="mcp"||e==="rest"?"Fix wires: targets must exist in this flow, and port count must not exceed the node outputs.":"wires must list existing node ids (or add those nodes in the same patch)."}function Pc(e){return e==="mcp"?"Call list_node_types or search_palette. Ask the user to install via Manage palette, then recheck. This API never installs.":e==="rest"?"Call GET /node-types?q= or GET /palette/search?q=. Ask the user to install via Manage palette, then recheck. This API never installs.":"Call GET \u2026/node-types?q= or \u2026/palette/search?q=. Ask the user to install via Manage palette, then recheck. This API never installs."}function Je(e,t,n,r){return e==="mcp"?t:e==="rest"?n:r}function Mo(e,t,n={}){switch(e){case"version_mismatch":return Je(t,"Re-read with list_flows/get_flow and retry with the current rev.","Re-read the flow and retry with the current rev.","Re-read the flow and retry with the current rev.");case"node_not_found":return Je(t,"Call get_flow or get_node and use a node id from that response.","Call GET /flows/{id} and use a node id from that response.","Call GET \u2026/flows/<id> and use a node id from that response.");case"flow_not_found":return Je(t,"Call list_flows to list valid ids.","Call GET /flows to list valid ids.","Use an id from GET ./flows.");case"proposal_not_found":return Je(t,"Call list_proposals and use an id from that response.","List proposals and use an id from that response.","List proposals and use an id from that response.");case"proposal_not_owned":return"Only the access link that created a pending proposal can withdraw it.";case"proposal_not_pending":return"Only pending proposals can be withdrawn or decided.";case"confirm_required":return"Add confirm=1 to the withdraw URL (required so link prefetchers cannot withdraw).";case"invalid_wires":return _c(t,n.cause);case"node_type_not_found":case"unknown_node_type":return Pc(t);case"missing_node_type":return'Each add op needs node.type as a non-empty string (e.g. "inject").';case"invalid_line_range":return Je(t,'Pass lines as "A" or "A-B" (1-based inclusive). Reversed ranges (end < start) are invalid.',"Use lines=A or lines=A-B (1-based inclusive). Reversed ranges (end < start) are invalid.","Use lines=A or lines=A-B (1-based inclusive). Reversed ranges (end < start) are invalid.");case"line_range_out_of_bounds":return"start is past the end of the field \u2014 check totalLines and request a lower lines range.";case"ambiguous_match":return"find matched more than once \u2014 pass occurrence=N (1-based) or a more unique find string.";case"missing_expected_rev":return Je(t,"Pass expectedRev from list_flows/get_flow (top-level, next to type and payload).","Pass expectedRev from GET /flows or GET /flows/{id} (top-level, next to type and payload).","Pass rev from GET \u2026/flows or \u2026/flows/<id>.");case"missing_query":return Je(t,"Pass a non-empty q string.","q query parameter is required.","q query parameter is required.");case"unsupported_media_type":return"Send Content-Type: application/json with a JSON body.";case"text_not_found":return"Re-read the node field; find must match exactly (case-sensitive).";case"field_not_found":return"Re-read the node; field must be an existing string property (template, func, \u2026).";case"field_not_string":return Je(t,"This tool only returns string fields (template, func, \u2026). Use get_node for other property types.","This endpoint only returns string fields (template, func, \u2026). Use GET /nodes/{id} for other property types.","This endpoint only returns string fields (template, func, \u2026). Use GET /nodes/{id} for other property types.");default:return}}function _n(e,t,n={}){let r=n.canonicalizeMessage!==!1,o=null;if(e instanceof b?o={statusCode:e.statusCode,body:{error:e.message,code:e.code,...e.details??{}}}:e instanceof W?o={statusCode:e.statusCode,body:{error:e.message,code:e.code,expectedRev:e.expectedRev,currentRev:e.currentRev}}:e instanceof B?o={statusCode:e.statusCode,body:{error:e.message,code:e.code,flowId:e.flowId}}:e instanceof re||e instanceof It||e instanceof ee||e instanceof J?o={statusCode:e.statusCode,body:{error:e.message,code:e.code}}:(e instanceof Le||e instanceof be)&&(o={statusCode:e.statusCode,body:{error:e.message,code:e.code}}),!o)return null;let s=o.body.code;if(r&&!Array.isArray(o.body.errors)){let i=Fe[s];i&&(o.body.error=i)}if(typeof o.body.hint!="string"||!o.body.hint){let i=Mo(s,t,{cause:o.body.cause})??Rn(s);i&&(o.body.hint=i)}return o}function di(e,t,n={}){let o={error:n.error??Fe[t]??"Request failed",code:t,...n.details??{}},s=Mo(t,e,{cause:o.cause})??Rn(t);return s&&(o.hint=s),{statusCode:n.statusCode??400,body:o}}var Fe,Ne=j(()=>{"use strict";ve();Ue();q();G();mr();Fe={node_not_found:"Node was not found",flow_not_found:"Flow was not found",field_not_found:"Field was not found",field_not_string:"Field is not a string",missing_query:"q query parameter is required",missing_flow_id:"Missing flow id",missing_node_id:"Missing node id",missing_field:"Missing field name",invalid_line_range:"lines must be A or A-B (1-based inclusive; end >= start)",line_range_out_of_bounds:"lines range is out of bounds",proposal_not_found:"Proposal was not found",proposal_not_owned:"Proposal was not created by this access link",proposal_not_pending:"Proposal is not pending",confirm_required:"Withdraw over GET requires confirm=1",unsupported_media_type:"Content-Type must be application/json",upstream_error:"Upstream request failed",internal_error:"Internal server error"};a(ie,"channelErrorHint");a(_c,"invalidWiresHint");a(Pc,"nodeTypeHint");a(Je,"pickChannel");a(Mo,"hintFor");a(_n,"mapErrorForChannel");a(di,"buildChannelError")});var Uf={};rs(Uf,{ACCESS_LINK_INVALID_MESSAGE:()=>et,ACCESS_LINK_MISSING_MESSAGE:()=>le,AccessLinkStore:()=>yt,EDITOR_SIDEBAR_ASSET_VERSION:()=>Jt,MAX_PROPOSAL_JSON_BYTES:()=>$,SettingsStore:()=>Tt,areAiFeaturesEnabled:()=>ye,bindSettingsStore:()=>Gn,createPluginServices:()=>Jr,default:()=>Hf,getStorageDir:()=>Dr,isAdminAuthEnabled:()=>V,packageVersion:()=>zt,registerRoutes:()=>ld,renderEditorSidebarJs:()=>hr,renderEditorSidebarLoaderHtml:()=>pi,requireAccessLink:()=>Ct,resolveAiAccessSettings:()=>ge});module.exports=hd(Uf);var dd=require("fs"),Vr=require("path");var jn=require("fs"),cn=require("path");q();function Hn(e){let t=(0,cn.join)(e,".ai-access");return{root:t,proposals:(0,cn.join)(t,"proposals"),logs:(0,cn.join)(t,"logs"),settings:(0,cn.join)(t,"config.json")}}a(Hn,"resolveAiAccessPaths");function Yr(e){(0,jn.mkdirSync)(e,{recursive:!0,mode:Xr});try{(0,jn.chmodSync)(e,Xr)}catch{}}a(Yr,"mkdirSecure");function ds(e){Yr(e.root),Yr(e.proposals),Yr(e.logs)}a(ds,"ensureAiAccessDirs");tt();Pe();function Kn(e){let t=ne(e);return`auth-tokens${(t==="/"?"":t).replace(/\//g,"-")}`}a(Kn,"editorAuthTokenKey");Pe();Ne();function Rc(e){if(!e)return!1;let t=e.split(";")[0]?.trim().toLowerCase()??"";return t==="application/json"||t.endsWith("+json")}a(Rc,"isJsonContentType");function Lc(e){let t=e.headers["content-length"];if(typeof t=="string"&&t.length>0){let n=Number(t);if(Number.isFinite(n)&&n>0)return!0;if(n===0)return!1}return e.headers["transfer-encoding"]!==void 0}a(Lc,"hasRequestBody");function Ve(e="rest"){return(t,n,r)=>{let o=t.headers["content-type"];if(Rc(o)){r();return}if(!o&&!Lc(t)){r();return}let s=di(e,"unsupported_media_type",{statusCode:415});n.status(s.statusCode).json(s.body)}}a(Ve,"requireJsonContentType");var Cc="text/plain; charset=utf-8",Ec="text/html; charset=utf-8",Ic="default-src 'none'; base-uri 'self'; form-action 'none'; frame-ancestors 'none'";function Nc(e){let t=e.query?.format;return typeof t=="string"?t.toLowerCase():void 0}a(Nc,"formatParam");function Fc(e){let t=Nc(e);if(t==="md"||t==="json")return"native";if(!e.get("accept"))return"html";let n=e.accepts(["text/html","application/json","text/markdown","text/plain"]);return n==="application/json"||n==="text/markdown"?"native":n==="text/plain"?"plain":"html"}a(Fc,"chooseGetLinkRepresentation");function li(e){return e.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&#39;")}a(li,"escapeHtml");var Oc=/(?<![A-Za-z0-9_.~\-/])\.\/[A-Za-z0-9._~\-/]+(?:\?[A-Za-z0-9._~\-=&%]+)?/g;function Mc(e){return e.replace(Oc,(t,n)=>{if(e.startsWith("&lt;",n+t.length))return t;let r=t.replace(/[.]+$/,""),o=t.slice(r.length);return r.length<=2||r.includes("..")||r.includes("//")||r.includes("&")?t:`<a href="${r}">${r}</a>${o}`})}a(Mc,"linkifyEscaped");function $c(e,t){return`<!doctype html>
8
- <html><head><meta charset="utf-8">${t?`<base href="${li(t)}">`:""}<title>Node-RED AI Access</title></head><body><pre>${Mc(li(e))}</pre></body></html>
9
- `}a($c,"renderGetLinkHtml");function Dc(e){let t=e.split("?")[0]??"",n=/^(.*?\/mcp\/a\/[A-Za-z0-9_-]+)(?:\/|$)/.exec(t);if(!(!n?.[1]||!/^[A-Za-z0-9._~\-/]*$/.test(n[1])))return`${n[1]}/`}a(Dc,"getLinkBaseHref");function jc(e){try{return JSON.stringify(JSON.parse(e),null,2)}catch{return e}}a(jc,"prettyJson");function ci(){return(e,t,n)=>{let r=Fc(e),o=Dc(e.originalUrl);t.vary("Accept"),t.setHeader("Cache-Control","no-store"),t.setHeader("Referrer-Policy","no-referrer"),t.setHeader("X-Robots-Tag","noindex, nofollow"),t.setHeader("X-Content-Type-Options","nosniff");let s=t.send.bind(t);t.send=(i=>{let d=String(t.getHeader("Content-Type")??"").toLowerCase(),l=d.startsWith("application/json"),c=d.startsWith("text/markdown");if(r==="native"||!l&&!c||t.headersSent)return s(i);let u=typeof i=="string"?i:Buffer.isBuffer(i)?i.toString("utf8"):void 0;return u===void 0?s(i):r==="plain"?(t.setHeader("Content-Type",Cc),s(u)):(t.setHeader("Content-Type",Ec),t.setHeader("Content-Security-Policy",Ic),s($c(l?jc(u):u,o)))}),n()}}a(ci,"getLinkContentNegotiation");Un();Pe();function Hc(e){let t=de(e.settings.httpAdminRoot),n=je(e.settings.httpAdminRoot),r="0.2.0-beta.1",o=[{method:"GET",path:"/",auth:"none",summary:"This discovery index"},{method:"GET",path:"/openapi.yaml",auth:"none",summary:"OpenAPI 3.1 specification (YAML)"},{method:"GET",path:"/openapi.json",auth:"none",summary:"OpenAPI 3.1 specification (JSON)"},{method:"GET",path:"/status",auth:"admin-or-access-link",summary:"Plugin status and caller info"},{method:"GET",path:"/flows",auth:"admin-or-access-link",summary:"List flows + rev"},{method:"GET",path:"/flows/{id}",auth:"admin-or-access-link",summary:"Get one flow tab + rev (?summary=1 for node list without field contents)"},{method:"GET",path:"/nodes/{id}",auth:"admin-or-access-link",summary:"Get one full node (optional ?flow=)"},{method:"GET",path:"/nodes/{id}/field/{name}",auth:"admin-or-access-link",summary:"Get one node field (?lines=A-B optional)"},{method:"GET",path:"/search",auth:"admin-or-access-link",summary:"Text search across string fields (?q=&flow=)"},{method:"GET",path:"/node-types",auth:"admin-or-access-link",summary:"List installed palette node types (?q= filter)"},{method:"GET",path:"/node-types/{type}",auth:"admin-or-access-link",summary:"Detail for one installed node type (defaults JSON, inputs/outputs, help; ?help=0 omits help)"},{method:"GET",path:"/palette/search",auth:"admin-or-access-link",summary:"Search Node-RED catalogue (recommend only; never installs) (?q=)"},{method:"POST",path:"/proposals",auth:"access-link",summary:"Create a replace_flow proposal"},{method:"GET",path:"/proposals",auth:"admin-or-access-link",summary:"List proposals (admin: all; link: own)"},{method:"GET",path:"/proposals/{id}",auth:"admin-or-access-link",summary:"Get one proposal"},{method:"GET",path:"/proposals/{id}/diff",auth:"admin-or-access-link",summary:"Diff proposal vs current flow"}];return{ok:!0,name:"node-red-ai-access",version:r,description:"Temporary, owner-approved Node-RED access for AI clients over plain HTTP (REST) or MCP.",auth:{scheme:"Bearer",header:"Authorization: Bearer <token>",description:"An admin with * mints tokens on the /mcp Access links page (POST /links only \u2014 never under GET /mcp/a/<token>). Each token has kind get_link | rest | mcp and only works on that surface. Send rest/mcp tokens as Authorization: Bearer <token>. Missing/invalid \u2192 401 access_link_missing / access_link_invalid; wrong kind \u2192 403 wrong_token_kind. GET /mcp/a links cannot create tokens.",tokenKinds:["get_link","rest","mcp"],mint:{path:"/links",method:"POST",auth:"admin",note:"Admin * only. Not available under /mcp/a/<token>."}},openapi:`${t}/openapi.yaml`,openapiJson:`${t}/openapi.json`,mcp:n,apiBase:t,getLink:{base:"/mcp/a/<token>",canMintTokens:!1,note:"Self-contained GET surface for get_link tokens only. No POST /links equivalent."},endpoints:o}}a(Hc,"buildDiscoveryIndex");function $o(e){return(t,n)=>{n.status(200).json(Hc(e))}}a($o,"discoveryIndexHandler");function ui(){return(e,t)=>{t.status(404).json({error:"Not found",code:"not_found"})}}a(ui,"mcpApiNotFoundHandler");var Gt;function zt(){return Gt||(Gt="0.2.0-beta.1",Gt)}a(zt,"packageVersion");var Jt=zt();function Uc(){return`#red-ui-sidebar-ai-access {
3
+ `,o=Buffer.from(r,"utf8");if(o.byteLength>t)throw new X(n.kind,o.byteLength,t);return{bytes:o}}function us(e,t){let n=(0,cn.resolve)(t),r=(0,cn.resolve)(e);if(r!==n&&!r.startsWith(n+cn.sep))throw new Error("Path escapes .ai-access storage root");return r}function Rt(e,t,n){let r=us(t,e),o=`${r}.${process.pid}.tmp`;(0,Me.writeFileSync)(o,n.bytes,{mode:Hn}),(0,Me.renameSync)(o,r),(0,Me.chmodSync)(r,Hn)}function ps(e,t,n){let r=us(t,e);(0,Me.appendFileSync)(r,n.bytes,{mode:Hn});try{(0,Me.chmodSync)(r,Hn)}catch{}}var Me,cn,Yr,Hn,j,cs,X,G=U(()=>{"use strict";Me=require("fs"),cn=require("path"),Yr=448,Hn=384,j=15e5,cs=8192,X=class extends Error{constructor(n,r,o){super(`${n} size ${r} bytes exceeds limit of ${o} bytes`);this.kind=n;this.size=r;this.maxBytes=o;this.name="StoragePayloadTooLargeError"}static{a(this,"StoragePayloadTooLargeError")}code="payload_too_large";statusCode=413};a(Be,"encodeTrustedJson");a(us,"assertUnderAiAccess");a(Rt,"writeTrustedAtomic");a(ps,"appendTrusted")});function pn(e){if(typeof e!="string"||e.length===0||e.length>2048)return;let t;try{t=new URL(e)}catch{return}if(!(t.protocol!=="http:"&&t.protocol!=="https:")&&!(t.username||t.password||t.search||t.hash)&&!(t.pathname!=="/"||/^[a-z][a-z0-9+.-]*:\/\/[^/]*\//i.test(e)))return t.origin.toLowerCase()}function Ed(e,t){let n=e.getHeader("Vary"),r=String(n??"").split(",").map(o=>o.trim()).filter(Boolean);r.some(o=>o.toLowerCase()===t.toLowerCase())||r.push(t),e.setHeader("Vary",r.join(", "))}function ms(e){return(t,n,r)=>{if(!Rd.test(t.path)){r();return}for(let c of n.getHeaderNames())c.startsWith("access-control-")&&n.removeHeader(c);let o=n.setHeader.bind(n),s=!1;n.setHeader=((c,u)=>!s&&/^access-control-/i.test(c)?n:o(c,u));let i=pn(t.get("origin")),d=i===void 0?void 0:e().find(c=>c===i),l=d!==void 0;if(Ed(n,"Origin"),!l){if(t.method==="OPTIONS"&&t.get("access-control-request-method")){n.status(204).end();return}r();return}if(s=!0,n.setHeader("Access-Control-Allow-Origin",d),n.setHeader("Access-Control-Expose-Headers",Cd),t.method==="OPTIONS"&&t.get("access-control-request-method")){n.setHeader("Access-Control-Allow-Methods",_d),n.setHeader("Access-Control-Allow-Headers",Ld),n.setHeader("Access-Control-Max-Age",String(600)),n.status(204).end();return}s=!1,r()}}function hs(e,t){let n=e.parent,r=n&&typeof n.use=="function"?n:e;r.use(t);let o=r,i=(o._router??o.router)?.stack;if(!Array.isArray(i)||i.length<2)return;let d=i.pop(),l=0;for(;l<i.length;){let c=i[l].name;if(c!=="query"&&c!=="expressInit")break;l+=1}i.splice(l,0,d)}var Rd,_d,Ld,Cd,qn=U(()=>{"use strict";Rd=/^\/mcp(?:\/?$|\/(?:a(?:\/|$)|api(?:\/|$)|rpc\/?$))/,_d="GET, POST, DELETE, OPTIONS",Ld="Authorization, Content-Type, Accept, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID",Cd="Mcp-Session-Id, WWW-Authenticate, Retry-After, RateLimit, RateLimit-Policy, RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset";a(pn,"canonicalOrigin");a(Ed,"appendVary");a(ms,"aiCorsMiddleware");a(hs,"installAiCorsMiddleware")});function Nd(e){return e==="nodes"||e==="flows"||e==="full"}function Wn(e,t){if(!e||typeof e!="object"||Array.isArray(e))return{};let n=e,r={};if(typeof n.enabled=="boolean"&&(r.enabled=n.enabled),typeof n.allowWithoutAdminAuth=="boolean"&&(r.allowWithoutAdminAuth=n.allowWithoutAdminAuth),typeof n.defaultLinkTtlSeconds=="number"&&Number.isFinite(n.defaultLinkTtlSeconds)&&n.defaultLinkTtlSeconds>0&&(r.defaultLinkTtlSeconds=Math.floor(n.defaultLinkTtlSeconds)),Nd(n.defaultDeploymentType)&&(r.defaultDeploymentType=n.defaultDeploymentType),typeof n.maxActiveLinks=="number"&&Number.isFinite(n.maxActiveLinks)&&n.maxActiveLinks>0&&(r.maxActiveLinks=Math.floor(n.maxActiveLinks)),typeof n.publicBaseUrl=="string")try{r.publicBaseUrl=Ct(n.publicBaseUrl)}catch{t?.(`AI Access: invalid publicBaseUrl value ${JSON.stringify(n.publicBaseUrl)} was dropped`)}if(Array.isArray(n.corsAllowedOrigins)){let o=new Set;for(let s of n.corsAllowedOrigins){let i=pn(s);i?o.add(i):t?.(`AI Access: invalid CORS origin value ${JSON.stringify(s)} was dropped`)}r.corsAllowedOrigins=[...o]}for(let o of["rateLimitWindowMs","rateLimitMaxPerIp","rateLimitMaxPerToken"]){let s=n[o],i=o==="rateLimitWindowMs"?100:1,d=o==="rateLimitWindowMs"?36e5:1e6;typeof s=="number"&&Number.isInteger(s)&&s>=i&&s<=d&&(r[o]=s)}if(n.oauth&&typeof n.oauth=="object"&&!Array.isArray(n.oauth)){let o=n.oauth;r.oauth={},typeof o.enabled=="boolean"&&(r.oauth.enabled=o.enabled);for(let i of["accessTokenTtlSec","refreshIdleTtlSec","dcrMaxClients"]){let d=o[i];typeof d=="number"&&Number.isInteger(d)&&d>0&&d<=31536e3&&(r.oauth[i]=d)}let s=o.refreshMaxLifetimeSec;typeof s=="number"&&Number.isInteger(s)&&s>=0&&s<=31536e4&&(r.oauth.refreshMaxLifetimeSec=s)}return r}function Fd(e,t){if(!(0,mn.existsSync)(e))return{};try{let n=(0,mn.readFileSync)(e,"utf8");return Wn(JSON.parse(n),t)}catch(n){return t?.(`AI Access: config.json is corrupt or unreadable; defaults are used (${String(n)})`),{}}}function zn(e,t){let n={},r={...fn};for(let s of Object.keys(fn).filter(i=>i!=="oauth"))n[s]=e[s]!==void 0?"settings.js":t[s]!==void 0?"disk":"default",Object.assign(r,{[s]:e[s]??t[s]??fn[s]});let o={...r,sources:n};return e.oauth!==void 0||t.oauth!==void 0?(o.oauth={..._t,...t.oauth??{},...e.oauth??{}},n.oauth=e.oauth!==void 0?"settings.js":"disk"):(o.oauth={..._t},n.oauth="default"),o}function eo(e,t){let n=Wn(e??{},t);for(let o of Object.keys(fn).filter(s=>s!=="oauth"))e&&Object.hasOwn(e,o)&&n[o]===void 0&&Object.assign(n,{[o]:fn[o]});let r=e?.oauth;if(r&&typeof r=="object"&&!Array.isArray(r)){let o=n.oauth??={};for(let s of Object.keys(_t))Object.hasOwn(r,s)&&o[s]===void 0&&(t?.(`AI Access: settings.js aiAccess.oauth.${s} is invalid; using the default (${String(_t[s])})`),Object.assign(o,{[s]:_t[s]}))}return n}var mn,Zr,Id,_t,fn,Lt,Gn=U(()=>{"use strict";mn=require("fs"),Zr=require("path");ie();qn();G();Id=16384,_t={enabled:!1,accessTokenTtlSec:3600,refreshIdleTtlSec:2592e3,refreshMaxLifetimeSec:7776e3,dcrMaxClients:5},fn={enabled:!0,allowWithoutAdminAuth:!1,defaultLinkTtlSeconds:14400,maxActiveLinks:10,defaultDeploymentType:"nodes",publicBaseUrl:"",corsAllowedOrigins:[],rateLimitWindowMs:6e4,rateLimitMaxPerIp:600,rateLimitMaxPerToken:300,oauth:{..._t}};a(Nd,"isDeploymentType");a(Wn,"sanitizePartial");a(Fd,"readDiskFile");a(zn,"mergeAiAccessSettings");a(eo,"settingsJsPartial");Lt=class{constructor(t,n,r){this.paths=t;this.filePath=(0,Zr.join)(t.root,"config.json"),this.settingsJs=eo(n,r),this.disk=Fd((0,mn.existsSync)(this.filePath)?this.filePath:(0,Zr.join)(t.root,"settings.json"),r),this.cached=zn(this.settingsJs,this.disk)}static{a(this,"SettingsStore")}filePath;disk;settingsJs;cached;getEffective(){return this.cached}getDisk(){return{...this.disk}}update(t){let n=Wn(t);for(let r of Object.keys(this.settingsJs))delete n[r];return this.persist(n),this.disk=n,this.cached=zn(this.settingsJs,this.disk),this.cached}patch(t){let n=Wn(t);return this.update({...this.disk,...n,...n.oauth?{oauth:{...this.disk.oauth,...n.oauth}}:{}})}persist(t){let n=Be(t,Id,{kind:"settings",pretty:!0});Rt(this.paths.root,this.filePath,n)}}});function Jn(e){to=e}function W(e){return e.adminAuth!=null&&e.adminAuth!==!1}function he(e){return to?to.getEffective():zn(eo(e.aiAccess),{})}function ge(e){let t=he(e);return t.enabled?W(e)?!0:t.allowWithoutAdminAuth:!1}var to,ye=U(()=>{"use strict";Gn();to=null;a(Jn,"bindSettingsStore");a(W,"isAdminAuthEnabled");a(he,"resolveAiAccessSettings");a(ge,"areAiFeaturesEnabled")});function ae(e){let t=`/${e??""}`.replace(/\/{2,}/g,"/");for(;t.length>1&&t.endsWith("/");)t=t.slice(0,-1);return t}function de(e){let t=ae(e);return t==="/"?"/mcp/api/v1":`${t}/mcp/api/v1`}function hn(e){let t=ae(e);return t==="/"?"/ai-access":`${t}/ai-access`}function qe(e){let t=ae(e);return t==="/"?"/mcp/rpc":`${t}/mcp/rpc`}function Od(e){let t=e.authInfo?.scope;if(t)return Array.isArray(t)?t:[t];let n=e.user?.permissions;return n?Array.isArray(n)?n:[n]:[]}function We(e){if(e.auth)return e.auth.scopes.includes("propose")?"propose":e.auth.scopes.includes("read")?"read":null;let t=Od(e);return t.includes("*")?"*":t.includes("read")?"read":null}function $d(e,t){return!t||!e?{view:!0,manageLinks:!1,approveProposals:!1}:e==="*"?{view:!0,manageLinks:!0,approveProposals:!0}:{view:!0,manageLinks:!1,approveProposals:!1}}function gs(e,t={error:"Authentication required",code:"auth_required"},n){let r=e.end.bind(e),o=!0;return e.end=((...s)=>o&&e.statusCode===401&&!e.headersSent?(o=!1,e.removeHeader("Content-Length"),e.setHeader("Content-Type","application/json; charset=utf-8"),n&&e.setHeader("WWW-Authenticate",n),r(JSON.stringify(t))):r(...s)),()=>{o=!1,e.end=r}}function we(e,t){let n="read";return(r,o,s)=>{if(!W(e.settings)){s();return}let i=t==="api"?gs(o):null;e.auth.needsPermission(n)(r,o,d=>{if(i?.(),d){s(d);return}if(!r.user){if(t==="page"){o.redirect(ae(e.settings.httpAdminRoot));return}o.status(401).json({error:"Authentication required",code:"auth_required"});return}if(!We(r)){o.status(401).json({error:"Insufficient permissions",code:"insufficient_permissions"});return}s()})}}function Kn(e,t){let n=W(e.settings),r=he(e.settings),o=ge(e.settings),s=null;return n?s=We(t):o&&(s="*"),{adminAuthEnabled:n,aiFeaturesEnabled:o,allowWithoutAdminAuth:r.allowWithoutAdminAuth,permission:s,capabilities:$d(s,o)}}function Et(e){return(t,n,r)=>{if(!ge(e.settings)){n.status(403).json({error:"AI Access features are disabled. Enable adminAuth, or set aiAccess.allowWithoutAdminAuth to true for local-only use.",code:"ai_features_disabled"});return}r()}}function gn(e){return(t,n,r)=>{if(!W(e.settings)){if(ge(e.settings)){r();return}n.status(403).json({error:"AI Access features are disabled",code:"ai_features_disabled"});return}if(We(t)!=="*"){n.status(403).json({error:"Full admin permission (*) is required for this action",code:"insufficient_permissions"});return}r()}}function Md(e){return e.startsWith("aia_")}function Dd(e){return Md(e)?!0:e.length<24}function jd(e){if(e.length<24||e.startsWith("aia_")&&e.length<40)return"The token may be truncated \u2014 ask the user for the full access-link token."}function ys(e,t,n,r){if(!W(e.settings)){r();return}let o={error:le,code:Le},s=gs(n,o,Y);e.auth.needsPermission("read")(t,n,i=>{if(s(),i){r(i);return}if(!n.headersSent){if(!t.user||!We(t)){n.setHeader("WWW-Authenticate",Y),n.status(401).json(o);return}r()}})}function ws(e,t,n,r,o){let s=e.headers.authorization;if(typeof s!="string"||!/^Bearer\s+\S+/i.test(s.trim()))return"continue";let d=/^Bearer\s+(\S+)$/i.exec(s.trim())?.[1];if(!d)return"continue";let l=n(d);if(l.status==="active")return l.link.kind!==r?(bn(t,r,l.link.kind),"wrong_kind"):(e.accessLink=l.link,"ok");if(l.status==="expired")return wn(e,t,o,l.expiresAt),"invalid";if(Dd(d)){t.setHeader("WWW-Authenticate",Y);let c=jd(d);return t.status(401).json({error:st,code:yn,...c?{hint:c}:{}}),"invalid"}return"continue"}function bs(e,t,n="rest"){return(r,o,s)=>{if(!ge(e.settings)){o.status(403).json({error:"AI Access features are disabled. Enable adminAuth, or set aiAccess.allowWithoutAdminAuth to true for local-only use.",code:"ai_features_disabled"});return}let i=ws(r,o,t,n,e.settings.httpAdminRoot);if(i==="ok"){s();return}i==="invalid"||i==="wrong_kind"||ys(e,r,o,s)}}function vs(e,t,n="rest"){return(r,o,s)=>{let i=ws(r,o,t,n,e.settings.httpAdminRoot);if(i==="ok"){s();return}i==="invalid"||i==="wrong_kind"||ys(e,r,o,s)}}var _e=U(()=>{"use strict";it();ye();a(ae,"normalizeHttpAdminRoot");a(de,"mcpApiBase");a(hn,"dashboardPath");a(qe,"mcpRpcPath");a(Od,"scopesFromRequest");a(We,"resolvePermission");a($d,"capabilitiesFor");a(gs,"interceptNeedsPermissionUnauthorized");a(we,"requireAdminSession");a(Kn,"describeAccess");a(Et,"requireAiFeatures");a(gn,"requireStarPermission");a(Md,"looksLikeAccessLinkToken");a(Dd,"looksLikeFailedAccessLinkAttempt");a(jd,"truncatedTokenHint");a(ys,"continueAsAdminSessionOrAccessLinkMissing");a(ws,"tryAccessLinkBearer");a(bs,"requireAdminReadOrAccessLink");a(vs,"requireAdminSessionOrAccessLink")});function Hd(e,t){let n=ae(e);return`${n==="/"?"/mcp/a":`${n}/mcp/a`}/${t}`}function Ud(e){return!!e.app.get("trust proxy")}function ks(e,t){let n=e.get(t);if(typeof n!="string"||n.length===0)return;let r=n.split(",")[0]?.trim();return r&&r.length>0?r:void 0}function Bd(e){if(typeof e!="string"||e.trim().length===0)return;let t;try{t=new URL(e.trim())}catch{return}if(!(t.protocol!=="http:"&&t.protocol!=="https:"))return`${t.origin}${t.pathname.replace(/\/+$/,"")}`}function at(e){try{let n=new URL(`http://${e.includes(":")&&!e.startsWith("[")?`[${e}]`:e}`).hostname.replace(/^\[|\]$/g,"");if(!(0,no.isIP)(n))return;if(n.startsWith("::ffff:")){let r=n.slice(7);if((0,no.isIP)(r)===4)return r;let o=r.split(":");if(o.length===2){let s=parseInt(o[0],16)*65536+parseInt(o[1],16);return[24,16,8,0].map(i=>s>>>i&255).join(".")}}return n}catch{return}}function Ct(e){if(e==="")return"";let t="publicBaseUrl must be an HTTP(S) origin with no path, query, fragment, or credentials";if(typeof e!="string")throw new Error(t);let n;try{n=new URL(e.trim())}catch{throw new Error(t)}if(n.protocol!=="https:"&&n.protocol!=="http:"||!/^https?:\/\/[^/?#]+\/?$/i.test(e.trim())||n.pathname!=="/"||n.search||n.hash||n.username||n.password||e.includes("?")||e.includes("#"))throw new Error(t);return n.origin}function Vn(e){ro=Bd(e)}function xs(e){if(typeof e!="string"||e.length===0||e.length>260)return!1;let t,n,r=/^(\[[^\]]*\])(?::(\d+))?$/.exec(e);if(r){if(t=r[1],n=r[2],!zd.test(t))return!1;try{new URL(`http://${t}/`)}catch{return!1}}else{let o=/^([^:]+)(?::(\d+))?$/.exec(e);if(!o||(t=o[1],n=o[2],t.length>253)||!Wd.test(t)&&!qd.test(t))return!1}if(n!==void 0){if(!/^\d{1,5}$/.test(n))return!1;let o=Number(n);if(o<1||o>65535)return!1}return!0}function Ts(e){let t=e?.trim().toLowerCase();return t==="http"||t==="https"?t:void 0}function Gd(e){let t=e.socket;return t?.encrypted?"https":t?"http":Ts(e.protocol)??"http"}function oo(e){if(ro)return ro;let t=Ud(e),n=t?ks(e,"x-forwarded-host"):void 0,r=e.get("host")?.trim(),o=xs(n)?n:xs(r)?r:void 0;if(!o)return;let s=(t?Ts(ks(e,"x-forwarded-proto")):void 0)??Gd(e);try{return new URL(`${s}://${o}`).origin}catch{return}}function vn(e,t){let n=oo(e);return n?`${n}${t}`:t}function Ps(e,t,n){return vn(e,Hd(t,n))}function so(e){return e.replace(/\/mcp\/a\/[^/?#]+/gi,"/mcp/a/<redacted>")}var no,It,Ss,ro,As,qd,Wd,zd,ie=U(()=>{"use strict";no=require("net");_e();It=8192,Ss=1800*1e3;a(Hd,"mcpAccessLinkPath");a(Ud,"isTrustProxyEnabled");a(ks,"firstForwardedValue");a(Bd,"normalizePublicBaseUrl");a(at,"canonicalizeIp");a(Ct,"validateSettingsPublicBaseUrl");a(Vn,"bindPublicBaseUrl");As="[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?",qd=new RegExp(`^${As}(?:\\.${As})*$`),Wd=/^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/,zd=/^\[[0-9A-Fa-f:.]+\]$/;a(xs,"isValidHostHeader");a(Ts,"validScheme");a(Gd,"connectionScheme");a(oo,"requestPublicOrigin");a(vn,"absolutizePath");a(Ps,"buildAccessLinkUrl");a(so,"redactAccessLinkTokenInPath")});function Qd(e){if(!e||e.trim().length===0)return{kind:"missing"};let t=e.trim();if(/^Basic\s+/i.test(t))return{kind:"basic"};let n=/^Bearer\s+(\S+)$/i.exec(t);return n?.[1]?{kind:"bearer",token:n[1]}:{kind:"other"}}function io(e,t,n,r){e.setHeader("WWW-Authenticate",Y),e.status(401).json({error:t,code:n,...r?{hint:r}:{}})}function Zd(e){let t=e??"";for(;t.endsWith("/");)t=t.slice(0,-1);return`${t}/ai-access#links`}function el(e,t,n,r){let o=vn(e,Zd(t));return{error:Jd,code:Vd,...n?{expiredAt:n}:{},accessLinksUrl:o,hint:r??`Ask the user to create a new access link in Node-RED on the /ai-access dashboard \u2192 Access tokens (${o}).`}}function wn(e,t,n,r,o){t.setHeader("WWW-Authenticate",Y),t.status(401).json(el(e,n,r,o))}function tl(e){if(e.length<24||e.startsWith("aia_")&&e.length<40)return"The token may be truncated \u2014 ask the user for the full access-link token."}function Xn(e){return e==="get_link"?"Please open Node-RED \u2192 /ai-access \u2192 Access tokens and create a get_link access URL, then send me that /mcp/a/<token> link.":`Please open Node-RED \u2192 /ai-access \u2192 Access tokens and create a kind=${e} token, then send me the Bearer token (not a get_link path URL).`}function rl(e,t){let n=nl;return e==="get_link"?`This URL requires a get_link token. This token is kind=${t} \u2014 ${n} for a get_link access URL (/mcp/a/<token>/\u2026). What to tell the user: ${Xn("get_link")}`:e==="rest"?`This REST endpoint requires a rest token. This token is kind=${t} \u2014 ${n}. What to tell the user: ${Xn("rest")}`:`This MCP endpoint requires an mcp token. This token is kind=${t} \u2014 ${n}. What to tell the user: ${Xn("mcp")}`}function bn(e,t,n){e.setHeader("WWW-Authenticate",Y),e.status(403).json({error:`Access link kind mismatch: expected ${t}, got ${n}`,code:Yd,expectedKind:t,actualKind:n,hint:rl(t,n),tellUser:Xn(t)})}function Nt(e,t,n){return(r,o,s)=>{if(!ge(e.settings)){o.status(403).json({error:"AI Access features are disabled. Enable adminAuth, or set aiAccess.allowWithoutAdminAuth to true for local-only use.",code:"ai_features_disabled"});return}let i=Qd(r.headers.authorization);if(i.kind==="basic"){io(o,Kd,Xd);return}if(i.kind==="missing"||i.kind==="other"){io(o,le,Le);return}let d=t.validateResult(i.token);if(d.status==="expired"){wn(r,o,e.settings.httpAdminRoot,d.expiresAt);return}if(d.status!=="active"){io(o,st,yn,tl(i.token));return}if(d.link.kind!==n){bn(o,n,d.link.kind);return}r.accessLink=d.link,s()}}var st,Jd,le,Kd,Y,Le,yn,Vd,Xd,Yd,nl,it=U(()=>{"use strict";ie();ye();st="Access link is invalid, revoked, or no longer available (it may have expired, or Node-RED may have restarted). Ask the user for a new access link.",Jd="Access link token has expired. Ask the user for a new access link.",le="Access link Bearer token is missing. Ask the user for a new access link and send Authorization: Bearer <token>.",Kd="Authorization must use the Bearer scheme (Authorization: Bearer <access-link-token>), not Basic. Ask the user for a new access link.",Y='Bearer realm="node-red-ai-access", charset="UTF-8"',Le="access_link_missing",yn="access_link_invalid",Vd="token_expired",Xd="bearer_scheme_required",Yd="wrong_token_kind";a(Qd,"parseAuthorizationHeader");a(io,"sendUnauthorized");a(Zd,"accessLinksPagePath");a(el,"tokenExpiredBody");a(wn,"sendTokenExpired");a(tl,"truncatedBearerHint");nl="ask the owner: Node-RED /ai-access \u2192 Access tokens \u2192 create kind=rest/mcp (or get_link)";a(Xn,"whatToTellUserWrongTokenKind");a(rl,"wrongTokenKindHint");a(bn,"sendWrongTokenKind");a(Nt,"requireAccessLink")});var J,z,be,Ce,Qn,ve=U(()=>{"use strict";J=class extends Error{static{a(this,"FlowRevisionConflictError")}code="version_mismatch";statusCode=409;expectedRev;currentRev;context;constructor(t,n,r="updateFlow"){super(`Flow revision conflict in ${r}: expectedRev '${t}' does not match current rev '${n}'. The flows changed since you last read them \u2014 re-read flows, re-apply your change, and retry.`),this.name="FlowRevisionConflictError",this.expectedRev=t,this.currentRev=n,this.context=r}},z=class extends Error{static{a(this,"FlowNotFoundError")}code="flow_not_found";statusCode=404;flowId;constructor(t){super(`Flow '${t}' was not found`),this.name="FlowNotFoundError",this.flowId=t}},be=class extends Error{static{a(this,"AdapterUnavailableError")}code="adapter_unavailable";statusCode=502;constructor(t){super(t),this.name="AdapterUnavailableError"}},Ce=class extends Error{static{a(this,"PluginMisconfiguredError")}code="plugin_misconfigured";statusCode=502;constructor(t){super(t),this.name="PluginMisconfiguredError"}},Qn="Node-RED AI Access could not use the in-process flow runtime and the HTTP Admin API fallback returned Unauthorized. The plugin is misconfigured \u2014 tell the admin to ensure the plugin loads inside Node-RED so RED.runtime.flows is available (do not rely on unauthenticated localhost HTTP when adminAuth is enabled)."});function Ee(e,t){if(!e||!ol.test(e)||e.includes(".."))throw new Error(`Invalid ${t}`);return e}function Zn(e){if(!e||!sl.test(e)||e.includes(".."))throw new Error("Invalid proposal id");return e}function ao(e,t){let n=(0,dt.basename)(t);if(!n||n!==t||n.includes(".."))throw new Error("Invalid file name");let r=(0,dt.resolve)(e),o=(0,dt.resolve)(r,n);if(o!==r&&!o.startsWith(r+dt.sep))throw new Error("Path escapes storage directory");return o}function Rs(e,t=1e5){if(typeof e=="string")return e.replace(/\0/g,"").slice(0,t)}function Ft(e){let t=e&&typeof e=="object"&&!Array.isArray(e)?e:{},n={};for(let o of il){if(!(o in t))continue;let s=t[o];if(o==="disabled"&&typeof s=="boolean"){n[o]=s;continue}if(o==="env"&&Array.isArray(s)){let d=JSON.stringify(s);d.length<=5e5&&(n[o]=JSON.parse(d));continue}let i=Rs(s,1e4);i!==void 0&&(n[o]=i)}for(let o of["nodes","configs"]){let s=t[o];if(!Array.isArray(s))continue;let i=JSON.stringify(s),d=Buffer.byteLength(i,"utf8");if(d>j)throw new X(`flow.${o}`,d,j);n[o]=JSON.parse(i)}let r=Buffer.byteLength(JSON.stringify(n),"utf8");if(r>j)throw new X("flow",r,j);return n}function _s(e){let t={};for(let n of al){if(!(n in e))continue;let r=e[n];if(dl.has(n)){if(typeof r=="number"&&Number.isFinite(r))t[n]=r;else if(typeof r=="string"&&r.trim()!==""){let s=Number(r);Number.isFinite(s)&&(t[n]=s)}continue}let o=Rs(r,2e3);o!==void 0&&(t[n]=o)}return t}var dt,ol,sl,il,al,dl,er=U(()=>{"use strict";dt=require("path");G();ol=/^[A-Za-z0-9_.-]+$/,sl=/^prop-[A-Za-z0-9_-]+$/,il=["id","type","label","disabled","info","env"],al=["proposalId","flowId","expectedRev","resultRev","reason","code","currentRev","title","displayName","flowLabel","linkId","label","expiresAt","createdAt","ttlSeconds","kind"],dl=new Set(["ttlSeconds"]);a(Ee,"assertSafeSegment");a(Zn,"assertSafeProposalId");a(ao,"resolveUnderDir");a(Rs,"sanitizeString");a(Ft,"sanitizeFlowTab");a(_s,"sanitizeAuditDetails")});function lo(e,t){return`oauth-${(0,nr.createHash)("sha256").update(`${e}\0${t}`).digest("hex")}`}function lt(e){return e.kind==="oauth"?lo(e.clientId,e.user):e.linkId}function ll(e){return e==="pending"||e==="approved"||e==="rejected"||e==="failed"||e==="withdrawn"||e==="expired"||e==="superseded"}function Ls(e){if(!Array.isArray(e)||e.length===0)return;let t=[];for(let n of e.slice(0,cl)){if(!n||typeof n!="object"||Array.isArray(n))continue;let r=n;if(typeof r.code!="string"||!r.code||typeof r.error!="string"||!r.error)continue;let o={code:r.code.replace(/\0/g,"").slice(0,80),error:r.error.replace(/\0/g,"").slice(0,500)};typeof r.nodeId=="string"&&r.nodeId&&(o.nodeId=r.nodeId.replace(/\0/g,"").slice(0,200)),typeof r.type=="string"&&r.type&&(o.type=r.type.replace(/\0/g,"").slice(0,200)),typeof r.property=="string"&&r.property&&(o.property=r.property.replace(/\0/g,"").slice(0,200)),typeof r.configType=="string"&&r.configType&&(o.configType=r.configType.replace(/\0/g,"").slice(0,200)),Array.isArray(r.candidates)&&(o.candidates=r.candidates.filter(s=>typeof s=="string"&&s.length>0).slice(0,20).map(s=>s.replace(/\0/g,"").slice(0,200))),t.push(o)}return t.length>0?t:void 0}var nr,ze,Cs,ne,te,Ot,kn,cl,tr,Ge=U(()=>{"use strict";nr=require("crypto"),ze=require("fs"),Cs=require("path");er();G();a(lo,"oauthOwnerId");a(lt,"proposalOwnerId");ne=class extends Error{static{a(this,"ProposalNotFoundError")}code="proposal_not_found";statusCode=404;constructor(t){super(`Proposal '${t}' was not found`),this.name="ProposalNotFoundError"}},te=class extends Error{static{a(this,"ProposalNotPendingError")}code="proposal_not_pending";statusCode=409;constructor(t,n){super(`Proposal '${t}' is '${n}', not pending`),this.name="ProposalNotPendingError"}},Ot=class extends Error{static{a(this,"ProposalNotOwnedError")}code="proposal_not_owned";statusCode=403;constructor(t){super(`Proposal '${t}' was not created by this access link`),this.name="ProposalNotOwnedError"}},kn=class extends Error{static{a(this,"ProposalConcurrentApproveError")}code="proposal_approve_in_progress";statusCode=409;constructor(t){super(`Proposal '${t}' is already being approved`),this.name="ProposalConcurrentApproveError"}};a(ll,"isProposalStatus");cl=50;a(Ls,"sanitizeProposalWarnings");tr=class{constructor(t,n={}){this.paths=t;this.now=n.now??(()=>Date.now()),this.randomId=n.randomId??(()=>(0,nr.randomBytes)(12).toString("hex")),this.maxProposalBytes=n.maxProposalBytes??j}static{a(this,"ProposalStore")}inFlight=new Set;now;randomId;maxProposalBytes;fileFor(t){let n;try{n=Zn(t)}catch{throw new ne(t)}return ao(this.paths.proposals,`${n}.json`)}toDiskRecord(t){let n=Zn(t.id),r=Ee(String(t.expectedRev).slice(0,200),"expectedRev"),o=Ee(String(t.payload.flowId).slice(0,200),"flowId"),s=Ft(t.payload.flow),i=t.baseFlow!==void 0?Ft(t.baseFlow):void 0,d=t.createdBy.kind==="oauth"?{kind:"oauth",clientId:Ee(String(t.createdBy.clientId).slice(0,200),"clientId"),user:String(t.createdBy.user).replace(/\0/g,"").slice(0,200)}:{kind:"access-link",linkId:Ee(String(t.createdBy.linkId).slice(0,200),"linkId"),...typeof t.createdBy.label=="string"?{label:t.createdBy.label.replace(/\0/g,"").slice(0,200)}:{}},l={id:n,type:"replace_flow",status:ll(t.status)?t.status:"pending",expectedRev:r,payload:{flowId:o,flow:s},createdBy:d,createdAt:String(t.createdAt).slice(0,64),updatedAt:String(t.updatedAt).slice(0,64)};i!==void 0&&(l.baseFlow=i),typeof t.title=="string"&&t.title.length>0&&(l.title=t.title.replace(/\0/g,"").slice(0,80).trim(),l.title||delete l.title);let c=Ls(t.warnings);if(c&&(l.warnings=c),t.decidedBy)if(t.decidedBy.kind==="access-link"){let u={kind:"access-link",linkId:Ee(String(t.decidedBy.linkId).slice(0,200),"linkId")};typeof t.decidedBy.label=="string"&&(u.label=t.decidedBy.label.replace(/\0/g,"").slice(0,200)),l.decidedBy=u}else{let u={kind:"admin"};typeof t.decidedBy.username=="string"&&(u.username=t.decidedBy.username.replace(/\0/g,"").slice(0,200)),l.decidedBy=u}return typeof t.reason=="string"&&(l.reason=t.reason.replace(/\0/g,"").slice(0,2e3)),typeof t.resultRev=="string"&&(l.resultRev=Ee(t.resultRev.slice(0,200),"resultRev")),l}writeAtomic(t,n){let r=this.fileFor(t),o=this.toDiskRecord(n),s=Be(o,this.maxProposalBytes,{kind:"proposal",pretty:!0});Rt(this.paths.root,r,s)}create(t){let n=new Date(this.now()).toISOString(),r=Zn(`prop-${this.randomId()}`),o={id:r,type:"replace_flow",status:"pending",expectedRev:t.expectedRev,payload:{flowId:t.payload.flowId,flow:t.payload.flow},baseFlow:t.baseFlow,title:t.title,createdBy:t.createdBy,createdAt:n,updatedAt:n},s=Ls(t.warnings);return s&&(o.warnings=s),this.writeAtomic(r,o),this.get(r)}get(t){let n=this.fileFor(t);if(!(0,ze.existsSync)(n))throw new ne(t);return JSON.parse((0,ze.readFileSync)(n,"utf8"))}list(t){if(!(0,ze.existsSync)(this.paths.proposals))return[];let n=(0,ze.readdirSync)(this.paths.proposals).filter(o=>o.endsWith(".json")),r=[];for(let o of n){let s=(0,Cs.basename)(o);if(!(s!==o||s.includes("..")))try{let i=JSON.parse((0,ze.readFileSync)(ao(this.paths.proposals,s),"utf8"));if(t?.linkId&&lt(i.createdBy)!==t.linkId||t?.status&&i.status!==t.status||t?.flowId&&i.payload.flowId!==t.flowId)continue;r.push(i)}catch{}}return r.sort((o,s)=>o.createdAt<s.createdAt?1:-1)}beginApprove(t){if(this.inFlight.has(t))throw new kn(t);let n=this.get(t);if(n.status!=="pending")throw new te(t,n.status);return this.inFlight.add(t),n}releaseLock(t){this.inFlight.delete(t)}markApproved(t,n,r){let o=this.get(t);return o.status="approved",o.decidedBy=n,o.resultRev=r,o.updatedAt=new Date(this.now()).toISOString(),this.writeAtomic(t,o),this.get(t)}markRejected(t,n,r){let o=this.get(t);return o.status="rejected",n&&(o.decidedBy=n),o.reason=r,o.updatedAt=new Date(this.now()).toISOString(),this.writeAtomic(t,o),this.get(t)}markFailed(t,n,r){let o=this.get(t);return o.status="failed",n&&(o.decidedBy=n),o.reason=r,o.updatedAt=new Date(this.now()).toISOString(),this.writeAtomic(t,o),this.get(t)}markWithdrawn(t,n,r){let o=this.get(t);if(o.status!=="pending")throw new te(t,o.status);return o.status="withdrawn",o.decidedBy=n,o.reason=r,o.updatedAt=new Date(this.now()).toISOString(),this.writeAtomic(t,o),this.get(t)}markClosed(t,n,r,o){let s=this.get(t);if(s.status!=="pending")throw new te(t,s.status);return s.status=n,o&&(s.decidedBy=o),s.reason=r,s.updatedAt=new Date(this.now()).toISOString(),this.writeAtomic(t,s),this.get(t)}}});function Es(e,t){if(!e||typeof e!="object"||Array.isArray(e))throw new Error(`${t} must be a node object`);let n=e;if(typeof n.id!="string"||n.id.length===0)throw new Error(`${t} needs a non-empty string id`);if(typeof n.type!="string"||n.type.length===0)throw new Error(`${t} '${n.id}' needs a non-empty string type`);return n}function rr(e,t,n){let r=e.find(d=>d.id===t);if(!r||r.type!=="tab"&&r.type!=="subflow")throw new z(t);let o={...r,id:t,type:r.type};delete o.nodes,delete o.configs,delete o.subflows,n.label!==void 0&&(o.label=n.label),n.disabled!==void 0&&(o.disabled=n.disabled),n.info!==void 0&&(o.info=n.info),n.env!==void 0&&(o.env=n.env);let s=[];for(let[d,l]of(n.nodes??[]).entries())s.push({...Es(l,`flowData.nodes[${d}]`),z:t});for(let[d,l]of(n.configs??[]).entries())s.push({...Es(l,`flowData.configs[${d}]`),z:t});return[...e.filter(d=>d.id!==t&&d.z!==t),o,...s]}function An(e,t){if(!e||typeof e!="object"||Array.isArray(e))throw new Error(`Expected ${t} to be an object with rev and flows`);let n=e;if(typeof n.rev!="string"||n.rev.length===0)throw new Error(`Expected ${t}.rev to be a non-empty string`);if(!Array.isArray(n.flows))throw new Error(`Expected ${t}.flows to be an array`);return{rev:n.rev,flows:n.flows}}function $t(e,t){if(Array.isArray(t)){let d=t.find(u=>u&&typeof u=="object"&&u.id===e&&(u.type==="tab"||u.type==="subflow"));if(!d)throw new z(e);let l=t.filter(u=>u&&typeof u=="object"&&u.id!==e&&u.z===e),c={id:e,type:d.type,nodes:l};return typeof d.label=="string"&&(c.label=d.label),typeof d.disabled=="boolean"&&(c.disabled=d.disabled),typeof d.info=="string"&&(c.info=d.info),c}if(!t||typeof t!="object")throw new z(e);let n=t,r=typeof n.id=="string"&&n.id.length>0?n.id:e,o=typeof n.type=="string"&&n.type.length>0?n.type:"tab",s=Array.isArray(n.nodes)?n.nodes:[],i={...n,id:r,type:o,nodes:s};return Array.isArray(n.configs)&&(i.configs=n.configs),i}var or=U(()=>{"use strict";ve();a(Es,"asFlowNode");a(rr,"replaceFlowTabInFlatFlows");a(An,"assertFlowsSnapshot");a($t,"normalizeFlowTab")});function fo(e){return e?ul.has(e)?!0:e.startsWith("subflow:"):!1}function Je(e,t="node.type"){if(e==null||e==="")throw new y(`${t} is required`,"missing_node_type");if(typeof e!="string")throw new y(`${t} must be a non-empty string (got ${typeof e})`,"invalid_node_type",{details:{type:e,got:typeof e}});if(!e.trim())throw new y(`${t} is required`,"missing_node_type");return e}function wl(e,t){if(typeof e!="string"||typeof t!="string")return Number.MAX_SAFE_INTEGER;let n=e.toLowerCase(),r=t.toLowerCase();if(n===r)return 0;let o=n.length,s=r.length;if(o===0)return s;if(s===0)return o;let i=new Array(s+1),d=new Array(s+1);for(let l=0;l<=s;l++)i[l]=l;for(let l=1;l<=o;l++){d[0]=l;let c=n.charAt(l-1);for(let u=1;u<=s;u++){let p=c===r.charAt(u-1)?0:1;d[u]=Math.min((d[u-1]??0)+1,(i[u]??0)+1,(i[u-1]??0)+p)}for(let u=0;u<=s;u++)i[u]=d[u]??0}return i[s]??s}function bl(e,t){let n=e.toLowerCase(),r=t.toLowerCase(),o=Math.min(n.length,r.length),s=0;for(;s<o&&n.charAt(s)===r.charAt(s);)s+=1;return s}function kl(e){return Math.max(2,Math.floor(e.length/3))}function mo(e,t,n=5){if(typeof e!="string"||!e)return[];let r=kl(e);return t.filter(s=>typeof s=="string"&&s!==e&&!vl.has(s)&&!s.startsWith("_")&&!s.startsWith("subflow:")).map(s=>({c:s,dist:wl(e,s),prefix:bl(e,s)})).filter(s=>s.dist<=r).sort((s,i)=>s.dist-i.dist||i.prefix-s.prefix||s.c.localeCompare(i.c)).slice(0,n).map(s=>s.c)}function Al(e){return e.endsWith("/")?e.slice(0,-1):e}function Ds(){if(typeof require=="function")return require;let e=(0,eval)("import.meta.url");return(0,Ms.createRequire)(e)}function xl(){try{let t=Ds()("@node-red/runtime/lib/settings");return typeof t.available=="function"&&t.available()===!0}catch{return!1}}function js(e){let t=e.types,n=Array.isArray(t)?t.filter(i=>typeof i=="string"&&i.length>0):[],r="";if(typeof e.module=="string"&&e.module)r=e.module;else if(typeof e.name=="string"&&e.name)r=e.name;else if(typeof e.id=="string"){let i=String(e.id).split("/")[0];r=i&&i.length>0?i:String(e.id)}let o=typeof e.id=="string"&&e.id?e.id:n.length===1&&r?`${r}/${n[0]}`:r;if(!o||n.length===0)return null;let s={id:o,module:r.length>0?r:o,types:n};return typeof e.version=="string"&&(s.version=e.version),typeof e.enabled=="boolean"&&(s.enabled=e.enabled),typeof e.name=="string"&&(s.name=e.name),s}function ho(e=yl,t={}){let n=t.module??"node-red",r=t.version??"4.0.0",o=[...e],s=t.configs??{};return{listNodeSets(){return Promise.resolve([{id:n,module:n,version:r,types:o,enabled:!0,name:n}])},getNodeConfigHtml(i){if(s[i])return Promise.resolve(s[i]??null);let d=i.includes("/")?i.slice(i.lastIndexOf("/")+1):i;return s[d]?Promise.resolve(s[d]??null):Promise.resolve(null)}}}function Hs(e={}){if(xl())try{let n=Ds()("@node-red/runtime");if(n.nodes&&typeof n.nodes.getNodeList=="function")return new po(n.nodes)}catch{}if(e.baseUrl)return new uo({baseUrl:e.baseUrl,...e.accessToken?{accessToken:e.accessToken}:{},...e.fetch?{fetch:e.fetch}:{}});throw new Error("No usable NodeTypesAdapter: runtime.nodes unavailable and no HTTP baseUrl")}async function go(e){let t=await e.listNodeSets(),n=new Map;for(let r of t)for(let o of r.types){if(n.has(o))continue;let s={type:o,module:r.module,setId:r.id};typeof r.enabled=="boolean"&&(s.enabled=r.enabled),r.version&&(s.version=r.version),n.set(o,s)}return{byType:n,allNames:[...n.keys()]}}function Sl(e,t){let n=t.toLowerCase();return[e.type,e.module,e.category??"",e.description??""].join(`
4
+ `).toLowerCase().includes(n)}async function ir(e,t={}){let n=t.limit??pl,{byType:r}=await go(e),o=[...r.values()].sort((u,p)=>u.type.localeCompare(p.type)),s=t.q?.trim();s&&(o=o.filter(u=>Sl(u,s)));let i=o.length,d=i>n,c={types:o.slice(0,n),total:i,truncated:d};return s&&(c.q=s),c}function Tl(e,t){let n=`data-help-name="${t}"`,r=`data-help-name='${t}'`,o=e.toLowerCase(),s=0;for(;s<e.length;){let i=e.indexOf(n,s),d=e.indexOf(r,s),l=-1;if(i>=0&&(d<0||i<d)?l=i:d>=0&&(l=d),l<0)return null;let c=o.lastIndexOf("<script",l);if(c<0||c<s){s=l+1;continue}let u=e.indexOf(">",l);if(u<0)return null;let p=o.indexOf("</script",u+1);if(p<0)return null;let f=p+8;for(;f<e.length&&/\s/.test(e.charAt(f));)f+=1;if(e.charAt(f)!==">"){s=p+1;continue}return e.slice(u+1,p)}return null}function Ns(e,t){let n=`<${t}`,r=`</${t}`,o="",s=0,i=e.toLowerCase();for(;s<e.length;){let d=i.indexOf(n,s);if(d<0){o+=e.slice(s);break}o+=e.slice(s,d);let l=e.indexOf(">",d);if(l<0)break;let c=i.indexOf(r,l+1);if(c<0)break;let u=c+r.length;for(;u<e.length&&/\s/.test(e.charAt(u));)u+=1;e.charAt(u)===">"?(s=u+1,o+=" "):s=c+r.length}return o}function Pl(e,t=2e3){let n=Ns(e,"script");return n=Ns(n,"style"),n=n.replace(/<[^>]+>/g," ").replace(/&nbsp;/gi," ").replace(/[#*_`]+/g," ").replace(/\s+/g," ").trim(),n.length>t&&(n=`${n.slice(0,t-1)}\u2026`),n}function Rl(e,t){try{let n=/RED\.nodes\.registerType\s*\(\s*['"]([^'"]+)['"]\s*,\s*\{/.exec(e);if(!n?.[1])return null;let r=n[1];if(t&&r!==t){let o=/RED\.nodes\.registerType\s*\(\s*['"]([^'"]+)['"]\s*,\s*\{/g,s,i=null,d=-1;for(;(s=o.exec(e))!==null;)if(s[1]===t){i=s[1],d=s.index;break}return!i||d<0?null:Fs(e,d,i)}return Fs(e,n.index,r)}catch{return null}}function Fs(e,t,n){let r=e.slice(t),o=r.indexOf("{");if(o<0)return{type:n};let s=xn(r.slice(o));if(!s)return{type:n};let i=Os(s,"category"),d=Os(s,"color"),l=$s(s,"inputs"),c=$s(s,"outputs"),u=Ll(s),p=[];if(u){for(let g of Object.values(u))if(g&&typeof g=="object"&&!Array.isArray(g)){let x=g.type;typeof x=="string"&&x.length>0&&p.push(x)}}let f,m=Tl(e,n);m&&(f=Pl(m));let h={type:n};return i&&(h.category=i),d&&(h.color=d),l!==void 0&&(h.inputs=l),c!==void 0&&(h.outputs=c),u&&(h.defaults=u),p.length>0&&(h.requiredConfigNodeTypes=[...new Set(p)]),f&&(h.help=f),h}function _l(e){let t=e.trim();if(!t)return{};if(!/\n(?=#{1,3}\s*(inputs?|outputs?)\b)/i.test(`
5
+ ${t}`))return{text:t,summary:t};let r=t.split(/\n(?=#{1,3}\s+)/),o="",s,i;for(let l of r){let c=l.trim();if(!c)continue;let u=/^(#{1,3})\s*(inputs?|outputs?)\b[\s:]*\n?([\s\S]*)$/i.exec(c);if(u){let p=(u[2]??"").toLowerCase(),f=(u[3]??"").trim();p.startsWith("input")?s=f||c:i=f||c}else o||(o=c.replace(/^#{1,3}\s*/,"").trim())}let d={};return o&&(d.summary=o),s&&(d.inputs=s),i&&(d.outputs=i),!d.summary&&!d.inputs&&!d.outputs&&(d.text=t),d}function xn(e){if(!e.startsWith("{"))return null;let t=0,n=null,r=!1;for(let o=0;o<e.length;o++){let s=e[o];if(n){if(r){r=!1;continue}if(s==="\\"){r=!0;continue}s===n&&(n=null);continue}if(s==='"'||s==="'"||s==="`"){n=s;continue}if(s==="{")t+=1;else if(s==="}"&&(t-=1,t===0))return e.slice(0,o+1)}return null}function Os(e,t){let n=new RegExp(`\\b${t}\\s*:\\s*['"]([^'"]+)['"]`);return e.match(n)?.[1]}function $s(e,t){let n=new RegExp(`\\b${t}\\s*:\\s*(-?\\d+(?:\\.\\d+)?)`),r=e.match(n);if(!r?.[1])return;let o=Number(r[1]);return Number.isFinite(o)?o:void 0}function Ll(e){let t=e.search(/\bdefaults\s*:/);if(t<0)return;let n=e.slice(t),r=n.indexOf("{");if(r<0)return;let o=xn(n.slice(r));if(!o)return;let s={},i=/([a-zA-Z_$][\w$]*)\s*:\s*\{/g,d;for(;(d=i.exec(o))!==null;){let l=d[1];if(!l||l==="defaults")continue;let c=xn(o.slice(d.index+d[0].length-1));if(!c)continue;let u={},p=/\btype\s*:\s*['"]([^'"]+)['"]/.exec(c);p?.[1]&&(u.type=p[1]),/\brequired\s*:\s*true\b/.test(c)&&(u.required=!0);let f=/\bvalue\s*:\s*/.exec(c);if(f){let m=c.slice(f.index+f[0].length).trimStart();u.value=Il(m)}s[l]=u}return Object.keys(s).length>0?s:void 0}function Cl(e,t){let n=e.charAt(t);if(n==="u"&&/^[0-9a-fA-F]{4}/.test(e.slice(t+1,t+5)))return{char:String.fromCharCode(Number.parseInt(e.slice(t+1,t+5),16)),next:t+5};if(n==="x"&&/^[0-9a-fA-F]{2}/.test(e.slice(t+1,t+3)))return{char:String.fromCharCode(Number.parseInt(e.slice(t+1,t+3),16)),next:t+3};let r;switch(n){case"n":r=`
6
+ `;break;case"r":r="\r";break;case"t":r=" ";break;case"b":r="\b";break;case"f":r="\f";break;case"v":r="\v";break;case"0":r="\0";break;case"\\":r="\\";break;case"'":r="'";break;case'"':r='"';break;case"/":r="/";break;default:r=n;break}return{char:r,next:t+1}}function El(e){return e==="_DEFAULT_"?"":e}function sr(e){let t=e.trimStart();if(t.startsWith("'")||t.startsWith('"')){let r=t[0],o=1,s="";for(;o<t.length;){let i=t[o];if(i==="\\"&&o+1<t.length){let d=Cl(t,o+1);s+=d.char,o=d.next;continue}if(i===r)break;s+=i,o+=1}return El(s)}if(t.startsWith("[")){let r=Us(t);if(!r)return;let o=r.slice(1,-1).trim();if(!o)return[];let s=[],i=0;for(;i<o.length;){for(;i<o.length&&/[\s,]/.test(o.charAt(i));)i+=1;if(i>=o.length)break;let d=o.slice(i),l=sr(d);s.push(l);let c=co(d);i+=c>0?c:1}return s}if(t.startsWith("{")){let r=xn(t);if(!r)return;let o=r.slice(1,-1);if(!o.trim())return{};let s={},i=0;for(;i<o.length;){for(;i<o.length&&/[\s,]/.test(o.charAt(i));)i+=1;if(i>=o.length)break;let d;if(o[i]==="'"||o[i]==='"'){let c=sr(o.slice(i));d=typeof c=="string"?c:"",i+=co(o.slice(i))}else{let c=/^[a-zA-Z_$][\w$]*/.exec(o.slice(i));if(!c)break;d=c[0],i+=d.length}for(;i<o.length&&/\s/.test(o.charAt(i));)i+=1;if(o[i]!==":")break;for(i+=1;i<o.length&&/\s/.test(o.charAt(i));)i+=1;let l=o.slice(i);s[d]=sr(l),i+=co(l)}return s}let n=/^-?\d+(?:\.\d+)?/.exec(t);if(n)return Number(n[0]);if(t.startsWith("true"))return!0;if(t.startsWith("false"))return!1;if(t.startsWith("null"))return null}function co(e){let t=e.trimStart(),n=e.length-t.length;if(t.startsWith("'")||t.startsWith('"')){let o=t[0],s=1;for(;s<t.length;){if(t[s]==="\\"&&s+1<t.length){s+=2;continue}if(t[s]===o)return n+s+1;s+=1}return n+t.length}if(t.startsWith("[")){let o=Us(t);return n+(o?.length??1)}if(t.startsWith("{")){let o=xn(t);return n+(o?.length??1)}let r=/^(?:-?\d+(?:\.\d+)?|true|false|null)/.exec(t);return r?n+r[0].length:n+1}function Us(e){if(!e.startsWith("["))return null;let t=0,n=null,r=!1;for(let o=0;o<e.length;o++){let s=e[o];if(n){if(r){r=!1;continue}if(s==="\\"){r=!0;continue}s===n&&(n=null);continue}if(s==="'"||s==='"'){n=s;continue}if(s==="[")t+=1;else if(s==="]"&&(t-=1,t===0))return e.slice(0,o+1)}return null}function Il(e){return sr(e)}function Nl(e,t){for(let n of e)if(n.types.includes(t)){let r=n.id.includes("/")||n.types.length===1?n.id:`${n.module}/${t}`;return{set:n,setId:r}}return null}function yo(e,t){let n=e.toLowerCase();return t.includes(e)?e:t.find(r=>r.toLowerCase()===n)}async function ct(e,t,n={}){if(!t||typeof t!="string")throw new y("Missing node type","missing_node_type");let r=n.includeHelp!==!1;if(fo(t)){let l={type:t,module:"node-red",category:"config",inputs:0,outputs:0,defaults:null};return r&&(l.help={summary:"Built-in structural Node-RED type (not a palette node)."}),l}let o=await e.listNodeSets(),s=o.flatMap(l=>l.types),i=Nl(o,t);if(!i){let l=yo(t,s);throw l&&l!==t?new y(`Node type '${t}' was not found \u2014 did you mean '${l}'? Types are case-sensitive.`,"node_type_not_found",{statusCode:404,details:{type:t,didYouMean:l,caseSensitive:!0,nearest:[l]}}):new y(`Node type '${t}' was not found`,"node_type_not_found",{statusCode:404,details:{type:t,nearest:mo(t,s,5)}})}let d={type:t,module:i.set.module,setId:i.setId,inputs:1,outputs:1,defaults:null};if(i.set.version&&(d.version=i.set.version),typeof i.set.enabled=="boolean"&&(d.enabled=i.set.enabled),typeof e.getNodeConfigHtml=="function")try{let l=await e.getNodeConfigHtml(i.setId)??await e.getNodeConfigHtml(`${i.set.module}/${t}`)??await e.getNodeConfigHtml(t);if(l){let c=Rl(l,t);c&&(c.category&&(d.category=c.category),c.color&&(d.color=c.color),d.inputs=c.inputs??d.inputs,d.outputs=c.outputs??d.outputs,c.defaults&&Object.keys(c.defaults).length>0?d.defaults=c.defaults:d.defaults=null,c.requiredConfigNodeTypes&&(d.requiredConfigNodeTypes=c.requiredConfigNodeTypes),r&&c.help&&(d.help=_l(c.help)))}}catch{}return d}async function Bs(e,t){let{allNames:n}=await go(e),r=new Set(n),o=new Set,s=a(l=>{let c=Je(l,"node.type");if(o.has(c)||fo(c)){o.add(c);return}if(o.add(c),r.has(c))return;let u=yo(c,n);throw u&&u!==c?new y(`Unknown node type '${c}' \u2014 did you mean '${u}'? Types are case-sensitive.`,"unknown_node_type",{details:{type:c,didYouMean:u,caseSensitive:!0,nearest:[u]}}):new y(`Unknown node type '${c}'. Call node-types / palette search; ask the user to install via Manage palette, then recheck.`,"unknown_node_type",{details:{type:c,nearest:mo(c,n,5)}})},"checkType"),i=a(l=>{if(!l||typeof l!="object"||Array.isArray(l))return;let c=l;typeof c.id=="string"&&c.id.length>0&&s(c.type)},"visitNodeOrConfig");a(l=>{if(!l||typeof l!="object"||Array.isArray(l))return;let c=l;if(c.type!==void 0&&s(c.type),Array.isArray(c.nodes))for(let u of c.nodes)i(u);if(Array.isArray(c.configs))for(let u of c.configs)i(u)},"visitTab")(t)}async function qs(e,t){let n=Je(t,"node.type");if(fo(n))return;let{allNames:r}=await go(e);if(r.includes(n))return;let o=yo(n,r);throw o&&o!==n?new y(`Unknown node type '${n}' \u2014 did you mean '${o}'? Types are case-sensitive.`,"unknown_node_type",{details:{type:n,didYouMean:o,caseSensitive:!0,nearest:[o]}}):new y(`Unknown node type '${n}'. Call node-types / palette search; ask the user to install via Manage palette, then recheck.`,"unknown_node_type",{details:{type:n,nearest:mo(n,r,5)}})}async function Fl(e,t){let n=new Headers({Accept:"application/json"});t&&n.set("If-None-Match",t);let r=await fetch(e,{headers:n}),o=r.headers.get("etag")??void 0;if(r.status===304)return{status:304,...o?{etag:o}:{}};if(!r.ok)return{status:r.status,body:null,...o?{etag:o}:{}};let s=await r.json();return{status:r.status,body:s,...o?{etag:o}:{}}}async function Ol(e,t){let n=Date.now();if(ke&&n-ke.fetchedAt<gl)return ke.body;let r=ke?.etag,o=await e(t,r);if(o.status===304&&ke)return ke={...ke,fetchedAt:n,...o.etag?{etag:o.etag}:{}},ke.body;if(!o.body||!Array.isArray(o.body.modules)){if(ke)return ke.body;throw new y("Node-RED catalogue unavailable","catalogue_unavailable",{statusCode:502})}return ke={fetchedAt:n,body:o.body,...o.etag?{etag:o.etag}:{}},o.body}async function ar(e,t,n={}){let r=t.trim();if(!r)throw new y("q query parameter is required","missing_query");let o=n.limit??fl,s=n.catalogueUrl??hl,i=n.catalogueFetcher??Fl,d=await Ol(i,s),l=Array.isArray(d.modules)?d.modules:[],c=await e.listNodeSets(),u=new Set(c.flatMap(g=>{let x=[g.module,g.id];return g.id.includes("/")&&x.push(g.id.split("/")[0]??g.module),x.map(S=>S.toLowerCase())})),p=r.toLowerCase(),f=[];for(let g of l){let x=g.id.toLowerCase(),S=(g.keywords??[]).map(ot=>String(ot).toLowerCase()),E=(g.description??"").toLowerCase(),I=$l(p,x,S,E);if(I<=0)continue;let N={module:g.id,installed:u.has(x)};typeof g.description=="string"&&(N.description=g.description),typeof g.version=="string"&&(N.version=g.version),typeof g.updated_at=="string"&&(N.updated=g.updated_at),typeof g.downloads=="number"&&(N.downloads=g.downloads),typeof g.url=="string"&&(N.link=g.url),f.push({item:N,score:I})}f.sort((g,x)=>x.score-g.score||(x.item.downloads??0)-(g.item.downloads??0)||g.item.module.localeCompare(x.item.module));let m=f.length,h=m>o;return{q:r,modules:f.slice(0,o).map(g=>g.item),total:m,truncated:h,hint:ml}}function $l(e,t,n,r){if(!e)return 0;if(t===e||t===`node-red-contrib-${e}`||t===`node-red-node-${e}`)return 1e4;let o=0;t.startsWith(`node-red-contrib-${e}`)||t.startsWith(`node-red-node-${e}`)?o+=500:t.startsWith(e)?o+=80:t.includes(e)&&(o+=60);for(let s of n)s===e?o+=50:s.startsWith(e)||e.startsWith(s)?o+=35:s.includes(e)&&(o+=25);return r.includes(e)&&(o+=10),o}var Ms,ul,pl,fl,ml,hl,gl,yl,vl,uo,Is,po,ke,ut=U(()=>{"use strict";Ms=require("module");K();ul=new Set(["tab","subflow","group","junction"]),pl=100,fl=30,ml="Recommend only \u2014 ask the user to install via Manage palette, then recheck node-types. This API never installs. Core Node-RED nodes (module node-red) are not listed in the public catalogue; use node-types for installed core types.",hl="https://catalogue.nodered.org/catalogue.json",gl=3600*1e3,yl=["inject","debug","function","template","comment","change","switch","delay","trigger","link in","link out","link call","mqtt in","mqtt out","http in","http response","http request","websocket in","websocket out","tcp in","tcp out","udp in","udp out","catch","status","complete","join","split","sort","batch","csv","html","json","xml","yaml","file","file in","watch","exec","range","rbe"];a(fo,"isStructuralNodeType");a(Je,"requireNodeTypeName");a(wl,"editDistance");a(bl,"sharedPrefixLength");vl=new Set(["unknown","tab","subflow","group"]);a(kl,"nearestDistanceCutoff");a(mo,"nearestNodeTypes");a(Al,"trimTrailingSlash");a(Ds,"createPluginRequire");a(xl,"isRuntimeSettingsAvailable");a(js,"normalizeNodeSet");a(ho,"createStaticNodeTypesAdapter");uo=class{static{a(this,"HttpNodeTypesAdapter")}baseUrl;accessToken;fetchImpl;constructor(t){this.baseUrl=Al(t.baseUrl),this.accessToken=t.accessToken,this.fetchImpl=t.fetch??globalThis.fetch.bind(globalThis)}headers(t){let n=new Headers;return n.set("Accept",t),this.accessToken&&n.set("Authorization",`Bearer ${this.accessToken}`),n}async listNodeSets(){let t=await this.fetchImpl(`${this.baseUrl}/nodes`,{headers:this.headers("application/json")});if(!t.ok)throw new Error(`Admin API GET /nodes failed (${t.status})`);let n=await t.json();if(!Array.isArray(n))throw new Error("Admin API GET /nodes returned non-array JSON");let r=[];for(let o of n){if(!o||typeof o!="object"||Array.isArray(o))continue;let s=js(o);s&&r.push(s)}return r}async getNodeConfigHtml(t,n){let r=t.split("/").map(i=>encodeURIComponent(i)).join("/"),o=n?`?lang=${encodeURIComponent(n)}`:"",s=await this.fetchImpl(`${this.baseUrl}/nodes/${r}${o}`,{headers:this.headers("text/html")});return s.status===404||!s.ok?null:s.text()}},Is={user:null,req:null},po=class{constructor(t){this.nodes=t}static{a(this,"RedNodeTypesAdapter")}async listNodeSets(){if(typeof this.nodes.getNodeList!="function")return[];let t,n=this.nodes.getNodeList;try{t=await Promise.resolve(n(Is))}catch{try{t=await Promise.resolve(n())}catch{return[]}}if(!Array.isArray(t))return[];let r=[];for(let o of t){if(!o||typeof o!="object"||Array.isArray(o))continue;let s=js(o);s&&r.push(s)}return r}async getNodeConfigHtml(t,n){if(typeof this.nodes.getNodeConfig!="function")return null;try{let r=await Promise.resolve(this.nodes.getNodeConfig({id:t,lang:n??"en-US",...Is}));if(typeof r=="string"&&r.length>0)return r}catch{}try{let r=await Promise.resolve(this.nodes.getNodeConfig(t,n));return typeof r=="string"&&r.length>0?r:null}catch{return null}}};a(Hs,"createNodeTypesAdapter");a(go,"flattenInstalledTypes");a(Sl,"matchesQuery");a(ir,"listNodeTypes");a(Tl,"extractHelpScriptBody");a(Ns,"removeHtmlBlocks");a(Pl,"stripHelpText");a(Rl,"parseRegisterTypeFromHtml");a(Fs,"parseRegisterTypeAt");a(_l,"splitHelpSections");a(xn,"extractBalancedObject");a(Os,"matchStringProp");a($s,"matchNumberProp");a(Ll,"parseDefaultsBlock");a(Cl,"unescapeJsStringEscape");a(El,"normalizeDefaultValue");a(sr,"parseJsLiteral");a(co,"measureJsLiteralLength");a(Us,"extractBalancedArray");a(Il,"parseLooseValue");a(Nl,"findSetForType");a(yo,"findCaseInsensitiveType");a(ct,"getNodeTypeDetail");a(Bs,"assertKnownNodeTypes");a(qs,"assertKnownNodeType");ke=null;a(Fl,"defaultCatalogueFetcher");a(Ol,"loadCatalogue");a(ar,"searchPalette");a($l,"paletteMatchScore")});function Ke(e){if(e.length===0)return;let t=e[0];if(!t)return;let n=e.map(s=>{let i={...s};return delete i.statusCode,i});if(e.length>1)throw new y(`${e.length} validation errors`,"validation_failed",{statusCode:400,details:{errors:n,errorCount:e.length}});let r={errors:n};for(let[s,i]of Object.entries(t))s==="code"||s==="error"||s==="errors"||s==="statusCode"||i!==void 0&&(r[s]=i);let o=typeof t.statusCode=="number"?t.statusCode:400;throw new y(t.error,t.code,{statusCode:o,details:r})}function wo(e,t="payload.flow"){let n=new Map;for(let o of e)for(let s of o){if(!s||typeof s!="object"||Array.isArray(s))continue;let i=s.id;typeof i=="string"&&i.length>0&&n.set(i,(n.get(i)??0)+1)}let r=[];for(let[o,s]of n)s>1&&r.push({code:"duplicate_node_id",error:`Node id '${o}' appears ${s} times in ${t}; node ids must be unique`,nodeId:o,count:s});return r}function Ml(e){let t=typeof e.label=="string"&&e.label?` '${e.label}'`:"";return e.type==="tab"?`tab${t||` '${e.id}'`}`:e.type==="subflow"?`subflow${t||` '${e.id}'`}`:typeof e.z=="string"&&e.z?`${e.type} node on '${e.z}'`:`global ${e.type} config node`}function zs(e,t,n){let r=new Map;for(let i of n)!i||typeof i.id!="string"||i.id===t||i.z===t||r.set(i.id,i);let o=[],s=new Set;for(let i of[e.nodes,e.configs])if(Array.isArray(i))for(let d of i){let l=d&&typeof d=="object"?d.id:void 0;if(typeof l!="string"||!l||s.has(l))continue;if(s.add(l),l===t){o.push({code:"duplicate_node_id",error:`Node id '${l}' equals the tab id; node ids must be unique across all flows`,nodeId:l});continue}let c=r.get(l);c&&o.push({code:"duplicate_node_id",error:`Node id '${l}' is already used by ${Ml(c)}; node ids must be unique across all flows`,nodeId:l})}return o}function Ws(e){let t=new Map;for(let n of[e.nodes,e.configs])if(Array.isArray(n))for(let r of n){if(!r||typeof r!="object"||Array.isArray(r))continue;let o=r;typeof o.id=="string"&&t.set(o.id,o)}return t}function jl(e,t){let n=new Set([...Object.keys(e),...Object.keys(t)]);for(let r of n)if(!Dl.has(r)&&JSON.stringify(e[r])!==JSON.stringify(t[r]))return!0;return!1}function Hl(e,t){let n=Ws(t);if(!e)return new Set(n.keys());let r=Ws(e),o=new Set;for(let[s,i]of n){let d=r.get(s);(!d||jl(d,i))&&o.add(s)}return o}function Gs(e,t){let n=[],r=e;typeof r.id!="string"||r.id.length===0?n.push({code:"missing_flow_id",error:"payload.flow.id is required and must match payload.flowId"}):r.id!==t&&n.push({code:"invalid_flow_id",error:`payload.flow.id ('${r.id}') must match payload.flowId ('${t}')`}),"nodes"in r?Array.isArray(r.nodes)||n.push({code:"invalid_nodes",error:"payload.flow.nodes must be an array"}):n.push({code:"invalid_nodes",error:"payload.flow.nodes must be an array (use [] to clear the tab intentionally)"}),"configs"in r&&r.configs!==void 0&&!Array.isArray(r.configs)&&n.push({code:"invalid_nodes",error:"payload.flow.configs must be an array"});let o=[];Array.isArray(r.nodes)&&o.push({key:"nodes",list:r.nodes}),Array.isArray(r.configs)&&o.push({key:"configs",list:r.configs}),n.push(...wo(o.map(s=>s.list)));for(let{key:s,list:i}of o)for(let d=0;d<i.length;d+=1){let l=i[d];if(!l||typeof l!="object"||Array.isArray(l)){n.push({code:"invalid_node",error:`payload.flow.${s}[${d}] must be an object`});continue}let c=l;(typeof c.id!="string"||c.id.length===0)&&n.push({code:"missing_node_id",error:`payload.flow.${s}[${d}] needs a non-empty string id`});try{Je(c.type,"node.type")}catch(u){u instanceof y&&n.push({code:u.code,error:u.message,...typeof c.id=="string"?{nodeId:c.id}:{}})}"wires"in c&&Mt(c.wires,n,typeof c.id=="string"?c.id:void 0)}return n}function Mt(e,t,n){if(!Array.isArray(e))return t.push({code:"invalid_wires",error:"wires must be an array of output ports (each a list of node ids)",...n?{nodeId:n}:{}}),!1;for(let r of e){if(!Array.isArray(r))return t.push({code:"invalid_wires",error:"wires must be an array of output ports (each a list of node ids)",...n?{nodeId:n}:{}}),!1;for(let o of r)if(typeof o!="string"||o.length===0)return t.push({code:"invalid_wires",error:"wires targets must be non-empty node id strings",...n?{nodeId:n}:{}}),!1}return!0}async function Ul(e,t){if(typeof e.outputs=="number"&&Number.isFinite(e.outputs)&&e.outputs>=0)return Math.trunc(e.outputs);let n=typeof e.type=="string"?e.type:"";if(!(!t||!n))try{return(await ct(t,n,{includeHelp:!1})).outputs}catch{return}}function Bl(e,t,n){let r=typeof e.type=="string"?e.type:"node";if(!(n.length<=t))return t===0?{code:"invalid_wires",error:`${r} has 0 outputs; wires must be []`,nodeId:e.id,type:r}:{code:"invalid_wires",error:`${r} has ${t} outputs; wires has ${n.length} port arrays (max ${t})`,nodeId:e.id,type:r}}function ql(e){return Array.isArray(e)&&e.some(t=>typeof t=="string"&&t.length>0)}function Wl(e,t){return t<=0?!1:!("wires"in e)||e.wires===void 0?!0:Array.isArray(e.wires)?!e.wires.some(ql):!1}async function bo(e,t,n={}){let r=[],o=[],s=new Set(e.map(d=>d.id)),i=n.softWarningNodeIds;for(let d of e){let l=await Ul(d,t),c=typeof d.type=="string"?d.type:"node",u=!i||i.has(d.id);if("wires"in d&&d.wires!==void 0){if(!Mt(d.wires,r,d.id))continue;let p=d.wires;if(l!==void 0){let f=Bl(d,l,p);f?(f.cause="excess_ports",r.push(f)):u&&p.length<l&&o.push({code:"incomplete_wires",error:`${c} '${d.id}' declares ${l} outputs but wires has only ${p.length} port(s)`,nodeId:d.id,type:c})}for(let f of p)for(let m of f){let h=String(m);s.has(h)||r.push({code:"invalid_wires",error:`Wire target '${h}' on node '${d.id}' was not found in this flow (add the node in the same patch or use an existing id)`,nodeId:d.id,cause:"missing_target",targetId:h})}}u&&l!==void 0&&Wl(d,l)&&o.push({code:"unwired_node",error:`Node '${d.id}' (${c}) has ${l} output(s) but is not wired to anything yet`,nodeId:d.id,type:c})}return{errors:r,warnings:o}}function vo(e,t){let n=[];for(let r of e)r.z===void 0||r.z===""||typeof r.z=="string"&&r.z!==t&&n.push({code:"z_mismatch",error:`Node '${r.id}' has z='${r.z}' but flow id is '${t}'`,nodeId:r.id});return n}function zl(e,t){return e.filter(n=>n.type===t).map(n=>n.id)}async function ko(e,t,n){let r=[],o=[...t];for(let s of e){let i;try{i=Je(s.type,"node.type")}catch{continue}let d;try{d=await ct(n,i,{includeHelp:!1})}catch{continue}let l=d.defaults;if(!l||typeof l!="object")continue;let c=new Set(d.requiredConfigNodeTypes??[]);for(let[u,p]of Object.entries(l)){if(!p||typeof p!="object"||Array.isArray(p))continue;let f=p.type;if(typeof f!="string"||!f)continue;let h=p.required===!0||c.has(f),g=s[u],x=zl(o,f);if(typeof g!="string"||g.length===0){h&&r.push({code:"missing_config_node",error:`Node '${s.id}' (${i}) has no ${f} on '${u}' yet \u2014 configure before deploy if needed`,nodeId:s.id,type:i,property:u,configType:f,candidates:x});continue}o.find(E=>E.id===g)?.type!==f&&r.push({code:"missing_config_node",error:`Node '${s.id}' property '${u}' does not resolve to a ${f} config yet`,nodeId:s.id,type:i,property:u,configType:f,candidates:x})}}return r}async function Js(e,t={}){let n=t.flowId??(typeof e.id=="string"?e.id:""),r=Array.isArray(e.nodes)?e.nodes:[],o=Array.isArray(e.configs)?e.configs:[],s=[],i=[];n&&s.push(...vo(r,n));let d=t.baseFlow?Hl(t.baseFlow,e):void 0,l=await bo(r,t.nodeTypes,{...d?{softWarningNodeIds:d}:{}});if(s.push(...l.errors),i.push(...l.warnings),t.nodeTypes){let c=await ko(r,o,t.nodeTypes);d?i.push(...c.filter(u=>!u.nodeId||d.has(u.nodeId))):i.push(...c)}return{errors:s,warnings:i}}var Dl,Ao=U(()=>{"use strict";K();ut();a(Ke,"throwValidationIssues");a(wo,"collectDuplicateNodeIdIssues");a(Ml,"describeLiveNode");a(zs,"collectCrossFlowDuplicateIssues");Dl=new Set(["x","y","w","h","g","id","z"]);a(Ws,"collectNodesMap");a(jl,"nodeContentChanged");a(Hl,"changedNodeIds");a(Gs,"collectFullFlowShapeIssues");a(Mt,"assertValidWiresShape");a(Ul,"resolveNodeOutputCount");a(Bl,"wireCountIssue");a(ql,"portHasTargets");a(Wl,"nodeIsUnwired");a(bo,"collectWireValidation");a(vo,"collectZMismatchIssues");a(zl,"configCandidates");a(ko,"collectConfigWarnings");a(Js,"validateFlowTab")});function Ve(e,t="node"){return typeof e=="string"&&e.trim()?e.trim():t}function dr(e){let t=new Map;for(let n of e)n.type==="tab"&&typeof n.id=="string"&&typeof n.label=="string"&&n.label.trim()&&t.set(n.id,n.label.trim());return t}function Vs(e,t){e[Ks]=t}function Kl(e){if(!e)return;let t=e[Ks];return t instanceof Map?t:void 0}function Vl(e){return typeof e!="string"||!e.trim()?new Set:new Set(e.split(",").map(t=>t.trim()).filter(Boolean))}function Xl(e){if(e===!0||e===1)return!0;if(typeof e=="string"){let t=e.trim().toLowerCase();return t==="1"||t==="true"||t==="yes"}return!1}function re(e){let t=Vl(e.include);return Xl(e.include_payload)&&(t.add("payload"),t.add("baseFlow")),t}function q(e,t){let n={req:e};t!==void 0&&(n.httpAdminRoot=t);let r=Kl(e);return r&&(n.liveFlowLabels=r),n}function Dt(e){return typeof e.name=="string"&&e.name.trim()?e.name.trim():typeof e.label=="string"&&e.label.trim()?e.label.trim():""}function Sn(e){return`"${e.replace(/"/g,"'")}"`}function jt(e){let t=new Map;for(let n of e.nodes??[])t.set(n.id,n);for(let n of e.configs??[])t.set(n.id,n);return t}function Yl(e,t){let n=new Set(["x","y","w","h","g","id","z"]),r=new Set([...Object.keys(e),...Object.keys(t)]);for(let o of r)if(!n.has(o)&&JSON.stringify(e[o])!==JSON.stringify(t[o]))return!0;return!1}function Ql(e,t){let n=Dt(e),r=Dt(t);if(!n||!r||n===r)return!1;let o=new Set(["x","y","w","h","g","id","z","name","label"]),s=new Set([...Object.keys(e),...Object.keys(t)]);for(let i of s)if(!o.has(i)&&JSON.stringify(e[i])!==JSON.stringify(t[i]))return!1;return!0}function xo(e,t){let n=[],r=[],o=[];for(let[s,i]of t)e.has(s)||n.push(i);for(let[s,i]of e)t.has(s)||r.push(i);for(let[s,i]of t){let d=e.get(s);d&&Yl(d,i)&&o.push({before:d,after:i})}return{added:n,removed:r,modified:o}}function lr(e){return e===1?"node":"nodes"}function Zl(e){return e.length===1&&e[0]?`adds 1 ${Ve(e[0].type)}`:`adds ${e.length} ${lr(e.length)}`}function ec(e){return`deletes ${e} ${lr(e)}`}function tc(e){return`edits ${e} ${lr(e)}`}function nc(e){let t=[];e.removed>0&&t.push(ec(e.removed));let n=e.added??[],r=e.addedCount??n.length;r>0&&t.push(n.length===r?Zl(n):`adds ${r} ${lr(r)}`),(e.modified??0)>0&&t.push(tc(e.modified??0));let o=t.length?t.join(", "):"replaces nodes",s=e.flowLabel?.trim();return s?`Replace ${s}: ${o}`:`Replace flow: ${o}`}function rc(e){let{added:t,removed:n,modified:r}=e;if(t.length+n.length+r.length!==1)return;if(t[0]){let u=t[0],p=Ve(u.type),f=Dt(u);return f?`Add ${p} node ${Sn(f)}`:`Add ${p} node`}if(n[0]){let u=n[0],p=Ve(u.type),f=Dt(u);return f?`Remove ${p} node ${Sn(f)}`:`Remove ${p} node`}let s=r[0];if(!s)return;let i=Ve(s.after.type,Ve(s.before.type)),d=Dt(s.before),l=Dt(s.after);if(Ql(s.before,s.after)&&d&&l)return`Rename ${i} node ${Sn(d)} \u2192 ${Sn(l)}`;let c=l||d;return c?`Edit ${i} ${Sn(c)}`:`Edit ${i}`}function So(e,t){return typeof e?.label=="string"&&e.label.trim()?e.label.trim():typeof t?.label=="string"&&t.label.trim()?t.label.trim():""}function cr(e,t){if(typeof e.flowLabel=="string"&&e.flowLabel.trim())return e.flowLabel.trim();let n=So(e.payload?.flow,e.baseFlow);if(n)return n;let r=e.payload?.flowId;return r&&t?.has(r)?t.get(r)??"":""}function Xs(e,t){let n=t.trim(),r=e.trim();if(!n||!r||r.includes(n))return r;let o=r.lastIndexOf(" in ");if(o>=0){let s=r.slice(o+4);if(s.length>0&&n.startsWith(s))return r}return`${r} in ${n}`}function To(e,t){let n=xo(jt(e??{}),jt(t)),r=n.added.length+n.removed.length+n.modified.length,o=So(t,e);if(r===0)return o?`Update ${o}`:"Update flow";if(n.removed.length>0)return nc({...o?{flowLabel:o}:{},removed:n.removed.length,added:n.added,modified:n.modified.length});if(r>Jl)return o?`${r} changes in ${o}`:`${r} changes`;let s=rc(n)??(o?`Update ${o}`:"Update flow");return Xs(s,o)}function oc(e,t){let n=(e??"").trim();return!!(!n||n.includes("[object Object]")||/^Update(\s|$)/i.test(n)||t>0&&!/^Replace\b/i.test(n))}function Po(e,t){return`${e} \xB7 ${ic(t)}`}function sc(e){let t=e>=0?"+":"-",n=Math.abs(e),r=String(Math.floor(n/60)).padStart(2,"0"),o=String(n%60).padStart(2,"0");return`${t}${r}:${o}`}function ic(e,t=new Date(e)){let n=Number.isNaN(t.getTime())?new Date(e):t;if(Number.isNaN(n.getTime()))return e.slice(0,16);let r=n.getDate(),s=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"][n.getMonth()]??"Jan",i=String(n.getHours()).padStart(2,"0"),d=String(n.getMinutes()).padStart(2,"0"),l=sc(-n.getTimezoneOffset());return`${r} ${s} ${i}:${d} ${l}`}function ur(e){if(typeof e!="string")return;let t=e.replace(/\0/g,"").replace(/[\u0001-\u001F\u007F]/g," ").replace(/\s+/g," ").trim().slice(0,Gl).trim();return t.length>0?t:void 0}function ac(e,t){let n=ae(t);return`${n==="/"?"/ai-access":`${n}/ai-access`}#proposal=${encodeURIComponent(e)}`}function Ro(e,t={}){let n=cr(e,t.liveFlowLabels),r=ur(e.title),o=To(e.baseFlow,n&&!So(e.payload.flow,e.baseFlow)?{...e.payload.flow,label:n}:e.payload.flow),s=xo(jt(e.baseFlow??{}),jt(e.payload.flow)),i=oc(r,s.removed.length)?o:r??o;return Xs(i,n)}function dc(e){return typeof e.storedTitle=="string"&&e.storedTitle?e.storedTitle:ur(e.title)}function oe(e,t={}){let n=Ro(e,t),r=cr(e,t.liveFlowLabels),o=dc(e)??n,s=ac(e.id,t.httpAdminRoot),i=t.req?vn(t.req,s):s;return{...e,title:n,storedTitle:o,flowLabel:r,displayName:Po(n,e.createdAt),reviewUrl:i,reviewPath:s}}function lc(e){let t=xo(jt(e.baseFlow??{}),jt(e.payload.flow)),n=[];return t.added.length>0&&n.push(t.added.length===1?"Adds 1 node":`Adds ${t.added.length} nodes`),t.removed.length>0&&n.push(t.removed.length===1?"Deletes 1 node":`Deletes ${t.removed.length} nodes`),t.modified.length>0&&n.push(t.modified.length===1?"Edits 1 node":`Edits ${t.modified.length} nodes`),n.length===0?"No node changes":n.map((r,o)=>o===0?r:r.charAt(0).toLowerCase()+r.slice(1)).join(", ")}function cc(e){if(e.kind==="oauth")return{kind:"oauth",clientId:e.clientId,user:e.user};let t={kind:e.kind,linkId:e.linkId};return typeof e.label=="string"&&e.label.length>0&&(t.label=e.label),t}function ce(e,t={}){let n=oe(e,t),r=t.include??new Set,o={id:n.id,status:n.status,type:n.type,title:n.title,storedTitle:n.storedTitle,displayName:n.displayName,flowLabel:n.flowLabel,reviewUrl:n.reviewUrl,reviewPath:n.reviewPath,createdAt:n.createdAt,updatedAt:n.updatedAt,expectedRev:n.expectedRev,createdBy:cc(n.createdBy),changeSummary:lc(e)};return r.has("payload")&&(o.payload=n.payload),r.has("baseFlow")&&n.baseFlow!==void 0&&(o.baseFlow=n.baseFlow),n.warnings&&n.warnings.length>0&&(o.warnings=n.warnings),n.baseRevStale!==void 0&&(o.baseRevStale=n.baseRevStale),n.decidedBy!==void 0&&(o.decidedBy=n.decidedBy),n.reason!==void 0&&(o.reason=n.reason),n.resultRev!==void 0&&(o.resultRev=n.resultRev),o}function Ys(e,t={}){return e.map(n=>ce(n,t))}var Gl,Jl,Ks,Ae=U(()=>{"use strict";ie();_e();Gl=80,Jl=1;a(Ve,"nodeTypeText");Ks=Symbol.for("ai-access.liveFlowLabels");a(dr,"liveFlowLabelsFromFlows");a(Vs,"setRequestFlowLabels");a(Kl,"getRequestFlowLabels");a(Vl,"parseIncludeQuery");a(Xl,"isTruthyFlag");a(re,"resolveIncludeFromQuery");a(q,"presentOptionsFor");a(Dt,"nodeLabel");a(Sn,"quoteName");a(jt,"collectNodes");a(Yl,"nodeContentChanged");a(Ql,"isRenameOnly");a(xo,"classifyFlowChanges");a(lr,"pluralNodes");a(Zl,"describeAddedClause");a(ec,"describeRemovedClause");a(tc,"describeModifiedClause");a(nc,"buildReplaceFlowTitle");a(rc,"describeOneChange");a(So,"flowLabelHint");a(cr,"resolveFlowLabel");a(Xs,"ensureTitleIncludesFlowName");a(To,"autoGenerateProposalTitle");a(oc,"shouldRefreshDisplayTitle");a(Po,"buildProposalDisplayName");a(sc,"formatUtcOffset");a(ic,"formatProposalSubmittedAt");a(ur,"sanitizeProposalTitle");a(ac,"buildProposalReviewPath");a(Ro,"resolveProposalTitle");a(dc,"storedTitleOf");a(oe,"presentProposal");a(lc,"buildChangeSummary");a(cc,"presentCreatedByForApi");a(ce,"presentProposalForApi");a(Ys,"presentProposalsForApi")});function Qs(e){let t=new Map;for(let n of e.nodes??[])t.set(n.id,n);for(let n of e.configs??[])t.set(n.id,n);return t}function pc(e,t){let n=new Set([...Object.keys(e),...Object.keys(t)]),r=[];for(let o of n)uc.has(o)||JSON.stringify(e[o])!==JSON.stringify(t[o])&&r.push(o);return r.sort()}function Zs(e){let t=Qs(e.baseFlow??{}),n=Qs(e.payload.flow),r=new Map;for(let[o,s]of n){let i=t.get(o);if(!i){r.set(o,new Set(["*"]));continue}let d=pc(i,s);d.length>0&&r.set(o,new Set(d))}for(let[o]of t)n.has(o)||r.set(o,new Set(["*"]));return r}function fc(e,t){let n=[],r=[];for(let[o,s]of e){let i=t.get(o);if(!i)continue;n.push(o);let d=[];for(let c of s){if(c==="*"){d.push(...[...i].filter(u=>u!=="*")),i.has("*")&&d.push("*");continue}(i.has(c)||i.has("*"))&&d.push(c)}let l=[...new Set(d)].sort();l.length>0&&r.push({nodeId:o,fields:l})}return n.sort(),{nodeIds:n,fields:r}}function V(e,t,n={}){let r=0;for(let o of e.list())if(o.status==="pending"&&!(n.flowId&&o.payload.flowId!==n.flowId)&&o.expectedRev!==t)try{e.markClosed(o.id,"expired",mc),r+=1}catch{}return r}function ei(e,t,n,r){let o=`Superseded by approved proposal ${n} (resultRev ${r}). Re-read the flow and resubmit if still needed.`,s=0;for(let i of e.list())if(!(i.id===n||i.status!=="pending"||i.payload.flowId!==t))try{e.markClosed(i.id,"superseded",o),s+=1}catch{}return s}function Ie(e,t,n){let r=Zs(t),o=[];for(let s of e.list()){if(s.id===t.id||s.status!=="pending"||s.payload.flowId!==t.payload.flowId)continue;let i=Ro(s),d=n?lt(s.createdBy)===n:lt(s.createdBy)===lt(t.createdBy),l=fc(r,Zs(s)),c=s.expectedRev===t.expectedRev,u=l.nodeIds.length>0;if(!c&&!u)continue;let p={id:s.id,title:i,displayName:Po(i,s.createdAt),mine:d};l.nodeIds.length>0&&(p.overlappingNodeIds=l.nodeIds),l.fields.length>0&&(p.overlappingFields=l.fields),o.push(p)}return{conflictingProposals:o}}function Ne(e){let t={};return e.conflictingProposals.length>0&&(t.conflictingProposals=e.conflictingProposals,t.warning="Other pending proposals touch the same flow \u2014 approve order may conflict (like a git merge)."),t}function Q(e){return e>0?{autoClosedCount:e}:{}}var uc,mc,Xe=U(()=>{"use strict";Ge();Ae();uc=new Set(["x","y","w","h","g","id","z"]);a(Qs,"collectNodes");a(pc,"changedFieldKeys");a(Zs,"computeProposalTouches");a(fc,"overlapBetween");mc="Proposal expired: the live flow revision moved past expectedRev. Re-read the flow and resubmit.";a(V,"expireStalePendingProposals");a(ei,"supersedePendingOnFlow");a(Ie,"findConflictingProposals");a(Ne,"conflictWarningFields");a(Q,"autoClosedFields")});var di={};ds(di,{applyFlowNodeOps:()=>pt,applyReplaceTextToValue:()=>ai,buildPatchedFlowTab:()=>Tn,collectSetProps:()=>_o,decodeBase64UrlJson:()=>Pn,findTextOccurrences:()=>ii,interpretPatch:()=>Rn});function si(e){return e===null?"null":Array.isArray(e)?"array":typeof e}function hc(e){if(typeof e!="object"||e===null||Array.isArray(e))throw new y(`add op needs node as an object (got ${si(e)})`,"invalid_node")}async function Tn(e,t,n){return pt(e,t,n.map(r=>({op:"set",nodeId:r.nodeId,set:r.set})))}function ii(e,t){if(!t)return[];let n=[],r=0;for(;r<=e.length;){let o=e.indexOf(t,r);if(o<0)break;n.push(o),r=o+t.length}return n}function ai(e,t,n,r,o){if(oi.has(t))throw new y(`Cannot change protected node property '${t}' via replace_text`,"protected_property");if(typeof n!="string"||n.length===0)throw new y("find must be a non-empty string","missing_find");if(typeof r!="string")throw new y("replace must be a string","invalid_replace");if(e===void 0)throw new y(`Field '${t}' was not found on the node`,"field_not_found");if(typeof e!="string")throw new y(`Field '${t}' is not a string (got ${e===null?"null":typeof e})`,"field_not_string");let s=ii(e,n);if(s.length===0)throw new y(`Text not found in field '${t}'`,"text_not_found",{details:{matchCount:0}});let i=o;if(i===void 0){if(s.length>1)throw new y(`Found ${s.length} matches in '${t}'; pass occurrence (1-based) to pick one`,"ambiguous_match",{statusCode:409,details:{matchCount:s.length}});i=1}if(!Number.isInteger(i)||i<1||i>s.length)throw new y(`occurrence must be an integer from 1 to ${s.length}`,"invalid_occurrence",{details:{matchCount:s.length}});let d=s[i-1];if(d===void 0)throw new y(`occurrence must be an integer from 1 to ${s.length}`,"invalid_occurrence",{details:{matchCount:s.length}});return e.slice(0,d)+r+e.slice(d+n.length)}function gc(e){let t=[];for(let n of e)if(n.op==="add"){let r=n.node;if(typeof r!="object"||r===null||Array.isArray(r)){t.push({code:"invalid_node",error:`add op needs node as an object (got ${si(r)})`});continue}let o=r;(!o.id||typeof o.id!="string")&&t.push({code:"missing_node_id",error:"add op needs node.id"});try{Je(o.type,"node.type")}catch(s){s instanceof y&&t.push({code:s.code,error:s.message,nodeId:o.id})}"wires"in o&&Mt(o.wires,t,typeof o.id=="string"?o.id:void 0)}else n.op==="set"&&typeof n.set=="object"&&"wires"in n.set&&Mt(n.set.wires,t,n.nodeId);return t}function yc(e){let t=new Set;for(let n of e)n.op==="add"&&typeof n.node.id=="string"?t.add(n.node.id):n.op!=="add"&&typeof n.nodeId=="string"&&t.add(n.nodeId);return t}async function pt(e,t,n,r={}){if(!t)throw new y("flow id is required","missing_flow_id");if(!n.length)throw new y("At least one node edit is required","missing_edits");let o=gc(n);o.push(...wo([n.flatMap(h=>h.op==="add"?[h.node]:[])],"the batch (add ops)")),Ke(o);let s=await e.getFlow(t);if(r.expectedRev!==void 0&&s.rev!==r.expectedRev){let h=Array.isArray(s.flow.nodes)?[...s.flow.nodes]:[];return{expectedRev:s.rev,flow:{...s.flow,id:s.flow.id,type:s.flow.type??"tab",nodes:h}}}let i=Array.isArray(s.flow.nodes)?[...s.flow.nodes]:[],d=Array.isArray(s.flow.configs)?[...s.flow.configs]:[],l={...s.flow,id:s.flow.id,type:s.flow.type??"tab",nodes:i,configs:d},c=i.map(h=>({...h})),u=[];for(let h of n)try{h.op==="add"&&r.nodeTypes&&await qs(r.nodeTypes,h.node.type),ni(c,t,h)}catch(g){if(g instanceof y)u.push({code:g.code,error:g.message,...g.statusCode!==400?{statusCode:g.statusCode}:{},...g.details??{}});else throw g}u.push(...vo(c,t));let p=yc(n),f=await bo(c,r.nodeTypes,{softWarningNodeIds:p});u.push(...f.errors),Ke(u);for(let h of n)ni(i,t,h);let m=[...f.warnings];if(r.nodeTypes){let h=await ko(i,d,r.nodeTypes);m.push(...h.filter(g=>!g.nodeId||p.has(g.nodeId)))}return{expectedRev:s.rev,flow:l,...m.length>0?{warnings:m}:{}}}function ti(e,t,n){if(!t||typeof t!="string")throw new y("Each edit needs a node id","missing_node_id");let r=e.findIndex(o=>o.id===t);if(r<0||!e[r])throw new y(`Node '${t}' was not found in flow '${n}'`,"node_not_found",{statusCode:404});return r}function ni(e,t,n){if(n.op==="add"){let i=n.node;if(!i.id||typeof i.id!="string")throw new y("add op needs node.id","missing_node_id");if(Je(i.type,"node.type"),e.some(d=>d.id===i.id))throw new y(`Node '${i.id}' already exists in flow '${t}'`,"node_already_exists");e.push({...i,id:i.id,type:i.type,z:typeof i.z=="string"&&i.z?i.z:t});return}if(n.op==="remove"){let i=ti(e,n.nodeId,t);e.splice(i,1);return}let r=ti(e,n.nodeId,t),o=e[r];if(!o)throw new y(`Node '${n.nodeId}' was not found in flow '${t}'`,"node_not_found",{statusCode:404});if(n.op==="replace_text"){if(!(n.field in o))throw new y(`Field '${n.field}' was not found on the node`,"field_not_found");let i=ai(o[n.field],n.field,n.find,n.replace,n.occurrence);e[r]={...o,id:o.id,type:o.type,[n.field]:i};return}let s={...o,id:o.id,type:o.type};for(let[i,d]of Object.entries(n.set)){if(oi.has(i))throw new y(`Cannot change protected node property '${i}' via GET propose`,"protected_property");if(i==="wires"){let l=[];Mt(d,l,n.nodeId),Ke(l)}s[i]=d}e[r]=s}function ri(e){if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean"||typeof e=="bigint")return String(e)}function _o(e){let t={},n=e.set;if(n&&typeof n=="object"&&!Array.isArray(n))for(let[r,o]of Object.entries(n)){let s=ri(o);s!==void 0&&(t[r]=s)}for(let[r,o]of Object.entries(e)){if(!r.startsWith("set.")||r.length<=4)continue;let s=r.slice(4);if(typeof o=="string")t[s]=o;else if(Array.isArray(o)&&typeof o[0]=="string")t[s]=o[0];else{let i=ri(o);i!==void 0&&(t[s]=i)}}return t}function Pn(e){let t;try{t=Buffer.from(e,"base64url").toString("utf8")}catch{throw new y("patch must be base64url-encoded JSON","invalid_patch")}try{return JSON.parse(t)}catch{throw new y("patch JSON is malformed","invalid_patch_json")}}function wc(e,t){if(typeof e.flowId=="string"&&e.flowId)return e.flowId;let n=e.flow;if(n&&typeof n=="object"&&!Array.isArray(n)){let r=n.id;if(typeof r=="string"&&r)return r}return t}function Lo(e){return typeof e.id=="string"&&e.id?e.id:typeof e.nodeId=="string"&&e.nodeId?e.nodeId:typeof e.node=="string"&&e.node?e.node:""}function bc(e){if(e==null||e==="")return;let t;if(typeof e=="number")t=e;else if(typeof e=="string")t=Number(e);else if(typeof e=="boolean"||typeof e=="bigint")t=Number(e);else throw new y("occurrence must be a positive integer","invalid_occurrence");if(!Number.isFinite(t))throw new y("occurrence must be a positive integer","invalid_occurrence");return Math.trunc(t)}function vc(e){let t=e.node;if(t!==void 0)return hc(t),{op:"add",node:t};let n={...e};return delete n.op,{op:"add",node:n}}function kc(e){let t=Lo(e);if(!t)throw new y("replace_text op needs node id","missing_node_id");let n=typeof e.field=="string"?e.field:"";if(!n)throw new y("replace_text op needs field","missing_field");if(typeof e.find!="string")throw new y("replace_text op needs find string","missing_find");if(typeof e.replace!="string")throw new y("replace_text op needs replace string","invalid_replace");let r=bc(e.occurrence),o={op:"replace_text",nodeId:t,field:n,find:e.find,replace:e.replace};return r!==void 0&&(o.occurrence=r),o}function Ac(e){let t=Lo(e);if(!t)throw new y("Each patch.nodes entry needs id","missing_node_id");let n=e.set,r={};if(n&&typeof n=="object"&&!Array.isArray(n))Object.assign(r,n);else for(let[o,s]of Object.entries(e))o==="id"||o==="nodeId"||o==="node"||o==="set"||o==="op"||(r[o]=s);return{op:"set",nodeId:t,set:r}}function xc(e){let t=typeof e.op=="string"?e.op.toLowerCase():"set";if(t==="add")return vc(e);if(t==="remove"){let n=Lo(e);if(!n)throw new y("remove op needs id","missing_node_id");return{op:"remove",nodeId:n}}if(t==="replace_text")return kc(e);if(t!=="set")throw new y("nodes[].op must be 'add', 'remove', 'set', or 'replace_text'","invalid_patch");return Ac(e)}function Rn(e,t){if(!e||typeof e!="object"||Array.isArray(e))throw new y("patch must be a JSON object","invalid_patch");let n=e;if(n.type==="replace_flow"||n.flow&&typeof n.flow=="object"){let r=wc(n,t);if(!r)throw new y("patch.replace_flow needs flowId","missing_flow_id");return{kind:"replace_flow",flowId:r,flow:n.flow}}if(Array.isArray(n.nodes)){let r=typeof n.flowId=="string"?n.flowId:t;if(!r)throw new y("patch.nodes needs flow id","missing_flow_id");let o=[];for(let i of n.nodes){if(!i||typeof i!="object"||Array.isArray(i))throw new y("Each patch.nodes entry must be an object","invalid_patch");o.push(xc(i))}let s=o.filter(i=>i.op==="set").map(i=>({nodeId:i.nodeId,set:i.set}));return{kind:"nodes",flowId:r,ops:o,edits:s}}throw new y("patch must include nodes[] edits or type/flow for replace_flow","invalid_patch")}var oi,pr=U(()=>{"use strict";K();Ao();ut();oi=new Set(["id","type","z"]);a(si,"describeNonObjectNode");a(hc,"assertAddNodeObject");a(Tn,"buildPatchedFlowTab");a(ii,"findTextOccurrences");a(ai,"applyReplaceTextToValue");a(gc,"collectOpShapeIssues");a(yc,"touchedNodeIdsFromOps");a(pt,"applyFlowNodeOps");a(ti,"requireNodeIndex");a(ni,"applyOneNodeOp");a(ri,"stringifyQueryValue");a(_o,"collectSetProps");a(Pn,"decodeBase64UrlJson");a(wc,"resolveReplaceFlowId");a(Lo,"resolveNodeId");a(bc,"parseOccurrence");a(vc,"parseAddOp");a(kc,"parseReplaceTextOp");a(Ac,"parseSetOp");a(xc,"parseNodeOp");a(Rn,"interpretPatch")});function Ht(e,t){return lt(e.createdBy)===t.id}function Co(e,t){return!t||t.length===0||t.includes(e)}function Sc(e){return typeof e=="string"&&ci.has(e)}function Bt(e,t){let n=Tc[e],r=e!=="status"&&Array.isArray(t);return new y(r?`Invalid ${e} filter: pass it at most once (multiple values are not supported). Allowed: ${n}`:`Invalid ${e} filter. Allowed: ${n}`,"invalid_filter",{details:{parameter:e,allowed:n,hint:`Pass ${e} as ${n}, or omit it.`}})}function fr(e,t){if(Array.isArray(t))throw Bt(e,t);return t}function Pc(e){return typeof e=="number"?Number.isInteger(e)?e:null:typeof e=="string"&&/^-?\d+$/.test(e.trim())?Number(e.trim()):null}function Eo(e){return e==null||e===""}function Rc(e){if(!Eo(e)){if(e==="true"||e==="1"||e===!0)return!0;if(e==="false"||e==="0"||e===!1)return!1;throw Bt("mine",e)}}function Io(e){if(Eo(e))return;let t=(Array.isArray(e)?e:[e]).flatMap(r=>typeof r=="string"?r.split(","):[r]),n=[];for(let r of t){let o=typeof r=="string"?r.trim():r;if(o!==""){if(!Sc(o))throw Bt("status",e);n.includes(o)||n.push(o)}}return n.length>0?n:void 0}function li(e,t,n,r){if(Eo(t))return;let o=Pc(t);if(o===null||o<n||o>r)throw Bt(e,t);return o}function mr(e){let t={},n=Rc(fr("mine",e.mine));n!==void 0&&(t.mine=n);let r=Io(e.status);r&&(t.status=r);let o=fr("flowId",e.flowId??e.flow_id??e.flow);typeof o=="string"&&o.length>0&&(t.flowId=o);let s=li("limit",fr("limit",e.limit),1,Ut);s!==void 0&&(t.limit=s);let i=li("offset",fr("offset",e.offset),0,Number.MAX_SAFE_INTEGER);return i!==void 0&&(t.offset=i),t}function ft(e){let t=e.offset+e.returned,n=t<e.total;return{total:e.total,offset:e.offset,...e.limit!==null?{limit:e.limit}:{},hasMore:n,...n?{nextOffset:t}:{}}}function _c(e,t){let n=e.length,r=t.offset!==void 0&&t.offset>0?t.offset:0;if(t.limit===void 0)return{items:r>0?e.slice(r):e,total:n,limit:null,offset:r};let o=Math.min(Math.max(1,t.limit),Ut);return{items:e.slice(r,r+o),total:n,limit:o,offset:r}}function qt(e,t,n={},r={}){let o=r.mine===!0?t.id:void 0,s={};o&&(s.linkId=o),r.flowId&&(s.flowId=r.flowId);let d=e.list(s).filter(c=>r.mine!==!1||!Ht(c,t)).filter(c=>Co(c.status,r.status)).map(c=>({...oe(c,n),mine:Ht(c,t)})),l=_c(d,r);return{proposals:l.items,total:l.total,limit:l.limit,offset:l.offset}}function De(e,t,n,r={}){if(!n)throw new y("Missing proposal id","missing_proposal_id");let o=e.get(n);return{...oe(o,r),mine:Ht(o,t)}}function Lc(e){let t={kind:"access-link",linkId:e.id};return typeof e.label=="string"&&e.label&&(t.label=e.label),t}function Wt(e,t,n,r,o,s={}){if(!r)throw new y("Missing proposal id","missing_proposal_id");let i;try{i=e.get(r)}catch(f){throw f instanceof ne,f}if(!Ht(i,n))throw new Ot(r);if(i.status!=="pending")throw new te(r,i.status);let d=typeof o=="string"&&o.trim()?o.replace(/\0/g,"").trim().slice(0,2e3):"Withdrawn by access link",l=Lc(n),c=e.markWithdrawn(r,l,d),u=oe(c,s),p=u.flowLabel||void 0;return t.append("proposal.withdrawn",l,{proposalId:c.id,flowId:c.payload.flowId,title:u.title,displayName:u.displayName,reason:d,...p?{flowLabel:p}:{}}),{...u,mine:!0}}function Cc(e,t,n,r={}){let o;try{o=e.get(t.id)}catch{return{}}if(r.liveRev){V(e,r.liveRev,{flowId:o.payload.flowId});try{o=e.get(t.id)}catch{return{}}}return o.status!=="pending"?{}:Ne(Ie(e,o,n))}function ue(e,t,n,r={},o={}){let s="mine"in n?n:{...oe(n,r),mine:Ht(n,t)},i=0;if(o.liveRev)try{let c=e.get(s.id).payload.flowId;i=V(e,o.liveRev,{flowId:c});let u=e.get(s.id);s={...oe(u,r),mine:Ht(u,t)}}catch{}let d=ce(s,r),l={...d,mine:s.mine,...Q(i)};return o.includeConflicts!==!1&&d.status==="pending"&&Object.assign(l,Cc(e,d,t.id)),l}async function pe(e){let t=await e.getFlows();return{rev:t.rev,flows:t.flows}}async function xe(e,t){if(!t)throw new y("Missing flow id","missing_flow_id");let n=await e.getFlow(t);return{rev:n.rev,flow:$t(t,n.flow)}}function Ec(e){if(typeof e.expectedRev=="string"&&e.expectedRev.length>0)return e.expectedRev;if(typeof e.expected_rev=="string"&&e.expected_rev.length>0)return e.expected_rev}function ui(e,t){if("nodes"in e&&e.nodes!==void 0&&!Array.isArray(e.nodes))throw new y("payload.nodes must be an array of node ops","invalid_nodes");if(t!==void 0&&!Array.isArray(t))throw new y("nodes must be an array of node ops","invalid_nodes")}async function Ic(e,t,n,r,o){let{applyFlowNodeOps:s,interpretPatch:i}=await Promise.resolve().then(()=>(pr(),di));ui(n,r);let d=Array.isArray(n.nodes)?n.nodes:Array.isArray(r)?r:null;if(!d)throw new y("payload.flow is required","missing_flow");let l=n.flowId,c=typeof l=="string"?l:"";if(!c)throw new y("payload.flowId is required","missing_flow_id");let u=i({nodes:d,flowId:c},c);if(u.kind!=="nodes")throw new y("payload.nodes must be node ops","invalid_patch");let p=await s(e,u.flowId,u.ops,{expectedRev:t,...o?{nodeTypes:o}:{}});if(p.expectedRev!==t)throw new J(t,p.expectedRev,"submit_proposal");return{flowId:u.flowId,flow:p.flow}}async function Nc(e,t,n,r,o){let s=n.flowId,i=n.flow,d=!i||typeof i!="object"||Array.isArray(i);if(d&&ui(n,r),d&&(Array.isArray(n.nodes)||Array.isArray(r))){let l=await Ic(e,t,n,r,o);s=l.flowId,i=l.flow}if(typeof s!="string"||s.length===0)throw new y("payload.flowId is required","missing_flow_id");if(!i||typeof i!="object"||Array.isArray(i))throw new y("payload.flow is required","missing_flow");return{flowId:s,flow:i}}function Fc(e,t){try{return{safeExpectedRev:Ee(e.slice(0,200),"expectedRev"),safeFlowId:Ee(t.slice(0,200),"flowId")}}catch{throw new y("expectedRev and payload.flowId must be safe path segments","invalid_path_segment")}}async function Ye(e,t,n,r,o,s={},i){let d=r.type,l=Ec(r),c=r.payload;if(d!=="replace_flow")throw new y("type must be 'replace_flow'","invalid_proposal_type");if(!l)throw new y("expectedRev is required (top-level field next to type and payload)","missing_expected_rev");if(!c||typeof c!="object"||Array.isArray(c))throw new y("payload must be an object","invalid_payload");let p=await Nc(o,l,c,r.nodes,i),{safeExpectedRev:f,safeFlowId:m}=Fc(l,p.flowId),h=p.flow,g=Buffer.byteLength(JSON.stringify(c),"utf8");if(g>j)throw new X("proposal payload",g,j);Ke(Gs(h,m));let S=Ft(h),E=await o.getFlows();if(E.rev!==f)throw new J(f,E.rev,"submit_proposal");Ke(zs(S,m,E.flows));let I=await o.getFlow(m);if(I.rev!==f)throw new J(f,I.rev,"submit_proposal");let N=Ft(I.flow);i&&await Bs(i,S);let{errors:ot,warnings:me}=await Js(S,{flowId:m,baseFlow:N,...i?{nodeTypes:i}:{}});Ke(ot);let A={kind:"access-link",linkId:n.id};n.label!==void 0&&(A.label=n.label);let v=n.oauthClientId&&n.oauthUser?{kind:"oauth",clientId:n.oauthClientId,user:n.oauthUser}:A,w=ur(r.title)??To(N,S),b=e.create({type:"replace_flow",expectedRev:f,payload:{flowId:m,flow:S},baseFlow:N,title:w,createdBy:v,...me.length>0?{warnings:me}:{}}),k=oe(b,s),L=k.flowLabel||void 0;return t.append("proposal.created",A,{proposalId:b.id,flowId:m,expectedRev:f,title:k.title,displayName:k.displayName,...L?{flowLabel:L}:{},...me.length>0?{warningCount:me.length}:{}}),k}function O(e,t="rest"){return _n(e,t)}var Ut,ci,Tc,y,K=U(()=>{"use strict";ve();or();Ge();er();G();Fe();Ao();ut();Xe();Ae();a(Ht,"isMineProposal");Ut=200,ci=new Set(["pending","approved","rejected","failed","withdrawn","expired","superseded"]);a(Co,"matchesStatusFilter");a(Sc,"isProposalStatus");Tc={status:[...ci].join(", "),limit:`integer 1\u2013${Ut}`,offset:"integer \u2265 0",mine:"true, false, 1, 0",flowId:"one flow tab id"};a(Bt,"invalidFilterError");a(fr,"singleQueryValue");a(Pc,"parseIntegerFilter");a(Eo,"isUnset");a(Rc,"parseMineFilter");a(Io,"parseStatusFilter");a(li,"parseRangeFilter");a(mr,"parseListProposalsQuery");a(ft,"listPageFields");a(_c,"applyListPagination");a(qt,"listAccessibleProposals");a(De,"getAccessibleProposal");a(Lc,"accessLinkActor");a(Wt,"withdrawMyProposal");a(Cc,"conflictsForProposal");a(ue,"presentAccessibleProposalForApi");y=class extends Error{static{a(this,"AiOpsValidationError")}statusCode;code;details;constructor(t,n="validation_error",r={}){super(t),this.name="AiOpsValidationError",this.code=n,this.statusCode=r.statusCode??400,r.details&&(this.details=r.details)}};a(pe,"listFlows");a(xe,"getFlow");a(Ec,"resolveExpectedRev");a(ui,"assertNodesArrayOrAbsent");a(Ic,"materializePayloadFromNodes");a(Nc,"resolveReplaceFlowPayload");a(Fc,"assertSafeRevAndFlowId");a(Ye,"submitReplaceFlowProposal");a(O,"aiOpsErrorToHttp")});function No(e){return e==="get_link"?86400:2592e3}function Fo(e,t){return e==="get_link"?`get_link ttlSeconds must be \u2264 ${t} (24 hours)`:`ttlSeconds must be \u2264 ${t} (30 days) for ${e} tokens`}var mt,Ln=U(()=>{"use strict";a(No,"maxTtlSecondsForKind");a(Fo,"ttlCapErrorMessage");mt="ttlSeconds must be a positive number of seconds within the cap for the token kind: get_link \u2264 86400 (24h); rest and mcp \u2264 2592000 (30 days)."});function P(e,t,n,r,o=Jt,s="error"){return{code:e,status:t,meaning:n,hint:r,channels:o,kind:s}}function Cn(e){return $c.get(e)?.hint}function pi(e){return e.replace(/\\/g,"\\\\").replace(/\|/g,"\\|")}function fi(){let e=a(s=>`| \`${s.code}\` | ${s.status} | ${pi(s.meaning)} | ${pi(s.hint)} | ${s.channels.join(", ")} |`,"row"),t=["| Code | HTTP | Meaning | Next step | Channels |","| --- | --- | --- | --- | --- |"],n=hr.filter(s=>s.kind==="error"),r=hr.filter(s=>s.kind==="warning"),o=hr.filter(s=>s.kind==="status");return[...t,...n.map(e),"","**Warnings** (in `warnings[]` on a successful propose; never blocking):","",...t,...r.map(e),"","**Auto-closed proposal statuses.** Pending proposals are closed automatically: `expired` when the live flow rev moves past their `expectedRev`, `superseded` when another proposal on the same flow is approved. Submit, get, and list responses include `autoClosedCount` when that request auto-closed proposals (omitted when zero).","",...t,...o.map(e)].join(`
7
+ `)}var Jt,zt,Oo,Oc,Gt,hr,$c,gr=U(()=>{"use strict";Ln();Jt=["GET","REST","MCP"],zt=["GET"],Oo=["REST","MCP"],Oc=["GET","REST"],Gt=["admin"];a(P,"e");hr=[P("access_link_missing",401,"No Bearer / path token","Ask the user for a new access link of the right kind"),P("bearer_scheme_required",401,"Auth used Basic (or non-Bearer)","Send `Authorization: Bearer <token>`",Oo),P("token_expired",401,"Known token past `expiresAt` (body includes `expiredAt` and `accessLinksUrl`)","Ask the user to create a new link on the `/ai-access` dashboard \u2192 Access tokens (`accessLinksUrl`)"),P("access_link_invalid",401,"Unknown, revoked, or cleared after restart","Ask the user for a new access link"),P("wrong_token_kind",403,"Token kind does not match this surface","Mint the matching kind on `/ai-access` \u2192 Access tokens; show `tellUser` to the human"),P("ai_features_disabled",403,"Plugin AI features off","Owner enables AI Access / `adminAuth`"),P("auth_required",401,"Admin session required","Sign in to the Node-RED editor",Gt),P("insufficient_permissions",403,"Admin lacks required scope","Need `*` for link mint / approve",Gt),P("invalid_link_kind",400,"Unknown `kind` on create","Use `get_link`, `rest`, or `mcp`",Gt),P("invalid_ttl",400,"`ttlSeconds` is zero, negative, not a number, or over the cap for the kind",mt,Gt),P("access_link_limit",409,"Too many active links","Revoke an existing link first",Gt),P("access_link_not_found",404,"Unknown link id on revoke","List links and use an existing id",Gt),P("link_create_not_supported",403,"Attempted mint under a GET link","Admin mints via `POST /mcp/api/v1/links` only",zt),P("rate_limited",429,"Per-IP or per-token limit","Wait `retryAfter` / `Retry-After`"),P("payload_too_large",413,"Body / proposal over ~1.5MB","Shrink content"),P("invalid_json",400,"Malformed JSON body","Fix Content-Type / body",Oo),P("unsupported_media_type",415,"POST body is not `application/json`","Send `Content-Type: application/json`",Oo),P("url_too_long",414,"GET URL over soft cap (~8KB)","Use chunked drafts or REST/MCP",zt),P("not_found",404,"Unknown path or resource","Re-read docs / discovery"),P("missing_query",400,"Required search `q` missing","Pass `?q=`",Oc),P("invalid_filter",400,"Invalid list filter (`status`, `limit`, `offset`, `mine`); body names `parameter` and `allowed`","Use an allowed value: status one of the proposal statuses; limit integer 1\u2013200; offset integer \u2265 0; mine true|false"),P("invalid_arguments",400,"MCP tool arguments failed schema validation; body names `parameter`","Fix the named argument to match the tool input schema (see tools/list)",["MCP"]),P("invalid_diff_format",400,"Unknown `format` on proposal diff","Use `format=summary` (default) or `format=full`"),P("confirm_required",400,"GET withdraw without `confirm=1`","Repeat the request with `confirm=1`",zt),P("missing_expected_rev",400,"Proposal submitted without `expectedRev` / `rev`","Read the flow and pass its `rev`"),P("version_mismatch",409,"Stale `rev` / `expectedRev`","Re-read flows and resubmit"),P("proposal_not_found",404,"Unknown proposal id","List proposals and use an existing id"),P("proposal_not_pending",409,"Action needs a pending proposal","Only pending can withdraw / decide"),P("proposal_not_owned",403,"Withdraw of another token\u2019s proposal","Only the creating token can withdraw"),P("validation_failed",400,"Several validation errors at once (message `N validation errors`)","Fix every entry in errors[] and resubmit."),P("duplicate_node_id",400,"The same node id appears more than once in a full tab or batch","Give every node a unique id"),P("missing_node_id",400,"Node / op without an id","Include a non-empty string id"),P("missing_node_type",400,"Node without a `type`","Set `type` to an installed palette type name"),P("invalid_node_type",400,"`type` is not a string","Use the palette type name exactly"),P("invalid_nodes",400,"`flow.nodes` / `flow.configs` not an array","Send arrays (use `[]` to clear)"),P("invalid_node",400,"Node entry is not an object","Send `{ id, type, \u2026 }` objects"),P("invalid_wires",400,"Bad wires shape, missing target, or more ports than outputs","Wire only to existing node ids / ports"),P("z_mismatch",400,"Node `z` differs from the flow id","Set `z` to the tab id or omit it"),P("node_already_exists",400,"`add` op for an id already in the flow","Use `set` or a new id"),P("protected_property",400,"Tried to change id / type / z","Omit id, type, and z from edits"),P("node_not_found",404,"Unknown node (single error only; several errors \u2192 `validation_failed`)","Re-read the flow"),P("field_not_found",404,"Unknown field on a node","Re-read the node"),P("text_not_found",400,"`replace_text` find had no match","Re-read field; find is exact"),P("ambiguous_match",409,"`replace_text` matched more than once","Pass `occurrence` (1-based) or unique find"),P("unknown_node_type",400,"Proposal uses a type that is not installed","List node-types / palette search; ask user to install"),P("node_type_not_found",404,"Unknown type on node-types detail","List types or search palette"),P("chunk_conflict",409,"Same draft seq re-sent with different data","Re-send identical data or start a new draft",zt),P("missing_chunks",400,"Draft submit with gaps in seq","Upload every seq from 0 through max",zt),P("draft_hash_mismatch",400,"Draft sha256 does not match assembled bytes","Send the hex sha256 of the assembled UTF-8",zt),P("missing_config_node",201,"Warning: node has no (or an unresolved) config node yet","Configure before deploy if needed; see `candidates`",Jt,"warning"),P("unwired_node",201,"Warning: node has outputs but no wires","Wire it, or ignore while scaffolding",Jt,"warning"),P("incomplete_wires",201,"Warning: fewer wire ports than outputs","Add ports if outputs should connect",Jt,"warning"),P("expired",200,"Status: pending proposal auto-closed because the live flow rev moved past `expectedRev`","Re-read the flow and submit a new proposal",Jt,"status"),P("superseded",200,"Status: pending proposal auto-closed because another proposal on the same flow was approved","Re-read the flow and submit a new proposal if still needed",Jt,"status"),P("upstream_error",502,"Node-RED runtime/flows error","Retry; check Node-RED logs"),P("internal_error",500,"Unexpected server error","Retry; check Node-RED logs")],$c=new Map(hr.map(e=>[e.code,e]));a(Cn,"registryHint");a(pi,"cell");a(fi,"renderGetLinkErrorTable")});function se(e,t,n={}){return $o(e,t,n)}function Mc(e,t){return t==="excess_ports"?e==="mcp"?"This node has more wire ports than its type declares \u2014 remove extra ports or raise the node outputs, then retry.":e==="rest"?"This node has more wire ports than its type declares \u2014 trim wires[] or raise outputs, then retry.":"This node has more wire ports than its type declares \u2014 trim wires or raise outputs, then retry.":t==="missing_target"?e==="mcp"?"Wire targets must be node ids in this flow \u2014 add the node in the same patch or use get_flow ids.":e==="rest"?"Wire targets must be node ids in this flow \u2014 add the node in the same patch or use GET /flows/{id} ids.":"wires must list existing node ids (or add those nodes in the same patch).":e==="mcp"||e==="rest"?"Fix wires: targets must exist in this flow, and port count must not exceed the node outputs.":"wires must list existing node ids (or add those nodes in the same patch)."}function Dc(e){return e==="mcp"?"Call list_node_types or search_palette. Ask the user to install via Manage palette, then recheck. This API never installs.":e==="rest"?"Call GET /node-types?q= or GET /palette/search?q=. Ask the user to install via Manage palette, then recheck. This API never installs.":"Call GET \u2026/node-types?q= or \u2026/palette/search?q=. Ask the user to install via Manage palette, then recheck. This API never installs."}function Qe(e,t,n,r){return e==="mcp"?t:e==="rest"?n:r}function $o(e,t,n={}){switch(e){case"version_mismatch":return Qe(t,"Re-read with list_flows/get_flow and retry with the current rev.","Re-read the flow and retry with the current rev.","Re-read the flow and retry with the current rev.");case"node_not_found":return Qe(t,"Call get_flow or get_node and use a node id from that response.","Call GET /flows/{id} and use a node id from that response.","Call GET \u2026/flows/<id> and use a node id from that response.");case"flow_not_found":return Qe(t,"Call list_flows to list valid ids.","Call GET /flows to list valid ids.","Use an id from GET ./flows.");case"proposal_not_found":return Qe(t,"Call list_proposals and use an id from that response.","List proposals and use an id from that response.","List proposals and use an id from that response.");case"proposal_not_owned":return"Only the access link that created a pending proposal can withdraw it.";case"proposal_not_pending":return"Only pending proposals can be withdrawn or decided.";case"confirm_required":return"Add confirm=1 to the withdraw URL (required so link prefetchers cannot withdraw).";case"invalid_wires":return Mc(t,n.cause);case"node_type_not_found":case"unknown_node_type":return Dc(t);case"missing_node_type":return'Each add op needs node.type as a non-empty string (e.g. "inject").';case"invalid_line_range":return Qe(t,'Pass lines as "A" or "A-B" (1-based inclusive). Reversed ranges (end < start) are invalid.',"Use lines=A or lines=A-B (1-based inclusive). Reversed ranges (end < start) are invalid.","Use lines=A or lines=A-B (1-based inclusive). Reversed ranges (end < start) are invalid.");case"line_range_out_of_bounds":return"start is past the end of the field \u2014 check totalLines and request a lower lines range.";case"ambiguous_match":return"find matched more than once \u2014 pass occurrence=N (1-based) or a more unique find string.";case"missing_expected_rev":return Qe(t,"Pass expectedRev from list_flows/get_flow (top-level, next to type and payload).","Pass expectedRev from GET /flows or GET /flows/{id} (top-level, next to type and payload).","Pass rev from GET \u2026/flows or \u2026/flows/<id>.");case"missing_query":return Qe(t,"Pass a non-empty q string.","q query parameter is required.","q query parameter is required.");case"unsupported_media_type":return"Send Content-Type: application/json with a JSON body.";case"text_not_found":return"Re-read the node field; find must match exactly (case-sensitive).";case"field_not_found":return"Re-read the node; field must be an existing string property (template, func, \u2026).";case"field_not_string":return Qe(t,"This tool only returns string fields (template, func, \u2026). Use get_node for other property types.","This endpoint only returns string fields (template, func, \u2026). Use GET /nodes/{id} for other property types.","This endpoint only returns string fields (template, func, \u2026). Use GET /nodes/{id} for other property types.");default:return}}function _n(e,t,n={}){let r=n.canonicalizeMessage!==!1,o=null;if(e instanceof y?o={statusCode:e.statusCode,body:{error:e.message,code:e.code,...e.details??{}}}:e instanceof J?o={statusCode:e.statusCode,body:{error:e.message,code:e.code,expectedRev:e.expectedRev,currentRev:e.currentRev}}:e instanceof z?o={statusCode:e.statusCode,body:{error:e.message,code:e.code,flowId:e.flowId}}:e instanceof ne||e instanceof Ot||e instanceof te||e instanceof X?o={statusCode:e.statusCode,body:{error:e.message,code:e.code}}:(e instanceof Ce||e instanceof be)&&(o={statusCode:e.statusCode,body:{error:e.message,code:e.code}}),!o)return null;let s=o.body.code;if(r&&!Array.isArray(o.body.errors)){let i=Oe[s];i&&(o.body.error=i)}if(typeof o.body.hint!="string"||!o.body.hint){let i=$o(s,t,{cause:o.body.cause})??Cn(s);i&&(o.body.hint=i)}return o}function mi(e,t,n={}){let o={error:n.error??Oe[t]??"Request failed",code:t,...n.details??{}},s=$o(t,e,{cause:o.cause})??Cn(t);return s&&(o.hint=s),{statusCode:n.statusCode??400,body:o}}var Oe,Fe=U(()=>{"use strict";ve();Ge();G();K();gr();Oe={node_not_found:"Node was not found",flow_not_found:"Flow was not found",field_not_found:"Field was not found",field_not_string:"Field is not a string",missing_query:"q query parameter is required",missing_flow_id:"Missing flow id",missing_node_id:"Missing node id",missing_field:"Missing field name",invalid_line_range:"lines must be A or A-B (1-based inclusive; end >= start)",line_range_out_of_bounds:"lines range is out of bounds",proposal_not_found:"Proposal was not found",proposal_not_owned:"Proposal was not created by this access link",proposal_not_pending:"Proposal is not pending",confirm_required:"Withdraw over GET requires confirm=1",unsupported_media_type:"Content-Type must be application/json",upstream_error:"Upstream request failed",internal_error:"Internal server error"};a(se,"channelErrorHint");a(Mc,"invalidWiresHint");a(Dc,"nodeTypeHint");a(Qe,"pickChannel");a($o,"hintFor");a(_n,"mapErrorForChannel");a(mi,"buildChannelError")});var vm={};ds(vm,{ACCESS_LINK_INVALID_MESSAGE:()=>st,ACCESS_LINK_MISSING_MESSAGE:()=>le,AccessLinkStore:()=>At,EDITOR_SIDEBAR_ASSET_VERSION:()=>Xt,MAX_PROPOSAL_JSON_BYTES:()=>j,SettingsStore:()=>Lt,acceptedMediaTypes:()=>wd,areAiFeaturesEnabled:()=>ge,bindSettingsStore:()=>Jn,createPluginServices:()=>Vr,default:()=>bm,getStorageDir:()=>Hr,isAdminAuthEnabled:()=>W,isMcpStreamRequest:()=>bd,packageVersion:()=>Vt,registerRoutes:()=>vd,renderEditorSidebarJs:()=>yr,renderEditorSidebarLoaderHtml:()=>wi,requireAccessLink:()=>Nt,resolveAiAccessSettings:()=>he});module.exports=Pd(vm);var yd=require("fs");var Un=require("fs"),un=require("path");G();function Bn(e){let t=(0,un.join)(e,".ai-access");return{root:t,proposals:(0,un.join)(t,"proposals"),logs:(0,un.join)(t,"logs"),settings:(0,un.join)(t,"config.json")}}a(Bn,"resolveAiAccessPaths");function Qr(e){(0,Un.mkdirSync)(e,{recursive:!0,mode:Yr});try{(0,Un.chmodSync)(e,Yr)}catch{}}a(Qr,"mkdirSecure");function fs(e){Qr(e.root),Qr(e.proposals),Qr(e.logs)}a(fs,"ensureAiAccessDirs");it();_e();function Yn(e){let t=ae(e);return`auth-tokens${(t==="/"?"":t).replace(/\//g,"-")}`}a(Yn,"editorAuthTokenKey");_e();Fe();function jc(e){if(!e)return!1;let t=e.split(";")[0]?.trim().toLowerCase()??"";return t==="application/json"||t.endsWith("+json")}a(jc,"isJsonContentType");function Hc(e){let t=e.headers["content-length"];if(typeof t=="string"&&t.length>0){let n=Number(t);if(Number.isFinite(n)&&n>0)return!0;if(n===0)return!1}return e.headers["transfer-encoding"]!==void 0}a(Hc,"hasRequestBody");function je(e="rest"){return(t,n,r)=>{let o=t.headers["content-type"];if(jc(o)){r();return}if(!o&&!Hc(t)){r();return}let s=mi(e,"unsupported_media_type",{statusCode:415});n.status(s.statusCode).json(s.body)}}a(je,"requireJsonContentType");var Uc="text/plain; charset=utf-8",Bc="text/html; charset=utf-8",qc="default-src 'none'; base-uri 'self'; form-action 'none'; frame-ancestors 'none'";function Wc(e){let t=e.query?.format;return typeof t=="string"?t.toLowerCase():void 0}a(Wc,"formatParam");function zc(e){let t=Wc(e);if(t==="md"||t==="json")return"native";if(!e.get("accept"))return"html";let n=e.accepts(["text/html","application/json","text/markdown","text/plain"]);return n==="application/json"||n==="text/markdown"?"native":n==="text/plain"?"plain":"html"}a(zc,"chooseGetLinkRepresentation");function hi(e){return e.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&#39;")}a(hi,"escapeHtml");var Gc=/(?<![A-Za-z0-9_.~\-/])\.\/[A-Za-z0-9._~\-/]+(?:\?[A-Za-z0-9._~\-=&%]+)?/g;function Jc(e){return e.replace(Gc,(t,n)=>{if(e.startsWith("&lt;",n+t.length))return t;let r=t.replace(/[.]+$/,""),o=t.slice(r.length);return r.length<=2||r.includes("..")||r.includes("//")||r.includes("&")?t:`<a href="${r}">${r}</a>${o}`})}a(Jc,"linkifyEscaped");function Kc(e,t){return`<!doctype html>
8
+ <html><head><meta charset="utf-8">${t?`<base href="${hi(t)}">`:""}<title>Node-RED AI Access</title></head><body><pre>${Jc(hi(e))}</pre></body></html>
9
+ `}a(Kc,"renderGetLinkHtml");function Vc(e){let t=e.split("?")[0]??"",n=/^(.*?\/mcp\/a\/[A-Za-z0-9_-]+)(?:\/|$)/.exec(t);if(!(!n?.[1]||!/^[A-Za-z0-9._~\-/]*$/.test(n[1])))return`${n[1]}/`}a(Vc,"getLinkBaseHref");function Xc(e){try{return JSON.stringify(JSON.parse(e),null,2)}catch{return e}}a(Xc,"prettyJson");function gi(){return(e,t,n)=>{let r=zc(e),o=Vc(e.originalUrl);t.vary("Accept"),t.setHeader("Cache-Control","no-store"),t.setHeader("Referrer-Policy","no-referrer"),t.setHeader("X-Robots-Tag","noindex, nofollow"),t.setHeader("X-Content-Type-Options","nosniff");let s=t.send.bind(t);t.send=(i=>{let d=String(t.getHeader("Content-Type")??"").toLowerCase(),l=d.startsWith("application/json"),c=d.startsWith("text/markdown");if(r==="native"||!l&&!c||t.headersSent)return s(i);let u=typeof i=="string"?i:Buffer.isBuffer(i)?i.toString("utf8"):void 0;return u===void 0?s(i):r==="plain"?(t.setHeader("Content-Type",Uc),s(u)):(t.setHeader("Content-Type",Bc),t.setHeader("Content-Security-Policy",qc),s(Kc(l?Xc(u):u,o)))}),n()}}a(gi,"getLinkContentNegotiation");qn();_e();function Yc(e){let t=de(e.settings.httpAdminRoot),n=qe(e.settings.httpAdminRoot),r="0.2.0-beta.3",o=[{method:"GET",path:"/",auth:"none",summary:"This discovery index"},{method:"GET",path:"/openapi.yaml",auth:"none",summary:"OpenAPI 3.1 specification (YAML)"},{method:"GET",path:"/openapi.json",auth:"none",summary:"OpenAPI 3.1 specification (JSON)"},{method:"GET",path:"/status",auth:"admin-or-access-link",summary:"Plugin status and caller info"},{method:"GET",path:"/flows",auth:"admin-or-access-link",summary:"List flows + rev"},{method:"GET",path:"/flows/{id}",auth:"admin-or-access-link",summary:"Get one flow tab + rev (?summary=1 for node list without field contents)"},{method:"GET",path:"/nodes/{id}",auth:"admin-or-access-link",summary:"Get one full node (optional ?flow=)"},{method:"GET",path:"/nodes/{id}/field/{name}",auth:"admin-or-access-link",summary:"Get one node field (?lines=A-B optional)"},{method:"GET",path:"/search",auth:"admin-or-access-link",summary:"Text search across string fields (?q=&flow=)"},{method:"GET",path:"/node-types",auth:"admin-or-access-link",summary:"List installed palette node types (?q= filter)"},{method:"GET",path:"/node-types/{type}",auth:"admin-or-access-link",summary:"Detail for one installed node type (defaults JSON, inputs/outputs, help; ?help=0 omits help)"},{method:"GET",path:"/palette/search",auth:"admin-or-access-link",summary:"Search Node-RED catalogue (recommend only; never installs) (?q=)"},{method:"POST",path:"/proposals",auth:"access-link",summary:"Create a replace_flow proposal"},{method:"GET",path:"/proposals",auth:"admin-or-access-link",summary:"List proposals (admin: all; link: own)"},{method:"GET",path:"/proposals/{id}",auth:"admin-or-access-link",summary:"Get one proposal"},{method:"GET",path:"/proposals/{id}/diff",auth:"admin-or-access-link",summary:"Diff proposal vs current flow"}];return{ok:!0,name:"node-red-ai-access",version:r,description:"Temporary, owner-approved Node-RED access for AI clients over plain HTTP (REST) or MCP.",auth:{scheme:"Bearer",header:"Authorization: Bearer <token>",description:"An admin with * mints tokens on the /ai-access dashboard (Access tokens tab) (POST /links only \u2014 never under GET /mcp/a/<token>). Each token has kind get_link | rest | mcp and only works on that surface. Send rest/mcp tokens as Authorization: Bearer <token>. Missing/invalid \u2192 401 access_link_missing / access_link_invalid; wrong kind \u2192 403 wrong_token_kind. GET /mcp/a links cannot create tokens.",tokenKinds:["get_link","rest","mcp"],mint:{path:"/links",method:"POST",auth:"admin",note:"Admin * only. Not available under /mcp/a/<token>."}},openapi:`${t}/openapi.yaml`,openapiJson:`${t}/openapi.json`,mcp:n,apiBase:t,getLink:{base:"/mcp/a/<token>",canMintTokens:!1,note:"Self-contained GET surface for get_link tokens only. No POST /links equivalent."},endpoints:o}}a(Yc,"buildDiscoveryIndex");function Mo(e){return(t,n)=>{n.status(200).json(Yc(e))}}a(Mo,"discoveryIndexHandler");function yi(){return(e,t)=>{t.status(404).json({error:"Not found",code:"not_found"})}}a(yi,"mcpApiNotFoundHandler");var Kt;function Vt(){return Kt||(Kt="0.2.0-beta.3",Kt)}a(Vt,"packageVersion");var Xt=Vt();function Qc(){return`#red-ui-sidebar-ai-access {
10
+ position: relative;
10
11
  height: 100%;
11
12
  display: flex;
12
13
  flex-direction: column;
@@ -38,29 +39,41 @@ ${t}`))return{text:t,summary:t};let r=t.split(/\n(?=#{1,3}\s+)/),o="",s,i;for(le
38
39
  margin-bottom: 10px;
39
40
  line-height: 1.4;
40
41
  }
41
- #red-ui-sidebar-ai-access .ai-access-status-block {
42
- margin: 0 0 12px;
43
- padding: 8px 10px;
44
- background: var(--red-ui-secondary-background, #f3f3f3);
42
+ #red-ui-sidebar-ai-access .ai-access-oauth-warning {
43
+ color: #9b2c2c;
44
+ background: #fdf0f0;
45
+ border: 1px solid #f0c4c4;
45
46
  border-radius: 4px;
47
+ padding: 8px 10px;
48
+ margin: 8px 0 4px;
49
+ line-height: 1.4;
46
50
  }
47
- #red-ui-sidebar-ai-access .ai-access-status-block p {
48
- margin: 0 0 6px;
51
+ #red-ui-sidebar-ai-access .ai-access-oauth-warning p {
52
+ margin: 0 0 4px;
49
53
  }
50
- #red-ui-sidebar-ai-access .ai-access-oauth-warning {
51
- color: #9b2c2c;
52
- white-space: pre-wrap;
54
+ #red-ui-sidebar-ai-access .ai-access-oauth-warning ul {
55
+ margin: 0 0 6px 18px;
56
+ padding: 0;
53
57
  }
54
- #red-ui-sidebar-ai-access .ai-access-oauth-client {
55
- display: flex;
56
- align-items: center;
57
- justify-content: space-between;
58
- gap: 8px;
59
- padding: 5px 0;
60
- border-bottom: 1px solid var(--red-ui-secondary-border-color, #ddd);
58
+ #red-ui-sidebar-ai-access .ai-access-oauth-warning li {
59
+ margin: 2px 0;
60
+ overflow-wrap: anywhere;
61
61
  }
62
- #red-ui-sidebar-ai-access .ai-access-oauth-client span {
62
+ #red-ui-sidebar-ai-access .ai-access-oauth-warning code {
63
+ font-family: var(--red-ui-monospace-font, monospace);
64
+ color: var(--red-ui-primary-text-color, #333);
65
+ background: rgba(0, 0, 0, 0.06);
66
+ border: 1px solid rgba(0, 0, 0, 0.08);
67
+ border-radius: 3px;
68
+ padding: 0 4px;
69
+ font-size: 0.95em;
63
70
  overflow-wrap: anywhere;
71
+ word-break: break-all;
72
+ white-space: normal;
73
+ }
74
+ #red-ui-sidebar-ai-access .ai-access-oauth-warning a {
75
+ text-decoration: underline;
76
+ cursor: pointer;
64
77
  }
65
78
  #red-ui-sidebar-ai-access .ai-access-dialog {
66
79
  position: absolute;
@@ -74,68 +87,37 @@ ${t}`))return{text:t,summary:t};let r=t.split(/\n(?=#{1,3}\s+)/),o="",s,i;for(le
74
87
  gap: 8px;
75
88
  margin-top: 16px;
76
89
  }
77
- #red-ui-sidebar-ai-access .ai-access-ttl-presets {
78
- display: flex;
79
- flex-wrap: wrap;
80
- gap: 6px;
81
- margin-bottom: 4px;
82
- }
83
- #red-ui-sidebar-ai-access .ai-access-ttl-presets button[aria-pressed='true'] {
84
- font-weight: 700;
85
- border-color: var(--red-ui-form-input-focus-color, #999);
86
- }
87
90
  #red-ui-sidebar-ai-access .red-ui-settings-row {
88
91
  display: flex;
89
92
  gap: 10px;
90
93
  align-items: flex-start;
91
94
  padding: 5px 0 2px;
92
95
  }
93
- #red-ui-sidebar-ai-access .ai-access-public-url-controls {
94
- display: flex;
95
- gap: 6px;
96
- }
97
- #red-ui-sidebar-ai-access .ai-access-public-url-controls input {
98
- flex: 1;
96
+ #red-ui-sidebar-ai-access .red-ui-settings-row label {
97
+ display: inline-block;
99
98
  min-width: 0;
99
+ flex: 1 1 auto;
100
100
  }
101
- #red-ui-sidebar-ai-access .ai-access-public-url-controls button {
102
- flex: 0 0 auto;
101
+ #red-ui-sidebar-ai-access .red-ui-settings-row label p {
102
+ margin: 0 0 2px;
103
+ font-weight: 600;
103
104
  }
104
105
  #red-ui-sidebar-ai-access .ai-access-actions {
105
- margin-top: 12px;
106
+ margin-top: 14px;
106
107
  display: flex;
107
- flex-wrap: nowrap;
108
- align-items: center;
109
- gap: 8px;
108
+ flex-direction: column;
109
+ align-items: stretch;
110
+ gap: 6px;
110
111
  }
111
112
  #red-ui-sidebar-ai-access .ai-access-actions .red-ui-button {
112
113
  margin: 0;
113
- flex: 1 1 auto;
114
114
  white-space: nowrap;
115
115
  }
116
- #red-ui-sidebar-ai-access .red-ui-settings-row.stacked {
117
- flex-direction: column;
118
- align-items: stretch;
119
- gap: 4px;
120
- }
121
- #red-ui-sidebar-ai-access .red-ui-settings-row label {
122
- display: inline-block;
123
- min-width: 0;
124
- flex: 1 1 auto;
125
- }
126
- #red-ui-sidebar-ai-access .red-ui-settings-row.stacked label {
127
- flex: none;
128
- width: 100%;
129
- }
130
- #red-ui-sidebar-ai-access .red-ui-settings-row.stacked input,
131
- #red-ui-sidebar-ai-access .red-ui-settings-row.stacked select {
132
- width: 100%;
133
- max-width: 100%;
134
- box-sizing: border-box;
135
- }
136
- #red-ui-sidebar-ai-access .red-ui-settings-row label p {
137
- margin: 0 0 2px;
138
- font-weight: 600;
116
+ #red-ui-sidebar-ai-access .ai-access-muted {
117
+ color: var(--red-ui-secondary-text-color, #888);
118
+ font-size: 0.85em;
119
+ line-height: 1.35;
120
+ margin: 0;
139
121
  }
140
122
  /* Compact toast in the header \u2014 must not cover the intro description. */
141
123
  #red-ui-sidebar-ai-access #ai-access-flash {
@@ -175,19 +157,20 @@ ${t}`))return{text:t,summary:t};let r=t.split(/\n(?=#{1,3}\s+)/),o="",s,i;for(le
175
157
  color: #9b2c2c;
176
158
  background: #fdecec;
177
159
  }
178
- /* After fade: strip chrome so no empty green/red outline remains. */
179
160
  #red-ui-sidebar-ai-access #ai-access-flash:not(.is-visible) {
180
161
  padding: 0;
181
162
  border: 0;
182
163
  background: transparent;
183
164
  color: transparent;
184
- }`}a(Uc,"editorSidebarStyles");function Bc(){return`
165
+ }`}a(Qc,"editorSidebarStyles");function Zc(){return`
185
166
  /**
186
167
  * Editor right-hand sidebar tab: AI Access.
187
168
  * Served from /mcp/editor/ai-access-sidebar.js (no-store + ?v= cache-bust).
188
169
  *
189
- * RED.sidebar.addTab (Node-RED 4.x):
190
- * - id, label, name, iconClass, content (jQuery/DOM), onchange, action
170
+ * Since 0.2.0-beta.3 the sidebar holds only: the AI Access toggle, the MCP
171
+ * OAuth toggle, the OAuth route-conflict warning (with RED.view.reveal links),
172
+ * and a button opening the dashboard (<httpAdminRoot>/ai-access). Every other
173
+ * setting lives on the dashboard.
191
174
  */
192
175
  (function () {
193
176
  if (typeof RED === 'undefined' || !RED.sidebar || !RED.sidebar.addTab) {
@@ -208,13 +191,6 @@ ${t}`))return{text:t,summary:t};let r=t.split(/\n(?=#{1,3}\s+)/),o="",s,i;for(le
208
191
  var ADMIN_AUTH = true;
209
192
  var LAST = null;
210
193
  var dialogOpen = false;
211
- var FIELD_IDS = {
212
- enabled: 'enabled', allowWithoutAdminAuth: 'allow-without',
213
- defaultLinkTtlSeconds: 'ttl', defaultDeploymentType: 'deploy',
214
- maxActiveLinks: 'max-links', publicBaseUrl: 'public-url',
215
- corsAllowedOrigins: 'cors', rateLimitWindowMs: 'rate-window',
216
- rateLimitMaxPerIp: 'rate-ip', rateLimitMaxPerToken: 'rate-token'
217
- };
218
194
 
219
195
  function authHeaders() {
220
196
  var headers = { Accept: 'application/json', 'Content-Type': 'application/json' };
@@ -237,19 +213,18 @@ ${t}`))return{text:t,summary:t};let r=t.split(/\n(?=#{1,3}\s+)/),o="",s,i;for(le
237
213
  return headers;
238
214
  }
239
215
 
240
- function resolveApiBase() {
241
- if (API_BASE) return API_BASE;
216
+ function adminRoot() {
242
217
  var root = RED.settings.httpAdminRoot || '/';
243
- if (!root || root === '/') {
244
- API_BASE = '/mcp/api/v1';
245
- } else {
246
- API_BASE = root.replace(/\\/$/, '') + '/mcp/api/v1';
247
- }
218
+ return !root || root === '/' ? '' : root.replace(/\\/$/, '');
219
+ }
220
+
221
+ function resolveApiBase() {
222
+ if (!API_BASE) API_BASE = adminRoot() + '/mcp/api/v1';
248
223
  return API_BASE;
249
224
  }
250
225
 
251
- function mcpPageUrl(hash) {
252
- var url = (LAST && LAST.mcpPageUrl) || '/mcp';
226
+ function dashboardUrl(hash) {
227
+ var url = (LAST && LAST.mcpPageUrl) || adminRoot() + '/ai-access';
253
228
  if (hash) {
254
229
  return url.split('#')[0] + '#' + hash.replace(/^#/, '');
255
230
  }
@@ -310,6 +285,8 @@ ${t}`))return{text:t,summary:t};let r=t.split(/\n(?=#{1,3}\s+)/),o="",s,i;for(le
310
285
  dialogOpen = true;
311
286
  $('#ai-access-dialog-message').text(message);
312
287
  $('#ai-access-dialog').show();
288
+ var allowBtn = document.getElementById('ai-access-dialog-allow');
289
+ if (allowBtn && allowBtn.focus) allowBtn.focus();
313
290
  return new Promise(function (resolve) {
314
291
  function finish(accepted) {
315
292
  $('#ai-access-dialog').hide();
@@ -323,130 +300,89 @@ ${t}`))return{text:t,summary:t};let r=t.split(/\n(?=#{1,3}\s+)/),o="",s,i;for(le
323
300
  });
324
301
  }
325
302
 
326
- function hoursFromSeconds(seconds) {
327
- var s = Number(seconds);
328
- if (!Number.isFinite(s) || s <= 0) return 4;
329
- return Math.round(s / 3600);
330
- }
331
-
332
- function selectedTtlHours() {
333
- var pressed = $('#' + CONTENT_ID + ' .ai-access-ttl-presets button[aria-pressed="true"]');
334
- if (pressed.length) {
335
- return Number(pressed.attr('data-hours')) || 4;
336
- }
337
- return Number($('#ai-access-ttl').val()) || 4;
338
- }
339
-
340
- function setTtlHours(hours) {
341
- var h = Number(hours);
342
- if (!Number.isFinite(h) || h <= 0) h = 4;
343
- $('#ai-access-ttl').val(h);
344
- $('#' + CONTENT_ID + ' .ai-access-ttl-presets button').each(function () {
345
- var btn = $(this);
346
- btn.attr('aria-pressed', Number(btn.attr('data-hours')) === h ? 'true' : 'false');
347
- });
348
- }
349
-
350
303
  function applyForm(data) {
351
304
  LAST = data;
352
305
  CAN_EDIT = !!(data && data.canEdit);
353
306
  ADMIN_AUTH = !!(data && data.adminAuthEnabled);
354
307
  var s = (data && data.settings) || {};
308
+ var sources = (data && data.sources) || {};
309
+ var readonly = !CAN_EDIT;
355
310
 
356
- $('#ai-access-enabled').prop('checked', s.enabled !== false);
357
- $('#ai-access-allow-without').prop('checked', !!s.allowWithoutAdminAuth);
358
- setTtlHours(hoursFromSeconds(s.defaultLinkTtlSeconds || 14400));
359
- $('#ai-access-deploy').val(s.defaultDeploymentType || 'nodes');
360
- $('#ai-access-full-warn').toggle($('#ai-access-deploy').val() === 'full');
361
-
311
+ $('#ai-access-enabled').prop('checked', s.enabled !== false)
312
+ .prop('disabled', readonly || sources.enabled === 'settings.js');
313
+ $('#ai-access-allow-without').prop('checked', !!s.allowWithoutAdminAuth)
314
+ .prop('disabled', readonly || sources.allowWithoutAdminAuth === 'settings.js');
362
315
  $('#ai-access-adminauth-row').toggle(ADMIN_AUTH);
363
316
  $('#ai-access-local-row').toggle(!ADMIN_AUTH);
364
- $('#ai-access-local-warn').toggle(!ADMIN_AUTH);
365
317
 
366
- var readonly = !CAN_EDIT;
367
- $('#' + CONTENT_ID + ' input, #' + CONTENT_ID + ' select').prop('disabled', readonly);
368
- $('#' + CONTENT_ID + ' .ai-access-ttl-presets button').prop('disabled', readonly);
369
- $('#ai-access-readonly-note').toggle(readonly);
370
- ['maxActiveLinks', 'publicBaseUrl', 'rateLimitWindowMs', 'rateLimitMaxPerIp', 'rateLimitMaxPerToken'].forEach(function (key) {
371
- $('#ai-access-' + FIELD_IDS[key]).val(s[key] == null ? '' : s[key]);
372
- });
373
- $('#ai-access-cors').val((s.corsAllowedOrigins || []).join(', '));
374
- $('#ai-access-trust-proxy').text(String(data.trustProxy));
375
- Object.keys(FIELD_IDS).forEach(function (key) {
376
- var locked = data.sources && data.sources[key] === 'settings.js';
377
- $('#ai-access-' + FIELD_IDS[key]).prop('disabled', readonly || locked);
378
- $('#ai-access-lock-' + FIELD_IDS[key]).toggle(!!locked);
379
- });
380
- $('#' + CONTENT_ID + ' .ai-access-ttl-presets button').prop('disabled', readonly || (data.sources && data.sources.defaultLinkTtlSeconds === 'settings.js'));
381
- $('#ai-access-check-public-url').prop('disabled', readonly);
382
318
  var oauth = s.oauth || {};
383
- $('#ai-access-oauth-enabled').prop('checked', !!oauth.enabled);
384
- setDuration('access-ttl', oauth.accessTokenTtlSec || 3600, OAUTH_ACCESS_UNITS);
385
- setDuration('refresh-ttl', oauth.refreshIdleTtlSec || 2592000, OAUTH_REFRESH_UNITS);
386
- $('#ai-access-oauth-refresh-max').val(Math.round((typeof oauth.refreshMaxLifetimeSec === 'number' ? oauth.refreshMaxLifetimeSec : 7776000) / 86400));
387
- $('#ai-access-oauth-refresh-max-unit').val('days');
388
- $('#ai-access-oauth-dcr-cap').val(oauth.dcrMaxClients || 20);
389
- var oauthLocked = readonly || (data.sources && data.sources.oauth === 'settings.js');
390
- $('#ai-access-oauth-enabled, #ai-access-oauth-access-ttl, #ai-access-oauth-access-ttl-unit, #ai-access-oauth-refresh-ttl, #ai-access-oauth-refresh-ttl-unit, #ai-access-oauth-refresh-max, #ai-access-oauth-refresh-max-unit, #ai-access-oauth-dcr-cap').prop('disabled', oauthLocked);
391
- $('#ai-access-oauth-lock').toggle(!!(data.sources && data.sources.oauth === 'settings.js'));
392
- }
393
-
394
- var OAUTH_ACCESS_UNITS = [['minutes', 60, 'Minutes'], ['hours', 3600, 'Hours']];
395
- var OAUTH_REFRESH_UNITS = [['hours', 3600, 'Hours'], ['days', 86400, 'Days']];
396
- var OAUTH_MAX_UNITS = [['days', 86400, 'Days']];
397
-
398
- // Show seconds as the largest unit that divides them evenly (smallest unit otherwise).
399
- function setDuration(field, seconds, units) {
400
- var unit = units[0];
401
- for (var i = units.length - 1; i >= 0; i--) {
402
- if (seconds % units[i][1] === 0) { unit = units[i]; break; }
403
- }
404
- $('#ai-access-oauth-' + field).val(Math.max(1, Math.round(seconds / unit[1])));
405
- $('#ai-access-oauth-' + field + '-unit').val(unit[0]);
406
- }
407
-
408
- function readDuration(field, units) {
409
- var amount = Number($('#ai-access-oauth-' + field).val());
410
- var name = $('#ai-access-oauth-' + field + '-unit').val();
411
- var mult = units[0][1];
412
- units.forEach(function (u) { if (u[0] === name) mult = u[1]; });
413
- return Math.round(amount * mult);
319
+ $('#ai-access-oauth-enabled').prop('checked', !!oauth.enabled)
320
+ .prop('disabled', readonly || sources.oauth === 'settings.js');
321
+ $('#ai-access-readonly-note').toggle(readonly);
322
+ $('#ai-access-lock-enabled').toggle(sources.enabled === 'settings.js');
323
+ $('#ai-access-lock-allow-without').toggle(sources.allowWithoutAdminAuth === 'settings.js');
324
+ $('#ai-access-oauth-lock').toggle(sources.oauth === 'settings.js');
414
325
  }
415
326
 
416
327
  function refreshOAuth() {
417
328
  api('/oauth/status').then(function (status) {
418
- var warnings = [];
419
- if (status.httpNodeRootFallback) warnings.push('httpNodeRoot is disabled: OAuth metadata is served under the admin /mcp path.');
420
- if (status.authorization && status.authorization.length) warnings.push('OAuth authorization routes collide with: ' + status.authorization.join(', '));
421
- if (status.metadata && status.metadata.length) warnings.push('OAuth metadata routes collide with: ' + status.metadata.join(', '));
422
- $('#ai-access-oauth-route-warning').text(warnings.join('\\n')).toggle(warnings.length > 0);
423
- }).catch(function (err) {
424
- $('#ai-access-oauth-route-warning').text('OAuth route status unavailable: ' + err.message).show();
329
+ renderRouteWarning(status);
330
+ }).catch(function () {
331
+ $('#ai-access-oauth-route-warning').empty().hide();
425
332
  });
426
- api('/oauth/clients').then(function (data) {
427
- var list = $('#ai-access-oauth-clients').empty();
428
- var clients = (data && data.clients) || [];
429
- if (!clients.length) {
430
- $('<p></p>').text('No registered OAuth clients.').appendTo(list);
431
- return;
333
+ }
334
+
335
+ function collisionItem(c) {
336
+ var li = $('<li></li>');
337
+ var effect = c.kind === 'authorization' ? ' \u2014 OAuth login/token routes disabled' : ' \u2014 OAuth metadata disabled';
338
+ if (c.source === 'http-in') {
339
+ $('<span></span>').text('Route ').appendTo(li);
340
+ $('<code></code>').text(c.path).appendTo(li);
341
+ $('<span></span>').text(' is already used by http-in node ').appendTo(li);
342
+ var label = "'" + (c.nodeName || c.nodeId || 'unnamed') + "'";
343
+ if (c.nodeId && RED.view && RED.view.reveal) {
344
+ $('<a href="#"></a>').text(label).attr('title', 'Show node in the editor').appendTo(li)
345
+ .on('click', function (e) { e.preventDefault(); RED.view.reveal(c.nodeId); });
346
+ } else {
347
+ $('<span></span>').text(label).appendTo(li);
432
348
  }
433
- clients.forEach(function (client) {
434
- var row = $('<div class="ai-access-oauth-client"></div>').appendTo(list);
435
- $('<span></span>').text((client.client_name || 'Unnamed client') + ' (' + client.client_id + ')').appendTo(row);
436
- $('<button type="button" class="red-ui-button"></button>').text('Revoke').prop('disabled', !CAN_EDIT)
437
- .appendTo(row).on('click', function () {
438
- openConfirmDialog('Revoke OAuth client "' + (client.client_name || client.client_id) + '" and its tokens?')
439
- .then(function (accepted) {
440
- if (!accepted) return;
441
- api('/oauth/clients/' + encodeURIComponent(client.client_id), { method: 'DELETE' })
442
- .then(function () { setFlash($('#ai-access-flash'), 'OAuth client revoked.', true); refreshOAuth(); })
443
- .catch(function (err) { setFlash($('#ai-access-flash'), err.message || String(err), false); });
444
- });
445
- });
349
+ } else if (c.source === 'httpStatic') {
350
+ $('<span></span>').text('File ').appendTo(li);
351
+ $('<code></code>').text(c.path).appendTo(li);
352
+ $('<span></span>').text(' is served by httpStatic').appendTo(li);
353
+ } else if (c.source === 'middleware') {
354
+ $('<span></span>').text('Path ').appendTo(li);
355
+ $('<code></code>').text(c.path).appendTo(li);
356
+ $('<span></span>').text(' is already handled by other middleware (' + (c.layer || 'unknown') + ')').appendTo(li);
357
+ } else {
358
+ $('<span></span>').text('Route ').appendTo(li);
359
+ $('<code></code>').text(c.path).appendTo(li);
360
+ $('<span></span>').text(' is already registered by another node or plugin').appendTo(li);
361
+ }
362
+ $('<span></span>').text(effect).appendTo(li);
363
+ return li;
364
+ }
365
+
366
+ // Only shown when real conflicts exist (spec: sidebar item 3).
367
+ function renderRouteWarning(status) {
368
+ var box = $('#ai-access-oauth-route-warning').empty();
369
+ var details = status.details;
370
+ if (!details) {
371
+ details = [];
372
+ (status.authorization || []).forEach(function (t) { details.push({ kind: 'authorization', source: 'text', text: t }); });
373
+ (status.metadata || []).forEach(function (t) { details.push({ kind: 'metadata', source: 'text', text: t }); });
374
+ }
375
+ if (details.length) {
376
+ var intro = $('<p></p>').appendTo(box);
377
+ $('<strong></strong>').text('OAuth route conflict').appendTo(intro);
378
+ $('<span></span>').text(': these paths are already in use, so AI Access will not serve them. Remove or change the conflicting routes, then deploy.').appendTo(intro);
379
+ var ul = $('<ul></ul>').appendTo(box);
380
+ details.forEach(function (c) {
381
+ if (c.source === 'text') $('<li></li>').text(c.text).appendTo(ul);
382
+ else collisionItem(c).appendTo(ul);
446
383
  });
447
- }).catch(function (err) {
448
- $('#ai-access-oauth-clients').text('OAuth clients unavailable: ' + err.message);
449
- });
384
+ }
385
+ box.toggle(!!details.length);
450
386
  }
451
387
 
452
388
  function oauthPublicUrlWarning(value) {
@@ -462,58 +398,11 @@ ${t}`))return{text:t,summary:t};let r=t.split(/\n(?=#{1,3}\s+)/),o="",s,i;for(le
462
398
  }
463
399
  }
464
400
 
465
- function formatPendingBadge(live, stale) {
466
- var parts = [];
467
- if (live > 0) parts.push(live + ' to review');
468
- if (stale > 0) parts.push(stale + ' out of date');
469
- return parts.length ? parts.join(' \xB7 ') : '0';
470
- }
471
-
472
- function pendingSidebarText(live, stale, total) {
473
- if (total === 0) return '0 proposals waiting for review';
474
- if (total === 1 && stale === 0) return '1 proposal to review';
475
- return formatPendingBadge(live, stale);
476
- }
477
-
478
- function refreshPending() {
479
- api('/proposals')
480
- .then(function (data) {
481
- var proposals = (data && data.proposals) || [];
482
- var pending = proposals.filter(function (p) {
483
- return p.status === 'pending';
484
- });
485
- var stale = pending.filter(function (p) {
486
- return !!p.baseRevStale;
487
- }).length;
488
- var live = pending.length - stale;
489
- $('#ai-access-pending-line').text(pendingSidebarText(live, stale, pending.length));
490
- $('#ai-access-status-block').show();
491
- })
492
- .catch(function () {
493
- $('#ai-access-pending-line').text('\u2014');
494
- });
495
- }
496
-
497
- // /mcp page posts here after approve so the sidebar badge updates immediately.
498
- try {
499
- if (typeof BroadcastChannel !== 'undefined') {
500
- var pendingChannel = new BroadcastChannel('ai-access-pending');
501
- pendingChannel.onmessage = function (ev) {
502
- if (ev && ev.data && ev.data.type === 'refresh-pending') {
503
- refreshPending();
504
- }
505
- };
506
- }
507
- } catch (e) {
508
- /* ignore */
509
- }
510
-
511
401
  function loadSettings() {
512
402
  setFlash($('#ai-access-flash'), '', true);
513
403
  api('/settings')
514
404
  .then(function (data) {
515
405
  applyForm(data);
516
- refreshPending();
517
406
  refreshOAuth();
518
407
  })
519
408
  .catch(function (err) {
@@ -521,9 +410,7 @@ ${t}`))return{text:t,summary:t};let r=t.split(/\n(?=#{1,3}\s+)/),o="",s,i;for(le
521
410
  });
522
411
  }
523
412
 
524
- var saveTimer = null;
525
413
  var saving = false;
526
-
527
414
  var savedToastTimer = null;
528
415
 
529
416
  function showSavedToast() {
@@ -536,80 +423,44 @@ ${t}`))return{text:t,summary:t};let r=t.split(/\n(?=#{1,3}\s+)/),o="",s,i;for(le
536
423
  }, 2000);
537
424
  }
538
425
 
539
- function saveSettings(opts) {
540
- var silent = !!(opts && opts.silent);
426
+ // Partial PUT: only the toggles the sidebar owns (server merges the rest).
427
+ function saveSettings() {
541
428
  if (!CAN_EDIT || saving || dialogOpen) return;
542
- var payload = {
543
- enabled: $('#ai-access-enabled').is(':checked'),
544
- allowWithoutAdminAuth: $('#ai-access-allow-without').is(':checked'),
545
- defaultLinkTtlSeconds: Math.max(1, Math.floor(selectedTtlHours() * 3600)),
546
- defaultDeploymentType: $('#ai-access-deploy').val(),
547
- maxActiveLinks: Number($('#ai-access-max-links').val()),
548
- publicBaseUrl: String($('#ai-access-public-url').val()).trim(),
549
- corsAllowedOrigins: String($('#ai-access-cors').val()).split(',').map(function (s) { return s.trim(); }).filter(Boolean),
550
- rateLimitWindowMs: Number($('#ai-access-rate-window').val()),
551
- rateLimitMaxPerIp: Number($('#ai-access-rate-ip').val()),
552
- rateLimitMaxPerToken: Number($('#ai-access-rate-token').val()),
553
- oauth: {
554
- enabled: $('#ai-access-oauth-enabled').is(':checked'),
555
- accessTokenTtlSec: readDuration('access-ttl', OAUTH_ACCESS_UNITS),
556
- refreshIdleTtlSec: readDuration('refresh-ttl', OAUTH_REFRESH_UNITS),
557
- refreshMaxLifetimeSec: readDuration('refresh-max', OAUTH_MAX_UNITS),
558
- dcrMaxClients: Number($('#ai-access-oauth-dcr-cap').val()),
559
- },
560
- };
561
- Object.keys(payload).forEach(function (key) {
562
- if (LAST && LAST.sources && LAST.sources[key] === 'settings.js') delete payload[key];
563
- });
564
- // Persist the max lifetime only when the user changed it, so the default can evolve.
565
- var loadedOauth = (LAST && LAST.settings && LAST.settings.oauth) || {};
566
- var loadedMax = typeof loadedOauth.refreshMaxLifetimeSec === 'number' ? loadedOauth.refreshMaxLifetimeSec : 7776000;
567
- if (payload.oauth && Math.round(payload.oauth.refreshMaxLifetimeSec / 86400) === Math.round(loadedMax / 86400)) {
568
- delete payload.oauth.refreshMaxLifetimeSec;
569
- }
429
+ var sources = (LAST && LAST.sources) || {};
430
+ var payload = {};
431
+ if (sources.enabled !== 'settings.js') payload.enabled = $('#ai-access-enabled').is(':checked');
432
+ if (sources.allowWithoutAdminAuth !== 'settings.js') payload.allowWithoutAdminAuth = $('#ai-access-allow-without').is(':checked');
433
+ if (sources.oauth !== 'settings.js') payload.oauth = { enabled: $('#ai-access-oauth-enabled').is(':checked') };
570
434
  if (LAST && LAST.settings && !(LAST.settings.oauth && LAST.settings.oauth.enabled) && payload.oauth && payload.oauth.enabled) {
571
- var warning = oauthPublicUrlWarning(payload.publicBaseUrl === undefined ? LAST.settings.publicBaseUrl : payload.publicBaseUrl);
435
+ var warning = oauthPublicUrlWarning(LAST.settings.publicBaseUrl);
572
436
  if (warning) {
573
- openConfirmDialog(warning + '\\n\\nEnable OAuth anyway?').then(function (accepted) {
574
- if (accepted) persistSettings(payload, silent);
437
+ openConfirmDialog(warning + '\\n\\nEnable OAuth anyway? (Set the Public URL on the AI Access dashboard.)').then(function (accepted) {
438
+ if (accepted) persistSettings(payload);
575
439
  else $('#ai-access-oauth-enabled').prop('checked', false);
576
440
  });
577
441
  return;
578
442
  }
579
443
  }
580
- persistSettings(payload, silent);
444
+ persistSettings(payload);
581
445
  }
582
446
 
583
- function persistSettings(payload, silent) {
447
+ function persistSettings(payload) {
584
448
  saving = true;
585
449
  api('/settings', { method: 'PUT', body: JSON.stringify(payload) })
586
450
  .then(function (data) {
587
451
  applyForm(data);
588
- if (data.warnings && data.warnings.length) {
589
- $('#ai-access-check-result').text(data.warnings.join(' '));
590
- }
591
- if (silent) showSavedToast();
592
- else setFlash($('#ai-access-flash'), 'Settings saved.', true);
593
- refreshPending();
452
+ showSavedToast();
594
453
  refreshOAuth();
595
454
  })
596
455
  .catch(function (err) {
597
456
  setFlash($('#ai-access-flash'), err.message || String(err), false);
457
+ loadSettings();
598
458
  })
599
459
  .then(function () {
600
460
  saving = false;
601
461
  });
602
462
  }
603
-
604
- function scheduleAutoSave() {
605
- if (!CAN_EDIT) return;
606
- if (saveTimer) clearTimeout(saveTimer);
607
- saveTimer = setTimeout(function () {
608
- saveTimer = null;
609
- saveSettings({ silent: true });
610
- }, 400);
611
- }
612
- `}a(Bc,"editorSidebarScriptPartA");function qc(){return`
463
+ `}a(Zc,"editorSidebarScriptPartA");function eu(){return`
613
464
  function buildContent() {
614
465
  var content = $('<div></div>', { id: CONTENT_ID, class: 'red-ui-sidebar-ai-access' });
615
466
 
@@ -623,7 +474,7 @@ ${t}`))return{text:t,summary:t};let r=t.split(/\n(?=#{1,3}\s+)/),o="",s,i;for(le
623
474
  .on('click', loadSettings);
624
475
 
625
476
  var body = $('<div class="red-ui-sidebar-ai-access-body"></div>').appendTo(content);
626
- var dialog = $('<div id="ai-access-dialog" class="ai-access-dialog" role="dialog" aria-modal="true" aria-labelledby="ai-access-dialog-title" style="display:none"></div>').appendTo(body);
477
+ var dialog = $('<div id="ai-access-dialog" class="ai-access-dialog" role="dialog" aria-modal="true" aria-labelledby="ai-access-dialog-title" style="display:none"></div>').appendTo(content);
627
478
  $('<h3 id="ai-access-dialog-title"></h3>').text('Confirm OAuth action').appendTo(dialog);
628
479
  $('<p id="ai-access-dialog-message"></p>').appendTo(dialog);
629
480
  var dialogActions = $('<div class="ai-access-dialog-actions"></div>').appendTo(dialog);
@@ -631,35 +482,25 @@ ${t}`))return{text:t,summary:t};let r=t.split(/\n(?=#{1,3}\s+)/),o="",s,i;for(le
631
482
  $('<button type="button" id="ai-access-dialog-allow" class="red-ui-button"></button>').text('Continue').appendTo(dialogActions);
632
483
 
633
484
  $('<div class="red-ui-settings-section-description"></div>')
634
- .text('Configure temporary AI access features. Changes apply immediately (no restart).')
485
+ .text('Changes apply immediately. Tokens, proposals, and all other settings are on the AI Access dashboard.')
635
486
  .appendTo(body);
636
487
 
637
488
  $('<p id="ai-access-readonly-note" class="form-tips" style="display:none;"></p>')
638
489
  .text('Read-only: full admin permission (*) is required to change these settings.')
639
490
  .appendTo(body);
640
491
 
641
- var status = $(
642
- '<div id="ai-access-status-block" class="ai-access-status-block" style="display:none;"></div>'
643
- ).appendTo(body);
644
- $('<p id="ai-access-pending-line"></p>')
645
- .text('0 proposals waiting for review')
646
- .appendTo(status);
647
- $('<button type="button" id="ai-access-review" class="red-ui-button"></button>')
648
- .text('Review')
649
- .appendTo(status)
650
- .on('click', function () {
651
- window.open(mcpPageUrl('proposals'), '_blank');
652
- });
653
-
654
492
  var row;
655
493
 
494
+ // (1) Enable AI Access features (with adminAuth) / local-only opt-in (without).
656
495
  row = $('<div class="red-ui-settings-row" id="ai-access-adminauth-row"></div>').appendTo(body);
657
496
  $('<input id="ai-access-enabled" type="checkbox">').appendTo(row);
658
497
  $('<label for="ai-access-enabled"></label>')
659
498
  .html(
660
- '<p>Enable AI Access features</p>When off, access links, proposals, and AI tools are disabled.'
499
+ '<p>Enable AI Access features</p>When off, access tokens, proposals, and AI tools are disabled.'
661
500
  )
662
501
  .appendTo(row);
502
+ $('<span id="ai-access-lock-enabled" class="form-tips" style="display:none"></span>')
503
+ .text('\u{1F512} settings.js').appendTo(row);
663
504
 
664
505
  row = $(
665
506
  '<div class="red-ui-settings-row" id="ai-access-local-row" style="display:none;"></div>'
@@ -667,121 +508,36 @@ ${t}`))return{text:t,summary:t};let r=t.split(/\n(?=#{1,3}\s+)/),o="",s,i;for(le
667
508
  $('<input id="ai-access-allow-without" type="checkbox">').appendTo(row);
668
509
  $('<label for="ai-access-allow-without"></label>')
669
510
  .html(
670
- '<p>Allow AI Access without adminAuth (local-only)</p>Not for production. Anyone who can reach the editor admin port can use AI features.'
671
- )
672
- .appendTo(row);
673
-
674
- $('<div id="ai-access-local-warn" class="form-tips" style="display:none;color:#b33;"></div>')
675
- .text(
676
- 'Warning: adminAuth is disabled. Enabling this opt-in exposes AI Access without login.'
511
+ '<p>Enable AI Access features without adminAuth (local-only)</p>Not for production. Anyone who can reach the editor admin port can use AI features.'
677
512
  )
678
- .appendTo(body);
679
-
680
- row = $('<div class="red-ui-settings-row stacked"></div>').appendTo(body);
681
- $('<label for="ai-access-deploy"></label>').text('Default deploy type').appendTo(row);
682
- var select = $('<select id="ai-access-deploy"></select>').appendTo(row);
683
- $('<option value="nodes">Only changed nodes (recommended)</option>').appendTo(select);
684
- $('<option value="flows">Changed flows</option>').appendTo(select);
685
- $('<option value="full">Full redeploy</option>').appendTo(select);
686
- $('<p id="ai-access-full-warn" class="form-tips" style="display:none;color:#b33;"></p>')
687
- .text('Warning: full restarts the entire runtime.')
688
513
  .appendTo(row);
689
- select.on('change', function () {
690
- $('#ai-access-full-warn').toggle(select.val() === 'full');
691
- });
514
+ $('<span id="ai-access-lock-allow-without" class="form-tips" style="display:none"></span>')
515
+ .text('\u{1F512} settings.js').appendTo(row);
692
516
 
693
- row = $('<div class="red-ui-settings-row stacked"></div>').appendTo(body);
694
- $('<label for="ai-access-ttl"></label>').text('Default link expiry').appendTo(row);
695
- var presets = $(
696
- '<div class="ai-access-ttl-presets" role="group" aria-label="Link expiry"></div>'
697
- ).appendTo(row);
698
- [1, 4, 24].forEach(function (h) {
699
- $('<button type="button" class="red-ui-button"></button>')
700
- .attr('data-hours', String(h))
701
- .attr('aria-pressed', h === 4 ? 'true' : 'false')
702
- .text(h === 1 ? '1 hour' : h + ' hours')
703
- .appendTo(presets)
704
- .on('click', function () {
705
- setTtlHours(h);
706
- scheduleAutoSave();
707
- });
708
- });
709
- $('<input id="ai-access-ttl" type="hidden" value="4">').appendTo(row);
710
-
711
- [
712
- ['max-links', 'Maximum active links', 'number'],
713
- ['public-url', 'Public URL (HTTPS origin)', 'text'],
714
- ['cors', 'CORS allowed origins (comma separated)', 'text'],
715
- ['rate-window', 'Rate limit window (ms)', 'number'],
716
- ['rate-ip', 'Requests per IP per window', 'number'],
717
- ['rate-token', 'Requests per token per window', 'number']
718
- ].forEach(function (field) {
719
- var id = 'ai-access-' + field[0];
720
- var fieldRow = $('<div class="red-ui-settings-row stacked"></div>').appendTo(body);
721
- $('<label></label>').attr('for', id).text(field[1]).appendTo(fieldRow);
722
- var controls = field[0] === 'public-url' ? $('<div class="ai-access-public-url-controls"></div>').appendTo(fieldRow) : fieldRow;
723
- var input = $('<input>').attr('id', id).attr('type', field[2]).appendTo(controls);
724
- if (field[2] === 'number') input.attr('min', '1').attr('step', 'any');
725
- input.on('change', scheduleAutoSave);
726
- if (field[0] === 'public-url') {
727
- $('<button type="button" id="ai-access-check-public-url" class="red-ui-button"></button>')
728
- .text('Check').appendTo(controls).on('click', function () {
729
- $('#ai-access-check-result').text('Checking\u2026');
730
- api('/settings/check-public-url', { method: 'POST', body: JSON.stringify({ publicBaseUrl: $('#ai-access-public-url').val() }) })
731
- .then(function (result) {
732
- $('#ai-access-check-result').text((result.ok ? 'OK: ' : 'Failed: ') + (result.message || result.error) + (result.status ? ' (HTTP ' + result.status + ')' : ''));
733
- }).catch(function (err) { $('#ai-access-check-result').text(err.message); });
734
- });
735
- $('<p class="form-tips"></p>').text("Loopback checks use Node-RED's listening port; behind a reverse proxy, use the internal port. Unix-socket listeners are not supported. HTTPS checks fail with self-signed certificates.").appendTo(fieldRow);
736
- $('<p id="ai-access-check-result" aria-live="polite"></p>').appendTo(fieldRow);
737
- }
738
- });
739
- var trustRow = $('<div class="red-ui-settings-row stacked"></div>').appendTo(body);
740
- $('<label></label>').text('Express trust proxy (read-only)').appendTo(trustRow);
741
- $('<span id="ai-access-trust-proxy"></span>').appendTo(trustRow);
742
- $('<h3></h3>').text('MCP OAuth').appendTo(body);
743
- $('<p class="form-tips"></p>').text('Clients can request read or propose access through OAuth. Changes apply immediately.').appendTo(body);
517
+ // (2) Enable MCP OAuth.
744
518
  row = $('<div class="red-ui-settings-row"></div>').appendTo(body);
745
519
  $('<input id="ai-access-oauth-enabled" type="checkbox">').appendTo(row);
746
- $('<label for="ai-access-oauth-enabled"></label>').text('Enable MCP OAuth').appendTo(row);
520
+ $('<label for="ai-access-oauth-enabled"></label>')
521
+ .html('<p>Enable MCP OAuth</p>MCP clients (ChatGPT, Claude, \u2026) sign in at &lt;Public URL&gt;/mcp.')
522
+ .appendTo(row);
747
523
  $('<span id="ai-access-oauth-lock" class="form-tips" style="display:none"></span>')
748
- .text('\u{1F512} Locked by settings.js').appendTo(row);
749
- [
750
- ['access-ttl', 'Access token lifetime', OAUTH_ACCESS_UNITS],
751
- ['refresh-ttl', 'Refresh token idle lifetime', OAUTH_REFRESH_UNITS],
752
- ['refresh-max', 'Refresh token max lifetime (default 90 days, 0 = unlimited)', OAUTH_MAX_UNITS],
753
- ['dcr-cap', 'Maximum registered OAuth clients', null]
754
- ].forEach(function (field) {
755
- var id = 'ai-access-oauth-' + field[0];
756
- var oauthRow = $('<div class="red-ui-settings-row stacked"></div>').appendTo(body);
757
- $('<label></label>').attr('for', id).text(field[1]).appendTo(oauthRow);
758
- $('<input type="number" step="1" style="width:80px">').attr('id', id).attr('min', field[0] === 'refresh-max' ? 0 : 1).appendTo(oauthRow).on('change', scheduleAutoSave);
759
- if (!field[2]) return;
760
- var select = $('<select></select>').attr('id', id + '-unit').attr('aria-label', field[1] + ' unit')
761
- .appendTo(oauthRow).on('change', scheduleAutoSave);
762
- field[2].forEach(function (u) { $('<option></option>').attr('value', u[0]).text(u[2]).appendTo(select); });
763
- });
764
- $('<p id="ai-access-oauth-route-warning" class="form-tips ai-access-oauth-warning" role="status" style="display:none"></p>').appendTo(body);
765
- $('<h4></h4>').text('Registered OAuth clients').appendTo(body);
766
- $('<div id="ai-access-oauth-clients" aria-live="polite"></div>').appendTo(body);
767
- Object.keys(FIELD_IDS).forEach(function (key) {
768
- var id = FIELD_IDS[key];
769
- $('<span class="form-tips" style="display:none"></span>')
770
- .attr('id', 'ai-access-lock-' + id).text('\u{1F512} Locked by settings.js')
771
- .appendTo(content.find('#ai-access-' + id).closest('.red-ui-settings-row'));
772
- });
524
+ .text('\u{1F512} settings.js').appendTo(row);
525
+
526
+ // (3) OAuth route conflict warning \u2014 only rendered when conflicts exist.
527
+ $('<div id="ai-access-oauth-route-warning" class="form-tips ai-access-oauth-warning" role="status" style="display:none"></div>').appendTo(body);
773
528
 
774
- content.find('#ai-access-enabled, #ai-access-allow-without, #ai-access-deploy, #ai-access-oauth-enabled').on(
529
+ content.find('#ai-access-enabled, #ai-access-allow-without, #ai-access-oauth-enabled').on(
775
530
  'change',
776
- scheduleAutoSave
531
+ saveSettings
777
532
  );
778
533
 
779
- row = $('<div class="red-ui-settings-row ai-access-actions"></div>').appendTo(body);
534
+ // (4) Open the dashboard.
535
+ row = $('<div class="ai-access-actions"></div>').appendTo(body);
780
536
  $('<button type="button" id="ai-access-open-mcp" class="red-ui-button"></button>')
781
537
  .text('Open AI Access dashboard')
782
538
  .appendTo(row)
783
539
  .on('click', function () {
784
- window.open(mcpPageUrl(), '_blank');
540
+ window.open(dashboardUrl(), '_blank');
785
541
  });
786
542
 
787
543
  return content;
@@ -806,8 +562,8 @@ ${t}`))return{text:t,summary:t};let r=t.split(/\n(?=#{1,3}\s+)/),o="",s,i;for(le
806
562
  },
807
563
  });
808
564
  })();
809
- `}a(qc,"editorSidebarScriptPartB");function Wc(){return Bc()+qc()}a(Wc,"editorSidebarScriptBody");function hr(){let e=JSON.stringify(Uc());return[`/* @com55/node-red-ai-access editor sidebar v=${Jt} */`,"(function(){"," if (typeof document === 'undefined') return;"," if (!document.getElementById('com55-ai-access-sidebar-css')) {"," var style = document.createElement('style');"," style.id = 'com55-ai-access-sidebar-css';",` style.textContent = ${e};`," document.head.appendChild(style);"," }","})();",Wc()].join(`
810
- `)}a(hr,"renderEditorSidebarJs");function pi(e=Jt){return`<script>
565
+ `}a(eu,"editorSidebarScriptPartB");function tu(){return Zc()+eu()}a(tu,"editorSidebarScriptBody");function yr(){let e=JSON.stringify(Qc());return[`/* @com55/node-red-ai-access editor sidebar v=${Xt} */`,"(function(){"," if (typeof document === 'undefined') return;"," if (!document.getElementById('com55-ai-access-sidebar-css')) {"," var style = document.createElement('style');"," style.id = 'com55-ai-access-sidebar-css';",` style.textContent = ${e};`," document.head.appendChild(style);"," }","})();",tu()].join(`
566
+ `)}a(yr,"renderEditorSidebarJs");function wi(e=Xt){return`<script>
811
567
  /**
812
568
  * Thin loader for the AI Access editor sidebar.
813
569
  * Logic lives at GET /mcp/editor/ai-access-sidebar.js (Cache-Control: no-store)
@@ -841,8 +597,8 @@ ${t}`))return{text:t,summary:t};let r=t.split(/\n(?=#{1,3}\s+)/),o="",s,i;for(le
841
597
  (document.head || document.documentElement).appendChild(s);
842
598
  })();
843
599
  </script>
844
- `}a(pi,"renderEditorSidebarLoaderHtml");G();Ne();function fi(e){return Fe[e]??"Request failed"}a(fi,"safeErrorText");function Gc(e,t){let n=N(t,"rest");if(!n)return!1;let r=n.body.code,o={error:fi(r),code:r};return typeof n.body.hint=="string"&&(o.hint=n.body.hint),typeof n.body.expectedRev=="string"&&(o.expectedRev=n.body.expectedRev),typeof n.body.currentRev=="string"&&(o.currentRev=n.body.currentRev),e.setHeader("X-Content-Type-Options","nosniff"),e.status(n.statusCode).json(o),!0}a(Gc,"sendMappedFlowError");function mi(e){return async(t,n)=>{try{n.json(await pe(e))}catch(r){if(Gc(n,r))return;n.setHeader("X-Content-Type-Options","nosniff"),n.status(502).json({error:fi("upstream_error"),code:"upstream_error"})}}}a(mi,"listFlowsHandler");ae();G();Pe();Ne();function Vt(e){return e==null||e===""?{ok:!0,format:"summary"}:e==="summary"||e==="full"?{ok:!0,format:e}:{ok:!1}}a(Vt,"resolveDiffFormat");function zc(e,t=2){if(e.length===0)return{lines:e,truncated:!1};let n=new Array(e.length).fill(!1);for(let i=0;i<e.length;i+=1){let d=e[i];if(!d||d.op==="equal")continue;let l=Math.max(0,i-t),c=Math.min(e.length-1,i+t);for(let u=l;u<=c;u+=1)n[u]=!0}if(!n.some(Boolean)){let i=Math.min(e.length,t*2+1);for(let d=0;d<i;d+=1)n[d]=!0}let r=[],o=!1,s=0;for(;s<e.length;){if(n[s]){let l=e[s];l&&r.push(l),s+=1;continue}let i=s;for(;i<e.length&&!n[i];)i+=1;let d=i-s;o=!0,r.push({op:"equal",text:`\u2026 (${d} unchanged line${d===1?"":"s"}) \u2026`}),s=i}return{lines:r,truncated:o}}a(zc,"compactLineDiff");function Jc(e,t){let n=e.before??"",r=e.after??"",o=Buffer.byteLength(n,"utf8"),s=Buffer.byteLength(r,"utf8");if(t==="full")return{...e,before:n,after:r,beforeBytes:o,afterBytes:s};let i=zc(e.lines),d={key:e.key,kind:"text",beforeBytes:o,afterBytes:s,lines:i.lines};return i.truncated&&(d.truncated=!0),d}a(Jc,"formatTextChange");function Vc(e,t){return e.map(n=>n.kind==="text"?Jc(n,t):n)}a(Vc,"formatPropertyChanges");function hi(e,t){if(t==="full")return e;let n=a(r=>{if(typeof r!="string")return r;let o=Buffer.byteLength(r,"utf8");return o<=4e3?r:`${r.slice(0,500)}
845
- \u2026 (${o} bytes total; use ?format=full for complete text) \u2026`},"trimText");return{...e,keyFields:e.keyFields.map(r=>r.kind==="text"?{...r,value:n(r.value)}:r),properties:e.properties.map(r=>r.kind==="text"?{...r,value:n(r.value)}:r)}}a(hi,"formatPresenceText");function gi(e,t){return{summary:e.summary,added:e.added.map(n=>hi(n,t)),removed:e.removed.map(n=>hi(n,t)),modified:e.modified.map(n=>({...n,changes:Vc(n.changes,t)}))}}a(gi,"formatSections");function Kt(e,t="summary"){let n=gi(e,t),r={...e,...n,format:t};return e.vsCurrent&&(r.vsCurrent=gi(e.vsCurrent,t)),r}a(Kt,"applyDiffFormat");q();ae();mr();function Xt(e){return e.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&#39;")}a(Xt,"escapeForDocs");var yi="https://YOUR-NODE-RED/mcp/rpc";function Kc(e,t,n,r=yi){return`## Also available (prefer when your client can send headers)
600
+ `}a(wi,"renderEditorSidebarLoaderHtml");K();Fe();function bi(e){return Oe[e]??"Request failed"}a(bi,"safeErrorText");function nu(e,t){let n=O(t,"rest");if(!n)return!1;let r=n.body.code,o={error:bi(r),code:r};return typeof n.body.hint=="string"&&(o.hint=n.body.hint),typeof n.body.expectedRev=="string"&&(o.expectedRev=n.body.expectedRev),typeof n.body.currentRev=="string"&&(o.currentRev=n.body.currentRev),e.setHeader("X-Content-Type-Options","nosniff"),e.status(n.statusCode).json(o),!0}a(nu,"sendMappedFlowError");function vi(e){return async(t,n)=>{try{n.json(await pe(e))}catch(r){if(nu(n,r))return;n.setHeader("X-Content-Type-Options","nosniff"),n.status(502).json({error:bi("upstream_error"),code:"upstream_error"})}}}a(vi,"listFlowsHandler");ie();K();_e();Fe();function Yt(e){return e==null||e===""?{ok:!0,format:"summary"}:e==="summary"||e==="full"?{ok:!0,format:e}:{ok:!1}}a(Yt,"resolveDiffFormat");function ru(e,t=2){if(e.length===0)return{lines:e,truncated:!1};let n=new Array(e.length).fill(!1);for(let i=0;i<e.length;i+=1){let d=e[i];if(!d||d.op==="equal")continue;let l=Math.max(0,i-t),c=Math.min(e.length-1,i+t);for(let u=l;u<=c;u+=1)n[u]=!0}if(!n.some(Boolean)){let i=Math.min(e.length,t*2+1);for(let d=0;d<i;d+=1)n[d]=!0}let r=[],o=!1,s=0;for(;s<e.length;){if(n[s]){let l=e[s];l&&r.push(l),s+=1;continue}let i=s;for(;i<e.length&&!n[i];)i+=1;let d=i-s;o=!0,r.push({op:"equal",text:`\u2026 (${d} unchanged line${d===1?"":"s"}) \u2026`}),s=i}return{lines:r,truncated:o}}a(ru,"compactLineDiff");function ou(e,t){let n=e.before??"",r=e.after??"",o=Buffer.byteLength(n,"utf8"),s=Buffer.byteLength(r,"utf8");if(t==="full")return{...e,before:n,after:r,beforeBytes:o,afterBytes:s};let i=ru(e.lines),d={key:e.key,kind:"text",beforeBytes:o,afterBytes:s,lines:i.lines};return i.truncated&&(d.truncated=!0),d}a(ou,"formatTextChange");function su(e,t){return e.map(n=>n.kind==="text"?ou(n,t):n)}a(su,"formatPropertyChanges");function ki(e,t){if(t==="full")return e;let n=a(r=>{if(typeof r!="string")return r;let o=Buffer.byteLength(r,"utf8");return o<=4e3?r:`${r.slice(0,500)}
601
+ \u2026 (${o} bytes total; use ?format=full for complete text) \u2026`},"trimText");return{...e,keyFields:e.keyFields.map(r=>r.kind==="text"?{...r,value:n(r.value)}:r),properties:e.properties.map(r=>r.kind==="text"?{...r,value:n(r.value)}:r)}}a(ki,"formatPresenceText");function Ai(e,t){return{summary:e.summary,added:e.added.map(n=>ki(n,t)),removed:e.removed.map(n=>ki(n,t)),modified:e.modified.map(n=>({...n,changes:su(n.changes,t)}))}}a(Ai,"formatSections");function Qt(e,t="summary"){let n=Ai(e,t),r={...e,...n,format:t};return e.vsCurrent&&(r.vsCurrent=Ai(e.vsCurrent,t)),r}a(Qt,"applyDiffFormat");G();ie();gr();function Zt(e){return e.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&#39;")}a(Zt,"escapeForDocs");var xi="https://YOUR-NODE-RED/mcp/rpc";function iu(e,t,n,r=xi){return`## Also available (prefer when your client can send headers)
846
602
 
847
603
  **Prefer MCP or REST** whenever the AI client can send custom headers and JSON
848
604
  bodies \u2014 no URL length limits and no chunked drafts. Use this **GET link
@@ -854,7 +610,7 @@ from the Access links page (\`/mcp\` \u2192 Access links \u2192 create \`kind=re
854
610
  \`kind=mcp\`).
855
611
 
856
612
  **What to tell the user** (copy/paste when you get \`wrong_token_kind\`):
857
- > Please open Node-RED \u2192 /mcp \u2192 Access links and create a kind=rest or kind=mcp
613
+ > Please open Node-RED \u2192 /ai-access \u2192 Access tokens and create a kind=rest or kind=mcp
858
614
  > token (or a get_link URL if that is what I asked for), then send it to me.
859
615
 
860
616
  - REST: \`${e}\` with \`Authorization: Bearer <rest-token>\` (**rest** kind \u2014
@@ -911,7 +667,7 @@ curl -sS -X POST "${r}" \\
911
667
  -H "Accept: application/json, text/event-stream" \\
912
668
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
913
669
  \`\`\`
914
- `}a(Kc,"docsAlsoAvailable");function Xc(e,t){return`# Node-RED AI Access \u2014 self-contained GET link (get_link)${e}
670
+ `}a(iu,"docsAlsoAvailable");function au(e,t){return`# Node-RED AI Access \u2014 self-contained GET link (get_link)${e}
915
671
 
916
672
  **Git mental model:** a proposal is like a pull request. Human approve = merge;
917
673
  reject = close; withdraw = you close your own PR. If \`rev\`/\`expectedRev\` does
@@ -943,7 +699,7 @@ same either way.
943
699
  - Link expires at: **${t}** (ISO-8601); \`GET ./status\` also reports
944
700
  \`caller.minutesRemaining\` (rounded up) and \`caller.secondsRemaining\`
945
701
  - After expiry, every path under this link returns \`401 token_expired\` with
946
- \`expiredAt\` and \`accessLinksUrl\` (the /mcp page \u2192 Access links section)
702
+ \`expiredAt\` and \`accessLinksUrl\` (the /ai-access dashboard \u2192 Access tokens tab)
947
703
  - Ask the user for a **new** link there if you see that error (or \`access_link_invalid\`)
948
704
 
949
705
  ## What you can do
@@ -968,7 +724,7 @@ when it does not name the flow), \`storedTitle\` (the title as saved: your title
968
724
  \` in <flow>\` suffix, or the auto-generated title, which may already name the flow),
969
725
  \`flowLabel\`, \`displayName\` (\`title \xB7\` **server-local**
970
726
  datetime with offset, e.g. \`16 Oct 01:35 +00:00\`), \`reviewUrl\` (absolute when
971
- Host is known), and \`reviewPath\` (relative \`/mcp#proposal=<id>\`).
727
+ Host is known), and \`reviewPath\` (relative \`/ai-access#proposal=<id>\`).
972
728
  \`createdAt\` is ISO-8601. **Always tell the user about the proposal using
973
729
  \`displayName\` + \`reviewUrl\` \u2014 not the raw id.**
974
730
 
@@ -986,7 +742,7 @@ fields at the top level** (no \`ok\` / \`proposal\` wrapper). Read
986
742
 
987
743
  Lean by default (no \`payload\` / \`baseFlow\`). Pass
988
744
  \`?include=payload,baseFlow\` or \`?include_payload=true\` to include them.
989
- `}a(Xc,"docsIntro");function Yc(){return"## Step-by-step workflow\n\n1. `GET ./status` \u2014 confirm the link works; note `caller.expiresAt` /\n `caller.minutesRemaining`.\n2. `GET ./flows` \u2014 list tabs + current `rev`.\n3. **Layered read (prefer this before loading whole tabs):**\n - `GET ./flows/<id>?summary=1` \u2014 node list with `id`/`type`/`name`/`wires` +\n per-node `bytes` (no field contents).\n - `GET ./search?q=<text>&flow=<id>` \u2014 hits with node id, field, line numbers,\n short snippet (`truncated:true` when the result cap is hit).\n - `GET ./nodes/<id>` \u2014 one full node (`flowId` included when known).\n - `GET ./nodes/<id>/field/<name>?lines=A-B` \u2014 one field (optional 1-based\n line range) plus `totalLines` and `bytes`.\n4. Prefer a **targeted text replace** for small edits inside long string fields\n (`template`, `func`, \u2026) \u2014 unique `find` (or `occurrence`) keeps the diff short:\n\n```\nGET ./propose?flow=<flowId>&rev=<rev>&node=<nodeId>&field=<prop>&find=<text>&replace=<text>&occurrence=<n>&title=<text>\n```\n\n5. Or set whole properties (optional `title` \u226480 chars; `summary` is a title fallback):\n\n```\nGET ./propose?flow=<flowId>&rev=<rev>&node=<nodeId>&set.<prop>=<urlencoded>&title=<text>\n```\n\n6. Or multi-node / add / remove / `replace_text` / full tab via base64url JSON patch:\n\n```\nGET ./propose?flow=<flowId>&rev=<rev>&patch=<base64url-json>&title=<text>\n```\n\n7. Poll `GET ./proposals` / `./proposals/<id>` until approved, rejected, or\n withdrawn. Every token can list/read **all** proposals; each item has\n `mine:true|false`. Filter with `?mine=true` (own) or `?mine=false` (others'),\n `?status=pending,superseded` (comma-separated or repeated), `?flowId=`;\n paginate with `?limit=` (1\u2013200) / `?offset=` (response includes `total`,\n `offset`, `hasMore`, and `nextOffset` when more remain). Invalid filters\n return `400 invalid_filter` naming the parameter. Cite `displayName` +\n `reviewUrl` when updating the user.\n8. Optional: `GET ./proposals/<id>/diff` (default `format=summary` compact\n line hunks + byte sizes; `format=full` for complete text) against the\n **base snapshot** stored at submit. Added/removed nodes are presence blocks\n with a summary sentence; pending+stale may include `vsCurrent`.\n9. Withdraw your own pending proposal:\n `GET ./proposals/<id>/withdraw?confirm=1&reason=<text>` (status becomes `withdrawn`,\n not `rejected`). Only the link that created a proposal can withdraw it;\n others get `403 proposal_not_owned`.\n\n**Recommended edit loop:** summary \u2192 search \u2192 read a line range \u2192\n`replace_text` \u2192 propose.\n\nOn submit and on read of a pending proposal, the response may include\n`conflictingProposals` (same-rev or overlapping pending work; list omits this) with\n`overlappingNodeIds` / `overlappingFields` when detectable \u2014 treat like a\ngit conflict warning; prefer one pending change per flow when possible.\n\n## Node types and palette (read-only)\n\nBefore proposing unfamiliar nodes:\n\n1. `GET ./node-types?q=<text>` \u2014 list installed palette types (filter optional;\n results capped; may include `truncated` / `total`).\n2. `GET ./node-types/<type>` \u2014 detail (`defaults` as JSON, `inputs`/`outputs`,\n `requiredConfigNodeTypes`, help sections). Pass `?help=0` to omit help.\n Types are **case-sensitive** (`Debug` \u2192 did you mean `debug`?).\n3. `GET ./palette/search?q=<text>` \u2014 search the public Node-RED catalogue.\n **Recommend only** \u2014 ask the user to install via **Manage palette**, then\n recheck `./node-types`. This API **never installs** packages.\n Core `node-red` types are **not** in the catalogue \u2014 use `./node-types`.\n\nSubmit/propose with an unknown `node.type` returns `400 unknown_node_type`\nwith `nearest` suggestions \u2014 call node-types / palette search and ask the user\nto install the module, then retry.\n"}a(Yc,"docsWorkflow");function Qc(){return'## Exact URLs for this token (relative)\n\n| Purpose | Method + path |\n| --- | --- |\n| These docs | `GET ./` (this URL) |\n| Status | `GET ./status` |\n| List flows | `GET ./flows` |\n| One flow | `GET ./flows/<id>` |\n| Flow summary (no field bodies) | `GET ./flows/<id>?summary=1` |\n| One node | `GET ./nodes/<id>` |\n| One field (optional lines) | `GET ./nodes/<id>/field/<name>?lines=A-B` |\n| Text search | `GET ./search?q=&flow=` |\n| List node types | `GET ./node-types?q=` |\n| One node type | `GET ./node-types/<type>` (`?help=0` omits help) |\n| Palette search (recommend only) | `GET ./palette/search?q=` |\n| All proposals (`?mine=` / `?status=` / `?flowId=` / `?limit=` / `?offset=`) | `GET ./proposals` |\n| One proposal | `GET ./proposals/<id>` |\n| Proposal diff | `GET ./proposals/<id>/diff` |\n| Withdraw own pending | `GET ./proposals/<id>/withdraw?confirm=1&reason=` |\n| Propose (query) | `GET ./propose?\u2026` |\n| Draft start | `GET ./draft/new?flow=&rev=&node=&field=&title=` |\n| Draft chunk | `GET ./draft/<draftId>/append?seq=N&data=<base64url>` |\n| Draft submit | `GET ./draft/<draftId>/submit?sha256=<hex>&title=` |\n\nThere is **no** `POST ./links` (or any create-link) under this URL \u2014 GET links\n**cannot mint tokens**.\n\nUnknown paths under this link return JSON `{ "code": "not_found" }` (never HTML,\nand the token is never echoed). Attempted mint paths (`./links`) return\n`403 link_create_not_supported`.\n'}a(Qc,"docsUrlTable");function Zc(){return`## Propose examples
745
+ `}a(au,"docsIntro");function du(){return"## Step-by-step workflow\n\n1. `GET ./status` \u2014 confirm the link works; note `caller.expiresAt` /\n `caller.minutesRemaining`.\n2. `GET ./flows` \u2014 list tabs + current `rev`.\n3. **Layered read (prefer this before loading whole tabs):**\n - `GET ./flows/<id>?summary=1` \u2014 node list with `id`/`type`/`name`/`wires` +\n per-node `bytes` (no field contents).\n - `GET ./search?q=<text>&flow=<id>` \u2014 hits with node id, field, line numbers,\n short snippet (`truncated:true` when the result cap is hit).\n - `GET ./nodes/<id>` \u2014 one full node (`flowId` included when known).\n - `GET ./nodes/<id>/field/<name>?lines=A-B` \u2014 one field (optional 1-based\n line range) plus `totalLines` and `bytes`.\n4. Prefer a **targeted text replace** for small edits inside long string fields\n (`template`, `func`, \u2026) \u2014 unique `find` (or `occurrence`) keeps the diff short:\n\n```\nGET ./propose?flow=<flowId>&rev=<rev>&node=<nodeId>&field=<prop>&find=<text>&replace=<text>&occurrence=<n>&title=<text>\n```\n\n5. Or set whole properties (optional `title` \u226480 chars; `summary` is a title fallback):\n\n```\nGET ./propose?flow=<flowId>&rev=<rev>&node=<nodeId>&set.<prop>=<urlencoded>&title=<text>\n```\n\n6. Or multi-node / add / remove / `replace_text` / full tab via base64url JSON patch:\n\n```\nGET ./propose?flow=<flowId>&rev=<rev>&patch=<base64url-json>&title=<text>\n```\n\n7. Poll `GET ./proposals` / `./proposals/<id>` until approved, rejected, or\n withdrawn. Every token can list/read **all** proposals; each item has\n `mine:true|false`. Filter with `?mine=true` (own) or `?mine=false` (others'),\n `?status=pending,superseded` (comma-separated or repeated), `?flowId=`;\n paginate with `?limit=` (1\u2013200) / `?offset=` (response includes `total`,\n `offset`, `hasMore`, and `nextOffset` when more remain). Invalid filters\n return `400 invalid_filter` naming the parameter. Cite `displayName` +\n `reviewUrl` when updating the user.\n8. Optional: `GET ./proposals/<id>/diff` (default `format=summary` compact\n line hunks + byte sizes; `format=full` for complete text) against the\n **base snapshot** stored at submit. Added/removed nodes are presence blocks\n with a summary sentence; pending+stale may include `vsCurrent`.\n9. Withdraw your own pending proposal:\n `GET ./proposals/<id>/withdraw?confirm=1&reason=<text>` (status becomes `withdrawn`,\n not `rejected`). Only the link that created a proposal can withdraw it;\n others get `403 proposal_not_owned`.\n\n**Recommended edit loop:** summary \u2192 search \u2192 read a line range \u2192\n`replace_text` \u2192 propose.\n\nOn submit and on read of a pending proposal, the response may include\n`conflictingProposals` (same-rev or overlapping pending work; list omits this) with\n`overlappingNodeIds` / `overlappingFields` when detectable \u2014 treat like a\ngit conflict warning; prefer one pending change per flow when possible.\n\n## Node types and palette (read-only)\n\nBefore proposing unfamiliar nodes:\n\n1. `GET ./node-types?q=<text>` \u2014 list installed palette types (filter optional;\n results capped; may include `truncated` / `total`).\n2. `GET ./node-types/<type>` \u2014 detail (`defaults` as JSON, `inputs`/`outputs`,\n `requiredConfigNodeTypes`, help sections). Pass `?help=0` to omit help.\n Types are **case-sensitive** (`Debug` \u2192 did you mean `debug`?).\n3. `GET ./palette/search?q=<text>` \u2014 search the public Node-RED catalogue.\n **Recommend only** \u2014 ask the user to install via **Manage palette**, then\n recheck `./node-types`. This API **never installs** packages.\n Core `node-red` types are **not** in the catalogue \u2014 use `./node-types`.\n\nSubmit/propose with an unknown `node.type` returns `400 unknown_node_type`\nwith `nearest` suggestions \u2014 call node-types / palette search and ask the user\nto install the module, then retry.\n"}a(du,"docsWorkflow");function lu(){return'## Exact URLs for this token (relative)\n\n| Purpose | Method + path |\n| --- | --- |\n| These docs | `GET ./` (this URL) |\n| Status | `GET ./status` |\n| List flows | `GET ./flows` |\n| One flow | `GET ./flows/<id>` |\n| Flow summary (no field bodies) | `GET ./flows/<id>?summary=1` |\n| One node | `GET ./nodes/<id>` |\n| One field (optional lines) | `GET ./nodes/<id>/field/<name>?lines=A-B` |\n| Text search | `GET ./search?q=&flow=` |\n| List node types | `GET ./node-types?q=` |\n| One node type | `GET ./node-types/<type>` (`?help=0` omits help) |\n| Palette search (recommend only) | `GET ./palette/search?q=` |\n| All proposals (`?mine=` / `?status=` / `?flowId=` / `?limit=` / `?offset=`) | `GET ./proposals` |\n| One proposal | `GET ./proposals/<id>` |\n| Proposal diff | `GET ./proposals/<id>/diff` |\n| Withdraw own pending | `GET ./proposals/<id>/withdraw?confirm=1&reason=` |\n| Propose (query) | `GET ./propose?\u2026` |\n| Draft start | `GET ./draft/new?flow=&rev=&node=&field=&title=` |\n| Draft chunk | `GET ./draft/<draftId>/append?seq=N&data=<base64url>` |\n| Draft submit | `GET ./draft/<draftId>/submit?sha256=<hex>&title=` |\n\nThere is **no** `POST ./links` (or any create-link) under this URL \u2014 GET links\n**cannot mint tokens**.\n\nUnknown paths under this link return JSON `{ "code": "not_found" }` (never HTML,\nand the token is never echoed). Attempted mint paths (`./links`) return\n`403 link_create_not_supported`.\n'}a(lu,"docsUrlTable");function cu(){return`## Propose examples
990
746
 
991
747
  **Recommended for small edits to long fields** \u2014 \`replace_text\` (find must be
992
748
  **unique** unless you pass \`occurrence\` as a 1-based index). Creates a normal
@@ -1064,11 +820,11 @@ GET ./propose?flow=tab1&rev=abc123&patch=<base64url>
1064
820
  Node-level proposes create a normal \`replace_flow\` proposal; owners should
1065
821
  approve with deployment type **\`nodes\`** (plugin default) so only changed
1066
822
  nodes deploy.
1067
- `}a(Zc,"docsProposeExamples");function eu(){return`## Chunked drafts (long HTML / large fields)
823
+ `}a(cu,"docsProposeExamples");function uu(){return`## Chunked drafts (long HTML / large fields)
1068
824
 
1069
- When the GET URL would exceed **${Lt} bytes**, do not
825
+ When the GET URL would exceed **${It} bytes**, do not
1070
826
  stuff everything into \`propose\`. Use drafts (expire after **30 minutes**,
1071
- max **${$}** decoded bytes):
827
+ max **${j}** decoded bytes):
1072
828
 
1073
829
  ### Recommended chunk size
1074
830
 
@@ -1113,7 +869,7 @@ Or a full \`replace_flow\` document. JSON-mode drafts **may add nodes**, so
1113
869
  \`draft/new\` does not require an existing \`node\`/\`field\`. Use JSON mode for
1114
870
  structural changes; use field-mode only when you need to replace one whole
1115
871
  string field that is too large for a single propose URL.
1116
- `}a(eu,"docsDrafts");function tu(){return`## Last resort: content larger than chunked drafts
872
+ `}a(uu,"docsDrafts");function pu(){return`## Last resort: content larger than chunked drafts
1117
873
 
1118
874
  If the field is still too large for chunked drafts (decoded size over the
1119
875
  proposal cap), use this fallback only after drafts fail:
@@ -1127,9 +883,9 @@ proposal cap), use this fallback only after drafts fail:
1127
883
  **Trade-off:** pasted content is **not** reviewed through the proposal diff.
1128
884
  Prefer \`replace_text\` or chunked drafts whenever they fit so the human can
1129
885
  still see a readable change on \`/mcp\`.
1130
- `}a(tu,"docsLastResortPaste");function nu(){return`## Size cap and REST/MCP nodes payload shapes
886
+ `}a(pu,"docsLastResortPaste");function fu(){return`## Size cap and REST/MCP nodes payload shapes
1131
887
 
1132
- The proposal JSON cap is **${$} bytes**
888
+ The proposal JSON cap is **${j} bytes**
1133
889
  (\`MAX_PROPOSAL_JSON_BYTES\` / ~1.5MB) on **every** channel: GET propose/drafts,
1134
890
  REST \`POST /proposals\`, and MCP \`submit_proposal\`. Oversized bodies return
1135
891
  \`413 payload_too_large\` (never truncated).
@@ -1173,7 +929,7 @@ Example (canonical):
1173
929
  **Alternative (also accepted):** top-level \`nodes\` array as an alias for
1174
930
  \`payload.nodes\` (same op shapes). Full-tab replace remains supported via
1175
931
  \`payload.flow\` (from \`GET ./flows/<id>\` / REST \`GET /flows/{id}\`).
1176
- `}a(nu,"docsRestNodesPayload");function ru(){return`## Known issues
932
+ `}a(fu,"docsRestNodesPayload");function mu(){return`## Known issues
1177
933
 
1178
934
  - **Rate limits are per token (and per IP).** Each access-link token has its own
1179
935
  token bucket. Several tokens can each use the full per-token limit at the same
@@ -1181,28 +937,64 @@ Example (canonical):
1181
937
  - **\`RateLimit-*\` headers describe the per-token bucket** (default
1182
938
  \`RateLimit-Policy: 300;w=60\`) on token requests; requests without a token
1183
939
  report the per-IP bucket (\`600;w=60\`). Both are enforced.
1184
- `}a(ru,"docsKnownIssues");function ou(){return`## Error codes (JSON unless docs)
940
+ `}a(mu,"docsKnownIssues");function hu(){return`## Error codes (JSON unless docs)
1185
941
 
1186
- ${ai()}
942
+ ${fi()}
1187
943
 
1188
944
  All JSON responses use \`Cache-Control: no-store\`. Never put secrets in query
1189
945
  strings you will paste publicly beyond this temporary link.
1190
- `}a(ou,"docsErrorsAndAlso");function wi(e){let{link:t,openapiPath:n,restApiBase:r,mcpRpcPath:o,publicOrigin:s}=e,i=Xt(t.expiresAt),d=t.label?` (${Xt(t.label)})`:"",l=Xt(n),c=Xt(r),u=Xt(o);return[Xc(d,i),Kc(c,l,u,s?Xt(`${s}${o}`):yi),Yc(),Qc(),Zc(),nu(),eu(),tu(),ru(),ou()].join(`
1191
- `)}a(wi,"renderGetLinkDocs");var Li=require("crypto");q();G();Ne();Ge();Ae();G();function gr(e){return{error:e instanceof Error&&e.message.trim()?e.message:"Internal server error",code:"internal_error"}}a(gr,"internalErrorBody");function su(e,t){e.headersSent||e.status(500).json(gr(t))}a(su,"sendInternalError");function F(e,t){if(e.headersSent)return;let n=N(t);if(n){e.status(n.statusCode).json(n.body);return}su(e,t)}a(F,"sendCaughtRouteError");function P(e){return(t,n,r)=>{Promise.resolve().then(()=>e(t,n,r)).catch(o=>{if(n.headersSent){r(o instanceof Error?o:new Error(String(o)));return}F(n,o)})}}a(P,"asyncRoute");var wr=require("crypto");q();ae();var M=class extends Error{static{a(this,"DraftValidationError")}statusCode=400;code;constructor(t,n="draft_invalid"){super(t),this.name="DraftValidationError",this.code=n}},ct=class extends Error{static{a(this,"DraftNotFoundError")}statusCode=404;code="draft_not_found";constructor(t){super(`Draft '${t}' was not found or has expired`),this.name="DraftNotFoundError"}},ut=class extends Error{static{a(this,"DraftAlreadySubmittedError")}statusCode=409;code="draft_already_submitted";proposalId;constructor(t,n){super(`Draft '${t}' was already submitted as proposal '${n}'`),this.name="DraftAlreadySubmittedError",this.proposalId=n}},iu=/^[A-Za-z0-9_-]+={0,2}$/;function bi(e){if(!iu.test(e)||e.replace(/=+$/,"").length===0)throw new M("data must be non-empty base64url (A-Z, a-z, 0-9, -, _)","invalid_chunk_data");let t;try{t=Buffer.from(e,"base64url")}catch{throw new M("data must be non-empty base64url (A-Z, a-z, 0-9, -, _)","invalid_chunk_data")}if(t.byteLength===0&&e.length>0)throw new M("data must be non-empty base64url (A-Z, a-z, 0-9, -, _)","invalid_chunk_data");return t}a(bi,"assertValidBase64UrlChunk");var yr=class{static{a(this,"DraftStore")}drafts=new Map;submitted=new Map;now;randomId;maxBytes;ttlMs;constructor(t={}){this.now=t.now??(()=>Date.now()),this.randomId=t.randomId??(()=>(0,wr.randomBytes)(12).toString("hex")),this.maxBytes=t.maxBytes??$,this.ttlMs=t.ttlMs??bs}purgeExpired(t){for(let[n,r]of this.drafts)r.expiresAtMs<=t&&this.drafts.delete(n);for(let[n,r]of this.submitted)r.expiresAtMs<=t&&this.submitted.delete(n)}create(t){let n=this.now();this.purgeExpired(n);let r=this.randomId(),o={id:r,linkId:t.linkId,flowId:t.flowId,expectedRev:t.expectedRev,mode:t.mode,chunks:new Map,createdAtMs:n,expiresAtMs:n+this.ttlMs,totalBytes:0};return t.nodeId!==void 0&&(o.nodeId=t.nodeId),t.field!==void 0&&(o.field=t.field),t.targets!==void 0&&(o.targets=t.targets),t.summary!==void 0&&(o.summary=t.summary),t.title!==void 0&&(o.title=t.title),this.drafts.set(r,o),o}getForLink(t,n){let r=this.now();this.purgeExpired(r);let o=this.submitted.get(t);if(o?.linkId===n)throw new ut(t,o.proposalId);let s=this.drafts.get(t);if(s?.linkId!==n)throw new ct(t);if(s.expiresAtMs<=r)throw new ct(t);return s}markSubmitted(t,n,r){let o=this.now();this.drafts.delete(t),this.submitted.set(t,{linkId:n,proposalId:r,expiresAtMs:o+this.ttlMs})}append(t,n,r,o){if(!Number.isInteger(r)||r<0)throw new M("seq must be a non-negative integer","invalid_seq");if(typeof o!="string")throw new M("data must be a base64url string","invalid_chunk");let s=this.getForLink(t,n),i=s.chunks.get(r);if(i!==void 0){if(i!==o)throw new M(`Chunk seq ${r} already stored with different data`,"chunk_conflict")}else{let u=bi(o),p=s.totalBytes+u.byteLength;if(p>this.maxBytes)throw new M(`Draft exceeds limit of ${this.maxBytes} bytes (decoded)`,"payload_too_large");s.chunks.set(r,o),s.totalBytes=p}let d=[...s.chunks.keys()].sort((u,p)=>u-p),l=[],c=d.at(-1);if(c!==void 0)for(let u=0;u<=c;u+=1)s.chunks.has(u)||l.push(u);return{draft:s,receivedSeqs:d,missingSeqs:l,totalBytes:s.totalBytes,maxBytes:this.maxBytes}}getMaxBytes(){return this.maxBytes}assembleAndVerify(t,n,r){let o=this.getForLink(t,n);if(o.chunks.size===0)throw new M("Draft has no chunks","draft_empty");let i=[...o.chunks.keys()].sort((m,h)=>m-h).at(-1);if(i===void 0)throw new M("Draft has no chunks","draft_empty");let d=[];for(let m=0;m<=i;m+=1)o.chunks.has(m)||d.push(m);if(d.length>0)throw new M(`Missing chunk seqs: ${d.join(",")}`,"missing_chunks");let l=[];for(let m=0;m<=i;m+=1){let h=o.chunks.get(m);if(h===void 0)throw new M(`Missing chunk seqs: ${m}`,"missing_chunks");l.push(h)}let c=l.map(m=>bi(m)),u=Buffer.concat(c);if(u.byteLength>this.maxBytes)throw new M(`Assembled draft exceeds limit of ${this.maxBytes} bytes`,"payload_too_large");let p=r.trim().toLowerCase();if(!/^[0-9a-f]{64}$/.test(p))throw new M("invalid_sha256: expected 64 hex chars","invalid_sha256");if((0,wr.createHash)("sha256").update(u).digest("hex")!==p)throw new M("sha256 does not match assembled draft content","draft_hash_mismatch");return{draft:o,assembled:u.toString("utf8")}}size(){return this.purgeExpired(this.now()),this.drafts.size}clear(){this.drafts.clear(),this.submitted.clear()}};cr();tt();ae();var Ke=dn(require("express-rate-limit"),1);var jo=6e4,vi=600,ki=300;function Do(e,t){return typeof e=="number"&&Number.isFinite(e)&&e>0?Math.max(1,Math.floor(e)):t}a(Do,"positiveInt");function $e(e){if(!e)return{windowMs:jo,maxPerIp:vi,maxPerToken:ki};let t=e;return{windowMs:Do(t.windowMs??t.rateLimitWindowMs,jo),maxPerIp:Do(t.maxPerIp??t.rateLimitMaxPerIp,vi),maxPerToken:Do(t.maxPerToken??t.rateLimitMaxPerToken,ki)}}a($e,"resolveRateLimitConfig");function au(e,t){let r=e.rateLimit?.resetTime;return r instanceof Date&&!Number.isNaN(r.getTime())?Math.max(1,Math.ceil((r.getTime()-Date.now())/1e3)):Math.max(1,Math.ceil(t/1e3))}a(au,"retryAfterSeconds");function Ai(e,t,n=jo){let r=au(e,n);t.setHeader("Retry-After",String(r)),t.setHeader("Cache-Control","no-store"),t.status(429).json({error:"Too many requests. Slow down and retry.",code:"rate_limited",retryAfter:r,hint:"Wait retryAfter seconds, then retry. Prefer summary/search reads over full field downloads."})}a(Ai,"sendRateLimited");function du(e){return(t,n,r,o)=>{Ai(t,n,e)}}a(du,"rateLimitedHandler");function br(e){return`ip:${(0,Ke.ipKeyGenerator)(e.ip??"unknown")}`}a(br,"clientIpKey");function vr(e){let t=e.params.token;if(typeof t=="string"&&t.length>0)return`token:${t.slice(0,48)}`;let n=e.get("authorization")??e.headers.authorization;if(typeof n=="string"){let s=/^Bearer\s+(\S+)/i.exec(n.trim())?.[1];if(s&&(s.startsWith("aia_")||s.length>=24))return`token:${s.slice(0,48)}`}let r=e;return r.accessLink?.id?`token:link:${r.accessLink.id}`:null}a(vr,"accessTokenRateLimitKey");function xi(e,t,n,r){let o={windowMs:t,max:e,standardHeaders:!0,legacyHeaders:!1,keyGenerator:n,handler:du(t)};return r&&(o.skip=r),(0,Ke.default)(o)}a(xi,"createLimiter");function Ho(e){let t=$e(e);return xi(t.maxPerIp,t.windowMs,br)}a(Ho,"createAiAccessRateLimiter");function lu(e){let t=$e(e);return xi(t.maxPerToken,t.windowMs,n=>vr(n)??br(n),n=>vr(n)==null)}a(lu,"createAiAccessTokenRateLimiter");function Si(e){let t=$e(e),n=Ho(t),r=lu(t);return{ip:n,token:r,stack:[n,r],config:t}}a(Si,"createAiAccessDualRateLimiters");function Ti(e){let t=$e(e()).windowMs,n=$e(e()).maxPerIp,r=$e(e()).maxPerToken,o=new Ke.MemoryStore,s=new Ke.MemoryStore,i=a(()=>{let h=$e(e());if(h.windowMs!==t||h.maxPerIp!==n||h.maxPerToken!==r){let g=h.windowMs!==t;t=h.windowMs,n=h.maxPerIp,r=h.maxPerToken,o.resetAll(),s.resetAll(),g&&(o.init?.({windowMs:t}),s.init?.({windowMs:t}))}return h},"refreshWindow"),d=new WeakSet,l=a(h=>{if(d.has(h))return;d.add(h);let g=h.setHeader.bind(h);h.setHeader=((v,A)=>(v.toLowerCase()==="ratelimit-policy"&&typeof A=="string"&&(A=A.replace(/;w=\d+/,`;w=${Math.ceil(t/1e3)}`)),g(v,A)))},"useLiveWindowPolicy"),c=a((h,g)=>(0,Ke.default)({store:g,windowMs:t,limit:a(()=>{let v=$e(e());return h?v.maxPerToken:v.maxPerIp},"limit"),standardHeaders:!0,legacyHeaders:!1,keyGenerator:h?v=>vr(v)??br(v):br,skip:a(v=>h&&vr(v)==null,"skip"),handler:a((v,A,E)=>{Ai(v,A,$e(e()).windowMs)},"handler")}),"make"),u=c(!1,o),p=c(!0,s);return{ip:a((h,g,v)=>(i(),l(g),u(h,g,v)),"ip"),token:a((h,g,v)=>(i(),l(g),p(h,g,v)),"token")}}a(Ti,"createLiveAiAccessDualRateLimiters");_e();function I(e){e.setHeader("Cache-Control","no-store"),e.setHeader("Pragma","no-cache")}a(I,"setNoStore");function Uo(e){let t=e.originalUrl||e.url||"",n=so(t);n!==t&&(Object.defineProperty(e,"originalUrl",{value:n,writable:!0,configurable:!0}),typeof e.url=="string"&&Object.defineProperty(e,"url",{value:so(e.url),writable:!0,configurable:!0}))}a(Uo,"redactTokenFromRequestUrl");function _i(e,t){return(n,r,o)=>{I(r);let s=n.originalUrl||n.url||"";if(n.getLinkUrlBytes=Buffer.byteLength(s,"utf8"),Uo(n),!ye(e.settings)){r.status(403).json({error:"AI Access features are disabled. Enable adminAuth, or set aiAccess.allowWithoutAdminAuth to true for local-only use.",code:"ai_features_disabled",hint:"Ask the Node-RED owner to enable AI Access."});return}let i=n.params.token;if(typeof i!="string"||i.length===0){r.setHeader("WWW-Authenticate",K),r.status(401).json({error:le,code:Re,hint:"Ask the user for a new self-contained access link URL."});return}let d=i;try{d=decodeURIComponent(i)}catch{}let l=t.validateResult(d);if(l.status==="expired"){gn(n,r,e.settings.httpAdminRoot,l.expiresAt);return}if(l.status!=="active"){let c=d.length<24||d.startsWith("aia_")&&d.length<40?"The access-link URL may be truncated \u2014 ask the user for the full /mcp/a/<token> link.":"Ask the user for a new self-contained access link URL (it may have expired, or Node-RED may have restarted).";r.setHeader("WWW-Authenticate",K),r.status(401).json({error:et,code:hn,hint:c});return}if(l.link.kind!=="get_link"){yn(r,"get_link",l.link.kind);return}n.accessLink=l.link,n.getLinkToken=d,o()}}a(_i,"requireGetLinkToken");function Yt(e,t){let n=e.getLinkUrlBytes;return(typeof n=="number"?n:Buffer.byteLength(e.originalUrl||e.url||"","utf8"))>Lt?(I(t),t.status(414).json({error:`Request URL exceeds ${Lt} bytes`,code:"url_too_long",hint:"Use chunked drafts: GET \u2026/draft/new then \u2026/draft/<id>/append?seq=&data= then \u2026/draft/<id>/submit?sha256=, or use POST /mcp/api/v1/proposals with Authorization: Bearer <token>.",limitBytes:Lt}),!0):!1}a(Yt,"rejectOversizedGetUrl");function L(e,t,n,r,o,s){I(e),e.status(t).json({error:n,code:r,...o?{hint:o}:{},...s})}a(L,"jsonError");function Pi(){return(e,t)=>{Uo(e),I(t),t.status(404).json({error:"Not found",code:"not_found"})}}a(Pi,"getLinkNotFoundHandler");var cu="link_create_not_supported";function Ri(){return(e,t)=>{Uo(e),I(t),t.status(403).json({error:"GET access links cannot create tokens",code:cu,hint:"Ask an admin with * permission to mint tokens via POST /mcp/api/v1/links on the /mcp Access links page. This get_link URL cannot create, list, or revoke tokens."})}}a(Ri,"getLinkMintNotSupportedHandler");function R(e,t){let n=e[t];if(typeof n=="string"&&n.length>0)return n;if(Array.isArray(n)&&typeof n[0]=="string"&&n[0].length>0)return n[0]}a(R,"queryString");function Se(e){switch(e){case"draft_not_found":return"Start a new draft with GET \u2026/draft/new.";case"draft_already_submitted":return"This draft already created a proposal; poll that proposal or start a new draft.";case"chunk_conflict":return"Re-send the same data for that seq, or start a new draft.";case"payload_too_large":return"Shrink the content; totalBytes counts decoded bytes against maxBytes.";case"invalid_seq":return"seq must be a non-negative integer (0, 1, 2, \u2026).";case"invalid_chunk":case"invalid_chunk_data":return"data must be base64url of the raw chunk bytes (A-Z, a-z, 0-9, -, _).";case"missing_chunks":return"Upload every seq from 0 through max before submit.";case"invalid_sha256":return"sha256 must be exactly 64 hexadecimal characters.";case"draft_hash_mismatch":return"sha256 must be the hex digest of the assembled UTF-8 bytes (before base64url).";case"draft_empty":return"Append at least one chunk before submit.";case"node_not_found":return"Call GET \u2026/flows/<id> and use a node id from that response (or use mode=json with op:add).";case"field_not_found":return"Use a property that exists on the node, or mode=json to add/replace nodes.";case"missing_draft_params":return"Example: \u2026/draft/new?flow=tab1&rev=abc&node=n1&field=template";case"version_mismatch":return"Re-read the flow and start a new draft with the current rev.";case"protected_property":return"Omit id, type, and z from set props. wires are allowed when targets exist in the flow.";case"invalid_wires":return"wires must list existing node ids (or add those nodes in the same patch).";default:return}}a(Se,"draftErrorHint");async function uu(e,t,n){let r;try{r=await e.getFlow(t)}catch(s){let i=N(s);if(i)return{code:i.body.code,message:i.body.error};throw s}let o=[...Array.isArray(r.flow.nodes)?r.flow.nodes:[],...Array.isArray(r.flow.configs)?r.flow.configs:[]];for(let s of n){let i=o.find(d=>d.id===s.nodeId);if(!i)return{code:"node_not_found",message:`Node '${s.nodeId}' was not found in flow '${t}'`};if(!(s.field in i))return{code:"field_not_found",message:`Field '${s.field}' was not found on node '${s.nodeId}'`}}return null}a(uu,"validateFieldDraftTargets");function pu(e,t,n,r){if(r)try{let o=Sn(r);if(!Array.isArray(o)||o.length===0)throw new Error("targets must be a non-empty array");return{ok:!0,mode:"field",targets:o.map(i=>{if(!i||typeof i!="object")throw new Error("bad target");let d=i,l=typeof d.nodeId=="string"?d.nodeId:typeof d.node=="string"?d.node:"",c=typeof d.field=="string"?d.field:"";if(!l||!c)throw new Error("bad target");return{nodeId:l,field:c}})}}catch{return{ok:!1,status:400,error:"targets must be base64url JSON array of {nodeId,field}",code:"invalid_targets"}}if(n==="json"||!e&&!t)return{ok:!0,mode:"json"};if(!e||!t){let o=Se("missing_draft_params");return{ok:!1,status:400,error:"node and field are required for field drafts (or pass mode=json)",code:"missing_draft_params",...o?{hint:o}:{}}}return{ok:!0,mode:"field"}}a(pu,"planDraftMode");async function fu(e,t,n,r){try{let o=await uu(t,n,r);if(o){let s=o.code==="node_not_found"||o.code==="field_not_found"?404:400;return L(e,s,o.message,o.code,Se(o.code)),!0}}catch(o){let s=N(o);return s?(L(e,s.statusCode,s.body.error,s.body.code,Se(s.body.code)),!0):(F(e,o),!0)}return!1}a(fu,"rejectInvalidFieldTargets");function Ci(e,t){return async(n,r)=>{if(Yt(n,r))return;I(r);let o=R(n.query,"flow"),s=R(n.query,"rev"),i=R(n.query,"node"),d=R(n.query,"field"),l=R(n.query,"summary"),c=R(n.query,"title")??l,u=R(n.query,"mode"),p=R(n.query,"targets");if(!o||!s){L(r,400,"flow and rev query parameters are required","missing_draft_params",Se("missing_draft_params"));return}if(!n.accessLink){L(r,401,"Access link required","access_link_missing");return}try{let h=await t.getFlow(o);if(h.rev!==s){r.status(409).json({error:"Flow revision mismatch",code:"version_mismatch",expectedRev:s,currentRev:h.rev,hint:Se("version_mismatch")});return}}catch(h){let g=N(h);if(g){L(r,g.statusCode,g.body.error,g.body.code,g.body.code==="flow_not_found"?ie("flow_not_found","get")??"Use an id from GET ./flows":Se(g.body.code));return}F(r,h);return}let f=pu(i,d,u,p);if(!f.ok){L(r,f.status,f.error,f.code,f.hint);return}if(f.mode==="field"){let h=f.targets&&f.targets.length>0?f.targets:i&&d?[{nodeId:i,field:d}]:[];if(h.length>0&&await fu(r,t,o,h))return}let m=e.create({linkId:n.accessLink.id,flowId:o,expectedRev:s,mode:f.mode,...i?{nodeId:i}:{},...d?{field:d}:{},...f.targets?{targets:f.targets}:{},...l?{summary:l}:{},...c?{title:c}:{}});r.status(201).json({ok:!0,draftId:m.id,mode:m.mode,expiresAt:new Date(m.expiresAtMs).toISOString(),maxBytes:$,hint:"Upload base64url chunks (~4\u20135 KB raw each; base64 adds ~33%; keep URL under ~8 KB) with GET \u2026/draft/<draftId>/append?seq=N&data=\u2026 then submit with sha256."})}}a(Ci,"getLinkDraftNewHandler");function Ei(e){return(t,n)=>{if(Yt(t,n))return;I(n);let r=t.params.draftId,o=R(t.query,"seq"),s=R(t.query,"data");if(typeof r!="string"||!r){L(n,400,"Missing draft id","missing_draft_id");return}if(!t.accessLink){L(n,401,"Access link required","access_link_missing");return}if(o===void 0||s===void 0){L(n,400,"seq and data query parameters are required","missing_chunk_params","data must be base64url of the raw chunk bytes.");return}let i=Number(o);try{let d=e.append(r,t.accessLink.id,i,s);n.json({ok:!0,draftId:r,receivedSeqs:d.receivedSeqs,missingSeqs:d.missingSeqs,totalBytes:d.totalBytes,maxBytes:d.maxBytes,hint:d.missingSeqs.length===0?"No gaps so far; call submit when all chunks are uploaded":`Upload missing seqs: ${d.missingSeqs.join(",")}`})}catch(d){if(d instanceof ct||d instanceof ut||d instanceof M){L(n,d.statusCode,d.message,d.code,Se(d.code),d instanceof ut?{proposalId:d.proposalId}:void 0);return}F(n,d);return}}}a(Ei,"getLinkDraftAppendHandler");function mu(e,t){let n=[];if(e.targets&&e.targets.length>0){let r;try{r=JSON.parse(t)}catch{let o=e.targets[0];if(e.targets.length===1&&o)return[{nodeId:o.nodeId,set:{[o.field]:t}}];throw new M("Multi-target draft body must be JSON","invalid_draft_body")}if(Array.isArray(r)){for(let o of r){let s=o;if(!s.nodeId||!s.field)throw new M("Invalid multi-target entry","invalid_draft_body");n.push({nodeId:s.nodeId,set:{[s.field]:s.value}})}return n}if(r&&typeof r=="object"){let o=r;for(let s of e.targets){let i=`${s.nodeId}.${s.field}`;if(!(i in o)&&!(s.field in o))throw new M(`Missing value for ${i} in draft JSON`,"invalid_draft_body");n.push({nodeId:s.nodeId,set:{[s.field]:i in o?o[i]:o[s.field]}})}return n}throw new M("Invalid multi-target draft JSON","invalid_draft_body")}if(e.nodeId&&e.field)return[{nodeId:e.nodeId,set:{[e.field]:t}}];throw new M("Field draft missing node/field","invalid_draft")}a(mu,"editsFromFieldDraft");function Ii(e,t,n,r,o,s){return async(i,d)=>{if(Yt(i,d))return;I(d);let l=i.params.draftId,c=R(i.query,"sha256"),u=R(i.query,"title");if(typeof l!="string"||!l){L(d,400,"Missing draft id","missing_draft_id");return}if(!i.accessLink){L(d,401,"Access link required","access_link_missing");return}if(!c){L(d,400,"sha256 query parameter is required","missing_sha256","Pass sha256 hex of the assembled UTF-8 bytes (before base64url).");return}try{let{draft:p,assembled:f}=e.assembleAndVerify(l,i.accessLink.id,c),m=p.flowId,h,g=p.expectedRev;if(p.mode==="field"){let y=mu(p,f),w=await xn(r,p.flowId,y);if(w.expectedRev!==p.expectedRev){d.status(409).json({error:"Flow revision mismatch",code:"version_mismatch",expectedRev:p.expectedRev,currentRev:w.expectedRev,draftDeleted:!1,hint:"Draft was kept. Re-read the flow and start a new draft with the current rev (or retry if you refresh rev)."});return}h=w.flow,g=w.expectedRev}else{let y=Tn(JSON.parse(f),p.flowId);if(m=y.flowId,y.kind==="replace_flow")h={...y.flow,id:y.flow.id||m,type:y.flow.type??"tab",nodes:y.flow.nodes??[]};else{let w=await at(r,m,y.ops,{expectedRev:p.expectedRev,...s?{nodeTypes:s}:{}});if(w.expectedRev!==p.expectedRev){d.status(409).json({error:"Flow revision mismatch",code:"version_mismatch",expectedRev:p.expectedRev,currentRev:w.expectedRev,draftDeleted:!1,hint:"Draft was kept. Re-read the flow and start a new draft with the current rev."});return}h=w.flow,g=w.expectedRev}}let v=u??p.title??p.summary,A=U(i,o),E=oe(i.query),D=await ze(t,n,i.accessLink,{type:"replace_flow",expectedRev:g,payload:{flowId:m,flow:h},...v?{title:v}:{}},r,A,s);e.markSubmitted(l,i.accessLink.id,D.id);let O={...ce(D,{...A,include:E}),mine:!0},te=z(t,D.expectedRev,{flowId:D.payload.flowId}),me=Ee(t,t.get(D.id),i.accessLink.id);d.status(201).json({ok:!0,proposal:O,sha256:(0,Li.createHash)("sha256").update(Buffer.from(f,"utf8")).digest("hex"),...Ie(me),...X(te),hint:`Waiting for human approval. Tell the user about "${O.displayName}" (${O.reviewUrl}). Poll GET \u2026/proposals/<id> for status.`})}catch(p){if(p instanceof ut){L(d,p.statusCode,p.message,p.code,Se(p.code),{proposalId:p.proposalId});return}if(p instanceof ct||p instanceof M){L(d,p.statusCode,p.message,p.code,Se(p.code));return}if(p instanceof SyntaxError){L(d,400,"Assembled draft is not valid JSON","invalid_draft_body");return}let f=N(p);if(f){d.status(f.statusCode).json({...f.body,...Se(f.body.code)?{hint:Se(f.body.code)}:{}});return}F(d,p);return}}}a(Ii,"getLinkDraftSubmitHandler");G();Ne();mr();Ge();Ae();cr();var hu={node_already_exists:"Choose a new node id, or use op:set / op:remove on the existing node.",protected_property:"Omit id, type, and z from set props. wires are allowed when targets exist in the flow.",invalid_wires:"wires must list existing node ids (or add those nodes in the same patch).",invalid_patch:"Pass patch as base64url JSON with nodes[{op,\u2026.}] or type/flow for replace_flow.",invalid_patch_json:"Pass patch as base64url JSON with nodes[{op,\u2026.}] or type/flow for replace_flow.",missing_edits:"Provide set.<prop>=value, find+replace+field, or patch nodes with at least one op.",missing_node_id:"Pass node=<id> with set.<prop>= or find/replace, or include id on each patch.nodes entry.",missing_flow_id:"Pass flow=<id> from GET \u2026/flows.",missing_field:"Pass field=<prop> (e.g. template, func) with find= and replace=.",missing_find:"Pass a non-empty find= string (URL-encoded).",invalid_occurrence:"occurrence must be a 1-based index within the match count.",invalid_replace:"Pass replace= as a string (URL-encoded; empty string is allowed to delete the find text).",conflicting_edits:"Use either find/replace/field or set.<prop>=, not both in one propose URL.",invalid_node_type:"node.type must be a string (not a number or object). Use the palette type name exactly.",invalid_node:"add op needs node as a JSON object: { id, type, \u2026 } \u2014 not a string or array.",z_mismatch:"Each node.z must match the flow id (tab id) you are editing."};function Bo(e,t){return ie(e,"get",{cause:t})??hu[e]??Rn(e)}a(Bo,"proposeErrorHint");function gu(e,t,n){e.status(409).json({error:"Flow revision mismatch",code:"version_mismatch",expectedRev:t,currentRev:n,hint:Bo("version_mismatch")})}a(gu,"sendVersionMismatch");async function yu(e,t,n,r,o){let s=Tn(Sn(t),n),i=s.flowId;if(s.kind==="replace_flow")return{ok:!0,builtFlowId:i,builtFlow:{...s.flow,id:s.flow.id||i,type:s.flow.type??"tab",nodes:s.flow.nodes??[]},expectedRev:r};let d=await at(e,i,s.ops,{expectedRev:r,...o?{nodeTypes:o}:{}});return d.expectedRev!==r?{ok:!1,kind:"version_mismatch",expectedRev:r,currentRev:d.expectedRev}:{ok:!0,builtFlowId:i,builtFlow:d.flow,expectedRev:d.expectedRev}}a(yu,"buildFromPatch");async function wu(e,t,n,r,o){let s=await xn(e,t,[{nodeId:n,set:r}]);return s.expectedRev!==o?{ok:!1,kind:"version_mismatch",expectedRev:o,currentRev:s.expectedRev}:{ok:!0,builtFlowId:t,builtFlow:s.flow,expectedRev:s.expectedRev}}a(wu,"buildFromSetProps");async function bu(e,t,n,r,o,s,i,d){let l=await at(e,t,[{op:"replace_text",nodeId:n,field:r,find:o,replace:s,...i!==void 0?{occurrence:i}:{}}]);return l.expectedRev!==d?{ok:!1,kind:"version_mismatch",expectedRev:d,currentRev:l.expectedRev}:{ok:!0,builtFlowId:t,builtFlow:l.flow,expectedRev:l.expectedRev}}a(bu,"buildFromReplaceText");function vu(e){let t=Object.prototype.hasOwnProperty.call(e.query,"replace");return{flowId:R(e.query,"flow"),rev:R(e.query,"rev"),nodeId:R(e.query,"node"),patchEncoded:R(e.query,"patch"),setProps:Ro(e.query),findText:R(e.query,"find"),replaceText:t?R(e.query,"replace")??"":void 0,fieldName:R(e.query,"field"),occurrenceRaw:R(e.query,"occurrence")}}a(vu,"readProposeQuery");function Te(e,t,n){L(e,400,n,t,Bo(t))}a(Te,"jsonProposeError");function ku(e,t){if(t===void 0||t==="")return{ok:!0};let n=Number(t);return Number.isFinite(n)?{ok:!0,occurrence:Math.trunc(n)}:(Te(e,"invalid_occurrence","occurrence must be a positive integer"),{ok:!1})}a(ku,"parseOccurrenceQuery");async function Au(e,t,n,r,o){if(n.patchEncoded)return yu(t,n.patchEncoded,n.flowId,r,o);if(n.findText!==void 0||n.replaceText!==void 0||n.fieldName!==void 0){if(Object.keys(n.setProps).length>0)return Te(e,"conflicting_edits","Cannot combine replace_text (find/replace/field) with set.<prop> in one request"),null;if(!n.flowId)return Te(e,"missing_flow_id","flow query parameter is required"),null;if(!n.nodeId)return Te(e,"missing_node_id","node query parameter is required"),null;if(!n.fieldName)return Te(e,"missing_field","field query parameter is required for replace_text"),null;if(n.findText===void 0||n.findText.length===0)return Te(e,"missing_find","find query parameter must be a non-empty string"),null;if(n.replaceText===void 0)return Te(e,"invalid_replace","replace query parameter is required (may be empty)"),null;let i=ku(e,n.occurrenceRaw);return i.ok?bu(t,n.flowId,n.nodeId,n.fieldName,n.findText,n.replaceText,i.occurrence,r):null}return n.flowId?n.nodeId?Object.keys(n.setProps).length===0?(Te(e,"missing_edits","At least one set.<prop>=value is required"),null):wu(t,n.flowId,n.nodeId,n.setProps,r):(Te(e,"missing_node_id","node query parameter is required (or pass patch=)"),null):(Te(e,"missing_flow_id","flow query parameter is required"),null)}a(Au,"buildProposeFromQuery");function Ni(e,t,n,r,o){return async(s,i)=>{if(Yt(s,i))return;I(i);let d=vu(s),l=R(s.query,"summary"),c=R(s.query,"title")??l;if(!d.rev){Te(i,"missing_expected_rev","rev query parameter is required");return}if(!s.accessLink){L(i,401,"Access link required","access_link_missing");return}try{let u=await Au(i,n,d,d.rev,o);if(!u)return;if(!u.ok){gu(i,u.expectedRev,u.currentRev);return}let p=U(s,r),f=oe(s.query),m=await ze(e,t,s.accessLink,{type:"replace_flow",expectedRev:u.expectedRev,payload:{flowId:u.builtFlowId,flow:u.builtFlow},...c?{title:c}:{}},n,p,o),h={...ce(m,{...p,include:f}),mine:!0},g=z(e,m.expectedRev,{flowId:m.payload.flowId}),v=Ee(e,e.get(m.id),s.accessLink.id),A=m.warnings;i.status(201).json({ok:!0,proposal:h,...l?{summary:l}:{},...Ie(v),...X(g),...A&&A.length>0?{warnings:A}:{},hint:`Waiting for human approval. Tell the user about "${h.displayName}" (${h.reviewUrl}). Poll GET \u2026/proposals/<id> for status.${A&&A.length>0?` Soft warnings (${A.length}) are non-blocking \u2014 configure later if needed.`:""}`})}catch(u){let p=N(u,"get");if(p){let f=Bo(p.body.code,p.body.cause);i.status(p.statusCode).json({...p.body,...f?{hint:f}:{}});return}F(i,u)}}}a(Ni,"getLinkProposeHandler");G();Ne();G();var xu=50,Fi=40,Su=20;function Tu(e){return Buffer.byteLength(JSON.stringify(e),"utf8")}a(Tu,"nodeBytes");function _u(e){let t={id:e.id,type:e.type,bytes:Tu(e)};return typeof e.name=="string"&&(t.name=e.name),"wires"in e&&(t.wires=e.wires),t}a(_u,"summarizeNode");async function kr(e,t){let{rev:n,flow:r}=await xe(e,t),o=r,s=Array.isArray(o.nodes)?o.nodes.map(_u):[],i={rev:n,flowId:o.id||t,nodes:s,nodeCount:s.length};return typeof o.label=="string"&&(i.label=o.label),typeof o.type=="string"&&(i.type=o.type),i}a(kr,"getFlowSummary");function Oi(e){let t=[];for(let n of e){if(!n||typeof n!="object"||Array.isArray(n))continue;let r=n;(r.type==="tab"||r.type==="subflow"||Array.isArray(r.nodes))&&t.push(r)}return t}a(Oi,"collectTabs");async function Mi(e,t,n){if(!t)throw new b("Missing node id","missing_node_id");if(n){let{rev:d,flow:l}=await xe(e,n),c=l,u=Array.isArray(c.nodes)?c.nodes:[],p=Array.isArray(c.configs)?c.configs:[],f=[...u,...p].find(m=>m.id===t);if(!f)throw new b("Node was not found","node_not_found",{statusCode:404});return{rev:d,flowId:n,node:f}}let{rev:r,flows:o}=await pe(e),s,i=null;for(let d of o){if(!d||typeof d!="object"||Array.isArray(d))continue;let l=d;if(l.id===t&&typeof l.type=="string"&&l.type!=="tab"&&l.type!=="subflow"){s=l,i=typeof l.z=="string"&&l.z?l.z:null;break}}if(!s)for(let d of Oi(o)){let l=d.id;if(l)try{let u=(await xe(e,l)).flow,f=[...u.nodes??[],...u.configs??[]].find(m=>m.id===t);if(f){s=f,i=l;break}}catch{}}if(!s)throw new b("Node was not found","node_not_found",{statusCode:404});return{rev:r,flowId:i,node:s}}a(Mi,"findNodeInFlows");async function Ar(e,t,n){let r=await Mi(e,t,n);return{rev:r.rev,flowId:r.flowId,node:r.node}}a(Ar,"getNodeById");function Pu(e){if(e===void 0||e==="")return;let t=/^(\d+)(?:-(\d+))?$/.exec(e.trim());if(!t)throw new b("lines must be A or A-B (1-based inclusive; end >= start)","invalid_line_range");let n=Number(t[1]),r=t[2]!==void 0?Number(t[2]):n;if(!Number.isInteger(n)||!Number.isInteger(r)||n<1)throw new b("lines must be A or A-B (1-based inclusive; end >= start)","invalid_line_range");if(r<n)throw new b("lines must be A or A-B (1-based inclusive; end >= start)","invalid_line_range");return{start:n,end:r}}a(Pu,"parseLineRange");async function xr(e,t,n,r={}){if(!n)throw new b("Missing field name","missing_field");let o=await Mi(e,t,r.flowId);if(!(n in o.node))throw new b("Field was not found on the node","field_not_found");let s=o.node[n];if(typeof s!="string")throw new b("Field is not a string","field_not_string");let i=s.split(`
1192
- `),d=i.length,l=Buffer.byteLength(s,"utf8"),c=Pu(r.lines);if(!c)return{rev:o.rev,flowId:o.flowId,nodeId:t,field:n,value:s,totalLines:d,bytes:l};if(c.start>d)throw new b(`lines range is out of bounds (start ${c.start} > ${d} total lines)`,"line_range_out_of_bounds",{details:{totalLines:d}});let u=c.start-1,p=Math.min(c.end,i.length),f=i.slice(u,p).join(`
1193
- `);return{rev:o.rev,flowId:o.flowId,nodeId:t,field:n,value:f,totalLines:d,bytes:l,lineRange:{start:c.start,end:Math.min(c.end,d)}}}a(xr,"getNodeField");function Ru(e){let t=[];for(let[n,r]of Object.entries(e))n==="id"||n==="type"||n==="z"||n==="wires"||typeof r=="string"&&r.length>0&&t.push({field:n,value:r});return t}a(Ru,"stringFields");function Lu(e,t,n){let r=Math.max(0,t-Fi),o=Math.min(e.length,t+n+Fi),s=e.slice(r,o).replace(/\s+/g," ");return r>0&&(s=`\u2026${s}`),o<e.length&&(s=`${s}\u2026`),s}a(Lu,"snippetAround");function Cu(e,t){let n=1;for(let r=0;r<t&&r<e.length;r+=1)e.charCodeAt(r)===10&&(n+=1);return n}a(Cu,"lineNumberAtIndex");function qo(e,t,n,r,o){for(let{field:s,value:i}of Ru(e)){if(r.length>=o)return!0;let d=[],l=new Set,c=-1,u=a((f,m)=>{l.has(f)||(l.add(f),d.push(f)),c<0&&(c=m)},"pushLine");if(!n.includes(`
946
+ `}a(hu,"docsErrorsAndAlso");function Si(e){let{link:t,openapiPath:n,restApiBase:r,mcpRpcPath:o,publicOrigin:s}=e,i=Zt(t.expiresAt),d=t.label?` (${Zt(t.label)})`:"",l=Zt(n),c=Zt(r),u=Zt(o);return[au(d,i),iu(c,l,u,s?Zt(`${s}${o}`):xi),du(),lu(),cu(),fu(),uu(),pu(),mu(),hu()].join(`
947
+ `)}a(Si,"renderGetLinkDocs");var Oi=require("crypto");G();K();Fe();Xe();Ae();K();function wr(e){return{error:e instanceof Error&&e.message.trim()?e.message:"Internal server error",code:"internal_error"}}a(wr,"internalErrorBody");function gu(e,t){e.headersSent||e.status(500).json(wr(t))}a(gu,"sendInternalError");function $(e,t){if(e.headersSent)return;let n=O(t);if(n){e.status(n.statusCode).json(n.body);return}gu(e,t)}a($,"sendCaughtRouteError");function R(e){return(t,n,r)=>{Promise.resolve().then(()=>e(t,n,r)).catch(o=>{if(n.headersSent){r(o instanceof Error?o:new Error(String(o)));return}$(n,o)})}}a(R,"asyncRoute");var vr=require("crypto");G();ie();var D=class extends Error{static{a(this,"DraftValidationError")}statusCode=400;code;constructor(t,n="draft_invalid"){super(t),this.name="DraftValidationError",this.code=n}},ht=class extends Error{static{a(this,"DraftNotFoundError")}statusCode=404;code="draft_not_found";constructor(t){super(`Draft '${t}' was not found or has expired`),this.name="DraftNotFoundError"}},gt=class extends Error{static{a(this,"DraftAlreadySubmittedError")}statusCode=409;code="draft_already_submitted";proposalId;constructor(t,n){super(`Draft '${t}' was already submitted as proposal '${n}'`),this.name="DraftAlreadySubmittedError",this.proposalId=n}},yu=/^[A-Za-z0-9_-]+={0,2}$/;function Ti(e){if(!yu.test(e)||e.replace(/=+$/,"").length===0)throw new D("data must be non-empty base64url (A-Z, a-z, 0-9, -, _)","invalid_chunk_data");let t;try{t=Buffer.from(e,"base64url")}catch{throw new D("data must be non-empty base64url (A-Z, a-z, 0-9, -, _)","invalid_chunk_data")}if(t.byteLength===0&&e.length>0)throw new D("data must be non-empty base64url (A-Z, a-z, 0-9, -, _)","invalid_chunk_data");return t}a(Ti,"assertValidBase64UrlChunk");var br=class{static{a(this,"DraftStore")}drafts=new Map;submitted=new Map;now;randomId;maxBytes;ttlMs;constructor(t={}){this.now=t.now??(()=>Date.now()),this.randomId=t.randomId??(()=>(0,vr.randomBytes)(12).toString("hex")),this.maxBytes=t.maxBytes??j,this.ttlMs=t.ttlMs??Ss}purgeExpired(t){for(let[n,r]of this.drafts)r.expiresAtMs<=t&&this.drafts.delete(n);for(let[n,r]of this.submitted)r.expiresAtMs<=t&&this.submitted.delete(n)}create(t){let n=this.now();this.purgeExpired(n);let r=this.randomId(),o={id:r,linkId:t.linkId,flowId:t.flowId,expectedRev:t.expectedRev,mode:t.mode,chunks:new Map,createdAtMs:n,expiresAtMs:n+this.ttlMs,totalBytes:0};return t.nodeId!==void 0&&(o.nodeId=t.nodeId),t.field!==void 0&&(o.field=t.field),t.targets!==void 0&&(o.targets=t.targets),t.summary!==void 0&&(o.summary=t.summary),t.title!==void 0&&(o.title=t.title),this.drafts.set(r,o),o}getForLink(t,n){let r=this.now();this.purgeExpired(r);let o=this.submitted.get(t);if(o?.linkId===n)throw new gt(t,o.proposalId);let s=this.drafts.get(t);if(s?.linkId!==n)throw new ht(t);if(s.expiresAtMs<=r)throw new ht(t);return s}markSubmitted(t,n,r){let o=this.now();this.drafts.delete(t),this.submitted.set(t,{linkId:n,proposalId:r,expiresAtMs:o+this.ttlMs})}append(t,n,r,o){if(!Number.isInteger(r)||r<0)throw new D("seq must be a non-negative integer","invalid_seq");if(typeof o!="string")throw new D("data must be a base64url string","invalid_chunk");let s=this.getForLink(t,n),i=s.chunks.get(r);if(i!==void 0){if(i!==o)throw new D(`Chunk seq ${r} already stored with different data`,"chunk_conflict")}else{let u=Ti(o),p=s.totalBytes+u.byteLength;if(p>this.maxBytes)throw new D(`Draft exceeds limit of ${this.maxBytes} bytes (decoded)`,"payload_too_large");s.chunks.set(r,o),s.totalBytes=p}let d=[...s.chunks.keys()].sort((u,p)=>u-p),l=[],c=d.at(-1);if(c!==void 0)for(let u=0;u<=c;u+=1)s.chunks.has(u)||l.push(u);return{draft:s,receivedSeqs:d,missingSeqs:l,totalBytes:s.totalBytes,maxBytes:this.maxBytes}}getMaxBytes(){return this.maxBytes}assembleAndVerify(t,n,r){let o=this.getForLink(t,n);if(o.chunks.size===0)throw new D("Draft has no chunks","draft_empty");let i=[...o.chunks.keys()].sort((m,h)=>m-h).at(-1);if(i===void 0)throw new D("Draft has no chunks","draft_empty");let d=[];for(let m=0;m<=i;m+=1)o.chunks.has(m)||d.push(m);if(d.length>0)throw new D(`Missing chunk seqs: ${d.join(",")}`,"missing_chunks");let l=[];for(let m=0;m<=i;m+=1){let h=o.chunks.get(m);if(h===void 0)throw new D(`Missing chunk seqs: ${m}`,"missing_chunks");l.push(h)}let c=l.map(m=>Ti(m)),u=Buffer.concat(c);if(u.byteLength>this.maxBytes)throw new D(`Assembled draft exceeds limit of ${this.maxBytes} bytes`,"payload_too_large");let p=r.trim().toLowerCase();if(!/^[0-9a-f]{64}$/.test(p))throw new D("invalid_sha256: expected 64 hex chars","invalid_sha256");if((0,vr.createHash)("sha256").update(u).digest("hex")!==p)throw new D("sha256 does not match assembled draft content","draft_hash_mismatch");return{draft:o,assembled:u.toString("utf8")}}size(){return this.purgeExpired(this.now()),this.drafts.size}clear(){this.drafts.clear(),this.submitted.clear()}};pr();it();ie();var Ze=ln(require("express-rate-limit"),1);var jo=6e4,Pi=600,Ri=300;function Do(e,t){return typeof e=="number"&&Number.isFinite(e)&&e>0?Math.max(1,Math.floor(e)):t}a(Do,"positiveInt");function He(e){if(!e)return{windowMs:jo,maxPerIp:Pi,maxPerToken:Ri};let t=e;return{windowMs:Do(t.windowMs??t.rateLimitWindowMs,jo),maxPerIp:Do(t.maxPerIp??t.rateLimitMaxPerIp,Pi),maxPerToken:Do(t.maxPerToken??t.rateLimitMaxPerToken,Ri)}}a(He,"resolveRateLimitConfig");function wu(e,t){let r=e.rateLimit?.resetTime;return r instanceof Date&&!Number.isNaN(r.getTime())?Math.max(1,Math.ceil((r.getTime()-Date.now())/1e3)):Math.max(1,Math.ceil(t/1e3))}a(wu,"retryAfterSeconds");function _i(e,t,n=jo){let r=wu(e,n);t.setHeader("Retry-After",String(r)),t.setHeader("Cache-Control","no-store"),t.status(429).json({error:"Too many requests. Slow down and retry.",code:"rate_limited",retryAfter:r,hint:"Wait retryAfter seconds, then retry. Prefer summary/search reads over full field downloads."})}a(_i,"sendRateLimited");function bu(e){return(t,n,r,o)=>{_i(t,n,e)}}a(bu,"rateLimitedHandler");function kr(e){return`ip:${(0,Ze.ipKeyGenerator)(e.ip??"unknown")}`}a(kr,"clientIpKey");function Ar(e){let t=e.params.token;if(typeof t=="string"&&t.length>0)return`token:${t.slice(0,48)}`;let n=e.get("authorization")??e.headers.authorization;if(typeof n=="string"){let s=/^Bearer\s+(\S+)/i.exec(n.trim())?.[1];if(s&&(s.startsWith("aia_")||s.length>=24))return`token:${s.slice(0,48)}`}let r=e;return r.accessLink?.id?`token:link:${r.accessLink.id}`:null}a(Ar,"accessTokenRateLimitKey");function Li(e,t,n,r){let o={windowMs:t,max:e,standardHeaders:!0,legacyHeaders:!1,keyGenerator:n,handler:bu(t)};return r&&(o.skip=r),(0,Ze.default)(o)}a(Li,"createLimiter");function Ho(e){let t=He(e);return Li(t.maxPerIp,t.windowMs,kr)}a(Ho,"createAiAccessRateLimiter");function vu(e){let t=He(e);return Li(t.maxPerToken,t.windowMs,n=>Ar(n)??kr(n),n=>Ar(n)==null)}a(vu,"createAiAccessTokenRateLimiter");function Ci(e){let t=He(e),n=Ho(t),r=vu(t);return{ip:n,token:r,stack:[n,r],config:t}}a(Ci,"createAiAccessDualRateLimiters");function Ei(e){let t=He(e()).windowMs,n=He(e()).maxPerIp,r=He(e()).maxPerToken,o=new Ze.MemoryStore,s=new Ze.MemoryStore,i=a(()=>{let h=He(e());if(h.windowMs!==t||h.maxPerIp!==n||h.maxPerToken!==r){let g=h.windowMs!==t;t=h.windowMs,n=h.maxPerIp,r=h.maxPerToken,o.resetAll(),s.resetAll(),g&&(o.init?.({windowMs:t}),s.init?.({windowMs:t}))}return h},"refreshWindow"),d=new WeakSet,l=a(h=>{if(d.has(h))return;d.add(h);let g=h.setHeader.bind(h);h.setHeader=((x,S)=>(x.toLowerCase()==="ratelimit-policy"&&typeof S=="string"&&(S=S.replace(/;w=\d+/,`;w=${Math.ceil(t/1e3)}`)),g(x,S)))},"useLiveWindowPolicy"),c=a((h,g)=>(0,Ze.default)({store:g,windowMs:t,limit:a(()=>{let x=He(e());return h?x.maxPerToken:x.maxPerIp},"limit"),standardHeaders:!0,legacyHeaders:!1,keyGenerator:h?x=>Ar(x)??kr(x):kr,skip:a(x=>h&&Ar(x)==null,"skip"),handler:a((x,S,E)=>{_i(x,S,He(e()).windowMs)},"handler")}),"make"),u=c(!1,o),p=c(!0,s);return{ip:a((h,g,x)=>(i(),l(g),u(h,g,x)),"ip"),token:a((h,g,x)=>(i(),l(g),p(h,g,x)),"token")}}a(Ei,"createLiveAiAccessDualRateLimiters");ye();function F(e){e.setHeader("Cache-Control","no-store"),e.setHeader("Pragma","no-cache")}a(F,"setNoStore");function Uo(e){let t=e.originalUrl||e.url||"",n=so(t);n!==t&&(Object.defineProperty(e,"originalUrl",{value:n,writable:!0,configurable:!0}),typeof e.url=="string"&&Object.defineProperty(e,"url",{value:so(e.url),writable:!0,configurable:!0}))}a(Uo,"redactTokenFromRequestUrl");function Ii(e,t){return(n,r,o)=>{F(r);let s=n.originalUrl||n.url||"";if(n.getLinkUrlBytes=Buffer.byteLength(s,"utf8"),Uo(n),!ge(e.settings)){r.status(403).json({error:"AI Access features are disabled. Enable adminAuth, or set aiAccess.allowWithoutAdminAuth to true for local-only use.",code:"ai_features_disabled",hint:"Ask the Node-RED owner to enable AI Access."});return}let i=n.params.token;if(typeof i!="string"||i.length===0){r.setHeader("WWW-Authenticate",Y),r.status(401).json({error:le,code:Le,hint:"Ask the user for a new self-contained access link URL."});return}let d=i;try{d=decodeURIComponent(i)}catch{}let l=t.validateResult(d);if(l.status==="expired"){wn(n,r,e.settings.httpAdminRoot,l.expiresAt);return}if(l.status!=="active"){let c=d.length<24||d.startsWith("aia_")&&d.length<40?"The access-link URL may be truncated \u2014 ask the user for the full /mcp/a/<token> link.":"Ask the user for a new self-contained access link URL (it may have expired, or Node-RED may have restarted).";r.setHeader("WWW-Authenticate",Y),r.status(401).json({error:st,code:yn,hint:c});return}if(l.link.kind!=="get_link"){bn(r,"get_link",l.link.kind);return}n.accessLink=l.link,n.getLinkToken=d,o()}}a(Ii,"requireGetLinkToken");function en(e,t){let n=e.getLinkUrlBytes;return(typeof n=="number"?n:Buffer.byteLength(e.originalUrl||e.url||"","utf8"))>It?(F(t),t.status(414).json({error:`Request URL exceeds ${It} bytes`,code:"url_too_long",hint:"Use chunked drafts: GET \u2026/draft/new then \u2026/draft/<id>/append?seq=&data= then \u2026/draft/<id>/submit?sha256=, or use POST /mcp/api/v1/proposals with Authorization: Bearer <token>.",limitBytes:It}),!0):!1}a(en,"rejectOversizedGetUrl");function C(e,t,n,r,o,s){F(e),e.status(t).json({error:n,code:r,...o?{hint:o}:{},...s})}a(C,"jsonError");function Ni(){return(e,t)=>{Uo(e),F(t),t.status(404).json({error:"Not found",code:"not_found"})}}a(Ni,"getLinkNotFoundHandler");var ku="link_create_not_supported";function Fi(){return(e,t)=>{Uo(e),F(t),t.status(403).json({error:"GET access links cannot create tokens",code:ku,hint:"Ask an admin with * permission to mint tokens via POST /mcp/api/v1/links on the /ai-access dashboard (Access tokens tab). This get_link URL cannot create, list, or revoke tokens."})}}a(Fi,"getLinkMintNotSupportedHandler");function _(e,t){let n=e[t];if(typeof n=="string"&&n.length>0)return n;if(Array.isArray(n)&&typeof n[0]=="string"&&n[0].length>0)return n[0]}a(_,"queryString");function Se(e){switch(e){case"draft_not_found":return"Start a new draft with GET \u2026/draft/new.";case"draft_already_submitted":return"This draft already created a proposal; poll that proposal or start a new draft.";case"chunk_conflict":return"Re-send the same data for that seq, or start a new draft.";case"payload_too_large":return"Shrink the content; totalBytes counts decoded bytes against maxBytes.";case"invalid_seq":return"seq must be a non-negative integer (0, 1, 2, \u2026).";case"invalid_chunk":case"invalid_chunk_data":return"data must be base64url of the raw chunk bytes (A-Z, a-z, 0-9, -, _).";case"missing_chunks":return"Upload every seq from 0 through max before submit.";case"invalid_sha256":return"sha256 must be exactly 64 hexadecimal characters.";case"draft_hash_mismatch":return"sha256 must be the hex digest of the assembled UTF-8 bytes (before base64url).";case"draft_empty":return"Append at least one chunk before submit.";case"node_not_found":return"Call GET \u2026/flows/<id> and use a node id from that response (or use mode=json with op:add).";case"field_not_found":return"Use a property that exists on the node, or mode=json to add/replace nodes.";case"missing_draft_params":return"Example: \u2026/draft/new?flow=tab1&rev=abc&node=n1&field=template";case"version_mismatch":return"Re-read the flow and start a new draft with the current rev.";case"protected_property":return"Omit id, type, and z from set props. wires are allowed when targets exist in the flow.";case"invalid_wires":return"wires must list existing node ids (or add those nodes in the same patch).";default:return}}a(Se,"draftErrorHint");async function Au(e,t,n){let r;try{r=await e.getFlow(t)}catch(s){let i=O(s);if(i)return{code:i.body.code,message:i.body.error};throw s}let o=[...Array.isArray(r.flow.nodes)?r.flow.nodes:[],...Array.isArray(r.flow.configs)?r.flow.configs:[]];for(let s of n){let i=o.find(d=>d.id===s.nodeId);if(!i)return{code:"node_not_found",message:`Node '${s.nodeId}' was not found in flow '${t}'`};if(!(s.field in i))return{code:"field_not_found",message:`Field '${s.field}' was not found on node '${s.nodeId}'`}}return null}a(Au,"validateFieldDraftTargets");function xu(e,t,n,r){if(r)try{let o=Pn(r);if(!Array.isArray(o)||o.length===0)throw new Error("targets must be a non-empty array");return{ok:!0,mode:"field",targets:o.map(i=>{if(!i||typeof i!="object")throw new Error("bad target");let d=i,l=typeof d.nodeId=="string"?d.nodeId:typeof d.node=="string"?d.node:"",c=typeof d.field=="string"?d.field:"";if(!l||!c)throw new Error("bad target");return{nodeId:l,field:c}})}}catch{return{ok:!1,status:400,error:"targets must be base64url JSON array of {nodeId,field}",code:"invalid_targets"}}if(n==="json"||!e&&!t)return{ok:!0,mode:"json"};if(!e||!t){let o=Se("missing_draft_params");return{ok:!1,status:400,error:"node and field are required for field drafts (or pass mode=json)",code:"missing_draft_params",...o?{hint:o}:{}}}return{ok:!0,mode:"field"}}a(xu,"planDraftMode");async function Su(e,t,n,r){try{let o=await Au(t,n,r);if(o){let s=o.code==="node_not_found"||o.code==="field_not_found"?404:400;return C(e,s,o.message,o.code,Se(o.code)),!0}}catch(o){let s=O(o);return s?(C(e,s.statusCode,s.body.error,s.body.code,Se(s.body.code)),!0):($(e,o),!0)}return!1}a(Su,"rejectInvalidFieldTargets");function $i(e,t){return async(n,r)=>{if(en(n,r))return;F(r);let o=_(n.query,"flow"),s=_(n.query,"rev"),i=_(n.query,"node"),d=_(n.query,"field"),l=_(n.query,"summary"),c=_(n.query,"title")??l,u=_(n.query,"mode"),p=_(n.query,"targets");if(!o||!s){C(r,400,"flow and rev query parameters are required","missing_draft_params",Se("missing_draft_params"));return}if(!n.accessLink){C(r,401,"Access link required","access_link_missing");return}try{let h=await t.getFlow(o);if(h.rev!==s){r.status(409).json({error:"Flow revision mismatch",code:"version_mismatch",expectedRev:s,currentRev:h.rev,hint:Se("version_mismatch")});return}}catch(h){let g=O(h);if(g){C(r,g.statusCode,g.body.error,g.body.code,g.body.code==="flow_not_found"?se("flow_not_found","get")??"Use an id from GET ./flows":Se(g.body.code));return}$(r,h);return}let f=xu(i,d,u,p);if(!f.ok){C(r,f.status,f.error,f.code,f.hint);return}if(f.mode==="field"){let h=f.targets&&f.targets.length>0?f.targets:i&&d?[{nodeId:i,field:d}]:[];if(h.length>0&&await Su(r,t,o,h))return}let m=e.create({linkId:n.accessLink.id,flowId:o,expectedRev:s,mode:f.mode,...i?{nodeId:i}:{},...d?{field:d}:{},...f.targets?{targets:f.targets}:{},...l?{summary:l}:{},...c?{title:c}:{}});r.status(201).json({ok:!0,draftId:m.id,mode:m.mode,expiresAt:new Date(m.expiresAtMs).toISOString(),maxBytes:j,hint:"Upload base64url chunks (~4\u20135 KB raw each; base64 adds ~33%; keep URL under ~8 KB) with GET \u2026/draft/<draftId>/append?seq=N&data=\u2026 then submit with sha256."})}}a($i,"getLinkDraftNewHandler");function Mi(e){return(t,n)=>{if(en(t,n))return;F(n);let r=t.params.draftId,o=_(t.query,"seq"),s=_(t.query,"data");if(typeof r!="string"||!r){C(n,400,"Missing draft id","missing_draft_id");return}if(!t.accessLink){C(n,401,"Access link required","access_link_missing");return}if(o===void 0||s===void 0){C(n,400,"seq and data query parameters are required","missing_chunk_params","data must be base64url of the raw chunk bytes.");return}let i=Number(o);try{let d=e.append(r,t.accessLink.id,i,s);n.json({ok:!0,draftId:r,receivedSeqs:d.receivedSeqs,missingSeqs:d.missingSeqs,totalBytes:d.totalBytes,maxBytes:d.maxBytes,hint:d.missingSeqs.length===0?"No gaps so far; call submit when all chunks are uploaded":`Upload missing seqs: ${d.missingSeqs.join(",")}`})}catch(d){if(d instanceof ht||d instanceof gt||d instanceof D){C(n,d.statusCode,d.message,d.code,Se(d.code),d instanceof gt?{proposalId:d.proposalId}:void 0);return}$(n,d);return}}}a(Mi,"getLinkDraftAppendHandler");function Tu(e,t){let n=[];if(e.targets&&e.targets.length>0){let r;try{r=JSON.parse(t)}catch{let o=e.targets[0];if(e.targets.length===1&&o)return[{nodeId:o.nodeId,set:{[o.field]:t}}];throw new D("Multi-target draft body must be JSON","invalid_draft_body")}if(Array.isArray(r)){for(let o of r){let s=o;if(!s.nodeId||!s.field)throw new D("Invalid multi-target entry","invalid_draft_body");n.push({nodeId:s.nodeId,set:{[s.field]:s.value}})}return n}if(r&&typeof r=="object"){let o=r;for(let s of e.targets){let i=`${s.nodeId}.${s.field}`;if(!(i in o)&&!(s.field in o))throw new D(`Missing value for ${i} in draft JSON`,"invalid_draft_body");n.push({nodeId:s.nodeId,set:{[s.field]:i in o?o[i]:o[s.field]}})}return n}throw new D("Invalid multi-target draft JSON","invalid_draft_body")}if(e.nodeId&&e.field)return[{nodeId:e.nodeId,set:{[e.field]:t}}];throw new D("Field draft missing node/field","invalid_draft")}a(Tu,"editsFromFieldDraft");function Di(e,t,n,r,o,s){return async(i,d)=>{if(en(i,d))return;F(d);let l=i.params.draftId,c=_(i.query,"sha256"),u=_(i.query,"title");if(typeof l!="string"||!l){C(d,400,"Missing draft id","missing_draft_id");return}if(!i.accessLink){C(d,401,"Access link required","access_link_missing");return}if(!c){C(d,400,"sha256 query parameter is required","missing_sha256","Pass sha256 hex of the assembled UTF-8 bytes (before base64url).");return}try{let{draft:p,assembled:f}=e.assembleAndVerify(l,i.accessLink.id,c),m=p.flowId,h,g=p.expectedRev;if(p.mode==="field"){let A=Tu(p,f),v=await Tn(r,p.flowId,A);if(v.expectedRev!==p.expectedRev){d.status(409).json({error:"Flow revision mismatch",code:"version_mismatch",expectedRev:p.expectedRev,currentRev:v.expectedRev,draftDeleted:!1,hint:"Draft was kept. Re-read the flow and start a new draft with the current rev (or retry if you refresh rev)."});return}h=v.flow,g=v.expectedRev}else{let A=Rn(JSON.parse(f),p.flowId);if(m=A.flowId,A.kind==="replace_flow")h={...A.flow,id:A.flow.id||m,type:A.flow.type??"tab",nodes:A.flow.nodes??[]};else{let v=await pt(r,m,A.ops,{expectedRev:p.expectedRev,...s?{nodeTypes:s}:{}});if(v.expectedRev!==p.expectedRev){d.status(409).json({error:"Flow revision mismatch",code:"version_mismatch",expectedRev:p.expectedRev,currentRev:v.expectedRev,draftDeleted:!1,hint:"Draft was kept. Re-read the flow and start a new draft with the current rev."});return}h=v.flow,g=v.expectedRev}}let x=u??p.title??p.summary,S=q(i,o),E=re(i.query),I=await Ye(t,n,i.accessLink,{type:"replace_flow",expectedRev:g,payload:{flowId:m,flow:h},...x?{title:x}:{}},r,S,s);e.markSubmitted(l,i.accessLink.id,I.id);let N={...ce(I,{...S,include:E}),mine:!0},ot=V(t,I.expectedRev,{flowId:I.payload.flowId}),me=Ie(t,t.get(I.id),i.accessLink.id);d.status(201).json({ok:!0,proposal:N,sha256:(0,Oi.createHash)("sha256").update(Buffer.from(f,"utf8")).digest("hex"),...Ne(me),...Q(ot),hint:`Waiting for human approval. Tell the user about "${N.displayName}" (${N.reviewUrl}). Poll GET \u2026/proposals/<id> for status.`})}catch(p){if(p instanceof gt){C(d,p.statusCode,p.message,p.code,Se(p.code),{proposalId:p.proposalId});return}if(p instanceof ht||p instanceof D){C(d,p.statusCode,p.message,p.code,Se(p.code));return}if(p instanceof SyntaxError){C(d,400,"Assembled draft is not valid JSON","invalid_draft_body");return}let f=O(p);if(f){d.status(f.statusCode).json({...f.body,...Se(f.body.code)?{hint:Se(f.body.code)}:{}});return}$(d,p);return}}}a(Di,"getLinkDraftSubmitHandler");K();Fe();gr();Xe();Ae();pr();var Pu={node_already_exists:"Choose a new node id, or use op:set / op:remove on the existing node.",protected_property:"Omit id, type, and z from set props. wires are allowed when targets exist in the flow.",invalid_wires:"wires must list existing node ids (or add those nodes in the same patch).",invalid_patch:"Pass patch as base64url JSON with nodes[{op,\u2026.}] or type/flow for replace_flow.",invalid_patch_json:"Pass patch as base64url JSON with nodes[{op,\u2026.}] or type/flow for replace_flow.",missing_edits:"Provide set.<prop>=value, find+replace+field, or patch nodes with at least one op.",missing_node_id:"Pass node=<id> with set.<prop>= or find/replace, or include id on each patch.nodes entry.",missing_flow_id:"Pass flow=<id> from GET \u2026/flows.",missing_field:"Pass field=<prop> (e.g. template, func) with find= and replace=.",missing_find:"Pass a non-empty find= string (URL-encoded).",invalid_occurrence:"occurrence must be a 1-based index within the match count.",invalid_replace:"Pass replace= as a string (URL-encoded; empty string is allowed to delete the find text).",conflicting_edits:"Use either find/replace/field or set.<prop>=, not both in one propose URL.",invalid_node_type:"node.type must be a string (not a number or object). Use the palette type name exactly.",invalid_node:"add op needs node as a JSON object: { id, type, \u2026 } \u2014 not a string or array.",z_mismatch:"Each node.z must match the flow id (tab id) you are editing."};function Bo(e,t){return se(e,"get",{cause:t})??Pu[e]??Cn(e)}a(Bo,"proposeErrorHint");function Ru(e,t,n){e.status(409).json({error:"Flow revision mismatch",code:"version_mismatch",expectedRev:t,currentRev:n,hint:Bo("version_mismatch")})}a(Ru,"sendVersionMismatch");async function _u(e,t,n,r,o){let s=Rn(Pn(t),n),i=s.flowId;if(s.kind==="replace_flow")return{ok:!0,builtFlowId:i,builtFlow:{...s.flow,id:s.flow.id||i,type:s.flow.type??"tab",nodes:s.flow.nodes??[]},expectedRev:r};let d=await pt(e,i,s.ops,{expectedRev:r,...o?{nodeTypes:o}:{}});return d.expectedRev!==r?{ok:!1,kind:"version_mismatch",expectedRev:r,currentRev:d.expectedRev}:{ok:!0,builtFlowId:i,builtFlow:d.flow,expectedRev:d.expectedRev}}a(_u,"buildFromPatch");async function Lu(e,t,n,r,o){let s=await Tn(e,t,[{nodeId:n,set:r}]);return s.expectedRev!==o?{ok:!1,kind:"version_mismatch",expectedRev:o,currentRev:s.expectedRev}:{ok:!0,builtFlowId:t,builtFlow:s.flow,expectedRev:s.expectedRev}}a(Lu,"buildFromSetProps");async function Cu(e,t,n,r,o,s,i,d){let l=await pt(e,t,[{op:"replace_text",nodeId:n,field:r,find:o,replace:s,...i!==void 0?{occurrence:i}:{}}]);return l.expectedRev!==d?{ok:!1,kind:"version_mismatch",expectedRev:d,currentRev:l.expectedRev}:{ok:!0,builtFlowId:t,builtFlow:l.flow,expectedRev:l.expectedRev}}a(Cu,"buildFromReplaceText");function Eu(e){let t=Object.prototype.hasOwnProperty.call(e.query,"replace");return{flowId:_(e.query,"flow"),rev:_(e.query,"rev"),nodeId:_(e.query,"node"),patchEncoded:_(e.query,"patch"),setProps:_o(e.query),findText:_(e.query,"find"),replaceText:t?_(e.query,"replace")??"":void 0,fieldName:_(e.query,"field"),occurrenceRaw:_(e.query,"occurrence")}}a(Eu,"readProposeQuery");function Te(e,t,n){C(e,400,n,t,Bo(t))}a(Te,"jsonProposeError");function Iu(e,t){if(t===void 0||t==="")return{ok:!0};let n=Number(t);return Number.isFinite(n)?{ok:!0,occurrence:Math.trunc(n)}:(Te(e,"invalid_occurrence","occurrence must be a positive integer"),{ok:!1})}a(Iu,"parseOccurrenceQuery");async function Nu(e,t,n,r,o){if(n.patchEncoded)return _u(t,n.patchEncoded,n.flowId,r,o);if(n.findText!==void 0||n.replaceText!==void 0||n.fieldName!==void 0){if(Object.keys(n.setProps).length>0)return Te(e,"conflicting_edits","Cannot combine replace_text (find/replace/field) with set.<prop> in one request"),null;if(!n.flowId)return Te(e,"missing_flow_id","flow query parameter is required"),null;if(!n.nodeId)return Te(e,"missing_node_id","node query parameter is required"),null;if(!n.fieldName)return Te(e,"missing_field","field query parameter is required for replace_text"),null;if(n.findText===void 0||n.findText.length===0)return Te(e,"missing_find","find query parameter must be a non-empty string"),null;if(n.replaceText===void 0)return Te(e,"invalid_replace","replace query parameter is required (may be empty)"),null;let i=Iu(e,n.occurrenceRaw);return i.ok?Cu(t,n.flowId,n.nodeId,n.fieldName,n.findText,n.replaceText,i.occurrence,r):null}return n.flowId?n.nodeId?Object.keys(n.setProps).length===0?(Te(e,"missing_edits","At least one set.<prop>=value is required"),null):Lu(t,n.flowId,n.nodeId,n.setProps,r):(Te(e,"missing_node_id","node query parameter is required (or pass patch=)"),null):(Te(e,"missing_flow_id","flow query parameter is required"),null)}a(Nu,"buildProposeFromQuery");function ji(e,t,n,r,o){return async(s,i)=>{if(en(s,i))return;F(i);let d=Eu(s),l=_(s.query,"summary"),c=_(s.query,"title")??l;if(!d.rev){Te(i,"missing_expected_rev","rev query parameter is required");return}if(!s.accessLink){C(i,401,"Access link required","access_link_missing");return}try{let u=await Nu(i,n,d,d.rev,o);if(!u)return;if(!u.ok){Ru(i,u.expectedRev,u.currentRev);return}let p=q(s,r),f=re(s.query),m=await Ye(e,t,s.accessLink,{type:"replace_flow",expectedRev:u.expectedRev,payload:{flowId:u.builtFlowId,flow:u.builtFlow},...c?{title:c}:{}},n,p,o),h={...ce(m,{...p,include:f}),mine:!0},g=V(e,m.expectedRev,{flowId:m.payload.flowId}),x=Ie(e,e.get(m.id),s.accessLink.id),S=m.warnings;i.status(201).json({ok:!0,proposal:h,...l?{summary:l}:{},...Ne(x),...Q(g),...S&&S.length>0?{warnings:S}:{},hint:`Waiting for human approval. Tell the user about "${h.displayName}" (${h.reviewUrl}). Poll GET \u2026/proposals/<id> for status.${S&&S.length>0?` Soft warnings (${S.length}) are non-blocking \u2014 configure later if needed.`:""}`})}catch(u){let p=O(u,"get");if(p){let f=Bo(p.body.code,p.body.cause);i.status(p.statusCode).json({...p.body,...f?{hint:f}:{}});return}$(i,u)}}}a(ji,"getLinkProposeHandler");K();Fe();K();var Fu=50,Hi=40,Ou=20;function $u(e){return Buffer.byteLength(JSON.stringify(e),"utf8")}a($u,"nodeBytes");function Mu(e){let t={id:e.id,type:e.type,bytes:$u(e)};return typeof e.name=="string"&&(t.name=e.name),"wires"in e&&(t.wires=e.wires),t}a(Mu,"summarizeNode");async function xr(e,t){let{rev:n,flow:r}=await xe(e,t),o=r,s=Array.isArray(o.nodes)?o.nodes.map(Mu):[],i={rev:n,flowId:o.id||t,nodes:s,nodeCount:s.length};return typeof o.label=="string"&&(i.label=o.label),typeof o.type=="string"&&(i.type=o.type),i}a(xr,"getFlowSummary");function Ui(e){let t=[];for(let n of e){if(!n||typeof n!="object"||Array.isArray(n))continue;let r=n;(r.type==="tab"||r.type==="subflow"||Array.isArray(r.nodes))&&t.push(r)}return t}a(Ui,"collectTabs");async function Bi(e,t,n){if(!t)throw new y("Missing node id","missing_node_id");if(n){let{rev:d,flow:l}=await xe(e,n),c=l,u=Array.isArray(c.nodes)?c.nodes:[],p=Array.isArray(c.configs)?c.configs:[],f=[...u,...p].find(m=>m.id===t);if(!f)throw new y("Node was not found","node_not_found",{statusCode:404});return{rev:d,flowId:n,node:f}}let{rev:r,flows:o}=await pe(e),s,i=null;for(let d of o){if(!d||typeof d!="object"||Array.isArray(d))continue;let l=d;if(l.id===t&&typeof l.type=="string"&&l.type!=="tab"&&l.type!=="subflow"){s=l,i=typeof l.z=="string"&&l.z?l.z:null;break}}if(!s)for(let d of Ui(o)){let l=d.id;if(l)try{let u=(await xe(e,l)).flow,f=[...u.nodes??[],...u.configs??[]].find(m=>m.id===t);if(f){s=f,i=l;break}}catch{}}if(!s)throw new y("Node was not found","node_not_found",{statusCode:404});return{rev:r,flowId:i,node:s}}a(Bi,"findNodeInFlows");async function Sr(e,t,n){let r=await Bi(e,t,n);return{rev:r.rev,flowId:r.flowId,node:r.node}}a(Sr,"getNodeById");function Du(e){if(e===void 0||e==="")return;let t=/^(\d+)(?:-(\d+))?$/.exec(e.trim());if(!t)throw new y("lines must be A or A-B (1-based inclusive; end >= start)","invalid_line_range");let n=Number(t[1]),r=t[2]!==void 0?Number(t[2]):n;if(!Number.isInteger(n)||!Number.isInteger(r)||n<1)throw new y("lines must be A or A-B (1-based inclusive; end >= start)","invalid_line_range");if(r<n)throw new y("lines must be A or A-B (1-based inclusive; end >= start)","invalid_line_range");return{start:n,end:r}}a(Du,"parseLineRange");async function Tr(e,t,n,r={}){if(!n)throw new y("Missing field name","missing_field");let o=await Bi(e,t,r.flowId);if(!(n in o.node))throw new y("Field was not found on the node","field_not_found");let s=o.node[n];if(typeof s!="string")throw new y("Field is not a string","field_not_string");let i=s.split(`
948
+ `),d=i.length,l=Buffer.byteLength(s,"utf8"),c=Du(r.lines);if(!c)return{rev:o.rev,flowId:o.flowId,nodeId:t,field:n,value:s,totalLines:d,bytes:l};if(c.start>d)throw new y(`lines range is out of bounds (start ${c.start} > ${d} total lines)`,"line_range_out_of_bounds",{details:{totalLines:d}});let u=c.start-1,p=Math.min(c.end,i.length),f=i.slice(u,p).join(`
949
+ `);return{rev:o.rev,flowId:o.flowId,nodeId:t,field:n,value:f,totalLines:d,bytes:l,lineRange:{start:c.start,end:Math.min(c.end,d)}}}a(Tr,"getNodeField");function ju(e){let t=[];for(let[n,r]of Object.entries(e))n==="id"||n==="type"||n==="z"||n==="wires"||typeof r=="string"&&r.length>0&&t.push({field:n,value:r});return t}a(ju,"stringFields");function Hu(e,t,n){let r=Math.max(0,t-Hi),o=Math.min(e.length,t+n+Hi),s=e.slice(r,o).replace(/\s+/g," ");return r>0&&(s=`\u2026${s}`),o<e.length&&(s=`${s}\u2026`),s}a(Hu,"snippetAround");function Uu(e,t){let n=1;for(let r=0;r<t&&r<e.length;r+=1)e.charCodeAt(r)===10&&(n+=1);return n}a(Uu,"lineNumberAtIndex");function qo(e,t,n,r,o){for(let{field:s,value:i}of ju(e)){if(r.length>=o)return!0;let d=[],l=new Set,c=-1,u=a((f,m)=>{l.has(f)||(l.add(f),d.push(f)),c<0&&(c=m)},"pushLine");if(!n.includes(`
1194
950
  `)){let f=i.split(`
1195
- `);for(let m=0;m<f.length;m+=1){let g=(f[m]??"").indexOf(n);if(g>=0){let v=0;for(let A=0;A<m;A+=1)v+=(f[A]?.length??0)+1;u(m+1,v+g)}}}if(n.includes(`
1196
- `)){let f=0;for(;f<=i.length;){let m=i.indexOf(n,f);if(m<0)break;u(Cu(i,m),m),f=m+Math.max(1,n.length)}}if(d.length===0)continue;let p=d.length;r.push({flowId:t,nodeId:e.id,field:s,lines:d.slice(0,Su),lineCount:p,snippet:Lu(i,Math.max(0,c),n.length)})}return r.length>=o}a(qo,"searchInNode");function $i(e){return[...e.nodes??[],...e.configs??[]]}a($i,"nodePool");async function Eu(e,t,n,r,o){let s=await xe(e,t);for(let i of $i(s.flow))if(qo(i,t,n,r,o))return{rev:s.rev,truncated:!0};return{rev:s.rev,truncated:!1}}a(Eu,"searchOneFlow");async function Iu(e,t,n,r){let o=await pe(e),s=new Set;for(let i of Oi(o.flows))if(i.id)try{let d=await xe(e,i.id);for(let l of $i(d.flow))if(s.add(l.id),qo(l,i.id,t,n,r))return{rev:o.rev,truncated:!0}}catch{}for(let i of o.flows){if(!i||typeof i!="object"||Array.isArray(i))continue;let d=i;if(!d.id||s.has(d.id)||d.type==="tab"||d.type==="subflow")continue;let l=typeof d.z=="string"?d.z:null;if(qo(d,l,t,n,r))return{rev:o.rev,truncated:!0}}return{rev:o.rev,truncated:!1}}a(Iu,"searchAllFlows");async function Sr(e,t,n={}){if(typeof t!="string"||!t.trim())throw new b("q query parameter is required","missing_query");let r=typeof n.cap=="number"&&n.cap>0?Math.floor(n.cap):xu,o=[],s=n.flowId?await Eu(e,n.flowId,t,o,r):await Iu(e,t,o,r),i={rev:s.rev,q:t,hits:o,hitCount:o.length,truncated:s.truncated,cap:r};return n.flowId&&(i.flowId=n.flowId),i}a(Sr,"searchFlows");function Di(e){if(e===void 0)return!1;let t=e.toLowerCase();return t==="1"||t==="true"||t==="yes"}a(Di,"isTruthyQueryFlag");function Nu(e,t,n){switch(e){case"node_not_found":case"flow_not_found":case"field_not_found":return n??ie(e,t);default:return ie(e,t)}}a(Nu,"layeredReadErrorHint");function pt(e){return Fe[e]??"Request failed"}a(pt,"safeErrorText");function fe(e,t,n){e.setHeader("X-Content-Type-Options","nosniff"),e.setHeader("Content-Type","application/json; charset=utf-8"),e.status(t).json(n)}a(fe,"sendJson");function Tr(e,t,n,r){let o=N(t,n);if(!o)return!1;let s=o.body.code,i={error:pt(s),code:s};typeof o.body.matchCount=="number"&&(i.matchCount=o.body.matchCount),typeof o.body.totalLines=="number"&&(i.totalLines=o.body.totalLines),typeof o.body.expectedRev=="string"&&(i.expectedRev=o.body.expectedRev),typeof o.body.currentRev=="string"&&(i.currentRev=o.body.currentRev);let d=Nu(s,n,r);return d&&(i.hint=d),fe(e,o.statusCode,i),!0}a(Tr,"sendMappedError");function _r(e){fe(e,502,{error:pt("upstream_error"),code:"upstream_error"})}a(_r,"sendUpstreamError");function Pr(e,t="rest"){return async(n,r)=>{let o=n.params.id;if(typeof o!="string"||o.length===0){fe(r,400,{error:pt("missing_flow_id"),code:"missing_flow_id"});return}let s=Di(R(n.query,"summary"));try{if(s){fe(r,200,await kr(e,o));return}fe(r,200,await xe(e,o))}catch(i){if(Tr(r,i,t))return;_r(r)}}}a(Pr,"getFlowOrSummaryHandler");function Rr(e,t="rest"){return async(n,r)=>{let o=n.params.id;if(typeof o!="string"||!o){fe(r,400,{error:pt("missing_node_id"),code:"missing_node_id"});return}let s=R(n.query,"flow");try{fe(r,200,await Ar(e,o,s))}catch(i){if(Tr(r,i,t,t==="get"?"Call GET \u2026/flows/<id>?summary=1 or \u2026/search?q=\u2026":t==="mcp"?void 0:"Call GET \u2026/flows/<id>?summary=1 or \u2026/search?q=\u2026"))return;_r(r)}}}a(Rr,"getNodeHandler");function Lr(e,t="rest"){return async(n,r)=>{let o=n.params.id,s=n.params.name;if(typeof o!="string"||!o){fe(r,400,{error:pt("missing_node_id"),code:"missing_node_id"});return}if(typeof s!="string"||!s){fe(r,400,{error:pt("missing_field"),code:"missing_field"});return}let i=R(n.query,"flow"),d=R(n.query,"lines");try{fe(r,200,await xr(e,o,s,{...i?{flowId:i}:{},...d?{lines:d}:{}}))}catch(l){if(Tr(r,l,t,"Use summary/search to discover node ids and fields."))return;_r(r)}}}a(Lr,"getNodeFieldHandler");function Cr(e,t="rest"){return async(n,r)=>{let o=R(n.query,"q");if(!o){fe(r,400,{error:pt("missing_query"),code:"missing_query",hint:t==="get"?"Example: GET \u2026/search?q=TODO&flow=tab1":ie("missing_query",t)});return}let s=R(n.query,"flow");try{fe(r,200,await Sr(e,o,s?{flowId:s}:{}))}catch(i){if(Tr(r,i,t))return;_r(r)}}}a(Cr,"searchFlowsHandler");G();it();var Fu={missing_node_type:"Missing node type",missing_query:"q query parameter is required",node_type_not_found:"Node type was not found",catalogue_unavailable:"Node-RED catalogue unavailable",unknown_node_type:"Unknown node type",upstream_error:"Upstream error"};function Ln(e){return Fu[e]??"Request failed"}a(Ln,"safeErrorText");function Xe(e,t,n){e.setHeader("X-Content-Type-Options","nosniff"),e.setHeader("Content-Type","application/json; charset=utf-8"),e.setHeader("Cache-Control","no-store"),e.status(t).json(n)}a(Xe,"sendJson");function Wo(e,t){let n=N(t);if(!n)return!1;let r=n.body.code,o={error:Ln(r),code:r};return typeof n.body.type=="string"&&(o.type=n.body.type),Array.isArray(n.body.nearest)&&(o.nearest=n.body.nearest),typeof n.body.didYouMean=="string"?(o.didYouMean=n.body.didYouMean,o.caseSensitive=!0,o.hint=`Types are case-sensitive \u2014 use '${n.body.didYouMean}' exactly.`):(r==="node_type_not_found"||r==="unknown_node_type")&&(o.hint="Call GET \u2026/node-types?q= or \u2026/palette/search?q=. Ask the user to install via Manage palette, then recheck node-types. This API never installs."),r==="catalogue_unavailable"&&(o.hint="Retry later; palette search is recommend-only and never installs packages."),Xe(e,n.statusCode,o),!0}a(Wo,"sendMappedError");function Er(e){return async(t,n)=>{try{let r=R(t.query,"q"),o=await or(e.nodeTypes,r?{q:r}:{});Xe(n,200,o)}catch(r){if(Wo(n,r))return;Xe(n,502,{error:Ln("upstream_error"),code:"upstream_error"})}}}a(Er,"listNodeTypesHandler");function Ir(e){return async(t,n)=>{try{let r=t.params.type,o=typeof r=="string"?decodeURIComponent(r):"",s=R(t.query,"help"),i=!(s==="0"||s==="false"||s==="no"||s==="omit"),d=await st(e.nodeTypes,o,{includeHelp:i});Xe(n,200,d)}catch(r){if(Wo(n,r))return;Xe(n,502,{error:Ln("upstream_error"),code:"upstream_error"})}}}a(Ir,"getNodeTypeHandler");function Nr(e){return async(t,n)=>{try{let r=R(t.query,"q");if(!r){Xe(n,400,{error:Ln("missing_query"),code:"missing_query",hint:"Pass ?q= to search the Node-RED catalogue (recommend only)."});return}let o=await sr(e.nodeTypes,r,{...e.catalogueFetcher?{catalogueFetcher:e.catalogueFetcher}:{},...e.catalogueUrl?{catalogueUrl:e.catalogueUrl}:{}});Xe(n,200,o)}catch(r){if(Wo(n,r))return;Xe(n,502,{error:Ln("upstream_error"),code:"upstream_error"})}}}a(Nr,"searchPaletteHandler");Ge();ve();function Qt(e,t){return e[t]??""}a(Qt,"lineAt");function Cn(e,t,n){let r=e[t];return r?r[n]??0:0}a(Cn,"cell");function ji(e,t,n,r){let o=e[t];o&&(o[n]=r)}a(ji,"setCell");function Ui(e,t){let n=Hi(e),r=Hi(t),o=n.length,s=r.length,i=[];for(let u=0;u<=o;u+=1){let p=[];for(let f=0;f<=s;f+=1)p.push(0);i.push(p)}for(let u=o-1;u>=0;u-=1)for(let p=s-1;p>=0;p-=1)Qt(n,u)===Qt(r,p)?ji(i,u,p,Cn(i,u+1,p+1)+1):ji(i,u,p,Math.max(Cn(i,u+1,p),Cn(i,u,p+1)));let d=[],l=0,c=0;for(;l<o&&c<s;){let u=Qt(n,l),p=Qt(r,c);u===p?(d.push({op:"equal",text:u}),l+=1,c+=1):Cn(i,l+1,c)>=Cn(i,l,c+1)?(d.push({op:"remove",text:u}),l+=1):(d.push({op:"add",text:p}),c+=1)}for(;l<o;)d.push({op:"remove",text:Qt(n,l)}),l+=1;for(;c<s;)d.push({op:"add",text:Qt(r,c)}),c+=1;return d}a(Ui,"diffLines");function Hi(e){return e.length===0?[]:e.split(`
1197
- `)}a(Hi,"splitLines");Ae();var Wi=new Set(["func","template","info","payload","code","text","format","syntax"]),Ou=new Set(["x","y","w","h","wires","g"]),Mu=new Set(["w","h","g"]);function ft(e){if(typeof e.name=="string"&&e.name.length>0)return e.name;if(typeof e.label=="string"&&e.label.length>0)return e.label}a(ft,"nodeName");function Bi(e){let t=new Map;for(let n of e.nodes??[])t.set(n.id,n);for(let n of e.configs??[])t.set(n.id,n);return t}a(Bi,"collectNodes");function Mr(e){if(e===void 0)return"undefined";if(e===null||typeof e!="object")return JSON.stringify(e);if(Array.isArray(e))return`[${e.map(Mr).join(",")}]`;let t=e;return`{${Object.keys(t).sort().map(r=>`${JSON.stringify(r)}:${Mr(t[r])}`).join(",")}}`}a(Mr,"stableStringify");function Gi(e,t){return Mr(e)===Mr(t)}a(Gi,"valuesEqual");function $r(e){return typeof e=="string"?e:null}a($r,"asText");function $u(e){return/<\/?[a-z][\s\S]*>/i.test(e.slice(0,800))}a($u,"looksLikeHtml");function Du(e,t,n){if(t!=="template")return!1;let r=typeof e.format=="string"?e.format.toLowerCase():"";return r==="html"||r==="handlebars"||r==="mustache"?!0:$u(n)}a(Du,"isHtmlTemplateField");function zi(e,t,n){let r=$r(n);if(Wi.has(t)&&r!==null){let o={key:t,kind:"text",value:r};return Du(e,t,r)&&(o.preview="html"),o}return{key:t,kind:"scalar",value:n===void 0?null:n}}a(zi,"toPresenceProperty");function ju(e,t){let n=[];for(let r of[...Object.keys(e)].sort())if(!(Mu.has(r)||r==="id"||r==="z")){if(r==="wires"){let o=Ji(e,t);n.push({key:"wires",kind:"scalar",value:o.length>0?o:e.wires??null});continue}n.push(zi(e,r,e[r]))}return n}a(ju,"nodeProperties");function Ji(e,t){let n=e.wires;if(!Array.isArray(n)||n.length===0)return[];let r=[];return n.forEach((o,s)=>{if(!Array.isArray(o))return;let i=[];for(let d of o){if(typeof d!="string"||!d)continue;let l=t.get(d);i.push({id:d,name:l?ft(l)??l.type:d})}r.push({port:s,to:i})}),r}a(Ji,"describeWiresStructured");var Hu=new Set(["func","template","code","text"]);function Uu(e,t){let n=[],r=ft(e);r&&n.push({key:"name",kind:"scalar",value:r});let o=Ji(e,t);o.length>0&&n.push({key:"wires",kind:"scalar",value:o});for(let s of Hu){if(!(s in e))continue;let i=$r(e[s]);i===null||i.length===0||n.push(zi(e,s,i))}return n}a(Uu,"buildKeyFields");function Bu(e,t,n){let r=We(e.type),o=ft(e),s=o?` "${o}"`:"",i=t?` in "${t}"`:"",d=n===1?"1 field":`${n} fields`;return`Changes ${r} node${s}${i} (${d})`}a(Bu,"buildModifiedSummary");function qu(e,t,n){let r=We(e.type),o=ft(e),s=t==="added"?"Adds":"Removes",i=t==="added"?"to":"from",d=o?` ${JSON.stringify(o)}`:"",l=n?` ${i} flow ${JSON.stringify(n)}`:"",c=`${s} ${r} node${d}${l}`;if(t==="added"&&r==="template"){let u=typeof e.field=="string"&&e.field?e.field:"";u&&(c+=`, output to msg.${u}`)}return c}a(qu,"buildPresenceSummary");function Fr(e,t,n,r){let o={id:e.id,type:e.type,summary:qu(e,t,n),keyFields:Uu(e,r),properties:ju(e,r)},s=ft(e);return s!==void 0&&(o.name=s),o}a(Fr,"toPresenceNode");function Wu(e,t){let n=new Set([...Object.keys(e),...Object.keys(t)]),r=[];for(let o of[...n].sort()){if(Ou.has(o)||o==="id"||o==="z")continue;let s=e[o],i=t[o];if(Gi(s,i))continue;let d=$r(s),l=$r(i);if(Wi.has(o)&&(d!==null||l!==null)){let c=d??"",u=l??"";r.push({key:o,kind:"text",before:c,after:u,lines:Ui(c,u)});continue}r.push({key:o,kind:"scalar",before:s===void 0?null:s,after:i===void 0?null:i})}return r}a(Wu,"compareNodes");function Gu(e,t){let n=["label","disabled","info","env"],r=[];for(let o of n)Gi(e[o],t[o])||r.push(o);return r}a(Gu,"compareTabMeta");function Or(e){return typeof e.label=="string"&&e.label.trim()?e.label.trim():""}a(Or,"flowLabelOf");function Vi(e,t){let n=Bi(t),r=Bi(e),o=Or(e)||Or(t),s=Or(t)||Or(e),i=[],d=[],l=[];for(let c of e.nodes??[])n.has(c.id)||i.push(Fr(c,"added",o,r));for(let c of e.configs??[])n.has(c.id)||i.push(Fr(c,"added",o,r));for(let c of t.nodes??[])r.has(c.id)||d.push(Fr(c,"removed",s,n));for(let c of t.configs??[])r.has(c.id)||d.push(Fr(c,"removed",s,n));for(let[c,u]of r){let p=n.get(c);if(!p)continue;let f=Wu(p,u);if(f.length>0){let m={id:c,type:u.type||p.type,summary:Bu(u,o||s,f.length),changes:f},h=ft(u)??ft(p);h!==void 0&&(m.name=h),l.push(m)}}return l.sort((c,u)=>c.id.localeCompare(u.id)),{summary:{added:i.length,removed:d.length,modified:l.length,tabChanges:Gu(t,e),beforeNodeCount:n.size,afterNodeCount:r.size},added:i,removed:d,modified:l}}a(Vi,"buildDiffSections");function qi(e,t,n={}){let r=Vi(e.payload.flow,t.flow),o=n.currentRev??t.rev,s=e.expectedRev,i=n.baseRevStale??(e.status==="pending"&&s!==o),d={proposalId:e.id,flowId:e.payload.flowId,expectedRev:s,currentRev:o,baseRevStale:i,compareAgainst:n.compareAgainst??"current",...r};return e.warnings&&e.warnings.length>0&&(d.warnings=e.warnings.map(l=>({...l}))),n.baseSnapshotMissing&&(d.baseSnapshotMissing=!0),n.vsCurrent&&(d.vsCurrent=n.vsCurrent),d}a(qi,"buildProposalDiff");async function Zt(e,t){let n=e.payload.flowId,r={id:n,type:"tab",nodes:[],configs:[]},o=e.expectedRev,s=r;try{let c=await t.getFlow(n);o=c.rev,s=c.flow}catch(c){if(!(c instanceof B))throw c;try{o=(await t.getFlows()).rev}catch{}}let d=e.status==="pending"&&e.expectedRev!==o,l=e.baseFlow;if(l!==void 0){let c=d?Vi(e.payload.flow,s):void 0;return qi(e,{rev:e.expectedRev,flow:l},{currentRev:o,compareAgainst:"base",baseRevStale:d,...c?{vsCurrent:c}:{}})}return qi(e,{rev:o,flow:s},{compareAgainst:"current",baseSnapshotMissing:!0,baseRevStale:d})}a(Zt,"computeProposalDiff");Ae();Pe();_e();var Ki=require("path");function Dr(e){return(0,Ki.join)(e.userDir,".ai-access")}a(Dr,"getStorageDir");var zu=["status:read","flows:read","proposals:create","proposals:read-own"];function Go(){return zt()}a(Go,"readPackageVersion");function Ju(e,t=Date.now()){let n=Date.parse(e);return Number.isFinite(n)?Math.max(0,Math.ceil((n-t)/1e3)):0}a(Ju,"secondsRemainingUntil");function Vu(e,t=Date.now()){let n=Date.parse(e);return Number.isFinite(n)?Math.max(0,Math.ceil((n-t)/6e4)):0}a(Vu,"minutesRemainingUntil");function Ku(e,t){let n=de(t),r=je(t),o=`${n}/openapi.yaml`,s=`${n}/openapi.json`;return e==="rest"?{api:n,openapi:o,openapiJson:s,discovery:n}:e==="mcp"?{rpc:r,discovery:n}:{discovery:n}}a(Ku,"baseUrlsForTokenKind");function en(e,t){let n=zn(e,t),r=ge(e.settings);if(t.auth){let o=t.auth.extra?.user;return{ok:!0,version:Go(),adminAuthEnabled:n.adminAuthEnabled,aiAccessEnabled:r.enabled,aiFeaturesEnabled:n.aiFeaturesEnabled,allowWithoutAdminAuth:n.allowWithoutAdminAuth,permission:t.auth.scopes.includes("propose")?"propose":"read",capabilities:{view:!0,manageLinks:!1,approveProposals:!1},defaultLinkTtlSeconds:r.defaultLinkTtlSeconds,maxActiveLinks:r.maxActiveLinks,defaultDeploymentType:r.defaultDeploymentType,caller:{kind:"oauth",clientId:t.auth.clientId,user:typeof o=="string"?o:"owner",scopes:t.auth.scopes}}}if(t.accessLink){let o=Date.now();return{ok:!0,version:Go(),adminAuthEnabled:n.adminAuthEnabled,aiAccessEnabled:r.enabled,aiFeaturesEnabled:n.aiFeaturesEnabled,allowWithoutAdminAuth:n.allowWithoutAdminAuth,permission:null,capabilities:{view:!0,manageLinks:!1,approveProposals:!1},defaultLinkTtlSeconds:r.defaultLinkTtlSeconds,maxActiveLinks:r.maxActiveLinks,defaultDeploymentType:r.defaultDeploymentType,caller:{kind:"access-link",linkId:t.accessLink.id,tokenKind:t.accessLink.kind,...t.accessLink.label!==void 0?{label:t.accessLink.label}:{},expiresAt:t.accessLink.expiresAt,minutesRemaining:Vu(t.accessLink.expiresAt,o),secondsRemaining:Ju(t.accessLink.expiresAt,o),permissions:[...zu],baseUrls:Ku(t.accessLink.kind,e.settings.httpAdminRoot)}}}return{ok:!0,version:Go(),adminAuthEnabled:n.adminAuthEnabled,aiAccessEnabled:r.enabled,aiFeaturesEnabled:n.aiFeaturesEnabled,allowWithoutAdminAuth:n.allowWithoutAdminAuth,permission:n.permission,capabilities:n.capabilities,storageDir:Dr(e.settings),defaultLinkTtlSeconds:r.defaultLinkTtlSeconds,maxActiveLinks:r.maxActiveLinks,defaultDeploymentType:r.defaultDeploymentType,caller:{kind:"admin",...n.permission?{permission:n.permission}:{}}}}a(en,"buildStatus");function Xi(e,t){let n=t.draftStore??new yr,r=t.rateLimiters??Si(t.rateLimit??e.settings.aiAccess),o=e.httpAdmin,s="/mcp/a/:token",i=[r.ip,_i(e,t.linkStore),r.token];o.use("/mcp/a",ci()),o.get(s,...i,P((u,p)=>{I(p),p.setHeader("X-Content-Type-Options","nosniff");let f=de(e.settings.httpAdminRoot),m=u.accessLink;if(!m){L(p,401,"Access link required","access_link_missing");return}let h=wi({link:m,openapiPath:`${f}/openapi.yaml`,restApiBase:f,mcpRpcPath:je(e.settings.httpAdminRoot),publicOrigin:ro(u)});p.status(200).type("text/markdown; charset=utf-8").send(h)})),o.get(`${s}/status`,...i,P((u,p)=>{I(p),p.json(en(e,u))})),o.get(`${s}/flows`,...i,P(async(u,p)=>{I(p);try{p.json(await pe(t.flowAdapter))}catch(f){let m=N(f);if(m){p.status(m.statusCode).json({...m.body,hint:"Retry after the owner fixes the plugin runtime."});return}L(p,502,f instanceof Error?f.message:String(f),"upstream_error")}})),o.get(`${s}/flows/:id`,...i,P(async(u,p)=>{I(p),await Pr(t.flowAdapter,"get")(u,p)})),o.get(`${s}/nodes/:id`,...i,P(async(u,p)=>{I(p),await Rr(t.flowAdapter,"get")(u,p)})),o.get(`${s}/nodes/:id/field/:name`,...i,P(async(u,p)=>{I(p),await Lr(t.flowAdapter,"get")(u,p)})),o.get(`${s}/search`,...i,P(async(u,p)=>{I(p),await Cr(t.flowAdapter,"get")(u,p)}));let d={nodeTypes:t.nodeTypesAdapter,...t.catalogueFetcher?{catalogueFetcher:t.catalogueFetcher}:{},...t.catalogueUrl?{catalogueUrl:t.catalogueUrl}:{}};o.get(`${s}/node-types`,...i,P(async(u,p)=>{I(p),await Er(d)(u,p)})),o.get(`${s}/node-types/:type`,...i,P(async(u,p)=>{I(p),await Ir(d)(u,p)})),o.get(`${s}/palette/search`,...i,P(async(u,p)=>{I(p),await Nr(d)(u,p)}));let l=e.settings.httpAdminRoot;Xu(o,s,i,t,l),o.get(`${s}/propose`,...i,P(Ni(t.proposalStore,t.auditLog,t.flowAdapter,l,t.nodeTypesAdapter))),Zu(o,s,i,n,t,l);let c=Ri();return o.all(`${s}/links`,c),o.all(`${s}/links/:id`,c),o.use("/mcp/a",Pi()),n}a(Xi,"registerGetLinkRoutes");function Xu(e,t,n,r,o){e.get(`${t}/proposals`,...n,P(async(s,i)=>{I(i);let d=s.accessLink;if(!d){L(i,401,"Access link required","access_link_missing");return}let l=U(s,o),c=oe(s.query),u=pr(s.query),p;try{p=(await pe(r.flowAdapter)).rev}catch{}let f=p?z(r.proposalStore,p):0,m=Ht(r.proposalStore,d,l,u);i.json({proposals:m.proposals.map(h=>ue(r.proposalStore,d,h,{...l,include:c},{includeConflicts:!1})),...dt({...m,returned:m.proposals.length}),...X(f)})})),e.get(`${t}/proposals/:id`,...n,P(async(s,i)=>{I(i);let d=s.params.id,l=s.accessLink;if(typeof d!="string"||!d){L(i,400,"Missing proposal id","missing_proposal_id");return}if(!l){L(i,401,"Access link required","access_link_missing");return}try{let c=U(s,o),u=oe(s.query),p;try{p=(await pe(r.flowAdapter)).rev}catch{}let f=Me(r.proposalStore,l,d,c);i.json(ue(r.proposalStore,l,f,{...c,include:u},{...p?{liveRev:p}:{}}))}catch(c){let u=N(c,"get");if(u){i.status(u.statusCode).json({...u.body,hint:u.body.hint??"List proposals with GET \u2026/proposals."});return}F(i,c);return}})),Yu(e,t,n,r,o),Qu(e,t,n,r,o)}a(Xu,"registerGetLinkProposalRoutes");function Yu(e,t,n,r,o){e.get(`${t}/proposals/:id/withdraw`,...n,P((s,i)=>{I(i);let d=s.params.id,l=s.accessLink;if(typeof d!="string"||!d){L(i,400,"Missing proposal id","missing_proposal_id");return}if(!l){L(i,401,"Access link required","access_link_missing");return}let c=s.query.confirm;if(c!=="1"&&c!=="true"){L(i,400,"Withdraw over GET requires confirm=1","confirm_required",ie("confirm_required","get"));return}let u=typeof s.query.reason=="string"?s.query.reason:void 0,p=U(s,o),f=oe(s.query);try{let m=Ut(r.proposalStore,r.auditLog,l,d,u,p);i.json(ue(r.proposalStore,l,m,{...p,include:f}))}catch(m){let h=N(m,"get");if(h){let g=h.body.code;L(i,h.statusCode,Fe[g]??"Request failed",g,ie(g,"get"));return}L(i,500,"Internal server error","internal_error");return}}))}a(Yu,"registerGetLinkWithdrawRoute");function Qu(e,t,n,r,o){e.get(`${t}/proposals/:id/diff`,...n,P(async(s,i)=>{I(i);let d=s.params.id,l=s.accessLink;if(typeof d!="string"||!d){L(i,400,"Missing proposal id","missing_proposal_id");return}if(!l){L(i,401,"Access link required","access_link_missing");return}try{let c=Me(r.proposalStore,l,d,U(s,o)),u=R(s.query,"format"),p=Vt(u==="json"||u==="md"?void 0:u);if(!p.ok){L(i,400,"Invalid diff format","invalid_diff_format","Valid formats: summary, full (default summary).");return}i.json(Kt(await Zt(c,r.flowAdapter),p.format))}catch(c){let u=N(c);if(u){i.status(u.statusCode).json(u.body);return}F(i,c);return}}))}a(Qu,"registerGetLinkDiffRoute");function Zu(e,t,n,r,o,s){e.get(`${t}/draft/new`,...n,P(Ci(r,o.flowAdapter))),e.get(`${t}/draft/:draftId/append`,...n,P(Ei(r))),e.get(`${t}/draft/:draftId/submit`,...n,P(Ii(r,o.proposalStore,o.auditLog,o.flowAdapter,s,o.nodeTypesAdapter)))}a(Zu,"registerGetLinkDraftRoutes");q();var Yi=require("module");function ep(){if(typeof require=="function")return require;let e=(0,eval)("import.meta.url");return(0,Yi.createRequire)(e)}a(ep,"createPluginRequire");function tp(e){try{let r=ep()("@node-red/runtime")._?.adminApi?.httpAdmin;if(r&&typeof r.use=="function"&&r!==e.httpAdmin)return r}catch{}return null}a(tp,"resolveEditorHttpAdmin");function Qi(e){let t=[e.httpAdmin],n=tp(e);return n&&t.push(n),t}a(Qi,"httpAdminAppsForMcpErrors");var Zi=Symbol.for("com55.ai-access.mcpJsonErrorHandler");function ea(e){return/\/mcp\/rpc\/?$/.test(e)||e.includes("/mcp/rpc")}a(ea,"isMcpRpcPath");function zo(e){return e.includes("/mcp")}a(zo,"isMcpPath");function np(e){return e.path||e.url||e.originalUrl||""}a(np,"requestPath");function rp(e){if(!e||typeof e!="object")return!1;let t=e;if(t.type==="entity.parse.failed"||e instanceof SyntaxError)return!0;let n=typeof e.message=="string"?e.message:"";return/JSON/i.test(n)&&(t.status===400||t.statusCode===400)}a(rp,"isJsonParseError");function op(e){if(!e||typeof e!="object")return!1;let t=e;return t.type==="entity.too.large"||t.status===413||t.statusCode===413||t.message==="request entity too large"}a(op,"isPayloadTooLarge");function sp(){return(e,t,n,r)=>{if(n.headersSent){r(e);return}let o=np(t);if(rp(e)&&zo(o)){if(ea(o)){n.status(400).json({jsonrpc:"2.0",error:{code:-32700,message:"Parse error"},id:null});return}n.status(400).json({error:"Invalid JSON body",code:"invalid_json"});return}if(op(e)&&zo(o)){n.status(413).json({error:`Request body exceeds limit of ${$} bytes`,code:"payload_too_large"});return}if(zo(o)){if(ea(o)){n.status(500).json({jsonrpc:"2.0",error:{code:-32603,message:gr(e).error,data:{code:"internal_error"}},id:null});return}n.status(500).json(gr(e));return}r(e)}}a(sp,"mcpJsonErrorHandler");function ip(e){let t=e;t[Zi]||(t[Zi]=!0,e.use(sp()))}a(ip,"attachOnce");function ta(e){for(let t of Qi(e))ip(t)}a(ta,"attachMcpJsonErrorHandlers");Pn();var mt=class extends Error{static{a(this,"AccessLinkLimitError")}code="access_link_limit";statusCode=409;constructor(t){super(`Cannot create access link: at most ${t} active links are allowed. Revoke an existing link first.`),this.name="AccessLinkLimitError"}},ht=class extends Error{static{a(this,"AccessLinkNotFoundError")}code="access_link_not_found";statusCode=404;constructor(t){super(`Access link '${t}' was not found`),this.name="AccessLinkNotFoundError"}},En=class extends Error{static{a(this,"AccessLinkValidationError")}code="access_link_invalid";statusCode=400;constructor(t){super(t),this.name="AccessLinkValidationError"}},Ye=class extends Error{static{a(this,"AccessLinkTtlError")}code="invalid_ttl";statusCode=400;hint=lt;constructor(t){super(t),this.name="AccessLinkTtlError"}};var gt=require("crypto");Pn();function Jo(e){return e==="get_link"||e==="rest"||e==="mcp"}a(Jo,"isAccessLinkKind");function Vo(e){return e??"get_link"}a(Vo,"resolveAccessLinkKind");function na(e){return(0,gt.createHash)("sha256").update(e,"utf8").digest("hex")}a(na,"hashToken");function ap(e,t){let n=Buffer.from(e,"utf8"),r=Buffer.from(t,"utf8");return n.length!==r.length?!1:(0,gt.timingSafeEqual)(n,r)}a(ap,"hashesEqual");function In(e){let t={id:e.id,kind:Vo(e.kind),createdAt:new Date(e.createdAtMs).toISOString(),expiresAt:new Date(e.expiresAtMs).toISOString()};return e.label!==void 0&&(t.label=e.label),e.lastUsedAtMs!==void 0&&(t.lastUsedAt=new Date(e.lastUsedAtMs).toISOString()),e.createdBy!==void 0&&(t.createdBy=e.createdBy),t}a(In,"toPublic");var yt=class{static{a(this,"AccessLinkStore")}links=new Map;defaultTtlSeconds;maxActiveLinks;now;randomId;randomToken;constructor(t={}){this.defaultTtlSeconds=t.defaultTtlSeconds??14400,this.maxActiveLinks=t.maxActiveLinks??10,this.now=t.now??(()=>Date.now()),this.randomId=t.randomId??(()=>(0,gt.randomBytes)(12).toString("hex")),this.randomToken=t.randomToken??(()=>`aia_${(0,gt.randomBytes)(32).toString("base64url")}`)}configureDefaults(t){typeof t.defaultTtlSeconds=="number"&&Number.isFinite(t.defaultTtlSeconds)&&t.defaultTtlSeconds>0&&(this.defaultTtlSeconds=Math.floor(t.defaultTtlSeconds)),typeof t.maxActiveLinks=="number"&&Number.isFinite(t.maxActiveLinks)&&t.maxActiveLinks>0&&(this.maxActiveLinks=Math.floor(t.maxActiveLinks))}isActive(t,n){return t.revokedAtMs!==void 0?!1:t.expiresAtMs>n}list(){let t=this.now();return[...this.links.values()].filter(n=>this.isActive(n,t)).sort((n,r)=>r.createdAtMs-n.createdAtMs).map(In)}create(t={}){let n=this.now();if([...this.links.values()].filter(p=>this.isActive(p,n)).length>=this.maxActiveLinks)throw new mt(this.maxActiveLinks);let o=this.defaultTtlSeconds;if(t.ttlSeconds!==void 0){if(typeof t.ttlSeconds!="number"||!Number.isFinite(t.ttlSeconds)||t.ttlSeconds<=0)throw new Ye("ttlSeconds must be a positive number");o=Math.floor(t.ttlSeconds)}let s=typeof t.label=="string"&&t.label.trim().length>0?t.label.trim():void 0,i=Vo(t.kind),d=No(i);if(o>d)throw new Ye(Fo(i,d));let l=this.randomId(),c=this.randomToken(),u={id:l,tokenHash:na(c),token:c,kind:i,createdAtMs:n,expiresAtMs:n+o*1e3};return s!==void 0&&(u.label=s),t.createdBy!==void 0&&(u.createdBy=t.createdBy),this.links.set(l,u),{...In(u),token:c}}peekToken(t){let n=this.links.get(t);return!n||!this.isActive(n,this.now())?null:n.token}revoke(t){let n=this.links.get(t);if(!n)throw new ht(t);return n.revokedAtMs??=this.now(),In(n)}getById(t){if(typeof t!="string"||t.length===0)return null;let n=this.links.get(t);return n?In(n):null}validateResult(t){if(typeof t!="string"||t.length===0)return{status:"invalid"};let n=na(t),r=this.now();for(let o of this.links.values())if(ap(o.tokenHash,n))return o.revokedAtMs!==void 0?{status:"invalid"}:o.expiresAtMs<=r?{status:"expired",expiresAt:new Date(o.expiresAtMs).toISOString()}:(o.lastUsedAtMs=r,{status:"active",link:In(o)});return{status:"invalid"}}validate(t){let n=this.validateResult(t);return n.status==="active"?n.link:null}size(){return this.links.size}clear(){this.links.clear()}upsertRecordForTest(t){this.links.set(t.id,t)}};Pn();ae();_e();function Ko(e,t){return t}a(Ko,"storeForRequest");function oa(e){let t=typeof e.user?.username=="string"?e.user.username:void 0;return t?{kind:"admin",username:t}:{kind:"admin"}}a(oa,"adminActor");function jr(e){let t=ge(e.settings);return new yt({defaultTtlSeconds:t.defaultLinkTtlSeconds,maxActiveLinks:t.maxActiveLinks})}a(jr,"createConfiguredLinkStore");function sa(e,t){return(n,r)=>{let o=Ko(e,t),s=o.list().map(i=>{let d=o.peekToken(i.id);return d?i.kind==="get_link"?{...i,url:oo(n,e.settings.httpAdminRoot,d)}:{...i,token:d}:i});r.json({links:s})}}a(sa,"listLinksHandler");function ia(e,t,n){return(r,o)=>{let s=r.body??{},i={};if(s.ttlSeconds!==void 0){if(typeof s.ttlSeconds!="number"){o.status(400).json({error:"ttlSeconds must be a positive number",code:"invalid_ttl",hint:lt});return}i.ttlSeconds=s.ttlSeconds}if(typeof s.label=="string"&&(i.label=s.label),s.kind!==void 0){if(!Jo(s.kind)){o.status(400).json({error:"kind must be one of: 'get_link', 'rest', 'mcp'",code:"invalid_link_kind"});return}i.kind=s.kind}typeof r.user?.username=="string"&&(i.createdBy=r.user.username);try{let d=Ko(e,t).create(i),l=d.kind;if(n){let c=Date.parse(d.createdAt),u=Date.parse(d.expiresAt),p=Number.isFinite(c)&&Number.isFinite(u)&&u>c?Math.round((u-c)/1e3):void 0;n.append("link.created",oa(r),{linkId:d.id,label:d.label,expiresAt:d.expiresAt,createdAt:d.createdAt,kind:l,...p!==void 0?{ttlSeconds:p}:{}})}if(l==="get_link"){let c=oo(r,e.settings.httpAdminRoot,d.token);o.status(201).json({...d,url:c});return}o.status(201).json({...d})}catch(d){if(d instanceof mt){o.status(d.statusCode).json({error:d.message,code:d.code});return}if(d instanceof Ye){o.status(d.statusCode).json({error:d.message,code:d.code,hint:d.hint});return}if(d instanceof En){o.status(d.statusCode).json({error:d.message,code:d.code});return}F(o,d);return}}}a(ia,"createLinkHandler");function aa(e,t,n){return(r,o)=>{let s=r.params.id;if(typeof s!="string"||s.length===0){o.status(400).json({error:"Missing link id"});return}try{let i=Ko(e,t).revoke(s);if(n){let d=Date.parse(i.createdAt),l=Date.parse(i.expiresAt),c=Number.isFinite(d)&&Number.isFinite(l)&&l>d?Math.round((l-d)/1e3):void 0;n.append("link.revoked",oa(r),{linkId:i.id,label:i.label,expiresAt:i.expiresAt,createdAt:i.createdAt,kind:i.kind,...c!==void 0?{ttlSeconds:c}:{}})}o.json({revoked:!0,link:i})}catch(i){if(i instanceof ht){o.status(i.statusCode).json({error:i.message,code:i.code});return}F(o,i);return}}}a(aa,"revokeLinkHandler");function dp(e){let t=Date.parse(e.createdAt),n=Date.parse(e.expiresAt);if(Number.isFinite(t)&&Number.isFinite(n)&&n>t)return Math.round((n-t)/1e3)}a(dp,"ttlSecondsFromLink");function lp(e,t){if(!t||e.action!=="link.created"&&e.action!=="link.revoked")return e;let n={...e.details??{}},r=typeof n.linkId=="string"?n.linkId:"";if(!r)return e;let o=n.label==null||n.label==="",s=n.expiresAt==null||n.expiresAt==="",i=n.createdAt==null||n.createdAt==="",d=n.ttlSeconds==null;if(!o&&!s&&!i&&!d)return e;let l=t.getById(r);if(!l)return e;if(o&&l.label&&(n.label=l.label),s&&(n.expiresAt=l.expiresAt),i&&(n.createdAt=l.createdAt),d){let c=dp(l);c!==void 0&&(n.ttlSeconds=c)}return{...e,details:n}}a(lp,"enrichAuditLogEntry");function da(e,t){return(n,r)=>{let o=n.query.limit,s=100;if(typeof o=="string"&&o.length>0){let d=Number(o);Number.isFinite(d)&&d>0&&(s=Math.min(Math.floor(d),500))}let i=e.list(s).map(d=>lp(d,t));r.json({logs:i})}}a(da,"listLogsHandler");var Xo=require("@modelcontextprotocol/sdk/server/mcp.js"),la=require("@modelcontextprotocol/sdk/server/streamableHttp.js"),x=require("zod");Ue();q();tt();G();Ne();it();Ge();Ae();function ca(){return"0.2.0-beta.1"}a(ca,"pluginVersion");async function ua(e){try{return(await pe(e)).rev}catch{return}}a(ua,"tryLiveRev");function Y(e){return{content:[{type:"text",text:JSON.stringify(e)}]}}a(Y,"jsonResult");function Yo(e){return e===!0?new Set(["payload","baseFlow"]):new Set}a(Yo,"includeSetFromPayloadFlag");function Q(e,t=500){let n=_n(e,"mcp");if(n)return{isError:!0,content:[{type:"text",text:JSON.stringify({...n.body,statusCode:n.statusCode})}]};let r=e instanceof Error?e.message:String(e);return{isError:!0,content:[{type:"text",text:JSON.stringify({error:r,code:"internal_error",statusCode:t})}]}}a(Q,"jsonError");var cp=new Set(["list_proposals","list_my_proposals"]),up=new Set(["status","limit","offset","mine","flowId"]);function pp(e){return!!e&&typeof e=="object"&&typeof e.safeParseAsync=="function"}a(pp,"isZodLikeSchema");function fp(e,t,n){let r=t&&typeof t=="object"?t:{},o=n[0],s=o?.path&&o.path.length>0?String(o.path[0]):"(arguments)";if(cp.has(e)&&up.has(s))return jt(s,r[s]);let i=n.map(l=>({parameter:l.path&&l.path.length>0?l.path.map(String).join("."):"(arguments)",message:l.message??"Invalid value"})),d=i.length===1?`Invalid argument '${i[0]?.parameter}' for tool ${e}: ${i[0]?.message}`:`${i.length} invalid arguments for tool ${e}`;return new b(d,"invalid_arguments",{details:{parameter:s,errors:i,hint:`Fix '${s}' to match the ${e} input schema (see tools/list).`}})}a(fp,"mcpArgumentError");function pa(e){let t=e,n=t.validateToolInput;typeof n=="function"&&(t.validateToolInput=async(r,o,s)=>{let i=r.inputSchema;if(!pp(i))return n.call(e,r,o,s);let d=await i.safeParseAsync(o??{});if(d.success)return d.data;let l=_n(fp(s,o,d.error.issues),"mcp"),c=l?{...l.body,statusCode:l.statusCode}:{};throw new Error(JSON.stringify(c))})}a(pa,"installMcpArgumentErrorMapping");function fa(e,t,n,r){e.registerTool("get_status",{description:'Return plugin status (version, AI feature flags, storage path, deploy defaults). For access-link callers, permission is null and caller.kind is "access-link" \u2014 that is expected, not an error. Use before other tools to confirm the plugin is enabled.'},()=>{try{return Y(en(t,r))}catch(o){return Q(o)}}),e.registerTool("list_flows",{description:"List all flows as a flat Admin API v2 array plus the current rev string. Returns { rev, flows }. Call this (or get_flow) before submit_proposal so expectedRev matches."},async()=>{try{return Y(await pe(n.flowAdapter))}catch(o){return Q(o,502)}}),e.registerTool("get_flow",{description:"Get one tab/subflow by id with nested nodes/configs and the current rev. Pass summary=true for a lightweight node list (id/type/name/wires/bytes, no field contents). Prefer summary \u2192 search_flows \u2192 get_node_field(lines) \u2192 replace_text \u2192 submit_proposal. Returns { rev, flow } or summary shape. Use rev as expectedRev when submitting.",inputSchema:{id:x.z.string().min(1).describe("Flow / tab id"),summary:x.z.boolean().optional().describe("When true, return node list with sizes only (no field contents)")}},async({id:o,summary:s})=>{try{return Y(s?await kr(n.flowAdapter,o):await xe(n.flowAdapter,o))}catch(i){return Q(i,502)}}),e.registerTool("get_node",{description:"Get one full node by id (includes owning flowId when known). Optional flow scopes the lookup.",inputSchema:{id:x.z.string().min(1).describe("Node id"),flow:x.z.string().min(1).optional().describe("Optional tab id to scope the lookup")}},async({id:o,flow:s})=>{try{return Y(await Ar(n.flowAdapter,o,s))}catch(i){return Q(i,502)}}),e.registerTool("get_node_field",{description:'Get one string field from a node. Optional lines="A-B" (1-based inclusive) returns a slice; response always includes totalLines and bytes.',inputSchema:{id:x.z.string().min(1).describe("Node id"),field:x.z.string().min(1).describe("Field name (e.g. template, func)"),lines:x.z.string().optional().describe("Optional 1-based line range A or A-B"),flow:x.z.string().min(1).optional().describe("Optional tab id to scope the lookup")}},async({id:o,field:s,lines:i,flow:d})=>{try{return Y(await xr(n.flowAdapter,o,s,{...d?{flowId:d}:{},...i?{lines:i}:{}}))}catch(l){return Q(l,502)}}),e.registerTool("search_flows",{description:"Text search across string fields. Returns hits with node id, field, line numbers, and a short snippet. Results are capped; truncated=true when the cap is hit. Optional flow scopes to one tab.",inputSchema:{q:x.z.string().describe("Substring to find (case-sensitive; non-empty required)"),flow:x.z.string().min(1).optional().describe("Optional tab id")}},async({q:o,flow:s})=>{try{return Y(await Sr(n.flowAdapter,o,s?{flowId:s}:{}))}catch(i){return Q(i,502)}}),e.registerTool("list_node_types",{description:"List installed Node-RED palette node types (type, module, category, description, \u2026). Optional q filters type/module/category/description (case-insensitive). Results are capped. Call this before proposing nodes with unfamiliar types.",inputSchema:{q:x.z.string().optional().describe("Optional case-insensitive filter")}},async({q:o})=>{try{return Y(await or(n.nodeTypesAdapter,o?{q:o}:{}))}catch(s){return Q(s,502)}}),e.registerTool("get_node_type",{description:"Get detail for one installed node type: defaults (JSON), inputs/outputs (always set), requiredConfigNodeTypes, help sections (summary/inputs/outputs), category/module/color. Types are case-sensitive. Pass includeHelp=false to omit help. 404 node_type_not_found when unknown (didYouMean on case mismatch; else nearest names).",inputSchema:{type:x.z.string().min(1).describe("Node type id (e.g. inject, template) \u2014 case-sensitive"),includeHelp:x.z.boolean().optional().describe("When false, omit help sections (default true)")}},async({type:o,includeHelp:s})=>{try{return Y(await st(n.nodeTypesAdapter,o,{includeHelp:s!==!1}))}catch(i){return Q(i,502)}}),e.registerTool("search_palette",{description:"Search the public Node-RED catalogue for modules to recommend. Returns module id, description, version, updated, link, and installed boolean (cross-checked against installed modules). Core node-red types are not in the catalogue \u2014 use list_node_types. RECOMMEND ONLY \u2014 never installs.",inputSchema:{q:x.z.string().describe("Search string (id / description / keywords; non-empty required)")}},async({q:o})=>{try{return Y(await sr(n.nodeTypesAdapter,o,{...n.catalogueFetcher?{catalogueFetcher:n.catalogueFetcher}:{},...n.catalogueUrl?{catalogueUrl:n.catalogueUrl}:{}}))}catch(s){return Q(s,502)}})}a(fa,"registerReadTools");function ma(e,t,n,r,o){let s=U(r,t.settings.httpAdminRoot);e.registerTool("submit_proposal",{description:`Submit a replace_flow proposal for owner approval (does not deploy). Preferred for small string edits: payload: { flowId, nodes: [{ op:"replace_text", node, field, find, replace, occurrence? }] } \u2014 find must be unique unless occurrence (1-based) is set; multiple replace_text ops apply in order; yields a short readable diff. Or send a full tab: payload.flow from get_flow (id, type, label?, nodes[], configs?). Inputs (all top-level, next to each other): type="replace_flow", expectedRev (required string from list_flows/get_flow rev; expected_rev alias accepted), optional title (\u226480 chars), and payload: { flowId, flow? } and/or payload.nodes / top-level nodes. Do not nest expectedRev inside payload. Payload size limit is ${$} bytes (413 if exceeded). expectedRev is checked against the live rev at submit time (409 if stale). replace_text errors: 400 text_not_found; 409 ambiguous_match with matchCount. Unknown palette types return 400 unknown_node_type with nearest matches \u2014 call list_node_types / search_palette and ask the user to install via Manage palette (never auto-install). Returns a lean proposal (id, status, title, storedTitle, flowLabel, displayName, reviewUrl, changeSummary, \u2026) by default; title may gain " in <flow>"; storedTitle is the title as saved (your title without that suffix, or the auto-generated title, which may already name the flow). Pass include_payload=true to include payload and baseFlow. When telling the user about the proposal, cite displayName and reviewUrl \u2014 not the raw id. Humans approve on the /mcp page (reviewUrl opens/scrolls to it).`,inputSchema:{type:x.z.literal("replace_flow").describe("Must be 'replace_flow'"),expectedRev:x.z.string().min(1).optional().describe("Top-level current flow revision from get_flow/list_flows (required at submit; sit next to type and payload, not inside payload)"),expected_rev:x.z.string().min(1).optional().describe("Alias for expectedRev (snake_case); prefer expectedRev"),title:x.z.string().max(80).optional().describe("Optional short human title (\u226480 chars). Auto-generated from changes when omitted."),include_payload:x.z.boolean().optional().describe("When true, include payload and baseFlow in the response (default lean)"),nodes:x.z.array(x.z.record(x.z.string(),x.z.unknown())).optional().describe("Optional node ops (set/add/remove/replace_text). Same as payload.nodes when flow is omitted."),payload:x.z.object({flowId:x.z.string().min(1).describe("Tab id to replace"),flow:x.z.object({id:x.z.string().optional(),type:x.z.string().optional().describe("Usually 'tab' (included by get_flow)"),label:x.z.string().optional(),nodes:x.z.array(x.z.record(x.z.string(),x.z.unknown())).optional().describe("Child nodes for the tab (from get_flow.nodes)"),configs:x.z.array(x.z.record(x.z.string(),x.z.unknown())).optional()}).passthrough().optional().describe("Full tab from get_flow; omit when using nodes ops"),nodes:x.z.array(x.z.record(x.z.string(),x.z.unknown())).optional().describe("Node ops applied to the live tab (prefer replace_text for small string edits)")}).describe("Replacement flow payload and/or node ops")}},async i=>{try{let d=await ze(n.proposalStore,n.auditLog,o,{...i,expectedRev:typeof i.expectedRev=="string"&&i.expectedRev.length>0?i.expectedRev:i.expected_rev},n.flowAdapter,s,n.nodeTypesAdapter),l={...ce(d,{...s,include:Yo(i.include_payload)}),mine:!0},c=z(n.proposalStore,d.expectedRev,{flowId:d.payload.flowId}),u=Ie(Ee(n.proposalStore,n.proposalStore.get(d.id),o.id));return Y({...l,...u,...X(c)})}catch(d){return Q(d)}}),hp(e,n,o,s),gp(e,n,o,s)}a(ma,"registerProposalTools");function mp(e,t,n,r){let o={mine:x.z.boolean().optional().describe("true: only proposals created by this access link; false: only other links\u2019 proposals"),status:x.z.union([x.z.string(),x.z.array(x.z.string())]).optional().describe("Filter by proposal status: one value, an array, or a comma-separated string (pending, approved, rejected, failed, withdrawn, expired, superseded). An empty array or empty string means no status filter."),flowId:x.z.string().optional().describe("Filter by flow tab id (payload.flowId)"),limit:x.z.number().int().min(1).max(Dt).optional().describe(`Page size (1\u2013${Dt}). Omit to return all matches.`),offset:x.z.number().int().min(0).optional().describe("Skip this many filtered results (default 0)"),include_payload:x.z.boolean().optional().describe("When true, include payload and baseFlow on each proposal")},s=a(async i=>{try{let d=Yo(i.include_payload),l=await ua(t.flowAdapter),c=l?z(t.proposalStore,l):0,u={};typeof i.mine=="boolean"&&(u.mine=i.mine);let p=Io(i.status);p&&(u.status=p),typeof i.flowId=="string"&&i.flowId.length>0&&(u.flowId=i.flowId),typeof i.limit=="number"&&(u.limit=i.limit),typeof i.offset=="number"&&(u.offset=i.offset);let f=Ht(t.proposalStore,n,r,u);return Y({proposals:f.proposals.map(m=>ue(t.proposalStore,n,m,{...r,include:d},{includeConflicts:!1})),...dt({...f,returned:f.proposals.length}),...X(c)})}catch(d){return Q(d)}},"listProposalsHandler");e.registerTool("list_proposals",{description:"List proposals visible to this access link (all proposals by default). Each item includes mine:true|false for the caller. Filter with mine, status, and/or flowId; paginate with limit/offset (response includes total, offset, hasMore, nextOffset). Invalid filters return invalid_filter. Returns lean { proposals, total, \u2026 } with title, displayName, reviewUrl, changeSummary (no payload/baseFlow unless include_payload=true). Prefer displayName + reviewUrl when referring to a proposal with the user (not the raw id).",inputSchema:o},s),e.registerTool("list_my_proposals",{description:"Alias of list_proposals. Prefer list_proposals. Lists proposals visible to this access link with optional mine/status/flowId filters and limit/offset pagination.",inputSchema:o},s)}a(mp,"registerListProposalsTools");function hp(e,t,n,r){mp(e,t,n,r),e.registerTool("get_proposal",{description:"Get any proposal by id (all tokens can read). Response includes mine:true|false and, for pending proposals, conflictingProposals when other pending work touches the same flow. Lean by default (no payload/baseFlow); pass include_payload=true for full bodies. Cite displayName + reviewUrl when talking to the user.",inputSchema:{id:x.z.string().min(1).describe("Proposal id (prop-\u2026)"),include_payload:x.z.boolean().optional().describe("When true, include payload and baseFlow")}},async({id:o,include_payload:s})=>{try{let i=await ua(t.flowAdapter),d=Me(t.proposalStore,n,o,r);return Y(ue(t.proposalStore,n,d,{...r,include:Yo(s)},{...i?{liveRev:i}:{}}))}catch(i){return Q(i)}})}a(hp,"registerProposalQueryTools");function gp(e,t,n,r){e.registerTool("get_proposal_diff",{description:"Diff any proposal against its stored baseFlow snapshot (what changed). Optional format=summary|full (default summary). Pending + stale also includes vsCurrent.",inputSchema:{id:x.z.string().min(1).describe("Proposal id (prop-\u2026)"),format:x.z.enum(["summary","full"]).optional().describe("Diff payload format (default summary)")}},async({id:o,format:s})=>{try{let i=Me(t.proposalStore,n,o),d=Vt(s??"summary");if(!d.ok)return Q(new b("Invalid diff format","invalid_diff_format",{details:{hint:"Valid formats: summary, full (default summary)."}}));let l=Kt(await Zt(i,t.flowAdapter),d.format);return Y(l)}catch(i){return Q(i)}}),e.registerTool("withdraw_proposal",{description:"Withdraw one of YOUR pending proposals (status becomes withdrawn, not rejected). Optional reason. Only the creating access link can withdraw; other tokens get 403. Cite displayName + reviewUrl when telling the user.",inputSchema:{id:x.z.string().min(1).describe("Proposal id (prop-\u2026) owned by this access link"),reason:x.z.string().optional().describe("Optional withdraw reason")}},({id:o,reason:s})=>{try{let i=Ut(t.proposalStore,t.auditLog,n,o,s,r);return Y(ue(t.proposalStore,n,i,r))}catch(i){return Q(i)}})}a(gp,"registerProposalActionTools");function yp(e,t,n){let r=new Xo.McpServer({name:"node-red-ai-access",version:ca()});pa(r);let o=n.accessLink;if(!o)throw new Error("Access link required");return fa(r,e,t,n),ma(r,e,t,n,o),r}a(yp,"createAiMcpServer");function ha(e,t){return async(n,r,o)=>{try{if(!n.accessLink&&!n.auth){r.setHeader("WWW-Authenticate",K),r.status(401).json({error:le,code:Re});return}let s=n.accessLink?yp(e,t,n):wp(e,t,n),i=new la.StreamableHTTPServerTransport({enableJsonResponse:!0});await s.connect(i),await i.handleRequest(n,r,n.body),r.on("close",()=>{i.close(),s.close()})}catch(s){if(!r.headersSent){F(r,s);return}o(s instanceof Error?s:new Error(String(s)))}}}a(ha,"mcpRpcPostHandler");function wp(e,t,n){let r=new Xo.McpServer({name:"node-red-ai-access",version:ca()});if(pa(r),fa(r,e,t,n),(n.auth?.scopes??[]).includes("propose")){let s=typeof n.auth?.extra?.user=="string"?n.auth.extra.user:"owner",i=n.auth?.clientId??"",l={id:lo(i,s),kind:"mcp",createdAt:new Date().toISOString(),expiresAt:new Date((n.auth?.expiresAt??0)*1e3).toISOString(),createdBy:s,oauthClientId:i,oauthUser:s};ma(r,e,t,n,l)}return r}a(wp,"createOAuthMcpServer");function Qo(e,t){t.status(405).set("Allow","POST").json({jsonrpc:"2.0",error:{code:-32e3,message:"Method not allowed. Use POST for Streamable HTTP."},id:null})}a(Qo,"mcpRpcMethodNotAllowed");var Fn=require("crypto"),nn=require("fs"),Zo=require("path"),H=require("@modelcontextprotocol/sdk/server/auth/errors.js"),wa=require("@modelcontextprotocol/sdk/server/auth/handlers/authorize.js"),es=require("@modelcontextprotocol/sdk/server/auth/handlers/metadata.js"),ba=require("@modelcontextprotocol/sdk/server/auth/handlers/register.js"),va=require("@modelcontextprotocol/sdk/server/auth/handlers/revoke.js"),ka=require("@modelcontextprotocol/sdk/server/auth/handlers/token.js"),Aa=require("@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js"),xa=require("@modelcontextprotocol/sdk/server/auth/router.js"),Sa=dn(require("express"),1);q();Pe();var ga=`(function () {
951
+ `);for(let m=0;m<f.length;m+=1){let g=(f[m]??"").indexOf(n);if(g>=0){let x=0;for(let S=0;S<m;S+=1)x+=(f[S]?.length??0)+1;u(m+1,x+g)}}}if(n.includes(`
952
+ `)){let f=0;for(;f<=i.length;){let m=i.indexOf(n,f);if(m<0)break;u(Uu(i,m),m),f=m+Math.max(1,n.length)}}if(d.length===0)continue;let p=d.length;r.push({flowId:t,nodeId:e.id,field:s,lines:d.slice(0,Ou),lineCount:p,snippet:Hu(i,Math.max(0,c),n.length)})}return r.length>=o}a(qo,"searchInNode");function qi(e){return[...e.nodes??[],...e.configs??[]]}a(qi,"nodePool");async function Bu(e,t,n,r,o){let s=await xe(e,t);for(let i of qi(s.flow))if(qo(i,t,n,r,o))return{rev:s.rev,truncated:!0};return{rev:s.rev,truncated:!1}}a(Bu,"searchOneFlow");async function qu(e,t,n,r){let o=await pe(e),s=new Set;for(let i of Ui(o.flows))if(i.id)try{let d=await xe(e,i.id);for(let l of qi(d.flow))if(s.add(l.id),qo(l,i.id,t,n,r))return{rev:o.rev,truncated:!0}}catch{}for(let i of o.flows){if(!i||typeof i!="object"||Array.isArray(i))continue;let d=i;if(!d.id||s.has(d.id)||d.type==="tab"||d.type==="subflow")continue;let l=typeof d.z=="string"?d.z:null;if(qo(d,l,t,n,r))return{rev:o.rev,truncated:!0}}return{rev:o.rev,truncated:!1}}a(qu,"searchAllFlows");async function Pr(e,t,n={}){if(typeof t!="string"||!t.trim())throw new y("q query parameter is required","missing_query");let r=typeof n.cap=="number"&&n.cap>0?Math.floor(n.cap):Fu,o=[],s=n.flowId?await Bu(e,n.flowId,t,o,r):await qu(e,t,o,r),i={rev:s.rev,q:t,hits:o,hitCount:o.length,truncated:s.truncated,cap:r};return n.flowId&&(i.flowId=n.flowId),i}a(Pr,"searchFlows");function Wi(e){if(e===void 0)return!1;let t=e.toLowerCase();return t==="1"||t==="true"||t==="yes"}a(Wi,"isTruthyQueryFlag");function Wu(e,t,n){switch(e){case"node_not_found":case"flow_not_found":case"field_not_found":return n??se(e,t);default:return se(e,t)}}a(Wu,"layeredReadErrorHint");function yt(e){return Oe[e]??"Request failed"}a(yt,"safeErrorText");function fe(e,t,n){e.setHeader("X-Content-Type-Options","nosniff"),e.setHeader("Content-Type","application/json; charset=utf-8"),e.status(t).json(n)}a(fe,"sendJson");function Rr(e,t,n,r){let o=O(t,n);if(!o)return!1;let s=o.body.code,i={error:yt(s),code:s};typeof o.body.matchCount=="number"&&(i.matchCount=o.body.matchCount),typeof o.body.totalLines=="number"&&(i.totalLines=o.body.totalLines),typeof o.body.expectedRev=="string"&&(i.expectedRev=o.body.expectedRev),typeof o.body.currentRev=="string"&&(i.currentRev=o.body.currentRev);let d=Wu(s,n,r);return d&&(i.hint=d),fe(e,o.statusCode,i),!0}a(Rr,"sendMappedError");function _r(e){fe(e,502,{error:yt("upstream_error"),code:"upstream_error"})}a(_r,"sendUpstreamError");function Lr(e,t="rest"){return async(n,r)=>{let o=n.params.id;if(typeof o!="string"||o.length===0){fe(r,400,{error:yt("missing_flow_id"),code:"missing_flow_id"});return}let s=Wi(_(n.query,"summary"));try{if(s){fe(r,200,await xr(e,o));return}fe(r,200,await xe(e,o))}catch(i){if(Rr(r,i,t))return;_r(r)}}}a(Lr,"getFlowOrSummaryHandler");function Cr(e,t="rest"){return async(n,r)=>{let o=n.params.id;if(typeof o!="string"||!o){fe(r,400,{error:yt("missing_node_id"),code:"missing_node_id"});return}let s=_(n.query,"flow");try{fe(r,200,await Sr(e,o,s))}catch(i){if(Rr(r,i,t,t==="get"?"Call GET \u2026/flows/<id>?summary=1 or \u2026/search?q=\u2026":t==="mcp"?void 0:"Call GET \u2026/flows/<id>?summary=1 or \u2026/search?q=\u2026"))return;_r(r)}}}a(Cr,"getNodeHandler");function Er(e,t="rest"){return async(n,r)=>{let o=n.params.id,s=n.params.name;if(typeof o!="string"||!o){fe(r,400,{error:yt("missing_node_id"),code:"missing_node_id"});return}if(typeof s!="string"||!s){fe(r,400,{error:yt("missing_field"),code:"missing_field"});return}let i=_(n.query,"flow"),d=_(n.query,"lines");try{fe(r,200,await Tr(e,o,s,{...i?{flowId:i}:{},...d?{lines:d}:{}}))}catch(l){if(Rr(r,l,t,"Use summary/search to discover node ids and fields."))return;_r(r)}}}a(Er,"getNodeFieldHandler");function Ir(e,t="rest"){return async(n,r)=>{let o=_(n.query,"q");if(!o){fe(r,400,{error:yt("missing_query"),code:"missing_query",hint:t==="get"?"Example: GET \u2026/search?q=TODO&flow=tab1":se("missing_query",t)});return}let s=_(n.query,"flow");try{fe(r,200,await Pr(e,o,s?{flowId:s}:{}))}catch(i){if(Rr(r,i,t))return;_r(r)}}}a(Ir,"searchFlowsHandler");K();ut();var zu={missing_node_type:"Missing node type",missing_query:"q query parameter is required",node_type_not_found:"Node type was not found",catalogue_unavailable:"Node-RED catalogue unavailable",unknown_node_type:"Unknown node type",upstream_error:"Upstream error"};function En(e){return zu[e]??"Request failed"}a(En,"safeErrorText");function et(e,t,n){e.setHeader("X-Content-Type-Options","nosniff"),e.setHeader("Content-Type","application/json; charset=utf-8"),e.setHeader("Cache-Control","no-store"),e.status(t).json(n)}a(et,"sendJson");function Wo(e,t){let n=O(t);if(!n)return!1;let r=n.body.code,o={error:En(r),code:r};return typeof n.body.type=="string"&&(o.type=n.body.type),Array.isArray(n.body.nearest)&&(o.nearest=n.body.nearest),typeof n.body.didYouMean=="string"?(o.didYouMean=n.body.didYouMean,o.caseSensitive=!0,o.hint=`Types are case-sensitive \u2014 use '${n.body.didYouMean}' exactly.`):(r==="node_type_not_found"||r==="unknown_node_type")&&(o.hint="Call GET \u2026/node-types?q= or \u2026/palette/search?q=. Ask the user to install via Manage palette, then recheck node-types. This API never installs."),r==="catalogue_unavailable"&&(o.hint="Retry later; palette search is recommend-only and never installs packages."),et(e,n.statusCode,o),!0}a(Wo,"sendMappedError");function Nr(e){return async(t,n)=>{try{let r=_(t.query,"q"),o=await ir(e.nodeTypes,r?{q:r}:{});et(n,200,o)}catch(r){if(Wo(n,r))return;et(n,502,{error:En("upstream_error"),code:"upstream_error"})}}}a(Nr,"listNodeTypesHandler");function Fr(e){return async(t,n)=>{try{let r=t.params.type,o=typeof r=="string"?decodeURIComponent(r):"",s=_(t.query,"help"),i=!(s==="0"||s==="false"||s==="no"||s==="omit"),d=await ct(e.nodeTypes,o,{includeHelp:i});et(n,200,d)}catch(r){if(Wo(n,r))return;et(n,502,{error:En("upstream_error"),code:"upstream_error"})}}}a(Fr,"getNodeTypeHandler");function Or(e){return async(t,n)=>{try{let r=_(t.query,"q");if(!r){et(n,400,{error:En("missing_query"),code:"missing_query",hint:"Pass ?q= to search the Node-RED catalogue (recommend only)."});return}let o=await ar(e.nodeTypes,r,{...e.catalogueFetcher?{catalogueFetcher:e.catalogueFetcher}:{},...e.catalogueUrl?{catalogueUrl:e.catalogueUrl}:{}});et(n,200,o)}catch(r){if(Wo(n,r))return;et(n,502,{error:En("upstream_error"),code:"upstream_error"})}}}a(Or,"searchPaletteHandler");Xe();ve();function tn(e,t){return e[t]??""}a(tn,"lineAt");function In(e,t,n){let r=e[t];return r?r[n]??0:0}a(In,"cell");function zi(e,t,n,r){let o=e[t];o&&(o[n]=r)}a(zi,"setCell");function Ji(e,t){let n=Gi(e),r=Gi(t),o=n.length,s=r.length,i=[];for(let u=0;u<=o;u+=1){let p=[];for(let f=0;f<=s;f+=1)p.push(0);i.push(p)}for(let u=o-1;u>=0;u-=1)for(let p=s-1;p>=0;p-=1)tn(n,u)===tn(r,p)?zi(i,u,p,In(i,u+1,p+1)+1):zi(i,u,p,Math.max(In(i,u+1,p),In(i,u,p+1)));let d=[],l=0,c=0;for(;l<o&&c<s;){let u=tn(n,l),p=tn(r,c);u===p?(d.push({op:"equal",text:u}),l+=1,c+=1):In(i,l+1,c)>=In(i,l,c+1)?(d.push({op:"remove",text:u}),l+=1):(d.push({op:"add",text:p}),c+=1)}for(;l<o;)d.push({op:"remove",text:tn(n,l)}),l+=1;for(;c<s;)d.push({op:"add",text:tn(r,c)}),c+=1;return d}a(Ji,"diffLines");function Gi(e){return e.length===0?[]:e.split(`
953
+ `)}a(Gi,"splitLines");Ae();var Xi=new Set(["func","template","info","payload","code","text","format","syntax"]),Gu=new Set(["x","y","w","h","wires","g"]),Ju=new Set(["w","h","g"]);function wt(e){if(typeof e.name=="string"&&e.name.length>0)return e.name;if(typeof e.label=="string"&&e.label.length>0)return e.label}a(wt,"nodeName");function Ki(e){let t=new Map;for(let n of e.nodes??[])t.set(n.id,n);for(let n of e.configs??[])t.set(n.id,n);return t}a(Ki,"collectNodes");function Dr(e){if(e===void 0)return"undefined";if(e===null||typeof e!="object")return JSON.stringify(e);if(Array.isArray(e))return`[${e.map(Dr).join(",")}]`;let t=e;return`{${Object.keys(t).sort().map(r=>`${JSON.stringify(r)}:${Dr(t[r])}`).join(",")}}`}a(Dr,"stableStringify");function Yi(e,t){return Dr(e)===Dr(t)}a(Yi,"valuesEqual");function jr(e){return typeof e=="string"?e:null}a(jr,"asText");function Ku(e){return/<\/?[a-z][\s\S]*>/i.test(e.slice(0,800))}a(Ku,"looksLikeHtml");function Vu(e,t,n){if(t!=="template")return!1;let r=typeof e.format=="string"?e.format.toLowerCase():"";return r==="html"||r==="handlebars"||r==="mustache"?!0:Ku(n)}a(Vu,"isHtmlTemplateField");function Qi(e,t,n){let r=jr(n);if(Xi.has(t)&&r!==null){let o={key:t,kind:"text",value:r};return Vu(e,t,r)&&(o.preview="html"),o}return{key:t,kind:"scalar",value:n===void 0?null:n}}a(Qi,"toPresenceProperty");function Xu(e,t){let n=[];for(let r of[...Object.keys(e)].sort())if(!(Ju.has(r)||r==="id"||r==="z")){if(r==="wires"){let o=Zi(e,t);n.push({key:"wires",kind:"scalar",value:o.length>0?o:e.wires??null});continue}n.push(Qi(e,r,e[r]))}return n}a(Xu,"nodeProperties");function Zi(e,t){let n=e.wires;if(!Array.isArray(n)||n.length===0)return[];let r=[];return n.forEach((o,s)=>{if(!Array.isArray(o))return;let i=[];for(let d of o){if(typeof d!="string"||!d)continue;let l=t.get(d);i.push({id:d,name:l?wt(l)??l.type:d})}r.push({port:s,to:i})}),r}a(Zi,"describeWiresStructured");var Yu=new Set(["func","template","code","text"]);function Qu(e,t){let n=[],r=wt(e);r&&n.push({key:"name",kind:"scalar",value:r});let o=Zi(e,t);o.length>0&&n.push({key:"wires",kind:"scalar",value:o});for(let s of Yu){if(!(s in e))continue;let i=jr(e[s]);i===null||i.length===0||n.push(Qi(e,s,i))}return n}a(Qu,"buildKeyFields");function Zu(e,t,n){let r=Ve(e.type),o=wt(e),s=o?` "${o}"`:"",i=t?` in "${t}"`:"",d=n===1?"1 field":`${n} fields`;return`Changes ${r} node${s}${i} (${d})`}a(Zu,"buildModifiedSummary");function ep(e,t,n){let r=Ve(e.type),o=wt(e),s=t==="added"?"Adds":"Removes",i=t==="added"?"to":"from",d=o?` ${JSON.stringify(o)}`:"",l=n?` ${i} flow ${JSON.stringify(n)}`:"",c=`${s} ${r} node${d}${l}`;if(t==="added"&&r==="template"){let u=typeof e.field=="string"&&e.field?e.field:"";u&&(c+=`, output to msg.${u}`)}return c}a(ep,"buildPresenceSummary");function $r(e,t,n,r){let o={id:e.id,type:e.type,summary:ep(e,t,n),keyFields:Qu(e,r),properties:Xu(e,r)},s=wt(e);return s!==void 0&&(o.name=s),o}a($r,"toPresenceNode");function tp(e,t){let n=new Set([...Object.keys(e),...Object.keys(t)]),r=[];for(let o of[...n].sort()){if(Gu.has(o)||o==="id"||o==="z")continue;let s=e[o],i=t[o];if(Yi(s,i))continue;let d=jr(s),l=jr(i);if(Xi.has(o)&&(d!==null||l!==null)){let c=d??"",u=l??"";r.push({key:o,kind:"text",before:c,after:u,lines:Ji(c,u)});continue}r.push({key:o,kind:"scalar",before:s===void 0?null:s,after:i===void 0?null:i})}return r}a(tp,"compareNodes");function np(e,t){let n=["label","disabled","info","env"],r=[];for(let o of n)Yi(e[o],t[o])||r.push(o);return r}a(np,"compareTabMeta");function Mr(e){return typeof e.label=="string"&&e.label.trim()?e.label.trim():""}a(Mr,"flowLabelOf");function ea(e,t){let n=Ki(t),r=Ki(e),o=Mr(e)||Mr(t),s=Mr(t)||Mr(e),i=[],d=[],l=[];for(let c of e.nodes??[])n.has(c.id)||i.push($r(c,"added",o,r));for(let c of e.configs??[])n.has(c.id)||i.push($r(c,"added",o,r));for(let c of t.nodes??[])r.has(c.id)||d.push($r(c,"removed",s,n));for(let c of t.configs??[])r.has(c.id)||d.push($r(c,"removed",s,n));for(let[c,u]of r){let p=n.get(c);if(!p)continue;let f=tp(p,u);if(f.length>0){let m={id:c,type:u.type||p.type,summary:Zu(u,o||s,f.length),changes:f},h=wt(u)??wt(p);h!==void 0&&(m.name=h),l.push(m)}}return l.sort((c,u)=>c.id.localeCompare(u.id)),{summary:{added:i.length,removed:d.length,modified:l.length,tabChanges:np(t,e),beforeNodeCount:n.size,afterNodeCount:r.size},added:i,removed:d,modified:l}}a(ea,"buildDiffSections");function Vi(e,t,n={}){let r=ea(e.payload.flow,t.flow),o=n.currentRev??t.rev,s=e.expectedRev,i=n.baseRevStale??(e.status==="pending"&&s!==o),d={proposalId:e.id,flowId:e.payload.flowId,expectedRev:s,currentRev:o,baseRevStale:i,compareAgainst:n.compareAgainst??"current",...r};return e.warnings&&e.warnings.length>0&&(d.warnings=e.warnings.map(l=>({...l}))),n.baseSnapshotMissing&&(d.baseSnapshotMissing=!0),n.vsCurrent&&(d.vsCurrent=n.vsCurrent),d}a(Vi,"buildProposalDiff");async function nn(e,t){let n=e.payload.flowId,r={id:n,type:"tab",nodes:[],configs:[]},o=e.expectedRev,s=r;try{let c=await t.getFlow(n);o=c.rev,s=c.flow}catch(c){if(!(c instanceof z))throw c;try{o=(await t.getFlows()).rev}catch{}}let d=e.status==="pending"&&e.expectedRev!==o,l=e.baseFlow;if(l!==void 0){let c=d?ea(e.payload.flow,s):void 0;return Vi(e,{rev:e.expectedRev,flow:l},{currentRev:o,compareAgainst:"base",baseRevStale:d,...c?{vsCurrent:c}:{}})}return Vi(e,{rev:o,flow:s},{compareAgainst:"current",baseSnapshotMissing:!0,baseRevStale:d})}a(nn,"computeProposalDiff");Ae();_e();ye();var ta=require("path");function Hr(e){return(0,ta.join)(e.userDir,".ai-access")}a(Hr,"getStorageDir");var rp=["status:read","flows:read","proposals:create","proposals:read-own"];function zo(){return Vt()}a(zo,"readPackageVersion");function op(e,t=Date.now()){let n=Date.parse(e);return Number.isFinite(n)?Math.max(0,Math.ceil((n-t)/1e3)):0}a(op,"secondsRemainingUntil");function sp(e,t=Date.now()){let n=Date.parse(e);return Number.isFinite(n)?Math.max(0,Math.ceil((n-t)/6e4)):0}a(sp,"minutesRemainingUntil");function ip(e,t){let n=de(t),r=qe(t),o=`${n}/openapi.yaml`,s=`${n}/openapi.json`;return e==="rest"?{api:n,openapi:o,openapiJson:s,discovery:n}:e==="mcp"?{rpc:r,discovery:n}:{discovery:n}}a(ip,"baseUrlsForTokenKind");function rn(e,t){let n=Kn(e,t),r=he(e.settings);if(t.auth){let o=t.auth.extra?.user;return{ok:!0,version:zo(),adminAuthEnabled:n.adminAuthEnabled,aiAccessEnabled:r.enabled,aiFeaturesEnabled:n.aiFeaturesEnabled,allowWithoutAdminAuth:n.allowWithoutAdminAuth,permission:t.auth.scopes.includes("propose")?"propose":"read",capabilities:{view:!0,manageLinks:!1,approveProposals:!1},defaultLinkTtlSeconds:r.defaultLinkTtlSeconds,maxActiveLinks:r.maxActiveLinks,defaultDeploymentType:r.defaultDeploymentType,caller:{kind:"oauth",clientId:t.auth.clientId,user:typeof o=="string"?o:"owner",scopes:t.auth.scopes}}}if(t.accessLink){let o=Date.now();return{ok:!0,version:zo(),adminAuthEnabled:n.adminAuthEnabled,aiAccessEnabled:r.enabled,aiFeaturesEnabled:n.aiFeaturesEnabled,allowWithoutAdminAuth:n.allowWithoutAdminAuth,permission:null,capabilities:{view:!0,manageLinks:!1,approveProposals:!1},defaultLinkTtlSeconds:r.defaultLinkTtlSeconds,maxActiveLinks:r.maxActiveLinks,defaultDeploymentType:r.defaultDeploymentType,caller:{kind:"access-link",linkId:t.accessLink.id,tokenKind:t.accessLink.kind,...t.accessLink.label!==void 0?{label:t.accessLink.label}:{},expiresAt:t.accessLink.expiresAt,minutesRemaining:sp(t.accessLink.expiresAt,o),secondsRemaining:op(t.accessLink.expiresAt,o),permissions:[...rp],baseUrls:ip(t.accessLink.kind,e.settings.httpAdminRoot)}}}return{ok:!0,version:zo(),adminAuthEnabled:n.adminAuthEnabled,aiAccessEnabled:r.enabled,aiFeaturesEnabled:n.aiFeaturesEnabled,allowWithoutAdminAuth:n.allowWithoutAdminAuth,permission:n.permission,capabilities:n.capabilities,storageDir:Hr(e.settings),defaultLinkTtlSeconds:r.defaultLinkTtlSeconds,maxActiveLinks:r.maxActiveLinks,defaultDeploymentType:r.defaultDeploymentType,caller:{kind:"admin",...n.permission?{permission:n.permission}:{}}}}a(rn,"buildStatus");function na(e,t){let n=t.draftStore??new br,r=t.rateLimiters??Ci(t.rateLimit??e.settings.aiAccess),o=e.httpAdmin,s="/mcp/a/:token",i=[r.ip,Ii(e,t.linkStore),r.token];o.use("/mcp/a",gi()),o.get(s,...i,R((u,p)=>{F(p),p.setHeader("X-Content-Type-Options","nosniff");let f=de(e.settings.httpAdminRoot),m=u.accessLink;if(!m){C(p,401,"Access link required","access_link_missing");return}let h=Si({link:m,openapiPath:`${f}/openapi.yaml`,restApiBase:f,mcpRpcPath:qe(e.settings.httpAdminRoot),publicOrigin:oo(u)});p.status(200).type("text/markdown; charset=utf-8").send(h)})),o.get(`${s}/status`,...i,R((u,p)=>{F(p),p.json(rn(e,u))})),o.get(`${s}/flows`,...i,R(async(u,p)=>{F(p);try{p.json(await pe(t.flowAdapter))}catch(f){let m=O(f);if(m){p.status(m.statusCode).json({...m.body,hint:"Retry after the owner fixes the plugin runtime."});return}C(p,502,f instanceof Error?f.message:String(f),"upstream_error")}})),o.get(`${s}/flows/:id`,...i,R(async(u,p)=>{F(p),await Lr(t.flowAdapter,"get")(u,p)})),o.get(`${s}/nodes/:id`,...i,R(async(u,p)=>{F(p),await Cr(t.flowAdapter,"get")(u,p)})),o.get(`${s}/nodes/:id/field/:name`,...i,R(async(u,p)=>{F(p),await Er(t.flowAdapter,"get")(u,p)})),o.get(`${s}/search`,...i,R(async(u,p)=>{F(p),await Ir(t.flowAdapter,"get")(u,p)}));let d={nodeTypes:t.nodeTypesAdapter,...t.catalogueFetcher?{catalogueFetcher:t.catalogueFetcher}:{},...t.catalogueUrl?{catalogueUrl:t.catalogueUrl}:{}};o.get(`${s}/node-types`,...i,R(async(u,p)=>{F(p),await Nr(d)(u,p)})),o.get(`${s}/node-types/:type`,...i,R(async(u,p)=>{F(p),await Fr(d)(u,p)})),o.get(`${s}/palette/search`,...i,R(async(u,p)=>{F(p),await Or(d)(u,p)}));let l=e.settings.httpAdminRoot;ap(o,s,i,t,l),o.get(`${s}/propose`,...i,R(ji(t.proposalStore,t.auditLog,t.flowAdapter,l,t.nodeTypesAdapter))),cp(o,s,i,n,t,l);let c=Fi();return o.all(`${s}/links`,c),o.all(`${s}/links/:id`,c),o.use("/mcp/a",Ni()),n}a(na,"registerGetLinkRoutes");function ap(e,t,n,r,o){e.get(`${t}/proposals`,...n,R(async(s,i)=>{F(i);let d=s.accessLink;if(!d){C(i,401,"Access link required","access_link_missing");return}let l=q(s,o),c=re(s.query),u=mr(s.query),p;try{p=(await pe(r.flowAdapter)).rev}catch{}let f=p?V(r.proposalStore,p):0,m=qt(r.proposalStore,d,l,u);i.json({proposals:m.proposals.map(h=>ue(r.proposalStore,d,h,{...l,include:c},{includeConflicts:!1})),...ft({...m,returned:m.proposals.length}),...Q(f)})})),e.get(`${t}/proposals/:id`,...n,R(async(s,i)=>{F(i);let d=s.params.id,l=s.accessLink;if(typeof d!="string"||!d){C(i,400,"Missing proposal id","missing_proposal_id");return}if(!l){C(i,401,"Access link required","access_link_missing");return}try{let c=q(s,o),u=re(s.query),p;try{p=(await pe(r.flowAdapter)).rev}catch{}let f=De(r.proposalStore,l,d,c);i.json(ue(r.proposalStore,l,f,{...c,include:u},{...p?{liveRev:p}:{}}))}catch(c){let u=O(c,"get");if(u){i.status(u.statusCode).json({...u.body,hint:u.body.hint??"List proposals with GET \u2026/proposals."});return}$(i,c);return}})),dp(e,t,n,r,o),lp(e,t,n,r,o)}a(ap,"registerGetLinkProposalRoutes");function dp(e,t,n,r,o){e.get(`${t}/proposals/:id/withdraw`,...n,R((s,i)=>{F(i);let d=s.params.id,l=s.accessLink;if(typeof d!="string"||!d){C(i,400,"Missing proposal id","missing_proposal_id");return}if(!l){C(i,401,"Access link required","access_link_missing");return}let c=s.query.confirm;if(c!=="1"&&c!=="true"){C(i,400,"Withdraw over GET requires confirm=1","confirm_required",se("confirm_required","get"));return}let u=typeof s.query.reason=="string"?s.query.reason:void 0,p=q(s,o),f=re(s.query);try{let m=Wt(r.proposalStore,r.auditLog,l,d,u,p);i.json(ue(r.proposalStore,l,m,{...p,include:f}))}catch(m){let h=O(m,"get");if(h){let g=h.body.code;C(i,h.statusCode,Oe[g]??"Request failed",g,se(g,"get"));return}C(i,500,"Internal server error","internal_error");return}}))}a(dp,"registerGetLinkWithdrawRoute");function lp(e,t,n,r,o){e.get(`${t}/proposals/:id/diff`,...n,R(async(s,i)=>{F(i);let d=s.params.id,l=s.accessLink;if(typeof d!="string"||!d){C(i,400,"Missing proposal id","missing_proposal_id");return}if(!l){C(i,401,"Access link required","access_link_missing");return}try{let c=De(r.proposalStore,l,d,q(s,o)),u=_(s.query,"format"),p=Yt(u==="json"||u==="md"?void 0:u);if(!p.ok){C(i,400,"Invalid diff format","invalid_diff_format","Valid formats: summary, full (default summary).");return}i.json(Qt(await nn(c,r.flowAdapter),p.format))}catch(c){let u=O(c);if(u){i.status(u.statusCode).json(u.body);return}$(i,c);return}}))}a(lp,"registerGetLinkDiffRoute");function cp(e,t,n,r,o,s){e.get(`${t}/draft/new`,...n,R($i(r,o.flowAdapter))),e.get(`${t}/draft/:draftId/append`,...n,R(Mi(r))),e.get(`${t}/draft/:draftId/submit`,...n,R(Di(r,o.proposalStore,o.auditLog,o.flowAdapter,s,o.nodeTypesAdapter)))}a(cp,"registerGetLinkDraftRoutes");G();var ra=require("module");function up(){if(typeof require=="function")return require;let e=(0,eval)("import.meta.url");return(0,ra.createRequire)(e)}a(up,"createPluginRequire");function pp(e){try{let r=up()("@node-red/runtime")._?.adminApi?.httpAdmin;if(r&&typeof r.use=="function"&&r!==e.httpAdmin)return r}catch{}return null}a(pp,"resolveEditorHttpAdmin");function oa(e){let t=[e.httpAdmin],n=pp(e);return n&&t.push(n),t}a(oa,"httpAdminAppsForMcpErrors");var sa=Symbol.for("com55.ai-access.mcpJsonErrorHandler");function fp(e){let t=e??"/";for(;t.endsWith("/");)t=t.slice(0,-1);let n=new Set;for(let r of t?["",t]:[""])for(let o of["/mcp","/mcp/rpc"])n.add(r+o),n.add(`${r+o}/`);return r=>n.has(r)}a(fp,"mcpRpcMatcher");function Go(e){return e.includes("/mcp")}a(Go,"isMcpPath");function mp(e){return e.path||e.url||e.originalUrl||""}a(mp,"requestPath");function hp(e){if(!e||typeof e!="object")return!1;let t=e;if(t.type==="entity.parse.failed"||e instanceof SyntaxError)return!0;let n=typeof e.message=="string"?e.message:"";return/JSON/i.test(n)&&(t.status===400||t.statusCode===400)}a(hp,"isJsonParseError");function gp(e){if(!e||typeof e!="object")return!1;let t=e;return t.type==="entity.too.large"||t.status===413||t.statusCode===413||t.message==="request entity too large"}a(gp,"isPayloadTooLarge");function yp(e){let t=fp(e);return(n,r,o,s)=>{if(o.headersSent){s(n);return}let i=mp(r);if(hp(n)&&Go(i)){if(t(i)){o.status(400).json({jsonrpc:"2.0",error:{code:-32700,message:"Parse error"},id:null});return}o.status(400).json({error:"Invalid JSON body",code:"invalid_json"});return}if(gp(n)&&Go(i)){o.status(413).json({error:`Request body exceeds limit of ${j} bytes`,code:"payload_too_large"});return}if(Go(i)){if(t(i)){o.status(500).json({jsonrpc:"2.0",error:{code:-32603,message:wr(n).error,data:{code:"internal_error"}},id:null});return}o.status(500).json(wr(n));return}s(n)}}a(yp,"mcpJsonErrorHandler");function wp(e,t){let n=e;n[sa]||(n[sa]=!0,e.use(yp(t)))}a(wp,"attachOnce");function ia(e){for(let t of oa(e))wp(t,e.settings.httpAdminRoot)}a(ia,"attachMcpJsonErrorHandlers");Ln();var bt=class extends Error{static{a(this,"AccessLinkLimitError")}code="access_link_limit";statusCode=409;constructor(t){super(`Cannot create access link: at most ${t} active links are allowed. Revoke an existing link first.`),this.name="AccessLinkLimitError"}},vt=class extends Error{static{a(this,"AccessLinkNotFoundError")}code="access_link_not_found";statusCode=404;constructor(t){super(`Access link '${t}' was not found`),this.name="AccessLinkNotFoundError"}},Nn=class extends Error{static{a(this,"AccessLinkValidationError")}code="access_link_invalid";statusCode=400;constructor(t){super(t),this.name="AccessLinkValidationError"}},tt=class extends Error{static{a(this,"AccessLinkTtlError")}code="invalid_ttl";statusCode=400;hint=mt;constructor(t){super(t),this.name="AccessLinkTtlError"}};var kt=require("crypto");Ln();function Jo(e){return e==="get_link"||e==="rest"||e==="mcp"}a(Jo,"isAccessLinkKind");function Ko(e){return e??"get_link"}a(Ko,"resolveAccessLinkKind");function aa(e){return(0,kt.createHash)("sha256").update(e,"utf8").digest("hex")}a(aa,"hashToken");function bp(e,t){let n=Buffer.from(e,"utf8"),r=Buffer.from(t,"utf8");return n.length!==r.length?!1:(0,kt.timingSafeEqual)(n,r)}a(bp,"hashesEqual");function Fn(e){let t={id:e.id,kind:Ko(e.kind),createdAt:new Date(e.createdAtMs).toISOString(),expiresAt:new Date(e.expiresAtMs).toISOString()};return e.label!==void 0&&(t.label=e.label),e.lastUsedAtMs!==void 0&&(t.lastUsedAt=new Date(e.lastUsedAtMs).toISOString()),e.createdBy!==void 0&&(t.createdBy=e.createdBy),t}a(Fn,"toPublic");var At=class{static{a(this,"AccessLinkStore")}links=new Map;defaultTtlSeconds;maxActiveLinks;now;randomId;randomToken;constructor(t={}){this.defaultTtlSeconds=t.defaultTtlSeconds??14400,this.maxActiveLinks=t.maxActiveLinks??10,this.now=t.now??(()=>Date.now()),this.randomId=t.randomId??(()=>(0,kt.randomBytes)(12).toString("hex")),this.randomToken=t.randomToken??(()=>`aia_${(0,kt.randomBytes)(32).toString("base64url")}`)}configureDefaults(t){typeof t.defaultTtlSeconds=="number"&&Number.isFinite(t.defaultTtlSeconds)&&t.defaultTtlSeconds>0&&(this.defaultTtlSeconds=Math.floor(t.defaultTtlSeconds)),typeof t.maxActiveLinks=="number"&&Number.isFinite(t.maxActiveLinks)&&t.maxActiveLinks>0&&(this.maxActiveLinks=Math.floor(t.maxActiveLinks))}isActive(t,n){return t.revokedAtMs!==void 0?!1:t.expiresAtMs>n}list(){let t=this.now();return[...this.links.values()].filter(n=>this.isActive(n,t)).sort((n,r)=>r.createdAtMs-n.createdAtMs).map(Fn)}create(t={}){let n=this.now();if([...this.links.values()].filter(p=>this.isActive(p,n)).length>=this.maxActiveLinks)throw new bt(this.maxActiveLinks);let o=this.defaultTtlSeconds;if(t.ttlSeconds!==void 0){if(typeof t.ttlSeconds!="number"||!Number.isFinite(t.ttlSeconds)||t.ttlSeconds<=0)throw new tt("ttlSeconds must be a positive number");o=Math.floor(t.ttlSeconds)}let s=typeof t.label=="string"&&t.label.trim().length>0?t.label.trim():void 0,i=Ko(t.kind),d=No(i);if(o>d)throw new tt(Fo(i,d));let l=this.randomId(),c=this.randomToken(),u={id:l,tokenHash:aa(c),kind:i,createdAtMs:n,expiresAtMs:n+o*1e3};return s!==void 0&&(u.label=s),t.createdBy!==void 0&&(u.createdBy=t.createdBy),this.links.set(l,u),{...Fn(u),token:c}}revoke(t){let n=this.links.get(t);if(!n)throw new vt(t);return n.revokedAtMs??=this.now(),Fn(n)}getById(t){if(typeof t!="string"||t.length===0)return null;let n=this.links.get(t);return n?Fn(n):null}validateResult(t){if(typeof t!="string"||t.length===0)return{status:"invalid"};let n=aa(t),r=this.now();for(let o of this.links.values())if(bp(o.tokenHash,n))return o.revokedAtMs!==void 0?{status:"invalid"}:o.expiresAtMs<=r?{status:"expired",expiresAt:new Date(o.expiresAtMs).toISOString()}:(o.lastUsedAtMs=r,{status:"active",link:Fn(o)});return{status:"invalid"}}validate(t){let n=this.validateResult(t);return n.status==="active"?n.link:null}size(){return this.links.size}clear(){this.links.clear()}upsertRecordForTest(t){let n={...t};delete n.token,this.links.set(t.id,n)}};Ln();ie();ye();function Vo(e,t){return t}a(Vo,"storeForRequest");function la(e){let t=typeof e.user?.username=="string"?e.user.username:void 0;return t?{kind:"admin",username:t}:{kind:"admin"}}a(la,"adminActor");function Ur(e){let t=he(e.settings);return new At({defaultTtlSeconds:t.defaultLinkTtlSeconds,maxActiveLinks:t.maxActiveLinks})}a(Ur,"createConfiguredLinkStore");function ca(e){e.setHeader("Cache-Control","no-store"),e.setHeader("Pragma","no-cache")}a(ca,"noStore");function ua(e,t){return(n,r)=>{let s=Vo(e,t).list();ca(r),r.json({links:s})}}a(ua,"listLinksHandler");function pa(e,t,n){return(r,o)=>{let s=r.body??{},i={};if(s.ttlSeconds!==void 0){if(typeof s.ttlSeconds!="number"){o.status(400).json({error:"ttlSeconds must be a positive number",code:"invalid_ttl",hint:mt});return}i.ttlSeconds=s.ttlSeconds}if(typeof s.label=="string"&&(i.label=s.label),s.kind!==void 0){if(!Jo(s.kind)){o.status(400).json({error:"kind must be one of: 'get_link', 'rest', 'mcp'",code:"invalid_link_kind"});return}i.kind=s.kind}typeof r.user?.username=="string"&&(i.createdBy=r.user.username);try{let d=Vo(e,t).create(i),l=d.kind;if(n){let c=Date.parse(d.createdAt),u=Date.parse(d.expiresAt),p=Number.isFinite(c)&&Number.isFinite(u)&&u>c?Math.round((u-c)/1e3):void 0;n.append("link.created",la(r),{linkId:d.id,label:d.label,expiresAt:d.expiresAt,createdAt:d.createdAt,kind:l,...p!==void 0?{ttlSeconds:p}:{}})}if(ca(o),l==="get_link"){let c=Ps(r,e.settings.httpAdminRoot,d.token);o.status(201).json({...d,url:c});return}o.status(201).json({...d})}catch(d){if(d instanceof bt){o.status(d.statusCode).json({error:d.message,code:d.code});return}if(d instanceof tt){o.status(d.statusCode).json({error:d.message,code:d.code,hint:d.hint});return}if(d instanceof Nn){o.status(d.statusCode).json({error:d.message,code:d.code});return}$(o,d);return}}}a(pa,"createLinkHandler");function fa(e,t,n){return(r,o)=>{let s=r.params.id;if(typeof s!="string"||s.length===0){o.status(400).json({error:"Missing link id"});return}try{let i=Vo(e,t).revoke(s);if(n){let d=Date.parse(i.createdAt),l=Date.parse(i.expiresAt),c=Number.isFinite(d)&&Number.isFinite(l)&&l>d?Math.round((l-d)/1e3):void 0;n.append("link.revoked",la(r),{linkId:i.id,label:i.label,expiresAt:i.expiresAt,createdAt:i.createdAt,kind:i.kind,...c!==void 0?{ttlSeconds:c}:{}})}o.json({revoked:!0,link:i})}catch(i){if(i instanceof vt){o.status(i.statusCode).json({error:i.message,code:i.code});return}$(o,i);return}}}a(fa,"revokeLinkHandler");function vp(e){let t=Date.parse(e.createdAt),n=Date.parse(e.expiresAt);if(Number.isFinite(t)&&Number.isFinite(n)&&n>t)return Math.round((n-t)/1e3)}a(vp,"ttlSecondsFromLink");function kp(e,t){if(!t||e.action!=="link.created"&&e.action!=="link.revoked")return e;let n={...e.details??{}},r=typeof n.linkId=="string"?n.linkId:"";if(!r)return e;let o=n.label==null||n.label==="",s=n.expiresAt==null||n.expiresAt==="",i=n.createdAt==null||n.createdAt==="",d=n.ttlSeconds==null;if(!o&&!s&&!i&&!d)return e;let l=t.getById(r);if(!l)return e;if(o&&l.label&&(n.label=l.label),s&&(n.expiresAt=l.expiresAt),i&&(n.createdAt=l.createdAt),d){let c=vp(l);c!==void 0&&(n.ttlSeconds=c)}return{...e,details:n}}a(kp,"enrichAuditLogEntry");function ma(e,t){return(n,r)=>{let o=n.query.limit,s=100;if(typeof o=="string"&&o.length>0){let d=Number(o);Number.isFinite(d)&&d>0&&(s=Math.min(Math.floor(d),500))}let i=e.list(s).map(d=>kp(d,t));r.json({logs:i})}}a(ma,"listLogsHandler");var Xo=require("@modelcontextprotocol/sdk/server/mcp.js"),ha=require("@modelcontextprotocol/sdk/server/streamableHttp.js"),T=require("zod");Ge();G();it();K();Fe();ut();Xe();Ae();function ga(){return"0.2.0-beta.3"}a(ga,"pluginVersion");async function ya(e){try{return(await pe(e)).rev}catch{return}}a(ya,"tryLiveRev");function Z(e){return{content:[{type:"text",text:JSON.stringify(e)}]}}a(Z,"jsonResult");function Yo(e){return e===!0?new Set(["payload","baseFlow"]):new Set}a(Yo,"includeSetFromPayloadFlag");function ee(e,t=500){let n=_n(e,"mcp");if(n)return{isError:!0,content:[{type:"text",text:JSON.stringify({...n.body,statusCode:n.statusCode})}]};let r=e instanceof Error?e.message:String(e);return{isError:!0,content:[{type:"text",text:JSON.stringify({error:r,code:"internal_error",statusCode:t})}]}}a(ee,"jsonError");var Ap=new Set(["list_proposals","list_my_proposals"]),xp=new Set(["status","limit","offset","mine","flowId"]);function Sp(e){return!!e&&typeof e=="object"&&typeof e.safeParseAsync=="function"}a(Sp,"isZodLikeSchema");function Tp(e,t,n){let r=t&&typeof t=="object"?t:{},o=n[0],s=o?.path&&o.path.length>0?String(o.path[0]):"(arguments)";if(Ap.has(e)&&xp.has(s))return Bt(s,r[s]);let i=n.map(l=>({parameter:l.path&&l.path.length>0?l.path.map(String).join("."):"(arguments)",message:l.message??"Invalid value"})),d=i.length===1?`Invalid argument '${i[0]?.parameter}' for tool ${e}: ${i[0]?.message}`:`${i.length} invalid arguments for tool ${e}`;return new y(d,"invalid_arguments",{details:{parameter:s,errors:i,hint:`Fix '${s}' to match the ${e} input schema (see tools/list).`}})}a(Tp,"mcpArgumentError");function wa(e){let t=e,n=t.validateToolInput;typeof n=="function"&&(t.validateToolInput=async(r,o,s)=>{let i=r.inputSchema;if(!Sp(i))return n.call(e,r,o,s);let d=await i.safeParseAsync(o??{});if(d.success)return d.data;let l=_n(Tp(s,o,d.error.issues),"mcp"),c=l?{...l.body,statusCode:l.statusCode}:{};throw new Error(JSON.stringify(c))})}a(wa,"installMcpArgumentErrorMapping");function ba(e,t,n,r){e.registerTool("get_status",{description:'Return plugin status (version, AI feature flags, storage path, deploy defaults). For access-link callers, permission is null and caller.kind is "access-link" \u2014 that is expected, not an error. Use before other tools to confirm the plugin is enabled.'},()=>{try{return Z(rn(t,r))}catch(o){return ee(o)}}),e.registerTool("list_flows",{description:"List all flows as a flat Admin API v2 array plus the current rev string. Returns { rev, flows }. Call this (or get_flow) before submit_proposal so expectedRev matches."},async()=>{try{return Z(await pe(n.flowAdapter))}catch(o){return ee(o,502)}}),e.registerTool("get_flow",{description:"Get one tab/subflow by id with nested nodes/configs and the current rev. Pass summary=true for a lightweight node list (id/type/name/wires/bytes, no field contents). Prefer summary \u2192 search_flows \u2192 get_node_field(lines) \u2192 replace_text \u2192 submit_proposal. Returns { rev, flow } or summary shape. Use rev as expectedRev when submitting.",inputSchema:{id:T.z.string().min(1).describe("Flow / tab id"),summary:T.z.boolean().optional().describe("When true, return node list with sizes only (no field contents)")}},async({id:o,summary:s})=>{try{return Z(s?await xr(n.flowAdapter,o):await xe(n.flowAdapter,o))}catch(i){return ee(i,502)}}),e.registerTool("get_node",{description:"Get one full node by id (includes owning flowId when known). Optional flow scopes the lookup.",inputSchema:{id:T.z.string().min(1).describe("Node id"),flow:T.z.string().min(1).optional().describe("Optional tab id to scope the lookup")}},async({id:o,flow:s})=>{try{return Z(await Sr(n.flowAdapter,o,s))}catch(i){return ee(i,502)}}),e.registerTool("get_node_field",{description:'Get one string field from a node. Optional lines="A-B" (1-based inclusive) returns a slice; response always includes totalLines and bytes.',inputSchema:{id:T.z.string().min(1).describe("Node id"),field:T.z.string().min(1).describe("Field name (e.g. template, func)"),lines:T.z.string().optional().describe("Optional 1-based line range A or A-B"),flow:T.z.string().min(1).optional().describe("Optional tab id to scope the lookup")}},async({id:o,field:s,lines:i,flow:d})=>{try{return Z(await Tr(n.flowAdapter,o,s,{...d?{flowId:d}:{},...i?{lines:i}:{}}))}catch(l){return ee(l,502)}}),e.registerTool("search_flows",{description:"Text search across string fields. Returns hits with node id, field, line numbers, and a short snippet. Results are capped; truncated=true when the cap is hit. Optional flow scopes to one tab.",inputSchema:{q:T.z.string().describe("Substring to find (case-sensitive; non-empty required)"),flow:T.z.string().min(1).optional().describe("Optional tab id")}},async({q:o,flow:s})=>{try{return Z(await Pr(n.flowAdapter,o,s?{flowId:s}:{}))}catch(i){return ee(i,502)}}),e.registerTool("list_node_types",{description:"List installed Node-RED palette node types (type, module, category, description, \u2026). Optional q filters type/module/category/description (case-insensitive). Results are capped. Call this before proposing nodes with unfamiliar types.",inputSchema:{q:T.z.string().optional().describe("Optional case-insensitive filter")}},async({q:o})=>{try{return Z(await ir(n.nodeTypesAdapter,o?{q:o}:{}))}catch(s){return ee(s,502)}}),e.registerTool("get_node_type",{description:"Get detail for one installed node type: defaults (JSON), inputs/outputs (always set), requiredConfigNodeTypes, help sections (summary/inputs/outputs), category/module/color. Types are case-sensitive. Pass includeHelp=false to omit help. 404 node_type_not_found when unknown (didYouMean on case mismatch; else nearest names).",inputSchema:{type:T.z.string().min(1).describe("Node type id (e.g. inject, template) \u2014 case-sensitive"),includeHelp:T.z.boolean().optional().describe("When false, omit help sections (default true)")}},async({type:o,includeHelp:s})=>{try{return Z(await ct(n.nodeTypesAdapter,o,{includeHelp:s!==!1}))}catch(i){return ee(i,502)}}),e.registerTool("search_palette",{description:"Search the public Node-RED catalogue for modules to recommend. Returns module id, description, version, updated, link, and installed boolean (cross-checked against installed modules). Core node-red types are not in the catalogue \u2014 use list_node_types. RECOMMEND ONLY \u2014 never installs.",inputSchema:{q:T.z.string().describe("Search string (id / description / keywords; non-empty required)")}},async({q:o})=>{try{return Z(await ar(n.nodeTypesAdapter,o,{...n.catalogueFetcher?{catalogueFetcher:n.catalogueFetcher}:{},...n.catalogueUrl?{catalogueUrl:n.catalogueUrl}:{}}))}catch(s){return ee(s,502)}})}a(ba,"registerReadTools");function va(e,t,n,r,o){let s=q(r,t.settings.httpAdminRoot);e.registerTool("submit_proposal",{description:`Submit a replace_flow proposal for owner approval (does not deploy). Preferred for small string edits: payload: { flowId, nodes: [{ op:"replace_text", node, field, find, replace, occurrence? }] } \u2014 find must be unique unless occurrence (1-based) is set; multiple replace_text ops apply in order; yields a short readable diff. Or send a full tab: payload.flow from get_flow (id, type, label?, nodes[], configs?). Inputs (all top-level, next to each other): type="replace_flow", expectedRev (required string from list_flows/get_flow rev; expected_rev alias accepted), optional title (\u226480 chars), and payload: { flowId, flow? } and/or payload.nodes / top-level nodes. Do not nest expectedRev inside payload. Payload size limit is ${j} bytes (413 if exceeded). expectedRev is checked against the live rev at submit time (409 if stale). replace_text errors: 400 text_not_found; 409 ambiguous_match with matchCount. Unknown palette types return 400 unknown_node_type with nearest matches \u2014 call list_node_types / search_palette and ask the user to install via Manage palette (never auto-install). Returns a lean proposal (id, status, title, storedTitle, flowLabel, displayName, reviewUrl, changeSummary, \u2026) by default; title may gain " in <flow>"; storedTitle is the title as saved (your title without that suffix, or the auto-generated title, which may already name the flow). Pass include_payload=true to include payload and baseFlow. When telling the user about the proposal, cite displayName and reviewUrl \u2014 not the raw id. Humans approve on the /ai-access dashboard (reviewUrl opens/scrolls to it).`,inputSchema:{type:T.z.literal("replace_flow").describe("Must be 'replace_flow'"),expectedRev:T.z.string().min(1).optional().describe("Top-level current flow revision from get_flow/list_flows (required at submit; sit next to type and payload, not inside payload)"),expected_rev:T.z.string().min(1).optional().describe("Alias for expectedRev (snake_case); prefer expectedRev"),title:T.z.string().max(80).optional().describe("Optional short human title (\u226480 chars). Auto-generated from changes when omitted."),include_payload:T.z.boolean().optional().describe("When true, include payload and baseFlow in the response (default lean)"),nodes:T.z.array(T.z.record(T.z.string(),T.z.unknown())).optional().describe("Optional node ops (set/add/remove/replace_text). Same as payload.nodes when flow is omitted."),payload:T.z.object({flowId:T.z.string().min(1).describe("Tab id to replace"),flow:T.z.object({id:T.z.string().optional(),type:T.z.string().optional().describe("Usually 'tab' (included by get_flow)"),label:T.z.string().optional(),nodes:T.z.array(T.z.record(T.z.string(),T.z.unknown())).optional().describe("Child nodes for the tab (from get_flow.nodes)"),configs:T.z.array(T.z.record(T.z.string(),T.z.unknown())).optional()}).passthrough().optional().describe("Full tab from get_flow; omit when using nodes ops"),nodes:T.z.array(T.z.record(T.z.string(),T.z.unknown())).optional().describe("Node ops applied to the live tab (prefer replace_text for small string edits)")}).describe("Replacement flow payload and/or node ops")}},async i=>{try{let d=await Ye(n.proposalStore,n.auditLog,o,{...i,expectedRev:typeof i.expectedRev=="string"&&i.expectedRev.length>0?i.expectedRev:i.expected_rev},n.flowAdapter,s,n.nodeTypesAdapter),l={...ce(d,{...s,include:Yo(i.include_payload)}),mine:!0},c=V(n.proposalStore,d.expectedRev,{flowId:d.payload.flowId}),u=Ne(Ie(n.proposalStore,n.proposalStore.get(d.id),o.id));return Z({...l,...u,...Q(c)})}catch(d){return ee(d)}}),Rp(e,n,o,s),_p(e,n,o,s)}a(va,"registerProposalTools");function Pp(e,t,n,r){let o={mine:T.z.boolean().optional().describe("true: only proposals created by this access link; false: only other links\u2019 proposals"),status:T.z.union([T.z.string(),T.z.array(T.z.string())]).optional().describe("Filter by proposal status: one value, an array, or a comma-separated string (pending, approved, rejected, failed, withdrawn, expired, superseded). An empty array or empty string means no status filter."),flowId:T.z.string().optional().describe("Filter by flow tab id (payload.flowId)"),limit:T.z.number().int().min(1).max(Ut).optional().describe(`Page size (1\u2013${Ut}). Omit to return all matches.`),offset:T.z.number().int().min(0).optional().describe("Skip this many filtered results (default 0)"),include_payload:T.z.boolean().optional().describe("When true, include payload and baseFlow on each proposal")},s=a(async i=>{try{let d=Yo(i.include_payload),l=await ya(t.flowAdapter),c=l?V(t.proposalStore,l):0,u={};typeof i.mine=="boolean"&&(u.mine=i.mine);let p=Io(i.status);p&&(u.status=p),typeof i.flowId=="string"&&i.flowId.length>0&&(u.flowId=i.flowId),typeof i.limit=="number"&&(u.limit=i.limit),typeof i.offset=="number"&&(u.offset=i.offset);let f=qt(t.proposalStore,n,r,u);return Z({proposals:f.proposals.map(m=>ue(t.proposalStore,n,m,{...r,include:d},{includeConflicts:!1})),...ft({...f,returned:f.proposals.length}),...Q(c)})}catch(d){return ee(d)}},"listProposalsHandler");e.registerTool("list_proposals",{description:"List proposals visible to this access link (all proposals by default). Each item includes mine:true|false for the caller. Filter with mine, status, and/or flowId; paginate with limit/offset (response includes total, offset, hasMore, nextOffset). Invalid filters return invalid_filter. Returns lean { proposals, total, \u2026 } with title, displayName, reviewUrl, changeSummary (no payload/baseFlow unless include_payload=true). Prefer displayName + reviewUrl when referring to a proposal with the user (not the raw id).",inputSchema:o},s),e.registerTool("list_my_proposals",{description:"Alias of list_proposals. Prefer list_proposals. Lists proposals visible to this access link with optional mine/status/flowId filters and limit/offset pagination.",inputSchema:o},s)}a(Pp,"registerListProposalsTools");function Rp(e,t,n,r){Pp(e,t,n,r),e.registerTool("get_proposal",{description:"Get any proposal by id (all tokens can read). Response includes mine:true|false and, for pending proposals, conflictingProposals when other pending work touches the same flow. Lean by default (no payload/baseFlow); pass include_payload=true for full bodies. Cite displayName + reviewUrl when talking to the user.",inputSchema:{id:T.z.string().min(1).describe("Proposal id (prop-\u2026)"),include_payload:T.z.boolean().optional().describe("When true, include payload and baseFlow")}},async({id:o,include_payload:s})=>{try{let i=await ya(t.flowAdapter),d=De(t.proposalStore,n,o,r);return Z(ue(t.proposalStore,n,d,{...r,include:Yo(s)},{...i?{liveRev:i}:{}}))}catch(i){return ee(i)}})}a(Rp,"registerProposalQueryTools");function _p(e,t,n,r){e.registerTool("get_proposal_diff",{description:"Diff any proposal against its stored baseFlow snapshot (what changed). Optional format=summary|full (default summary). Pending + stale also includes vsCurrent.",inputSchema:{id:T.z.string().min(1).describe("Proposal id (prop-\u2026)"),format:T.z.enum(["summary","full"]).optional().describe("Diff payload format (default summary)")}},async({id:o,format:s})=>{try{let i=De(t.proposalStore,n,o),d=Yt(s??"summary");if(!d.ok)return ee(new y("Invalid diff format","invalid_diff_format",{details:{hint:"Valid formats: summary, full (default summary)."}}));let l=Qt(await nn(i,t.flowAdapter),d.format);return Z(l)}catch(i){return ee(i)}}),e.registerTool("withdraw_proposal",{description:"Withdraw one of YOUR pending proposals (status becomes withdrawn, not rejected). Optional reason. Only the creating access link can withdraw; other tokens get 403. Cite displayName + reviewUrl when telling the user.",inputSchema:{id:T.z.string().min(1).describe("Proposal id (prop-\u2026) owned by this access link"),reason:T.z.string().optional().describe("Optional withdraw reason")}},({id:o,reason:s})=>{try{let i=Wt(t.proposalStore,t.auditLog,n,o,s,r);return Z(ue(t.proposalStore,n,i,r))}catch(i){return ee(i)}})}a(_p,"registerProposalActionTools");function Lp(e,t,n){let r=new Xo.McpServer({name:"node-red-ai-access",version:ga()});wa(r);let o=n.accessLink;if(!o)throw new Error("Access link required");return ba(r,e,t,n),va(r,e,t,n,o),r}a(Lp,"createAiMcpServer");function Qo(e,t){return async(n,r,o)=>{try{if(!n.accessLink&&!n.auth){r.setHeader("WWW-Authenticate",Y),r.status(401).json({error:le,code:Le});return}let s=n.accessLink?Lp(e,t,n):Cp(e,t,n),i=new ha.StreamableHTTPServerTransport({enableJsonResponse:!0});await s.connect(i),await i.handleRequest(n,r,n.body),r.on("close",()=>{i.close(),s.close()})}catch(s){if(!r.headersSent){$(r,s);return}o(s instanceof Error?s:new Error(String(s)))}}}a(Qo,"mcpRpcPostHandler");function Cp(e,t,n){let r=new Xo.McpServer({name:"node-red-ai-access",version:ga()});if(wa(r),ba(r,e,t,n),(n.auth?.scopes??[]).includes("propose")){let s=typeof n.auth?.extra?.user=="string"?n.auth.extra.user:"owner",i=n.auth?.clientId??"",l={id:lo(i,s),kind:"mcp",createdAt:new Date().toISOString(),expiresAt:new Date((n.auth?.expiresAt??0)*1e3).toISOString(),createdBy:s,oauthClientId:i,oauthUser:s};va(r,e,t,n,l)}return r}a(Cp,"createOAuthMcpServer");function On(e,t){t.status(405).set("Allow","POST").json({jsonrpc:"2.0",error:{code:-32e3,message:"Method not allowed. Use POST for Streamable HTTP."},id:null})}a(On,"mcpRpcMethodNotAllowed");var $n=require("crypto"),sn=require("fs"),ts=require("path"),B=require("@modelcontextprotocol/sdk/server/auth/errors.js"),Ta=require("@modelcontextprotocol/sdk/server/auth/handlers/authorize.js"),ns=require("@modelcontextprotocol/sdk/server/auth/handlers/metadata.js"),Pa=require("@modelcontextprotocol/sdk/server/auth/handlers/register.js"),Ra=require("@modelcontextprotocol/sdk/server/auth/handlers/revoke.js"),_a=require("@modelcontextprotocol/sdk/server/auth/handlers/token.js"),La=require("@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js"),Ca=require("@modelcontextprotocol/sdk/server/auth/router.js"),Ea=ln(require("express"),1);G();_e();function $e(e){return e.replace(/[&<>"']/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[t]??t)}a($e,"esc");var ka={read:'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5c-5 0-9 4.5-10 7 1 2.5 5 7 10 7s9-4.5 10-7c-1-2.5-5-7-10-7Zm0 11.5a4.5 4.5 0 1 1 0-9 4.5 4.5 0 0 1 0 9Zm0-2.3a2.2 2.2 0 1 0 0-4.4 2.2 2.2 0 0 0 0 4.4Z"/></svg>',propose:'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 17.2V20h2.8l8.3-8.3-2.8-2.8L4 17.2Zm13.7-7.6a1 1 0 0 0 0-1.4l-1.9-1.9a1 1 0 0 0-1.4 0l-1.4 1.4 2.8 2.8 1.9-1.9Z"/></svg>'},Ep={read:["Read your flows","View flows, nodes, node types and AI Access logs."],propose:["Propose flow changes","Submit change proposals. Nothing is deployed until you approve it in Node-RED."]},Ip='<svg class="logo" viewBox="0 0 48 48" aria-hidden="true"><rect width="48" height="48" rx="11" fill="#8f0000"/><path d="M11 15h9a4 4 0 0 1 4 4v10a4 4 0 0 0 4 4h9" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round"/><rect x="6" y="11" width="9" height="8" rx="2.5" fill="#fff"/><rect x="33" y="29" width="9" height="8" rx="2.5" fill="#fff"/><circle cx="37.5" cy="16" r="5" fill="none" stroke="#fff" stroke-width="2.4"/><path d="M37.5 13.5v5M35 16h5" stroke="#fff" stroke-width="2.2" stroke-linecap="round"/></svg>',Np=`
954
+ :root{color-scheme:light dark;--bg:#f3f4f6;--card:#fff;--text:#111827;--muted:#4b5563;--line:#e5e7eb;--soft:#f9fafb;--brand:#8f0000;--brand-2:#b91c1c;--ok:#0f766e;--warn-bg:#fff7ed;--warn:#9a3412;--focus:#2563eb}
955
+ @media (prefers-color-scheme:dark){:root{--bg:#0b0f14;--card:#151b23;--text:#e5e7eb;--muted:#9ca3af;--line:#273140;--soft:#1b2330;--brand:#c0262d;--brand-2:#ef4444;--warn-bg:#2a1a0e;--warn:#fdba74;--focus:#60a5fa}}
956
+ *{box-sizing:border-box}
957
+ body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px 16px;background:var(--bg);color:var(--text);font:15px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
958
+ .card{width:100%;max-width:440px;background:var(--card);border:1px solid var(--line);border-radius:16px;box-shadow:0 10px 30px rgba(0,0,0,.08);overflow:hidden}
959
+ .head{display:flex;align-items:center;gap:12px;padding:18px 22px;border-bottom:1px solid var(--line)}
960
+ .logo{width:40px;height:40px;flex:none}
961
+ .brand b{display:block;font-size:15px}.brand span{font-size:13px;color:var(--muted)}
962
+ .body{padding:20px 22px 22px}
963
+ h1{font-size:19px;line-height:1.3;margin:0 0 6px}
964
+ .app{font-weight:700;overflow-wrap:anywhere}
965
+ .sub{margin:0 0 14px;color:var(--muted);font-size:14px}
966
+ .host{display:inline-block;font:600 13px ui-monospace,SFMono-Regular,Menlo,monospace;background:var(--soft);border:1px solid var(--line);border-radius:6px;padding:1px 6px;overflow-wrap:anywhere}
967
+ .warn{background:var(--warn-bg);color:var(--warn);border-radius:8px;padding:8px 10px;font-size:13px;margin:0 0 14px}
968
+ .perms{list-style:none;margin:0 0 14px;padding:0;border:1px solid var(--line);border-radius:12px}
969
+ .perms li{display:flex;gap:12px;padding:12px 14px}.perms li+li{border-top:1px solid var(--line)}
970
+ .perms svg{width:22px;height:22px;flex:none;fill:var(--brand-2);margin-top:2px}
971
+ .perms b{display:block;font-size:14px}.perms span{color:var(--muted);font-size:13px}
972
+ .perms li.dropped{opacity:.55}.perms li.dropped b::after{content:" \u2014 not granted";font-weight:400;color:var(--warn)}
973
+ .user{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--muted);margin:0 0 16px}
974
+ .avatar{width:26px;height:26px;border-radius:50%;background:var(--soft);border:1px solid var(--line);display:flex;align-items:center;justify-content:center;font-weight:700;color:var(--text);text-transform:uppercase}
975
+ .status{font-size:14px;color:var(--muted);margin:0 0 14px}.status.error{color:var(--brand-2)}
976
+ .actions{display:flex;gap:10px}
977
+ button{font:inherit;border-radius:10px;padding:10px 16px;cursor:pointer;border:1px solid var(--line);background:var(--card);color:var(--text)}
978
+ button:focus-visible,input:focus-visible,a:focus-visible{outline:3px solid var(--focus);outline-offset:2px}
979
+ button[disabled]{opacity:.6;cursor:default}
980
+ .primary{flex:1;background:var(--brand);border-color:var(--brand);color:#fff;font-weight:600}
981
+ .primary:hover:not([disabled]){background:var(--brand-2)}
982
+ .secondary{flex:none}
983
+ form.login{display:grid;gap:10px;margin:0 0 4px}
984
+ form.login label{display:grid;gap:4px;font-size:13px;color:var(--muted)}
985
+ form.login input{font:inherit;padding:9px 11px;border-radius:10px;border:1px solid var(--line);background:var(--soft);color:var(--text)}
986
+ .foot{padding:12px 22px;border-top:1px solid var(--line);font-size:12px;color:var(--muted);background:var(--soft)}
987
+ a{color:var(--focus)}
988
+ [hidden]{display:none!important}
989
+ @media (max-width:420px){.actions{flex-direction:column-reverse}.secondary{flex:1}}
990
+ `;function Aa(e){let t=e.scopes.map(n=>{let[r,o]=Ep[n]??[n,""];return`<li data-scope="${$e(n)}">${ka[n]??ka.read}<div><b>${$e(r)}</b><span>${$e(o)}</span></div></li>`}).join("");return`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="referrer" content="no-referrer"><title>Authorize MCP client \xB7 Node-RED AI Access</title><style>${Np}</style></head><body><main class="card" data-request="${$e(e.requestId)}" data-token-key="${$e(e.tokenKey)}" data-admin-root="${$e(e.adminRoot)}"><div class="head">${Ip}<div class="brand"><b>Node-RED AI Access</b><span>Authorization request</span></div></div><div class="body"><h1><span class="app">${$e(e.redirectHost)}</span> wants to access your Node-RED</h1><p class="sub">Access tokens will be sent to <span class="host">${$e(e.redirectHost)}</span>. App name (provided by the client, unverified): <b>${$e(e.clientName)}</b></p>${e.loopbackRedirect?'<p class="warn">Warning: this redirect address is reachable only on this machine.</p>':""}<ul class="perms" aria-label="Requested permissions">${t}</ul><p id="status" class="status" role="status" aria-live="polite">Checking your Node-RED session\u2026</p><form id="login" class="login" hidden autocomplete="on"><label>Username<input id="username" name="username" autocomplete="username" required></label><label>Password<input id="password" name="password" type="password" autocomplete="current-password" required></label><div class="actions"><button id="login-submit" class="primary" type="submit">Sign in</button></div></form><p id="grant" class="sub" hidden></p><p id="user" class="user" hidden><span class="avatar" id="avatar" aria-hidden="true"></span><span>Signed in as <b id="username-label"></b></span></p><div class="actions"><button id="deny" class="secondary" type="button" hidden>Deny</button><button id="allow" class="primary" type="button" hidden>Allow access</button><button id="retry" class="secondary" type="button" hidden>Retry</button></div></div><div class="foot">You can revoke this app at any time from the AI Access dashboard (OAuth tab) in Node-RED.</div></main><script nonce="${$e(e.nonce)}">${e.script}</script></body></html>`}a(Aa,"renderConsentPage");var xa=`(function () {
1198
991
  var main = document.querySelector('main');
1199
992
  var id = main.getAttribute('data-request');
1200
993
  var key = main.getAttribute('data-token-key');
1201
- var status = document.getElementById('status');
1202
- var grant = document.getElementById('grant');
1203
- var allow = document.getElementById('allow');
1204
- var deny = document.getElementById('deny');
1205
- var retry = document.getElementById('retry');
994
+ var $ = function (x) { return document.getElementById(x); };
995
+ var status = $('status'), grant = $('grant'), allow = $('allow'), deny = $('deny'), retry = $('retry');
996
+ var login = $('login'), user = $('user');
997
+ var adminRoot = new URL(main.getAttribute('data-admin-root') || '/', window.location.href);
1206
998
  var csrf = null;
1207
999
  function headers() {
1208
1000
  var h = { 'Content-Type': 'application/json', 'Node-RED-API-Version': 'v2' };
@@ -1213,31 +1005,87 @@ strings you will paste publicly beyond this temporary link.
1213
1005
  return h;
1214
1006
  }
1215
1007
  function show(el, on) { el.hidden = !on; }
1008
+ function setStatus(text, isError) {
1009
+ status.textContent = text;
1010
+ status.className = 'status' + (isError ? ' error' : '');
1011
+ show(status, !!text);
1012
+ }
1013
+ function editorLink(prefix, suffix) {
1014
+ status.textContent = '';
1015
+ status.className = 'status';
1016
+ status.append(prefix);
1017
+ var a = document.createElement('a');
1018
+ a.href = adminRoot.href; a.target = '_blank'; a.rel = 'noopener'; a.textContent = 'Node-RED editor';
1019
+ status.append(a, suffix);
1020
+ show(status, true);
1021
+ }
1022
+ function showSignIn() {
1023
+ show(allow, false); show(deny, false); show(user, false); show(grant, false);
1024
+ fetch(new URL('auth/login', adminRoot).href, { headers: { 'Node-RED-API-Version': 'v2' }, credentials: 'same-origin' })
1025
+ .then(function (r) { return r.json(); })
1026
+ .then(function (cfg) {
1027
+ if (cfg && cfg.type === 'credentials') {
1028
+ setStatus('Sign in with your Node-RED account to continue.');
1029
+ show(login, true);
1030
+ $('username').focus();
1031
+ } else {
1032
+ editorLink('Sign in to the ', ' in this browser, then press Retry.');
1033
+ show(retry, true);
1034
+ }
1035
+ })
1036
+ .catch(function () {
1037
+ editorLink('Sign in to the ', ' in this browser, then press Retry.');
1038
+ show(retry, true);
1039
+ });
1040
+ }
1216
1041
  function load() {
1217
- show(retry, false);
1218
- status.textContent = 'Checking your Node-RED editor session\u2026';
1042
+ show(retry, false); show(login, false);
1043
+ setStatus('Checking your Node-RED session\u2026');
1219
1044
  fetch('consent/' + encodeURIComponent(id), { headers: headers(), credentials: 'same-origin' })
1220
1045
  .then(function (r) { return r.json().catch(function () { return {}; }).then(function (b) { return { r: r, b: b }; }); })
1221
1046
  .then(function (x) {
1222
- if (x.r.status === 401) {
1223
- status.innerHTML = '';
1224
- status.append('Sign in to the ');
1225
- var a = document.createElement('a');
1226
- a.href = '../../'; a.target = '_blank'; a.rel = 'noopener'; a.textContent = 'Node-RED editor';
1227
- status.append(a, ' in this browser, then press Retry.');
1228
- show(retry, true);
1229
- return;
1230
- }
1231
- if (!x.r.ok) { status.textContent = x.b.error || ('Error ' + x.r.status); return; }
1047
+ if (x.r.status === 401) { showSignIn(); return; }
1048
+ if (!x.r.ok) { setStatus(x.b.error || ('Error ' + x.r.status), true); return; }
1232
1049
  csrf = x.b.csrf;
1233
- status.textContent = 'Signed in as ' + x.b.user + '.';
1050
+ setStatus('');
1051
+ $('username-label').textContent = String(x.b.user);
1052
+ $('avatar').textContent = String(x.b.user || '?').charAt(0);
1053
+ show(user, true);
1234
1054
  var dropped = x.b.requested.filter(function (s) { return x.b.grantable.indexOf(s) < 0; });
1055
+ document.querySelectorAll('.perms li').forEach(function (li) {
1056
+ li.classList.toggle('dropped', dropped.indexOf(li.getAttribute('data-scope')) >= 0);
1057
+ });
1235
1058
  grant.textContent = 'Will grant: ' + x.b.grantable.join(', ') +
1236
- (dropped.length ? ' (not granted, your permission is too low: ' + dropped.join(', ') + ')' : '');
1059
+ (dropped.length ? ' (your Node-RED permission does not allow: ' + dropped.join(', ') + ')' : '');
1237
1060
  show(grant, true); show(allow, true); show(deny, true);
1061
+ allow.focus();
1238
1062
  })
1239
- .catch(function (e) { status.textContent = 'Error: ' + e.message; show(retry, true); });
1063
+ .catch(function (e) { setStatus('Error: ' + e.message, true); show(retry, true); });
1240
1064
  }
1065
+ login.addEventListener('submit', function (ev) {
1066
+ ev.preventDefault();
1067
+ var btn = $('login-submit');
1068
+ btn.disabled = true;
1069
+ setStatus('Signing in\u2026');
1070
+ var body = new URLSearchParams({
1071
+ client_id: 'node-red-editor', grant_type: 'password', scope: '*',
1072
+ username: $('username').value, password: $('password').value
1073
+ });
1074
+ fetch(new URL('auth/token', adminRoot).href, {
1075
+ method: 'POST', credentials: 'same-origin',
1076
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Node-RED-API-Version': 'v2' },
1077
+ body: body.toString()
1078
+ })
1079
+ .then(function (r) { return r.json().catch(function () { return {}; }).then(function (b) { return { r: r, b: b }; }); })
1080
+ .then(function (x) {
1081
+ btn.disabled = false;
1082
+ $('password').value = '';
1083
+ if (!x.r.ok || !x.b.access_token) { setStatus('Sign in failed. Check your username and password.', true); return; }
1084
+ try { localStorage.setItem(key, JSON.stringify(x.b)); } catch (e) {}
1085
+ load();
1086
+ })
1087
+ .catch(function (e) { btn.disabled = false; setStatus('Error: ' + e.message, true); });
1088
+ });
1241
1089
  function decide(decision) {
1242
1090
  allow.disabled = deny.disabled = true;
1243
1091
  fetch('consent/' + encodeURIComponent(id), {
@@ -1246,16 +1094,367 @@ strings you will paste publicly beyond this temporary link.
1246
1094
  })
1247
1095
  .then(function (r) { return r.json().then(function (b) { return { r: r, b: b }; }); })
1248
1096
  .then(function (x) {
1249
- if (x.r.ok && x.b.redirect) { status.textContent = 'Redirecting\u2026'; window.location.assign(x.b.redirect); }
1250
- else status.textContent = x.b.error || ('Error ' + x.r.status);
1097
+ if (x.r.ok && x.b.redirect) { setStatus(decision === 'allow' ? 'Access allowed. Returning to the app\u2026' : 'Access denied. Returning to the app\u2026'); window.location.assign(x.b.redirect); }
1098
+ else { setStatus(x.b.error || ('Error ' + x.r.status), true); allow.disabled = deny.disabled = false; }
1251
1099
  })
1252
- .catch(function (e) { status.textContent = 'Error: ' + e.message; });
1100
+ .catch(function (e) { setStatus('Error: ' + e.message, true); allow.disabled = deny.disabled = false; });
1253
1101
  }
1254
1102
  allow.addEventListener('click', function () { decide('allow'); });
1255
1103
  deny.addEventListener('click', function () { decide('deny'); });
1256
1104
  retry.addEventListener('click', load);
1257
1105
  load();
1258
- })();`;_e();var Qe=a(e=>(0,Fn.createHash)("sha256").update(e).digest("hex"),"hash"),bt=a(()=>(0,Fn.randomBytes)(32).toString("base64url"),"secret"),bp=36e5,vp=100,kp=5,Ap=500,ya=a(()=>({clients:[],codes:[],tokens:[]}),"emptyDb"),Hr=class{constructor(t,n){this.maxClients=n;this.path=(0,Zo.join)(t.root,"oauth.json");try{let r=(0,nn.existsSync)(this.path)?JSON.parse((0,nn.readFileSync)(this.path,"utf8")):ya();if(!r||typeof r!="object"||!("clients"in r)||!("codes"in r)||!("tokens"in r)||!Array.isArray(r.clients)||!Array.isArray(r.codes)||!Array.isArray(r.tokens))throw new Error("Invalid OAuth store");this.db=r}catch{this.db=ya()}(0,nn.mkdirSync)(t.root,{recursive:!0,mode:448}),this.prune()}static{a(this,"OAuthStore")}db;path;save(){xt((0,Zo.join)(this.path,".."),this.path,De(this.db,8e6,{kind:"oauth",pretty:!0}))}prune(t=Date.now()){let n=this.db.clients.length+this.db.codes.length+this.db.tokens.length,r=new Set(this.db.clients.filter(o=>o.approved?(o.lastUsedAt??(o.client_id_issued_at??0)*1e3)>t-30*864e5:(o.client_id_issued_at??0)*1e3>t-bp).map(o=>o.client_id));this.db.clients=this.db.clients.filter(o=>r.has(o.client_id)),this.db.codes=this.db.codes.filter(o=>o.expires>t&&r.has(o.clientId)),this.db.tokens=this.db.tokens.filter(o=>r.has(o.clientId)&&o.expires>t),this.db.clients.length+this.db.codes.length+this.db.tokens.length!==n&&this.save()}getClient(t){return this.prune(),this.db.clients.find(n=>n.client_id===t)}registerClient(t){if(this.prune(),this.approvedCount()>=this.maxClients())throw new H.InvalidClientMetadataError("Maximum active OAuth clients reached");if(this.db.clients.length-this.approvedCount()>=vp)throw new H.InvalidClientMetadataError("Too many OAuth clients awaiting consent");if(!Array.isArray(t.redirect_uris)||!t.redirect_uris.length||t.redirect_uris.some(r=>{try{let o=new URL(r);return!!o.username||!!o.password||!!o.hash||o.protocol!=="https:"&&!(o.protocol==="http:"&&["localhost","127.0.0.1","[::1]"].includes(o.hostname))}catch{return!0}}))throw new H.InvalidClientMetadataError("Redirect URIs must use HTTPS or loopback HTTP");if(t.token_endpoint_auth_method&&t.token_endpoint_auth_method!=="none")throw new H.InvalidClientMetadataError("Only public clients are supported");let n={...t,client_secret:void 0,client_secret_expires_at:void 0,client_id:bt(),client_id_issued_at:Math.floor(Date.now()/1e3),token_endpoint_auth_method:"none",lastUsedAt:Date.now()};return this.db.clients.push(n),this.save(),n}approvedCount(){return this.db.clients.filter(t=>t.approved).length}approve(t){let n=this.db.clients.find(r=>r.client_id===t);if(!(!n||n.approved)){if(this.approvedCount()>=this.maxClients())throw new H.InvalidClientMetadataError("Maximum active OAuth clients reached");n.approved=!0,this.save()}}get data(){return this.db}persist(){this.touchTimer&&(clearTimeout(this.touchTimer),this.touchTimer=void 0),this.save()}touchTimer;touch(){this.touchTimer||(this.touchTimer=setTimeout(()=>{this.touchTimer=void 0,this.save()},5e3),this.touchTimer.unref?.())}revokeClient(t){let n=this.db.clients.length;return this.db.clients=this.db.clients.filter(r=>r.client_id!==t),this.db.tokens=this.db.tokens.filter(r=>r.clientId!==t),this.db.codes=this.db.codes.filter(r=>r.clientId!==t),this.save(),this.db.clients.length!==n}};function rn(e,t=["/.well-known/oauth-","/mcp/oauth/"]){let n=[],r=a(s=>{try{return e?.[s]}catch{return}},"read"),o=r("_router")?.stack??r("router")?.stack??[];for(let s of o){if(s.handle?.oauthOwned)continue;let i=s.route?.path?Array.isArray(s.route.path)?s.route.path:[s.route.path]:[];for(let l of i)t.some(c=>String(l).startsWith(c))&&n.push(`${String(l)} (${s.name??"route"})`);let d=s.matchers??(s.regexp?[l=>s.regexp?.test(l)]:void 0);if(!i.length&&d?.length&&s.name!=="router"){let l=a(u=>d.some(p=>!!p(u)),"matches");!l("/unrelated-ai-access-collision-probe")&&t.some(u=>l(`${u}token`)||l(`${u}mcp`))&&n.push(`mounted middleware (${s.name??"route"})`)}s.handle?.stack&&n.push(...rn({router:{stack:s.handle.stack}},t))}return n}a(rn,"detectOAuthRouteCollisions");var Ur=class{constructor(t,n,r,o,s=()=>0){this.clientsStore=t;this.resource=n;this.accessTtl=r;this.refreshTtl=o;this.refreshMax=s}static{a(this,"LocalOAuthProvider")}skipLocalPkceValidation=!1;inflight=new Map;async authorize(t,n,r){throw new H.InvalidGrantError("Admin consent is required")}async challengeForAuthorizationCode(t,n){let r=this.findCode(t,n);return this.clientsStore.data.codes=this.clientsStore.data.codes.filter(o=>o!==r),this.clientsStore.persist(),this.inflight.set(r.hash,r),setTimeout(()=>this.inflight.delete(r.hash),3e4).unref?.(),r.challenge}async exchangeAuthorizationCode(t,n,r,o,s){let i=this.inflight.get(Qe(n))??this.findCode(t,n);if(this.inflight.delete(i.hash),this.clientsStore.data.codes=this.clientsStore.data.codes.filter(d=>d!==i),this.clientsStore.persist(),i.clientId!==t.client_id||i.expires<=Date.now()||o!==i.redirectUri||s&&Ze(s.href)!==i.resource)throw new H.InvalidGrantError("Invalid authorization code");return this.issue(t.client_id,i.scopes,i.resource,i.user)}async exchangeRefreshToken(t,n,r,o){let s=this.clientsStore.data.tokens.find(l=>l.hash===Qe(n)&&l.type==="refresh"&&l.clientId===t.client_id);if(!s||s.expires<=Date.now())throw new H.InvalidGrantError("Invalid refresh token");if(s.rotated)throw this.clientsStore.data.tokens=this.clientsStore.data.tokens.filter(l=>l.family!==s.family),this.clientsStore.persist(),new H.InvalidGrantError("Refresh token reuse detected");let i=this.refreshMax()*1e3,d=s.familyStart??Date.now();if(i>0&&d+i<=Date.now())throw new H.InvalidGrantError("Refresh token family reached its maximum lifetime");if(o&&Ze(o.href)!==s.resource)throw new H.InvalidGrantError("Resource mismatch");if(r?.some(l=>!s.scopes.includes(l)))throw new H.InvalidGrantError("Invalid scope");return s.rotated=!0,this.issue(t.client_id,r??s.scopes,s.resource,s.user,s.family,d)}async verifyAccessToken(t){let n=this.clientsStore.data.tokens.find(o=>o.hash===Qe(t)&&o.type==="access"&&o.expires>Date.now());if(!n||n.resource!==Ze(this.resource()))throw new H.InvalidTokenError("Invalid access token");let r=this.clientsStore.data.clients.find(o=>o.client_id===n.clientId);if(!r)throw new H.InvalidTokenError("Invalid client");return r.lastUsedAt=Date.now(),this.clientsStore.touch(),{token:t,clientId:n.clientId,scopes:n.scopes,expiresAt:Math.floor(n.expires/1e3),resource:new URL(n.resource),extra:{user:n.user}}}async revokeToken(t,n){let r=this.clientsStore.data.tokens.find(o=>o.hash===Qe(n.token)&&o.clientId===t.client_id);r&&(this.clientsStore.data.tokens=this.clientsStore.data.tokens.filter(o=>o.family!==r.family)),this.clientsStore.persist()}createCode(t,n,r){let o=bt();return this.clientsStore.data.codes.push({hash:Qe(o),clientId:t.client_id,challenge:n.codeChallenge,redirectUri:n.redirectUri,scopes:n.scopes??["read"],resource:this.validateResource(n.resource),expires:Date.now()+6e4,user:r}),this.clientsStore.persist(),o}validateResource(t){let n=Ze(this.resource());if(t&&Ze(t.href)!==n)throw new H.InvalidGrantError("Resource mismatch");return n}findCode(t,n){let r=this.clientsStore.data.codes.find(o=>o.hash===Qe(n)&&o.clientId===t.client_id);if(!r||r.expires<=Date.now())throw new H.InvalidGrantError("Invalid or expired authorization code");return r}issue(t,n,r,o,s=bt(),i=Date.now()){let d=bt(),l=bt(),c=Date.now(),u=this.refreshMax()*1e3,p=Math.min(c+this.refreshTtl()*1e3,u>0?i+u:1/0),f=this.clientsStore.data,m=f.clients.find(h=>h.client_id===t);return m&&(m.lastUsedAt=c),f.tokens.push({hash:Qe(d),type:"access",clientId:t,family:s,scopes:n,resource:r,expires:c+this.accessTtl()*1e3,user:o,rotated:!1},{hash:Qe(l),type:"refresh",clientId:t,family:s,scopes:n,resource:r,expires:p,user:o,rotated:!1,familyStart:i}),this.clientsStore.persist(),{access_token:d,token_type:"Bearer",expires_in:this.accessTtl(),refresh_token:l,scope:n.join(" ")}}};function Ze(e){let t=new URL(e);if(!["http:","https:"].includes(t.protocol)||t.username||t.password||t.search||t.hash)throw new Error("Invalid resource URL");return`${t.protocol}//${t.host}${t.pathname.replace(/\/+$/,"")}`}a(Ze,"canonicalResource");var tn=["read","propose"],xp=new Set(["localhost","127.0.0.1","[::1]"]);function Nn(e){return e.replace(/[&<>"']/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[t])}a(Nn,"escapeHtml");function wt(e){return typeof e=="string"?e:""}a(wt,"value");function Ta(e,t,n,r){let o=a(y=>`${n().replace(/\/$/,"")}${y}`,"abs"),s=a((y,w,k,T)=>{let _=w.url;w.url="/",y(w,k,C=>{w.url=_,T(C)})},"atRoot"),i=new Map,d=a(()=>{let y=Date.now();for(let[w,k]of i)k.expires<=y&&i.delete(w)},"sweep"),l=a(y=>{d();let w=[...i].filter(([,k])=>k.clientId===y);for(;w.length>=kp;)i.delete(w.shift()[0]);return i.size<Ap},"reservePending"),c=a((y,w)=>{y.setHeader("Cache-Control","no-store"),y.setHeader("X-Frame-Options","DENY"),y.setHeader("Referrer-Policy","no-referrer"),y.setHeader("X-Content-Type-Options","nosniff"),y.setHeader("Content-Security-Policy",`default-src 'none'; ${w?`script-src 'nonce-${w}'; `:""}style-src 'unsafe-inline'; connect-src 'self'; form-action 'none'; base-uri 'none'; frame-ancestors 'none'`)},"securityHeaders"),u=Kn(e.settings.httpAdminRoot),p=a(y=>{if(!V(e.settings))return[...tn];let w=Pt(y);return w==="*"?[...tn]:w==="read"?["read"]:[]},"grantableFor"),f=a(y=>{let w=y.get("sec-fetch-site");if(w&&w!=="same-origin")return!1;let k=y.get("origin");if(!k)return!0;let T=new Set;try{T.add(new URL(n()).origin)}catch{}let _=y.get("host");return _&&(T.add(`http://${_}`),T.add(`https://${_}`)),T.has(k)},"sameOrigin"),m=a((y,w)=>{d();let k=y.query,T=t.clientsStore.getClient(wt(k.client_id));if(!T){w.status(400).send("Unknown OAuth client");return}let _=wt(k.redirect_uri)||(T.redirect_uris.length===1?T.redirect_uris[0]:"");if(!T.redirect_uris.some(Kr=>(0,wa.redirectUriMatches)(_,Kr))){w.status(400).send("Unregistered redirect URI");return}if(k.response_type!=="code"||k.code_challenge_method!=="S256"||!/^[A-Za-z0-9_-]{43,128}$/.test(wt(k.code_challenge))){w.status(400).send("Authorization code with PKCE S256 is required");return}let C=[...new Set((wt(k.scope)||"read").split(" ").filter(Boolean))];if(C.some(Kr=>!tn.includes(Kr))){w.status(400).send("Unsupported scope");return}let Z=wt(k.resource)||r(),he;try{he=Ze(Z)}catch{w.status(400).send("Invalid resource");return}if(he!==Ze(r())){w.status(400).send("Resource mismatch");return}if(!l(T.client_id)){w.status(429).send("Too many pending authorization requests; try again later");return}let sn=bt();i.set(sn,{clientId:T.client_id,redirect:_,challenge:wt(k.code_challenge),scopes:C,resource:Z,state:wt(k.state),csrf:bt(),expires:Date.now()+10*6e4});let kt=new URL(_).host,At=(0,Fn.randomBytes)(16).toString("base64");c(w,At),w.status(200).type("html").send(`<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Authorize MCP client</title><style>body{font-family:system-ui,sans-serif;max-width:560px;margin:40px auto;padding:0 16px}h1{font-size:1.4em;word-break:break-all}.warn{color:#9b2c2c}.muted{color:#555}button{margin-right:8px;padding:6px 14px}</style><main data-request="${Nn(sn)}" data-token-key="${Nn(u)}"><p class="muted">An MCP client wants to access Node-RED and will receive tokens at:</p><h1>${Nn(kt)}</h1>${xp.has(new URL(_).hostname)?'<p class="warn">Warning: this redirect URI is reachable only on this machine.</p>':""}<p class="muted">Client-provided name (unverified): ${Nn(T.client_name||"Unnamed client")}</p><p>Requested permissions: ${Nn(C.join(", "))}</p><p id="status">Checking your Node-RED editor session\u2026</p><p id="grant" hidden></p><p><button id="allow" type="button" hidden>Allow</button><button id="deny" type="button" hidden>Deny</button><button id="retry" type="button" hidden>Retry</button></p></main><script nonce="${At}">${ga}</script></html>`)},"authorizePage"),h=a((y,w,k)=>{c(w),Sa.default.json({limit:"4kb"})(y,w,()=>we(e,"api")(y,w,()=>{try{d();let T=/^\/mcp\/oauth\/consent\/([A-Za-z0-9_-]+)\/?$/.exec(y.path)?.[1]??"",_=i.get(T),C=_&&t.clientsStore.getClient(_.clientId);if(!_||!C){w.status(404).json({error:"Consent request expired or unknown"});return}let Z=V(e.settings)&&y.user?.username||"owner",he=_.scopes.filter(At=>p(y).includes(At));if(!he.length){w.status(403).json({error:"Your Node-RED permissions do not allow granting the requested scopes",grantable:he});return}if(y.method==="GET"){_.approver=Z,w.json({client_name:C.client_name??null,redirect_host:new URL(_.redirect).host,requested:_.scopes,grantable:he,user:Z,csrf:_.csrf});return}if(y.method!=="POST"){w.status(405).end();return}let sn=y.body??{};if(!f(y)||sn.csrf!==_.csrf||_.approver!==Z){w.status(403).json({error:"Consent request verification failed"});return}i.delete(T);let kt=new URL(_.redirect);if(sn.decision!=="allow")kt.searchParams.set("error","access_denied");else{try{t.clientsStore.approve(C.client_id)}catch{w.status(409).json({error:"Maximum active OAuth clients reached"});return}let At=t.createCode(C,{redirectUri:_.redirect,codeChallenge:_.challenge,scopes:he,resource:new URL(_.resource)},Z);kt.searchParams.set("code",At)}_.state&&kt.searchParams.set("state",_.state),w.json({redirect:kt.href})}catch(T){k(T)}}))},"consentApi"),g=(0,ba.clientRegistrationHandler)({clientsStore:t.clientsStore,rateLimit:{windowMs:36e5,max:10,standardHeaders:!0,legacyHeaders:!1}}),v=(0,ka.tokenHandler)({provider:t}),A=(0,va.revocationHandler)({provider:t}),E,D="";return{server:a((y,w,k)=>{let T=y.path,_=T.replace(/\/+$/,"").toLowerCase();if(_==="/mcp/oauth/authorize"){y.method==="GET"?m(y,w,k):w.status(405).set("Allow","GET").end();return}if(_.startsWith("/mcp/oauth/consent/")){h(y,w,k);return}if(!["/mcp/oauth/token","/mcp/oauth/register","/mcp/oauth/revoke"].includes(_)){k();return}let C=n();if(C.startsWith("https://")||/^http:\/\/(localhost|127\.0\.0\.1)(:|\/|$)/.test(C)){if(D!==C)try{E=(0,xa.mcpAuthRouter)({provider:t,issuerUrl:new URL(C),baseUrl:new URL(`${C.replace(/\/$/,"")}/mcp/oauth/`),resourceServerUrl:new URL(r()),scopesSupported:tn,clientRegistrationOptions:{rateLimit:{windowMs:36e5,max:10}}}),D=C}catch{E=void 0}if(E){let Z=y.url;y.url=y.url.slice(10)||"/",E(y,w,he=>{y.url=Z,k(he)});return}}if(T==="/mcp/oauth/token"){s(v,y,w,k);return}if(T==="/mcp/oauth/register"){s(g,y,w,k);return}if(T==="/mcp/oauth/revoke"){s(A,y,w,k);return}k()},"server"),metadata:a((y,w,k)=>{let T=y.path;if(T==="/.well-known/oauth-authorization-server"){s((0,es.metadataHandler)({issuer:n(),authorization_endpoint:o("/mcp/oauth/authorize"),token_endpoint:o("/mcp/oauth/token"),registration_endpoint:o("/mcp/oauth/register"),revocation_endpoint:o("/mcp/oauth/revoke"),response_types_supported:["code"],code_challenge_methods_supported:["S256"],grant_types_supported:["authorization_code","refresh_token"],token_endpoint_auth_methods_supported:["none"],scopes_supported:tn}),y,w,k);return}if(T==="/.well-known/oauth-protected-resource/mcp"||T==="/.well-known/oauth-protected-resource"){s((0,es.metadataHandler)({resource:Ze(r()),authorization_servers:[n()],scopes_supported:tn}),y,w,k);return}k()},"metadata"),auth:a((y,w,k)=>{try{let T=w.set.bind(w);w.set=((_,C)=>(_==="WWW-Authenticate"&&typeof C=="string"&&(C=C.replace('scope="read"','scope="read propose"')),typeof _=="string"?T(_,C??""):T(_))),(0,Aa.requireBearerAuth)({verifier:t,requiredScopes:["read"],resourceMetadataUrl:o(e.settings.httpNodeRoot===!1||!e.httpNode?"/mcp/.well-known/oauth-protected-resource/mcp":"/.well-known/oauth-protected-resource/mcp"),expectedResource:new URL(r())})(y,w,_=>{w.set=T,k(_)})}catch(T){k(T)}},"auth")}}a(Ta,"makeOAuthHandlers");var On=require("fs"),on=require("path"),_a=require("url"),Pa=require("yaml"),Pp={};function Sp(){return typeof __dirname<"u"?__dirname:(0,on.dirname)((0,_a.fileURLToPath)(Pp.url))}a(Sp,"moduleDir");function Ra(){let e=Sp(),t=[(0,on.join)(e,"..","docs","api","openapi.yaml"),(0,on.join)(e,"..","..","docs","api","openapi.yaml"),(0,on.join)(process.cwd(),"docs","api","openapi.yaml")];for(let n of t)if((0,On.existsSync)(n))return n;return null}a(Ra,"resolveOpenApiYamlPath");function Tp(){let e=Ra();if(!e)throw new Error("OpenAPI specification file was not found");return(0,On.readFileSync)(e,"utf8")}a(Tp,"loadOpenApiYaml");function La(){return(e,t)=>{try{let n=Tp();t.status(200).type("application/yaml").send(n)}catch(n){let r=n instanceof Error?n.message:String(n);t.status(500).json({error:r})}}}a(La,"openApiYamlHandler");var ts;function _p(){let e=Ra();if(!e)throw new Error("OpenAPI specification file was not found");return ts?.path!==e&&(ts={path:e,json:JSON.stringify((0,Pa.parse)((0,On.readFileSync)(e,"utf8")))}),ts.json}a(_p,"loadOpenApiJson");function Ca(){return(e,t)=>{try{t.status(200).type("application/json").send(_p())}catch(n){let r=n instanceof Error?n.message:String(n);t.status(500).json({error:r})}}}a(Ca,"openApiJsonHandler");function Lp(){return`
1106
+ })();`;ye();var nt=a(e=>(0,$n.createHash)("sha256").update(e).digest("hex"),"hash"),St=a(()=>(0,$n.randomBytes)(32).toString("base64url"),"secret"),Fp=36e5,Op=25,$p=5,Mp=500,Sa=a(()=>({clients:[],codes:[],tokens:[]}),"emptyDb"),Br=class{constructor(t,n){this.maxClients=n;this.path=(0,ts.join)(t.root,"oauth.json");try{let r=(0,sn.existsSync)(this.path)?JSON.parse((0,sn.readFileSync)(this.path,"utf8")):Sa();if(!r||typeof r!="object"||!("clients"in r)||!("codes"in r)||!("tokens"in r)||!Array.isArray(r.clients)||!Array.isArray(r.codes)||!Array.isArray(r.tokens))throw new Error("Invalid OAuth store");this.db=r}catch{this.db=Sa()}(0,sn.mkdirSync)(t.root,{recursive:!0,mode:448}),this.prune()}static{a(this,"OAuthStore")}db;path;save(){Rt((0,ts.join)(this.path,".."),this.path,Be(this.db,8e6,{kind:"oauth",pretty:!0}))}prune(t=Date.now()){let n=this.db.clients.length+this.db.codes.length+this.db.tokens.length,r=new Set(this.db.clients.filter(o=>o.approved?(o.lastUsedAt??(o.client_id_issued_at??0)*1e3)>t-30*864e5:(o.client_id_issued_at??0)*1e3>t-Fp).map(o=>o.client_id));this.db.clients=this.db.clients.filter(o=>r.has(o.client_id)),this.db.codes=this.db.codes.filter(o=>o.expires>t&&r.has(o.clientId)),this.db.tokens=this.db.tokens.filter(o=>r.has(o.clientId)&&o.expires>t),this.db.clients.length+this.db.codes.length+this.db.tokens.length!==n&&this.save()}getClient(t){return this.prune(),this.db.clients.find(n=>n.client_id===t)}registerClient(t){if(this.prune(),this.approvedCount()>=this.maxClients())throw new B.InvalidClientMetadataError("Maximum active OAuth clients reached");if(this.db.clients.length-this.approvedCount()>=Op)throw new B.InvalidClientMetadataError("Too many OAuth clients awaiting consent");if(!Array.isArray(t.redirect_uris)||!t.redirect_uris.length||t.redirect_uris.some(r=>{try{let o=new URL(r);return!!o.username||!!o.password||!!o.hash||o.protocol!=="https:"&&!(o.protocol==="http:"&&["localhost","127.0.0.1","[::1]"].includes(o.hostname))}catch{return!0}}))throw new B.InvalidClientMetadataError("Redirect URIs must use HTTPS or loopback HTTP");if(t.token_endpoint_auth_method&&t.token_endpoint_auth_method!=="none")throw new B.InvalidClientMetadataError("Only public clients are supported");let n={...t,client_secret:void 0,client_secret_expires_at:void 0,client_id:St(),client_id_issued_at:Math.floor(Date.now()/1e3),token_endpoint_auth_method:"none",lastUsedAt:Date.now()};return this.db.clients.push(n),this.save(),n}approvedCount(){return this.db.clients.filter(t=>t.approved).length}approve(t){let n=this.db.clients.find(r=>r.client_id===t);if(!(!n||n.approved)){if(this.approvedCount()>=this.maxClients())throw new B.InvalidClientMetadataError("Maximum active OAuth clients reached");n.approved=!0,this.save()}}get data(){return this.db}persist(){this.touchTimer&&(clearTimeout(this.touchTimer),this.touchTimer=void 0),this.save()}touchTimer;touch(){this.touchTimer||(this.touchTimer=setTimeout(()=>{this.touchTimer=void 0,this.save()},5e3),this.touchTimer.unref?.())}revokeClient(t){let n=this.db.clients.length;return this.db.clients=this.db.clients.filter(r=>r.client_id!==t),this.db.tokens=this.db.tokens.filter(r=>r.clientId!==t),this.db.codes=this.db.codes.filter(r=>r.clientId!==t),this.save(),this.db.clients.length!==n}};function os(e){switch(e.source){case"http-in":return`Route ${e.path} is already used by http-in node '${e.nodeName??e.nodeId??"unnamed"}'`;case"httpStatic":return`File ${e.path} is served by httpStatic`;case"middleware":return`Path prefix is already handled by mounted middleware (${e.layer??"unknown"})`;default:return`Route ${e.path} is already registered by another handler (${e.layer??"route"})`}}a(os,"describeOAuthRouteCollision");var ss=a((e,t)=>e.toLowerCase().startsWith(t.toLowerCase()),"startsWithCi");function Zo(e){return e===!1?void 0:`/${typeof e=="string"?e:"/"}`.replace(/\/+/g,"/").replace(/\/$/,"")}a(Zo,"normalizeRoot");function es(e,t){if(e!==void 0){if(!e)return t;if(ss(t,`${e}/`))return t.slice(e.length)}}a(es,"relativePrefix");function Dp(e){let t=new Map;for(let r of e)r.id&&t.set(r.id,r);let n=a((r,o=new Set)=>{if(!r)return!0;let s=t.get(r);return s?s.type==="tab"?!s.disabled&&!s.d:s.type!=="subflow"||o.has(r)?!1:(o.add(r),e.some(i=>i.type===`subflow:${r}`&&!i.d&&n(i.z,o))):!0},"live");return n}a(Dp,"makeLiveCheck");function jp(e,t){if(!e||!t.length)return[];let n=[];try{e(s=>n.push(s))}catch{return[]}let r=Dp(n),o=[];for(let s of n){if(s.type!=="http in"||typeof s.url!="string"||s.d)continue;let i=s.url.startsWith("/")?s.url:`/${s.url}`;!t.some(d=>ss(i,d))||!r(s.z)||o.push({path:i,source:"http-in",nodeId:s.id,nodeName:s.name?s.name:void 0,flowId:s.z})}return o}a(jp,"findHttpInCollisions");var Hp=new Set(["bound dispatch","handle"]),Up=["authorize","token","register","revoke"],Bp=["authorization-server","protected-resource"];function qp(e){let t=e.length;for(;t>0&&e.charCodeAt(t-1)===47;)t--;return e.slice(0,t)}a(qp,"trimTrailingSlashes");function Wp(e){let t=new Set;return e.filter(n=>{let r=`${n.source}\0${n.path.toLowerCase()}`;return t.has(r)?!1:(t.add(r),!0)})}a(Wp,"dedupe");function Ia(e){let t=Zo(e.httpAdminRoot),n=e.httpNode?Zo(e.httpNodeRoot):void 0,r=`${t??""}/mcp/oauth/`,o=e.metadataFallback?`${t??""}/mcp/.well-known/oauth-`:`${n??""}/.well-known/oauth-`,s=a(p=>{let f=es(t,p),m=es(n,p),h=m?jp(e.eachNode,[m]):[],g=new Set(h.map(E=>E.path.toLowerCase())),x=m?rs(e.httpNode,[m]).filter(E=>!(E.source==="route"&&Hp.has(E.layer??"")&&g.has(E.path.toLowerCase()))):[],S=f?rs(e.httpAdmin,[f]):[];return Wp([...S,...x,...h])},"scan"),i=s(r),d=s(o),l=e.fileExists??(()=>!1),c=(e.staticDirs??[]).map(p=>typeof p=="string"?{path:p,root:e.httpStaticRoot}:p),u=a((p,f)=>{for(let m of c){let h=Zo(m.root),g=qp(m.path);for(let x of p){let S=es(h,x);!S||!l(`${g}${S}`)||f.push({path:x,source:"httpStatic",file:`${g}${S}`})}}},"checkStatic");return u(Up.map(p=>`${r}${p}`),i),u(Bp.map(p=>`${o}${p}`),d),{authorization:i,metadata:d}}a(Ia,"scanOAuthRouteCollisions");function zp(e){let t=a(n=>{try{return e?.[n]}catch{return}},"read");return t("_router")?.stack??t("router")?.stack??[]}a(zp,"routerStack");function Gp(e){let t=e.route?.path;return t?(Array.isArray(t)?t:[t]).map(String):[]}a(Gp,"layerPaths");function Jp(e,t){if(e.name==="router")return!1;let n=e.regexp,r=e.matchers??(n?[s=>n.test(s)]:[]);if(!r.length)return!1;let o=a(s=>r.some(i=>!!i(s)),"matches");return o("/unrelated-ai-access-collision-probe")?!1:t.some(s=>o(`${s}token`)||o(`${s}mcp`))}a(Jp,"middlewareHits");function rs(e,t=["/.well-known/oauth-","/mcp/oauth/"]){let n=[];for(let r of zp(e)){if(r.handle?.oauthOwned)continue;let o=Gp(r);for(let s of o)t.some(i=>ss(s,i))&&n.push({path:s,source:"route",layer:r.name??"route"});!o.length&&Jp(r,t)&&n.push({path:t.join(", "),source:"middleware",layer:r.name??"route"}),r.handle?.stack&&n.push(...rs({router:{stack:r.handle.stack}},t))}return n}a(rs,"detectOAuthRouteCollisionDetails");var qr=class{constructor(t,n,r,o,s=()=>0){this.clientsStore=t;this.resource=n;this.accessTtl=r;this.refreshTtl=o;this.refreshMax=s}static{a(this,"LocalOAuthProvider")}skipLocalPkceValidation=!1;inflight=new Map;async authorize(t,n,r){throw new B.InvalidGrantError("Admin consent is required")}async challengeForAuthorizationCode(t,n){let r=this.findCode(t,n);return this.clientsStore.data.codes=this.clientsStore.data.codes.filter(o=>o!==r),this.clientsStore.persist(),this.inflight.set(r.hash,r),setTimeout(()=>this.inflight.delete(r.hash),3e4).unref?.(),r.challenge}async exchangeAuthorizationCode(t,n,r,o,s){let i=this.inflight.get(nt(n))??this.findCode(t,n);if(this.inflight.delete(i.hash),this.clientsStore.data.codes=this.clientsStore.data.codes.filter(d=>d!==i),this.clientsStore.persist(),i.clientId!==t.client_id||i.expires<=Date.now()||o!==i.redirectUri||s&&rt(s.href)!==i.resource)throw new B.InvalidGrantError("Invalid authorization code");return this.issue(t.client_id,i.scopes,i.resource,i.user)}async exchangeRefreshToken(t,n,r,o){let s=this.clientsStore.data.tokens.find(l=>l.hash===nt(n)&&l.type==="refresh"&&l.clientId===t.client_id);if(!s||s.expires<=Date.now())throw new B.InvalidGrantError("Invalid refresh token");if(s.rotated)throw this.clientsStore.data.tokens=this.clientsStore.data.tokens.filter(l=>l.family!==s.family),this.clientsStore.persist(),new B.InvalidGrantError("Refresh token reuse detected");let i=this.refreshMax()*1e3,d=s.familyStart??Date.now();if(i>0&&d+i<=Date.now())throw new B.InvalidGrantError("Refresh token family reached its maximum lifetime");if(o&&rt(o.href)!==s.resource)throw new B.InvalidGrantError("Resource mismatch");if(r?.some(l=>!s.scopes.includes(l)))throw new B.InvalidGrantError("Invalid scope");return s.rotated=!0,this.issue(t.client_id,r??s.scopes,s.resource,s.user,s.family,d)}async verifyAccessToken(t){let n=this.clientsStore.data.tokens.find(o=>o.hash===nt(t)&&o.type==="access"&&o.expires>Date.now());if(!n||n.resource!==rt(this.resource()))throw new B.InvalidTokenError("Invalid access token");let r=this.clientsStore.data.clients.find(o=>o.client_id===n.clientId);if(!r)throw new B.InvalidTokenError("Invalid client");return r.lastUsedAt=Date.now(),this.clientsStore.touch(),{token:t,clientId:n.clientId,scopes:n.scopes,expiresAt:Math.floor(n.expires/1e3),resource:new URL(n.resource),extra:{user:n.user}}}async revokeToken(t,n){let r=this.clientsStore.data.tokens.find(o=>o.hash===nt(n.token)&&o.clientId===t.client_id);r&&(this.clientsStore.data.tokens=this.clientsStore.data.tokens.filter(o=>o.family!==r.family)),this.clientsStore.persist()}createCode(t,n,r){let o=St();return this.clientsStore.data.codes.push({hash:nt(o),clientId:t.client_id,challenge:n.codeChallenge,redirectUri:n.redirectUri,scopes:n.scopes??["read"],resource:this.validateResource(n.resource),expires:Date.now()+6e4,user:r}),this.clientsStore.persist(),o}validateResource(t){let n=rt(this.resource());if(t&&rt(t.href)!==n)throw new B.InvalidGrantError("Resource mismatch");return n}findCode(t,n){let r=this.clientsStore.data.codes.find(o=>o.hash===nt(n)&&o.clientId===t.client_id);if(!r||r.expires<=Date.now())throw new B.InvalidGrantError("Invalid or expired authorization code");return r}issue(t,n,r,o,s=St(),i=Date.now()){let d=St(),l=St(),c=Date.now(),u=this.refreshMax()*1e3,p=Math.min(c+this.refreshTtl()*1e3,u>0?i+u:1/0),f=this.clientsStore.data,m=f.clients.find(h=>h.client_id===t);return m&&(m.lastUsedAt=c),f.tokens.push({hash:nt(d),type:"access",clientId:t,family:s,scopes:n,resource:r,expires:c+this.accessTtl()*1e3,user:o,rotated:!1},{hash:nt(l),type:"refresh",clientId:t,family:s,scopes:n,resource:r,expires:p,user:o,rotated:!1,familyStart:i}),this.clientsStore.persist(),{access_token:d,token_type:"Bearer",expires_in:this.accessTtl(),refresh_token:l,scope:n.join(" ")}}};function rt(e){let t=new URL(e);if(!["http:","https:"].includes(t.protocol)||t.username||t.password||t.search||t.hash)throw new Error("Invalid resource URL");return`${t.protocol}//${t.host}${t.pathname.replace(/\/+$/,"")}`}a(rt,"canonicalResource");var on=["read","propose"];function Kp(e){let t=(e??"/").split("/").filter(Boolean).join("/");return t?`/${t}/`:"/"}a(Kp,"consentAdminRoot");var Vp=new Set(["localhost","127.0.0.1","[::1]"]);function xt(e){return typeof e=="string"?e:""}a(xt,"value");function Na(e,t,n,r){let o=a(A=>`${n().replace(/\/$/,"")}${A}`,"abs"),s=a((A,v,w,b)=>{let k=v.url;v.url="/",A(v,w,L=>{v.url=k,b(L)})},"atRoot"),i=new Map,d=a(()=>{let A=Date.now();for(let[v,w]of i)w.expires<=A&&i.delete(v)},"sweep"),l=a(A=>{d();let v=[...i].filter(([,w])=>w.clientId===A);for(;v.length>=$p;)i.delete(v.shift()[0]);return i.size<Mp},"reservePending"),c=a((A,v)=>{A.setHeader("Cache-Control","no-store"),A.setHeader("X-Frame-Options","DENY"),A.setHeader("Referrer-Policy","no-referrer"),A.setHeader("X-Content-Type-Options","nosniff"),A.setHeader("Content-Security-Policy",`default-src 'none'; ${v?`script-src 'nonce-${v}'; `:""}style-src 'unsafe-inline'; connect-src 'self'; form-action 'none'; base-uri 'none'; frame-ancestors 'none'`)},"securityHeaders"),u=Yn(e.settings.httpAdminRoot),p=a(A=>{if(!W(e.settings))return[...on];let v=We(A);return v==="*"?[...on]:v==="read"?["read"]:[]},"grantableFor"),f=a(A=>{let v=A.get("sec-fetch-site");if(v&&v!=="same-origin")return!1;let w=A.get("origin");if(!w)return!0;let b=new Set;try{b.add(new URL(n()).origin)}catch{}let k=A.get("host");return k&&(b.add(`http://${k}`),b.add(`https://${k}`)),b.has(w)},"sameOrigin"),m=a((A,v)=>{d();let w=A.query,b=t.clientsStore.getClient(xt(w.client_id));if(!b){v.status(400).send("Unknown OAuth client");return}let k=xt(w.redirect_uri)||(b.redirect_uris.length===1?b.redirect_uris[0]:"");if(!b.redirect_uris.some(Xr=>(0,Ta.redirectUriMatches)(k,Xr))){v.status(400).send("Unregistered redirect URI");return}if(w.response_type!=="code"||w.code_challenge_method!=="S256"||!/^[A-Za-z0-9_-]{43,128}$/.test(xt(w.code_challenge))){v.status(400).send("Authorization code with PKCE S256 is required");return}let L=[...new Set((xt(w.scope)||"read").split(" ").filter(Boolean))];if(L.some(Xr=>!on.includes(Xr))){v.status(400).send("Unsupported scope");return}let H=xt(w.resource)||r(),M;try{M=rt(H)}catch{v.status(400).send("Invalid resource");return}if(M!==rt(r())){v.status(400).send("Resource mismatch");return}if(!l(b.client_id)){v.status(429).send("Too many pending authorization requests; try again later");return}let Re=St();i.set(Re,{clientId:b.client_id,redirect:k,challenge:xt(w.code_challenge),scopes:L,resource:H,state:xt(w.state),csrf:St(),expires:Date.now()+10*6e4});let Ue=new URL(k).host,Pt=(0,$n.randomBytes)(16).toString("base64");c(v,Pt),v.status(200).type("html").send(Aa({requestId:Re,tokenKey:u,adminRoot:Kp(e.settings.httpAdminRoot),nonce:Pt,redirectHost:Ue,loopbackRedirect:Vp.has(new URL(k).hostname),clientName:b.client_name||"Unnamed client",scopes:L,script:xa}))},"authorizePage"),h=a((A,v,w)=>{c(v),Ea.default.json({limit:"4kb"})(A,v,()=>we(e,"api")(A,v,()=>{try{d();let b=/^\/mcp\/oauth\/consent\/([A-Za-z0-9_-]+)\/?$/.exec(A.path)?.[1]??"",k=i.get(b),L=k&&t.clientsStore.getClient(k.clientId);if(!k||!L){v.status(404).json({error:"Consent request expired or unknown"});return}let H=W(e.settings)&&A.user?.username||"owner",M=k.scopes.filter(Pt=>p(A).includes(Pt));if(!M.length){v.status(403).json({error:"Your Node-RED permissions do not allow granting the requested scopes",grantable:M});return}if(A.method==="GET"){k.approver=H,v.json({client_name:L.client_name??null,redirect_host:new URL(k.redirect).host,requested:k.scopes,grantable:M,user:H,csrf:k.csrf});return}if(A.method!=="POST"){v.status(405).end();return}let Re=A.body??{};if(!f(A)||Re.csrf!==k.csrf||k.approver!==H){v.status(403).json({error:"Consent request verification failed"});return}i.delete(b);let Ue=new URL(k.redirect);if(Re.decision!=="allow")Ue.searchParams.set("error","access_denied");else{try{t.clientsStore.approve(L.client_id)}catch{v.status(409).json({error:"Maximum active OAuth clients reached"});return}let Pt=t.createCode(L,{redirectUri:k.redirect,codeChallenge:k.challenge,scopes:M,resource:new URL(k.resource)},H);Ue.searchParams.set("code",Pt)}k.state&&Ue.searchParams.set("state",k.state),v.json({redirect:Ue.href})}catch(b){w(b)}}))},"consentApi"),g=(0,Pa.clientRegistrationHandler)({clientsStore:t.clientsStore,rateLimit:{windowMs:36e5,max:10,standardHeaders:!0,legacyHeaders:!1}}),x=(0,_a.tokenHandler)({provider:t}),S=(0,Ra.revocationHandler)({provider:t}),E,I="";return{server:a((A,v,w)=>{let b=A.path,k=b.replace(/\/+$/,"").toLowerCase();if(k==="/mcp/oauth/authorize"){A.method==="GET"?m(A,v,w):v.status(405).set("Allow","GET").end();return}if(k.startsWith("/mcp/oauth/consent/")){h(A,v,w);return}if(!["/mcp/oauth/token","/mcp/oauth/register","/mcp/oauth/revoke"].includes(k)){w();return}let L=n();if(L.startsWith("https://")||/^http:\/\/(localhost|127\.0\.0\.1)(:|\/|$)/.test(L)){if(I!==L)try{E=(0,Ca.mcpAuthRouter)({provider:t,issuerUrl:new URL(L),baseUrl:new URL(`${L.replace(/\/$/,"")}/mcp/oauth/`),resourceServerUrl:new URL(r()),scopesSupported:on,clientRegistrationOptions:{rateLimit:{windowMs:36e5,max:10}}}),I=L}catch{E=void 0}if(E){let H=A.url;A.url=A.url.slice(10)||"/",E(A,v,M=>{A.url=H,w(M)});return}}if(b==="/mcp/oauth/token"){s(x,A,v,w);return}if(b==="/mcp/oauth/register"){s(g,A,v,w);return}if(b==="/mcp/oauth/revoke"){s(S,A,v,w);return}w()},"server"),metadata:a((A,v,w)=>{let b=A.path;if(b==="/.well-known/oauth-authorization-server"){s((0,ns.metadataHandler)({issuer:n(),authorization_endpoint:o("/mcp/oauth/authorize"),token_endpoint:o("/mcp/oauth/token"),registration_endpoint:o("/mcp/oauth/register"),revocation_endpoint:o("/mcp/oauth/revoke"),response_types_supported:["code"],code_challenge_methods_supported:["S256"],grant_types_supported:["authorization_code","refresh_token"],token_endpoint_auth_methods_supported:["none"],scopes_supported:on}),A,v,w);return}if(b==="/.well-known/oauth-protected-resource/mcp"||b==="/.well-known/oauth-protected-resource"){s((0,ns.metadataHandler)({resource:rt(r()),authorization_servers:[n()],scopes_supported:on}),A,v,w);return}w()},"metadata"),auth:a((A,v,w)=>{try{let b=v.set.bind(v);v.set=((k,L)=>(k==="WWW-Authenticate"&&typeof L=="string"&&(L=L.replace('scope="read"','scope="read propose"')),typeof k=="string"?b(k,L??""):b(k))),(0,La.requireBearerAuth)({verifier:t,requiredScopes:["read"],resourceMetadataUrl:o(e.settings.httpNodeRoot===!1||!e.httpNode?"/mcp/.well-known/oauth-protected-resource/mcp":"/.well-known/oauth-protected-resource/mcp"),expectedResource:new URL(r())})(A,v,k=>{v.set=b,w(k)})}catch(b){w(b)}},"auth")}}a(Na,"makeOAuthHandlers");var Mn=require("fs"),an=require("path"),Fa=require("url"),Oa=require("yaml"),Zp={};function Xp(){return typeof __dirname<"u"?__dirname:(0,an.dirname)((0,Fa.fileURLToPath)(Zp.url))}a(Xp,"moduleDir");function $a(){let e=Xp(),t=[(0,an.join)(e,"..","docs","api","openapi.yaml"),(0,an.join)(e,"..","..","docs","api","openapi.yaml"),(0,an.join)(process.cwd(),"docs","api","openapi.yaml")];for(let n of t)if((0,Mn.existsSync)(n))return n;return null}a($a,"resolveOpenApiYamlPath");function Yp(){let e=$a();if(!e)throw new Error("OpenAPI specification file was not found");return(0,Mn.readFileSync)(e,"utf8")}a(Yp,"loadOpenApiYaml");function Ma(){return(e,t)=>{try{let n=Yp();t.status(200).type("application/yaml").send(n)}catch(n){let r=n instanceof Error?n.message:String(n);t.status(500).json({error:r})}}}a(Ma,"openApiYamlHandler");var is;function Qp(){let e=$a();if(!e)throw new Error("OpenAPI specification file was not found");return is?.path!==e&&(is={path:e,json:JSON.stringify((0,Oa.parse)((0,Mn.readFileSync)(e,"utf8")))}),is.json}a(Qp,"loadOpenApiJson");function Da(){return(e,t)=>{try{t.status(200).type("application/json").send(Qp())}catch(n){let r=n instanceof Error?n.message:String(n);t.status(500).json({error:r})}}}a(Da,"openApiJsonHandler");function ef(){return`
1107
+ var settingsData = null;
1108
+ var settingsSaving = false;
1109
+ var settingsTimer = null;
1110
+ var SETTINGS_FIELDS = {
1111
+ enabled: 'enabled', allowWithoutAdminAuth: 'allow-without',
1112
+ defaultLinkTtlSeconds: 'ttl', defaultDeploymentType: 'deploy',
1113
+ maxActiveLinks: 'max-links', publicBaseUrl: 'public-url',
1114
+ corsAllowedOrigins: 'cors', rateLimitWindowMs: 'rate-window',
1115
+ rateLimitMaxPerIp: 'rate-ip', rateLimitMaxPerToken: 'rate-token'
1116
+ };
1117
+ var OAUTH_ACCESS_UNITS = [['minutes', 60], ['hours', 3600]];
1118
+ var OAUTH_REFRESH_UNITS = [['hours', 3600], ['days', 86400]];
1119
+
1120
+ function settingsCanEdit() { return !!(settingsData && settingsData.canEdit); }
1121
+ function fieldLocked(key) {
1122
+ return !!(settingsData && settingsData.sources && settingsData.sources[key] === 'settings.js');
1123
+ }
1124
+
1125
+ function setDurationInputs(id, seconds, units) {
1126
+ var unit = units[0];
1127
+ for (var i = units.length - 1; i >= 0; i--) {
1128
+ if (seconds % units[i][1] === 0) { unit = units[i]; break; }
1129
+ }
1130
+ $(id).value = String(Math.max(1, Math.round(seconds / unit[1])));
1131
+ $(id + '-unit').value = unit[0];
1132
+ }
1133
+ function readDurationInputs(id, units) {
1134
+ var amount = Number($(id).value);
1135
+ var name = $(id + '-unit') ? $(id + '-unit').value : units[0][0];
1136
+ var mult = units[0][1];
1137
+ units.forEach(function (u) { if (u[0] === name) mult = u[1]; });
1138
+ return Math.round(amount * mult);
1139
+ }
1140
+
1141
+ function trustProxyText(tp) {
1142
+ return tp === false || tp === undefined || tp === null || tp === 0
1143
+ ? 'Trust proxy: off (set httpServerOptions["trust proxy"] in settings.js if behind a reverse proxy). Rate limits key on the socket address.'
1144
+ : 'Trust proxy: ' + (tp === true ? 'on' : String(tp)) + '. Rate limits key on the forwarded client address.';
1145
+ }
1146
+
1147
+ function mcpServerUrl() {
1148
+ var s = (settingsData && settingsData.settings) || {};
1149
+ var base = s.publicBaseUrl ? String(s.publicBaseUrl).replace(/\\/$/, '') : location.origin;
1150
+ return base + mcpRpc.replace(/\\/rpc$/, '');
1151
+ }
1152
+
1153
+ function applySettings(data) {
1154
+ settingsData = data;
1155
+ var s = data.settings || {};
1156
+ var ro = !data.canEdit;
1157
+ var adminAuth = !!data.adminAuthEnabled;
1158
+ $('settings-readonly').hidden = !ro;
1159
+ $('row-enabled').hidden = !adminAuth;
1160
+ $('row-allow-without').hidden = adminAuth;
1161
+ $('set-enabled').checked = s.enabled !== false;
1162
+ $('set-allow-without').checked = !!s.allowWithoutAdminAuth;
1163
+ $('set-deploy').value = s.defaultDeploymentType || 'nodes';
1164
+ $('set-deploy-full-warn').hidden = $('set-deploy').value !== 'full';
1165
+ $('set-ttl').value = String(Math.max(1, Math.round((s.defaultLinkTtlSeconds || 14400) / 3600)));
1166
+ $('set-max-links').value = s.maxActiveLinks == null ? '' : String(s.maxActiveLinks);
1167
+ $('set-public-url').value = s.publicBaseUrl || '';
1168
+ $('set-cors').value = (s.corsAllowedOrigins || []).join(', ');
1169
+ $('set-rate-window').value = s.rateLimitWindowMs == null ? '' : String(s.rateLimitWindowMs);
1170
+ $('set-rate-ip').value = s.rateLimitMaxPerIp == null ? '' : String(s.rateLimitMaxPerIp);
1171
+ $('set-rate-token').value = s.rateLimitMaxPerToken == null ? '' : String(s.rateLimitMaxPerToken);
1172
+ var tpText = trustProxyText(data.trustProxy);
1173
+ $('set-trust-proxy').textContent = tpText;
1174
+ $('set-trust-proxy-inline').textContent = tpText;
1175
+ Object.keys(SETTINGS_FIELDS).forEach(function (key) {
1176
+ var id = SETTINGS_FIELDS[key];
1177
+ var locked = fieldLocked(key);
1178
+ var input = $('set-' + id);
1179
+ if (input) input.disabled = ro || locked;
1180
+ var badge = $('lock-' + id);
1181
+ if (badge) badge.hidden = !locked;
1182
+ });
1183
+ $('set-check-public-url').disabled = ro;
1184
+
1185
+ var o = s.oauth || {};
1186
+ var oauthLocked = fieldLocked('oauth');
1187
+ $('oauth-enabled').checked = !!o.enabled;
1188
+ setDurationInputs('oauth-access-ttl', o.accessTokenTtlSec || 3600, OAUTH_ACCESS_UNITS);
1189
+ setDurationInputs('oauth-refresh-ttl', o.refreshIdleTtlSec || 2592000, OAUTH_REFRESH_UNITS);
1190
+ $('oauth-refresh-max').value = String(Math.round((typeof o.refreshMaxLifetimeSec === 'number' ? o.refreshMaxLifetimeSec : 7776000) / 86400));
1191
+ $('oauth-dcr-cap').value = String(o.dcrMaxClients || 5);
1192
+ ['oauth-enabled', 'oauth-access-ttl', 'oauth-access-ttl-unit', 'oauth-refresh-ttl', 'oauth-refresh-ttl-unit',
1193
+ 'oauth-refresh-max', 'oauth-dcr-cap'].forEach(function (id) { $(id).disabled = ro || oauthLocked; });
1194
+ $('lock-oauth').hidden = !oauthLocked;
1195
+ $('oauth-server-url').textContent = mcpServerUrl();
1196
+ }
1197
+
1198
+ function oauthEnableWarning(value) {
1199
+ if (!value) return 'No Public URL is configured. OAuth clients may not discover this server.';
1200
+ try {
1201
+ var url = new URL(value);
1202
+ var host = url.hostname.toLowerCase().replace(/\\.$/, '');
1203
+ if (url.protocol === 'https:' || host === 'localhost' || host === '127.0.0.1' || host === '[::1]') return '';
1204
+ return 'The Public URL does not use HTTPS. OAuth tokens and redirects may be exposed.';
1205
+ } catch (e) {
1206
+ return 'The Public URL is invalid. OAuth clients may not discover this server.';
1207
+ }
1208
+ }
1209
+
1210
+ function collectSettingsPayload() {
1211
+ var payload = {
1212
+ enabled: $('set-enabled').checked,
1213
+ allowWithoutAdminAuth: $('set-allow-without').checked,
1214
+ defaultLinkTtlSeconds: Math.max(1, Math.floor(Number($('set-ttl').value || 4) * 3600)),
1215
+ defaultDeploymentType: $('set-deploy').value,
1216
+ maxActiveLinks: Number($('set-max-links').value),
1217
+ publicBaseUrl: String($('set-public-url').value).trim(),
1218
+ corsAllowedOrigins: String($('set-cors').value).split(',').map(function (x) { return x.trim(); }).filter(Boolean),
1219
+ rateLimitWindowMs: Number($('set-rate-window').value),
1220
+ rateLimitMaxPerIp: Number($('set-rate-ip').value),
1221
+ rateLimitMaxPerToken: Number($('set-rate-token').value),
1222
+ oauth: {
1223
+ enabled: $('oauth-enabled').checked,
1224
+ accessTokenTtlSec: readDurationInputs('oauth-access-ttl', OAUTH_ACCESS_UNITS),
1225
+ refreshIdleTtlSec: readDurationInputs('oauth-refresh-ttl', OAUTH_REFRESH_UNITS),
1226
+ refreshMaxLifetimeSec: Math.round(Number($('oauth-refresh-max').value) * 86400),
1227
+ dcrMaxClients: Number($('oauth-dcr-cap').value)
1228
+ }
1229
+ };
1230
+ Object.keys(payload).forEach(function (key) { if (fieldLocked(key)) delete payload[key]; });
1231
+ // Persist the max lifetime only when changed, so the default can evolve.
1232
+ var loaded = (settingsData && settingsData.settings && settingsData.settings.oauth) || {};
1233
+ var loadedMax = typeof loaded.refreshMaxLifetimeSec === 'number' ? loaded.refreshMaxLifetimeSec : 7776000;
1234
+ if (payload.oauth && Math.round(payload.oauth.refreshMaxLifetimeSec / 86400) === Math.round(loadedMax / 86400)) {
1235
+ delete payload.oauth.refreshMaxLifetimeSec;
1236
+ }
1237
+ return payload;
1238
+ }
1239
+
1240
+ `}a(ef,"settingsPartForm");function tf(){return`
1241
+ function settingsFlash(msg, ok) {
1242
+ var tab = document.querySelector('nav.tabs button[aria-selected="true"]');
1243
+ var target = tab && tab.getAttribute('data-tab') === 'oauth' ? 'oauth-flash' : 'settings-flash';
1244
+ if (ok) showToast(msg, true);
1245
+ else showFlash($(target), msg, false);
1246
+ }
1247
+
1248
+ async function persistSettings(payload) {
1249
+ settingsSaving = true;
1250
+ try {
1251
+ var data = await api('/settings', { method: 'PUT', body: JSON.stringify(payload) });
1252
+ applySettings(data);
1253
+ var warn = data.warnings && data.warnings.length ? data.warnings.join(' ') : '';
1254
+ $('set-public-url-warning').textContent = warn;
1255
+ $('set-public-url-warning').hidden = !warn;
1256
+ showFlash($('settings-flash'), '', true);
1257
+ showFlash($('oauth-flash'), '', true);
1258
+ settingsFlash('Settings saved.', true);
1259
+ refreshStatusQuiet();
1260
+ loadOAuthStatus();
1261
+ } catch (e) {
1262
+ settingsFlash(e.message || String(e), false);
1263
+ loadSettings();
1264
+ } finally {
1265
+ settingsSaving = false;
1266
+ }
1267
+ }
1268
+
1269
+ function saveSettingsNow() {
1270
+ if (!settingsCanEdit() || settingsSaving) return;
1271
+ var payload = collectSettingsPayload();
1272
+ var was = settingsData && settingsData.settings && settingsData.settings.oauth && settingsData.settings.oauth.enabled;
1273
+ if (!was && payload.oauth && payload.oauth.enabled) {
1274
+ var url = payload.publicBaseUrl === undefined ? settingsData.settings.publicBaseUrl : payload.publicBaseUrl;
1275
+ var warning = oauthEnableWarning(url);
1276
+ if (warning) {
1277
+ $('oauth-enabled').checked = false;
1278
+ openConfirmDialog({
1279
+ title: 'Enable MCP OAuth?',
1280
+ message: warning + ' Enable OAuth anyway?',
1281
+ confirmLabel: 'Enable anyway',
1282
+ onConfirm: function () { $('oauth-enabled').checked = true; persistSettings(payload); }
1283
+ });
1284
+ return;
1285
+ }
1286
+ }
1287
+ persistSettings(payload);
1288
+ }
1289
+
1290
+ function scheduleSettingsSave() {
1291
+ if (!settingsCanEdit()) return;
1292
+ if (settingsTimer) clearTimeout(settingsTimer);
1293
+ settingsTimer = setTimeout(function () { settingsTimer = null; saveSettingsNow(); }, 400);
1294
+ }
1295
+
1296
+ async function loadSettings() {
1297
+ try {
1298
+ applySettings(await api('/settings'));
1299
+ } catch (e) {
1300
+ settingsFlash(e.message || String(e), false);
1301
+ }
1302
+ }
1303
+
1304
+ async function refreshStatusQuiet() {
1305
+ try { applyStatus(await api('/status')); } catch (e) { /* ignore */ }
1306
+ }
1307
+
1308
+ function setCheckResult(state, text) {
1309
+ var el = $('set-check-result');
1310
+ el.className = 'check-result' + (state ? ' is-' + state : '');
1311
+ el.textContent = text || '';
1312
+ el.hidden = !text;
1313
+ }
1314
+
1315
+ async function checkPublicUrl() {
1316
+ setCheckResult('pending', 'Checking\u2026');
1317
+ try {
1318
+ var r = await api('/settings/check-public-url', {
1319
+ method: 'POST', body: JSON.stringify({ publicBaseUrl: $('set-public-url').value })
1320
+ });
1321
+ setCheckResult(r.ok ? 'ok' : 'fail', (r.ok ? '\u2713 Reachable: ' : '\u2717 Failed: ') + (r.message || r.error || '') +
1322
+ (r.status ? ' (HTTP ' + r.status + ')' : ''));
1323
+ } catch (e) {
1324
+ setCheckResult('fail', '\u2717 ' + (e.message || String(e)));
1325
+ }
1326
+ }
1327
+
1328
+ `}a(tf,"settingsPartSave");function nf(){return`
1329
+ function editorNodeHref(c) {
1330
+ var base = editorRoot && editorRoot !== '/' ? editorRoot.replace(/\\/$/, '') + '/' : '/';
1331
+ if (c.nodeId) return base + '#node/' + encodeURIComponent(c.nodeId);
1332
+ if (c.flowId) return base + '#flow/' + encodeURIComponent(c.flowId);
1333
+ return '';
1334
+ }
1335
+
1336
+ function collisionHtml(c) {
1337
+ var effect = c.kind === 'authorization' ? ' \u2014 OAuth login/token routes disabled' : ' \u2014 OAuth metadata disabled';
1338
+ var path = '<code>' + esc(c.path || '') + '</code>';
1339
+ var html;
1340
+ if (c.source === 'text') return esc(c.text || '');
1341
+ if (c.source === 'http-in') {
1342
+ var label = "'" + esc(c.nodeName || c.nodeId || 'unnamed') + "'";
1343
+ var href = editorNodeHref(c);
1344
+ html = 'Route ' + path + ' is already used by http-in node ' +
1345
+ (href ? '<a href="' + esc(href) + '" target="_blank" rel="noopener" title="Open node in the editor">' + label + '</a>' : label);
1346
+ } else if (c.source === 'httpStatic') {
1347
+ html = 'File ' + path + ' is served by httpStatic';
1348
+ } else if (c.source === 'middleware') {
1349
+ html = 'Path ' + path + ' is already handled by other middleware (' + esc(c.layer || 'unknown') + ')';
1350
+ } else {
1351
+ html = 'Route ' + path + ' is already registered by another node or plugin';
1352
+ }
1353
+ return html + esc(effect);
1354
+ }
1355
+
1356
+ function renderOAuthRouteWarning(status) {
1357
+ var box = $('oauth-route-warning');
1358
+ var details = status.details;
1359
+ if (!details) {
1360
+ details = [];
1361
+ (status.authorization || []).forEach(function (t) { details.push({ kind: 'authorization', source: 'text', text: t }); });
1362
+ (status.metadata || []).forEach(function (t) { details.push({ kind: 'metadata', source: 'text', text: t }); });
1363
+ }
1364
+ var html = '';
1365
+ if (status.httpNodeRootFallback) {
1366
+ html += '<p>httpNodeRoot is disabled: OAuth metadata is served under the admin /mcp path.</p>';
1367
+ }
1368
+ if (details.length) {
1369
+ html += '<h2>OAuth route conflict</h2><p>These paths are already in use, so AI Access will not serve them. ' +
1370
+ 'Remove or change the conflicting routes, then deploy.</p><ul class="conflict-list">' +
1371
+ details.map(function (c) { return '<li>' + collisionHtml(c) + '</li>'; }).join('') + '</ul>';
1372
+ }
1373
+ box.innerHTML = html;
1374
+ box.hidden = !html;
1375
+ }
1376
+
1377
+ async function loadOAuthStatus() {
1378
+ try {
1379
+ renderOAuthRouteWarning(await api('/oauth/status'));
1380
+ } catch (e) {
1381
+ var box = $('oauth-route-warning');
1382
+ box.textContent = 'OAuth route status unavailable: ' + (e.message || String(e));
1383
+ box.hidden = false;
1384
+ }
1385
+ }
1386
+
1387
+ async function loadOAuthClients() {
1388
+ var list = $('oauth-clients');
1389
+ try {
1390
+ var data = await api('/oauth/clients');
1391
+ var clients = (data && data.clients) || [];
1392
+ if (!clients.length) {
1393
+ list.innerHTML = '<p class="muted">No registered OAuth clients.</p>';
1394
+ return;
1395
+ }
1396
+ var canRevoke = settingsCanEdit();
1397
+ list.innerHTML = clients.map(function (c, i) {
1398
+ return '<div class="client-row"><div><strong>' + esc(c.client_name || 'Unnamed client') + '</strong>' +
1399
+ '<div class="client-meta"><code>' + esc(c.client_id) + '</code></div></div>' +
1400
+ '<button type="button" class="action danger" data-oauth-revoke="' + i + '"' + (canRevoke ? '' : ' disabled') +
1401
+ '>Revoke</button></div>';
1402
+ }).join('');
1403
+ list.querySelectorAll('[data-oauth-revoke]').forEach(function (btn) {
1404
+ btn.addEventListener('click', function () {
1405
+ var c = clients[Number(btn.getAttribute('data-oauth-revoke'))];
1406
+ if (!c) return;
1407
+ openConfirmDialog({
1408
+ title: 'Revoke OAuth client',
1409
+ message: 'Revoke OAuth client "' + (c.client_name || c.client_id) + '" and all of its tokens?',
1410
+ confirmLabel: 'Revoke',
1411
+ danger: true,
1412
+ onConfirm: async function () {
1413
+ try {
1414
+ await api('/oauth/clients/' + encodeURIComponent(c.client_id), { method: 'DELETE' });
1415
+ showToast('OAuth client revoked.', true);
1416
+ loadOAuthClients();
1417
+ } catch (e) {
1418
+ showFlash($('oauth-flash'), e.message || String(e), false);
1419
+ }
1420
+ }
1421
+ });
1422
+ });
1423
+ });
1424
+ } catch (e) {
1425
+ list.textContent = 'OAuth clients unavailable: ' + (e.message || String(e));
1426
+ }
1427
+ }
1428
+
1429
+ `}a(nf,"settingsPartOAuth");function rf(){return`
1430
+ function loadOAuthTab() {
1431
+ loadSettings();
1432
+ loadOAuthStatus();
1433
+ loadOAuthClients();
1434
+ }
1435
+
1436
+ (function wireSettings() {
1437
+ var sec = $('section-settings');
1438
+ var oauthSec = $('section-oauth');
1439
+ [sec, oauthSec].forEach(function (host) {
1440
+ if (!host) return;
1441
+ host.querySelectorAll('input, select').forEach(function (el) {
1442
+ if (el.id === 'set-public-url') return;
1443
+ el.addEventListener('change', scheduleSettingsSave);
1444
+ });
1445
+ });
1446
+ $('set-public-url').addEventListener('change', scheduleSettingsSave);
1447
+ $('set-deploy').addEventListener('change', function () {
1448
+ $('set-deploy-full-warn').hidden = $('set-deploy').value !== 'full';
1449
+ });
1450
+ $('set-check-public-url').addEventListener('click', checkPublicUrl);
1451
+ $('oauth-refresh').addEventListener('click', function () { loadOAuthStatus(); loadOAuthClients(); });
1452
+ $('oauth-copy-url').addEventListener('click', function () {
1453
+ copyTextToClipboard($('oauth-server-url').textContent || '');
1454
+ showToast('Server URL copied.', true);
1455
+ });
1456
+ })();
1457
+ `}a(rf,"settingsPartWire");function ja(){return ef()+tf()+nf()+rf()}a(ja,"pageClientSettingsScript");function sf(){return`
1259
1458
  (function () {
1260
1459
  var root = document.getElementById('mcp-root');
1261
1460
  if (!root) return;
@@ -1326,7 +1525,7 @@ strings you will paste publicly beyond this temporary link.
1326
1525
  var gate = $('auth-gate');
1327
1526
  var nav = $('main-nav');
1328
1527
  if (nav) nav.hidden = true;
1329
- ['section-status', 'section-links', 'section-proposals', 'section-logs'].forEach(function (id) {
1528
+ ['section-status', 'section-settings', 'section-links', 'section-oauth', 'section-proposals', 'section-logs'].forEach(function (id) {
1330
1529
  var el = $(id);
1331
1530
  if (el) el.hidden = true;
1332
1531
  });
@@ -1336,7 +1535,7 @@ strings you will paste publicly beyond this temporary link.
1336
1535
  if (ed) ed.setAttribute('href', editorLoginUrl());
1337
1536
  var ret = $('auth-gate-return');
1338
1537
  if (ret) {
1339
- ret.setAttribute('href', (typeof location !== 'undefined' ? location.pathname : '/mcp') +
1538
+ ret.setAttribute('href', (typeof location !== 'undefined' ? location.pathname : (cfg.dashboardPath || '/ai-access')) +
1340
1539
  (typeof location !== 'undefined' ? location.search : '') +
1341
1540
  (typeof location !== 'undefined' ? location.hash : ''));
1342
1541
  }
@@ -1476,7 +1675,7 @@ strings you will paste publicly beyond this temporary link.
1476
1675
  }
1477
1676
  return body;
1478
1677
  }
1479
- `}a(Lp,"pageClientScriptPartA");function Cp(){return`
1678
+ `}a(sf,"pageClientScriptPartA");function af(){return`
1480
1679
  // Time helpers \u2014 keep in sync with page-time.ts (always browser-local Intl).
1481
1680
  function titleFromDisplayName(displayName) {
1482
1681
  var s = String(displayName || '');
@@ -1583,7 +1782,7 @@ strings you will paste publicly beyond this temporary link.
1583
1782
  btn.disabled = false;
1584
1783
  }, 1600);
1585
1784
  }
1586
- `}a(Cp,"pageClientScriptPartATimeCopy");function Ep(){return`
1785
+ `}a(af,"pageClientScriptPartATimeCopy");function df(){return`
1587
1786
  function shortId(value, keep) {
1588
1787
  var s = String(value == null ? '' : value);
1589
1788
  var n = keep || 8;
@@ -1713,7 +1912,7 @@ strings you will paste publicly beyond this temporary link.
1713
1912
  }
1714
1913
  return '';
1715
1914
  }
1716
- `}a(Ep,"pageClientScriptPartAFormat");function Ip(){return`
1915
+ `}a(df,"pageClientScriptPartAFormat");function lf(){return`
1717
1916
  function lookupCachedProposal(id) {
1718
1917
  if (!id) return null;
1719
1918
  return cachedProposals.filter(function (x) { return x.id === id; })[0] || null;
@@ -1834,11 +2033,12 @@ strings you will paste publicly beyond this temporary link.
1834
2033
  return k + '=' + String(v);
1835
2034
  }).join(', '));
1836
2035
  }
1837
- `}a(Ip,"pageClientScriptPartAAudit");function Np(){return`
2036
+ `}a(lf,"pageClientScriptPartAAudit");function cf(){return`
1838
2037
  // Hash helpers \u2014 keep in sync with page-hash.ts
2038
+ var TAB_IDS = ['status', 'settings', 'links', 'oauth', 'proposals'];
1839
2039
  function hashForTab(tab) {
1840
2040
  if (tab === 'logs') return 'audit';
1841
- if (tab === 'status' || tab === 'links' || tab === 'proposals') return tab;
2041
+ if (TAB_IDS.indexOf(tab) >= 0) return tab;
1842
2042
  return 'status';
1843
2043
  }
1844
2044
 
@@ -1846,7 +2046,9 @@ strings you will paste publicly beyond this temporary link.
1846
2046
  var raw = String(hash || '').replace(/^#/, '');
1847
2047
  if (!raw || raw.indexOf('proposal=') === 0) return '';
1848
2048
  if (raw === 'audit' || raw === 'logs') return 'logs';
1849
- if (raw === 'status' || raw === 'links' || raw === 'proposals') return raw;
2049
+ if (raw === 'overview') return 'status';
2050
+ if (raw === 'tokens') return 'links';
2051
+ if (TAB_IDS.indexOf(raw) >= 0) return raw;
1850
2052
  return '';
1851
2053
  }
1852
2054
 
@@ -1906,6 +2108,8 @@ strings you will paste publicly beyond this temporary link.
1906
2108
  }
1907
2109
 
1908
2110
  function loadTabData(target) {
2111
+ if (target === 'settings') loadSettings();
2112
+ if (target === 'oauth') loadOAuthTab();
1909
2113
  if (target === 'links') loadLinks();
1910
2114
  if (target === 'proposals') loadProposals();
1911
2115
  if (target === 'logs') {
@@ -2094,29 +2298,28 @@ strings you will paste publicly beyond this temporary link.
2094
2298
  '<p id="link-ttl-warn" class="ttl-long-warn" hidden role="alert">' +
2095
2299
  'Long-lived tokens stay usable longer if they leak. Prefer the shortest expiry that works.' +
2096
2300
  '</p></form>' +
2097
- '<div id="token-once" class="token-once" hidden>' +
2098
- '<p id="token-kind-label" class="token-kind-label" hidden></p>' +
2099
- '<p id="token-docs-hint" class="muted"></p>' +
2100
- '<button type="button" class="action copy-ai-primary" id="token-copy-btn">Copy GET link for AI chat</button>' +
2101
- '<div id="mcp-setup-panel" class="mcp-setup-panel" hidden>' +
2102
- '<p class="muted mcp-setup-fallback">If your AI app cannot add MCP servers, create a REST or GET link instead.</p>' +
2103
- '<div class="mcp-setup-tabs" role="tablist" aria-label="MCP client setup" id="mcp-setup-tabs"></div>' +
2104
- '<div id="mcp-setup-body" class="mcp-setup-body"></div>' +
2301
+ '<div id="token-usage" class="token-usage" aria-live="polite">' +
2302
+ '<p id="token-usage-title" class="token-kind-label"></p>' +
2303
+ '<p id="token-usage-hint" class="muted"></p>' +
2304
+ '<div id="token-usage-mcp" class="mcp-setup-panel" hidden>' +
2305
+ '<div class="mcp-setup-tabs" role="tablist" aria-label="MCP client setup (example)" id="token-usage-tabs"></div>' +
2306
+ '<div id="token-usage-body" class="mcp-setup-body"></div>' +
2105
2307
  '</div>' +
2106
- '<details class="advanced-block" id="token-dev-details">' +
2107
- '<summary>For developers</summary>' +
2108
- '<p id="token-dev-blurb" class="muted">Details reusable while this token is active \u2014 also available via Copy in the table.</p>' +
2109
- '<code id="token-value"></code>' +
2110
- '<pre id="token-copy-block" class="token-copy-block"></pre>' +
2111
- '</details></div>';
2308
+ '<pre id="token-usage-block" class="token-copy-block"></pre>' +
2309
+ '<p class="muted token-usage-note">Replace &lt;YOUR_TOKEN&gt; with the token shown once after you create it. ' +
2310
+ 'Tokens cannot be viewed again later \u2014 revoke and create a new one instead.</p>' +
2311
+ '</div>';
2112
2312
  wireTtlControls();
2313
+ var kindSel = $('link-kind');
2314
+ if (kindSel) kindSel.addEventListener('change', renderTokenUsage);
2315
+ renderTokenUsage();
2113
2316
  var createForm = $('create-link-form');
2114
2317
  if (createForm) {
2115
2318
  createForm.addEventListener('submit', onCreateLink);
2116
2319
  }
2117
2320
  }
2118
2321
 
2119
- `}a(Np,"pageClientScriptPartATabs");function Fp(){return`
2322
+ `}a(cf,"pageClientScriptPartATabs");function uf(){return`
2120
2323
  async function loadLinks() {
2121
2324
  if (!aiOn || !canManage) {
2122
2325
  var dashLinksOff = $('dash-links');
@@ -2144,12 +2347,6 @@ strings you will paste publicly beyond this temporary link.
2144
2347
  '</tr></thead><tbody>' +
2145
2348
  links.map(function (l) {
2146
2349
  var kind = normalizeLinkKind(l.kind);
2147
- var material = linkCopyMaterial(kind, l.token || '', l.url || '');
2148
- var hasCopy = !!(material && material.primary);
2149
- var copyDisabled = hasCopy ? '' : ' disabled title="Copy unavailable \u2014 create a new token"';
2150
- var copyLabel = kind === 'get_link'
2151
- ? 'Copy GET link'
2152
- : (kind === 'rest' ? 'Copy REST prompt' : 'Copy MCP Bearer');
2153
2350
  return '<tr data-id="' + esc(l.id) + '" data-kind="' + esc(kind) + '">' +
2154
2351
  '<td>' + timeCell(l.createdAt) + '</td>' +
2155
2352
  '<td>' + kindBadgeHtml(kind) + '</td>' +
@@ -2157,37 +2354,9 @@ strings you will paste publicly beyond this temporary link.
2157
2354
  '<td>' + esc(l.label || '\u2014') + '</td>' +
2158
2355
  '<td>' + timeCell(l.expiresAt) + '</td>' +
2159
2356
  '<td class="row-actions">' +
2160
- '<button type="button" class="action secondary copy-link" data-id="' + esc(l.id) + '"' +
2161
- ' data-kind="' + esc(kind) + '"' +
2162
- ' data-token="' + esc(l.token || '') + '"' +
2163
- ' data-url="' + esc(l.url || '') + '"' +
2164
- ' data-label="' + esc(copyLabel) + '"' +
2165
- copyDisabled + '>' + esc(copyLabel) + '</button> ' +
2166
2357
  '<button type="button" class="action danger revoke-link" data-id="' + esc(l.id) + '">Revoke</button>' +
2167
2358
  '</td></tr>';
2168
2359
  }).join('') + '</tbody></table>';
2169
- list.querySelectorAll('.copy-link').forEach(function (btn) {
2170
- btn.addEventListener('click', function () {
2171
- var kind = normalizeLinkKind(btn.getAttribute('data-kind'));
2172
- var material = linkCopyMaterial(
2173
- kind,
2174
- btn.getAttribute('data-token') || '',
2175
- btn.getAttribute('data-url') || ''
2176
- );
2177
- var text = material.primary || '';
2178
- if (!text) {
2179
- showFlash(flash, 'Clipboard unavailable \u2014 create a new token to copy.', false);
2180
- return;
2181
- }
2182
- var okMsg = material.copiedOk || 'Copied.';
2183
- copyTextToClipboard(text).then(function () {
2184
- markCopied(btn, flash, okMsg);
2185
- showToast(okMsg, true);
2186
- }).catch(function () {
2187
- showFlash(flash, 'Clipboard unavailable \u2014 create a new token and copy from the flash block.', false);
2188
- });
2189
- });
2190
- });
2191
2360
  list.querySelectorAll('.revoke-link').forEach(function (btn) {
2192
2361
  btn.addEventListener('click', function () { revokeLink(btn.getAttribute('data-id')); });
2193
2362
  });
@@ -2212,8 +2381,6 @@ strings you will paste publicly beyond this temporary link.
2212
2381
  await api('/links/' + encodeURIComponent(id), { method: 'DELETE' });
2213
2382
  showFlash(flash, 'Link revoked.', true);
2214
2383
  showToast('Link revoked.', true);
2215
- var once = $('token-once');
2216
- if (once) once.hidden = true;
2217
2384
  loadLinks();
2218
2385
  loadLogs();
2219
2386
  } catch (e) {
@@ -2221,7 +2388,7 @@ strings you will paste publicly beyond this temporary link.
2221
2388
  showToast(e.message || String(e), false);
2222
2389
  }
2223
2390
  }
2224
- `}a(Fp,"pageClientScriptPartA2");function Op(){return`
2391
+ `}a(uf,"pageClientScriptPartA2");function pf(){return`
2225
2392
 
2226
2393
  function normalizeLinkKind(kind) {
2227
2394
  if (kind === 'rest' || kind === 'mcp' || kind === 'get_link') return kind;
@@ -2363,7 +2530,7 @@ strings you will paste publicly beyond this temporary link.
2363
2530
  label: 'GET link (chat paste)',
2364
2531
  buttonLabel: 'Copy GET link for AI chat',
2365
2532
  primary: getLink,
2366
- hint: 'Paste this GET link in your AI chat. It is get_link only \u2014 REST and MCP need separate tokens from this page. While active you can copy it again from the table.',
2533
+ hint: 'Paste this GET link in your AI chat. It is get_link only \u2014 REST and MCP need separate tokens from this page. It is shown only once.',
2367
2534
  flashOk: 'GET link created.',
2368
2535
  copiedOk: 'Copied GET link for AI chat.',
2369
2536
  block: [
@@ -2410,8 +2577,7 @@ strings you will paste publicly beyond this temporary link.
2410
2577
  var created = await api('/links', { method: 'POST', body: JSON.stringify(payload) });
2411
2578
  var createdKind = normalizeLinkKind(created.kind || kind);
2412
2579
  var material = linkCopyMaterial(createdKind, created.token || '', created.url || '');
2413
- var box = $('token-once');
2414
- box.hidden = false;
2580
+ var modal = openTokenModal(material.label);
2415
2581
  // Each new token starts with "For developers" collapsed.
2416
2582
  var devDetails = $('token-dev-details');
2417
2583
  if (devDetails) devDetails.open = false;
@@ -2433,11 +2599,11 @@ strings you will paste publicly beyond this temporary link.
2433
2599
  var devBlurb = $('token-dev-blurb');
2434
2600
  if (devBlurb) {
2435
2601
  if (createdKind === 'rest') {
2436
- devBlurb.textContent = 'REST prompt + Bearer (reusable while active \u2014 also via Copy REST prompt in the table).';
2602
+ devBlurb.textContent = 'REST prompt + Bearer token.';
2437
2603
  } else if (createdKind === 'mcp') {
2438
- devBlurb.textContent = 'MCP Bearer for /mcp/rpc (reusable while active \u2014 also via Copy MCP Bearer in the table).';
2604
+ devBlurb.textContent = 'MCP Bearer for /mcp/rpc.';
2439
2605
  } else {
2440
- devBlurb.textContent = 'GET link details (reusable while active \u2014 also via Copy GET link in the table).';
2606
+ devBlurb.textContent = 'GET link details.';
2441
2607
  }
2442
2608
  }
2443
2609
  var copyBtn = $('token-copy-btn');
@@ -2477,6 +2643,7 @@ strings you will paste publicly beyond this temporary link.
2477
2643
  }
2478
2644
  }
2479
2645
  if (labelInput) labelInput.value = '';
2646
+ showInPageDialog(modal);
2480
2647
  showFlash(flash, material.flashOk, true);
2481
2648
  showToast(material.flashOk, true);
2482
2649
  loadLinks();
@@ -2487,9 +2654,116 @@ strings you will paste publicly beyond this temporary link.
2487
2654
  }
2488
2655
  }
2489
2656
 
2490
- function renderMcpSetupPanel(snippets, flash) {
2491
- var tabsHost = $('mcp-setup-tabs');
2492
- var body = $('mcp-setup-body');
2657
+ var TOKEN_PLACEHOLDER = '<YOUR_TOKEN>';
2658
+
2659
+ /** Generic (placeholder-token) usage for the channel currently selected in the form. */
2660
+ function renderTokenUsage() {
2661
+ var kindSelect = $('link-kind');
2662
+ var kind = normalizeLinkKind(kindSelect && kindSelect.value);
2663
+ var material = linkCopyMaterial(kind, TOKEN_PLACEHOLDER, '');
2664
+ var titles = {
2665
+ get_link: 'How to use a GET link (chat paste)',
2666
+ rest: 'How to use a REST Bearer token',
2667
+ mcp: 'How to use an MCP token (/mcp/rpc)'
2668
+ };
2669
+ var hints = {
2670
+ get_link: 'Paste the GET link into your AI chat. It works only as a GET link \u2014 REST and MCP need their own tokens.',
2671
+ rest: 'Give your AI this prompt with the token filled in. The token works only for the REST API \u2014 not as a GET link or for /mcp/rpc.',
2672
+ mcp: 'Add Node-RED to your MCP client with one of these configs. The token works only on POST /mcp/rpc. If your AI app cannot add MCP servers, use a REST or GET link instead.'
2673
+ };
2674
+ var title = $('token-usage-title');
2675
+ if (title) title.textContent = titles[kind];
2676
+ var hint = $('token-usage-hint');
2677
+ if (hint) hint.textContent = hints[kind];
2678
+ var block = $('token-usage-block');
2679
+ var mcpPanel = $('token-usage-mcp');
2680
+ if (kind === 'mcp') {
2681
+ if (block) { block.textContent = ''; block.hidden = true; }
2682
+ if (mcpPanel) mcpPanel.hidden = false;
2683
+ renderMcpSetupPanel(material.mcpSnippets || {}, $('links-flash'), 'token-usage');
2684
+ } else {
2685
+ if (mcpPanel) mcpPanel.hidden = true;
2686
+ var tabs = $('token-usage-tabs');
2687
+ var tbody = $('token-usage-body');
2688
+ if (tabs) tabs.innerHTML = '';
2689
+ if (tbody) tbody.innerHTML = '';
2690
+ if (block) { block.hidden = false; block.textContent = kind === 'rest' ? material.primary : material.block; }
2691
+ }
2692
+ }
2693
+
2694
+ /**
2695
+ * One-time modal holding the real token. Closing it (button, Done, or Esc)
2696
+ * empties the dialog so the token no longer exists in the DOM.
2697
+ */
2698
+ function openTokenModal(label) {
2699
+ var old = $('token-modal');
2700
+ if (old && old.parentNode) old.parentNode.removeChild(old);
2701
+ var dialog = document.createElement('dialog');
2702
+ dialog.id = 'token-modal';
2703
+ dialog.className = 'token-modal';
2704
+ dialog.setAttribute('aria-labelledby', 'token-modal-title');
2705
+ dialog.setAttribute('aria-describedby', 'token-modal-warning');
2706
+ dialog.innerHTML =
2707
+ '<form method="dialog" class="token-modal-form">' +
2708
+ '<div class="token-modal-head">' +
2709
+ '<h3 id="token-modal-title">' + esc(label || 'Access token created') + '</h3>' +
2710
+ '<button type="submit" value="close" class="action secondary token-modal-close" aria-label="Close">\xD7</button>' +
2711
+ '</div>' +
2712
+ '<p id="token-modal-warning" class="token-modal-warning" role="alert"><strong>This is the only time the token is shown.</strong> ' +
2713
+ 'Copy it now \u2014 it cannot be viewed again. If you lose it, revoke it and create a new one.</p>' +
2714
+ '<div id="token-once" class="token-once">' +
2715
+ '<p id="token-kind-label" class="token-kind-label" hidden></p>' +
2716
+ '<p id="token-docs-hint" class="muted"></p>' +
2717
+ '<button type="button" class="action copy-ai-primary" id="token-copy-btn">Copy GET link for AI chat</button>' +
2718
+ '<div id="mcp-setup-panel" class="mcp-setup-panel" hidden>' +
2719
+ '<p class="muted mcp-setup-fallback">If your AI app cannot add MCP servers, create a REST or GET link instead.</p>' +
2720
+ '<div class="mcp-setup-tabs" role="tablist" aria-label="MCP client setup" id="mcp-setup-tabs"></div>' +
2721
+ '<div id="mcp-setup-body" class="mcp-setup-body"></div>' +
2722
+ '</div>' +
2723
+ '<details class="advanced-block" id="token-dev-details">' +
2724
+ '<summary>For developers</summary>' +
2725
+ '<p id="token-dev-blurb" class="muted"></p>' +
2726
+ '<code id="token-value"></code>' +
2727
+ '<pre id="token-copy-block" class="token-copy-block"></pre>' +
2728
+ '</details></div>' +
2729
+ '<div class="row dialog-actions"><button type="submit" value="done" class="action">Done</button></div>' +
2730
+ '</form>';
2731
+ var opener = document.activeElement;
2732
+ var cleaned = false;
2733
+ function cleanup() {
2734
+ if (cleaned) return;
2735
+ cleaned = true;
2736
+ dialog.innerHTML = '';
2737
+ document.removeEventListener('keydown', onKey, true);
2738
+ if (dialog.parentNode) dialog.parentNode.removeChild(dialog);
2739
+ if (opener && typeof opener.focus === 'function') opener.focus();
2740
+ }
2741
+ function closeTokenModal() {
2742
+ // Native <dialog>: close() fires 'close' -> cleanup. Fallback (open attribute): clean up directly.
2743
+ if (typeof dialog.close === 'function' && dialog.open) dialog.close();
2744
+ cleanup();
2745
+ }
2746
+ function onKey(e) {
2747
+ if (e.key === 'Escape' || e.key === 'Esc') {
2748
+ e.preventDefault();
2749
+ closeTokenModal();
2750
+ }
2751
+ }
2752
+ dialog.addEventListener('close', cleanup);
2753
+ dialog.addEventListener('cancel', function (e) { e.preventDefault(); closeTokenModal(); });
2754
+ document.addEventListener('keydown', onKey, true);
2755
+ var form = dialog.querySelector('form');
2756
+ if (form) {
2757
+ form.addEventListener('submit', function (e) { e.preventDefault(); closeTokenModal(); });
2758
+ }
2759
+ document.body.appendChild(dialog);
2760
+ return dialog;
2761
+ }
2762
+
2763
+ function renderMcpSetupPanel(snippets, flash, prefix) {
2764
+ var idp = prefix || 'mcp-setup';
2765
+ var tabsHost = $(idp + '-tabs');
2766
+ var body = $(idp + '-body');
2493
2767
  if (!tabsHost || !body) return;
2494
2768
  var keys = Object.keys(snippets);
2495
2769
  if (!keys.length) {
@@ -2508,15 +2782,15 @@ strings you will paste publicly beyond this temporary link.
2508
2782
  }).join('');
2509
2783
  var sn = snippets[active];
2510
2784
  body.innerHTML =
2511
- '<pre class="mcp-setup-snippet" id="mcp-setup-snippet">' + esc(sn.text) + '</pre>' +
2512
- '<button type="button" class="action" id="mcp-setup-copy">' + esc(sn.label) + '</button>';
2785
+ '<pre class="mcp-setup-snippet" id="' + idp + '-snippet">' + esc(sn.text) + '</pre>' +
2786
+ '<button type="button" class="action" id="' + idp + '-copy">' + esc(sn.label) + '</button>';
2513
2787
  tabsHost.querySelectorAll('[data-mcp-tab]').forEach(function (btn) {
2514
2788
  btn.addEventListener('click', function () {
2515
2789
  active = btn.getAttribute('data-mcp-tab') || keys[0];
2516
2790
  paint();
2517
2791
  });
2518
2792
  });
2519
- var copyBtn = $('mcp-setup-copy');
2793
+ var copyBtn = $(idp + '-copy');
2520
2794
  if (copyBtn) {
2521
2795
  copyBtn.onclick = function () {
2522
2796
  copyTextToClipboard(sn.text).then(function () {
@@ -2804,7 +3078,7 @@ strings you will paste publicly beyond this temporary link.
2804
3078
  decided: Math.max(decidedTotal, cachedProposals.filter(function (p) { return p.status !== 'pending'; }).length)
2805
3079
  };
2806
3080
  }
2807
- `}a(Op,"pageClientScriptPartB");function Mp(){return`
3081
+ `}a(pf,"pageClientScriptPartB");function ff(){return`
2808
3082
  var DIFF_MODE_KEY = 'ai-access-diff-mode';
2809
3083
  var DIFF_SPLIT_MIN_PX = ${900};
2810
3084
  var PRESENCE_SHORT_TEXT_MAX = ${120};
@@ -2921,7 +3195,7 @@ strings you will paste publicly beyond this temporary link.
2921
3195
  '</div>';
2922
3196
  }
2923
3197
 
2924
- `}a(Mp,"pageClientScriptPartBDiff");function $p(){return`
3198
+ `}a(ff,"pageClientScriptPartBDiff");function mf(){return`
2925
3199
  function isCollapsiblePresenceText(value) {
2926
3200
  var s = String(value == null ? '' : value);
2927
3201
  if (s.indexOf('\\n') >= 0) return true;
@@ -3022,7 +3296,7 @@ strings you will paste publicly beyond this temporary link.
3022
3296
  });
3023
3297
  });
3024
3298
  }
3025
- `}a($p,"pageClientScriptPartBDiffPresence");function Dp(){return`
3299
+ `}a(mf,"pageClientScriptPartBDiffPresence");function hf(){return`
3026
3300
  function textChangeLineCount(ch) {
3027
3301
  return (ch.lines || []).length;
3028
3302
  }
@@ -3081,7 +3355,7 @@ strings you will paste publicly beyond this temporary link.
3081
3355
  '<code class="diff-before" title="before">' + esc(JSON.stringify(ch.before)) + '</code>' +
3082
3356
  ' \u2192 <code class="diff-after" title="after">' + esc(JSON.stringify(ch.after)) + '</code></div>';
3083
3357
  }
3084
- `}a(Dp,"pageClientScriptPartBDiffView");function jp(){return`
3358
+ `}a(hf,"pageClientScriptPartBDiffView");function gf(){return`
3085
3359
  function renderDiffSections(sections, mode, emptyMsg) {
3086
3360
  var parts = '';
3087
3361
  if (sections.added && sections.added.length) {
@@ -3169,7 +3443,7 @@ strings you will paste publicly beyond this temporary link.
3169
3443
  // Full view kept for mode-toggle re-render inside .proposal-changes.
3170
3444
  return renderHumanDiff(diff, mode);
3171
3445
  }
3172
- `}a(jp,"pageClientScriptPartBDiffView2");function Hp(){return`
3446
+ `}a(gf,"pageClientScriptPartBDiffView2");function yf(){return`
3173
3447
  function updatePendingBadges() {
3174
3448
  var counts = countPendingBuckets();
3175
3449
  var fullText = formatPendingBadgeText(counts.live, counts.stale);
@@ -3285,7 +3559,7 @@ strings you will paste publicly beyond this temporary link.
3285
3559
  });
3286
3560
  });
3287
3561
  }
3288
- `}a(Hp,"pageClientScriptPartB2");function Up(){return`
3562
+ `}a(yf,"pageClientScriptPartB2");function wf(){return`
3289
3563
  function renderDeleteBanner(diff) {
3290
3564
  if (!isLargeDelete(diff)) return '';
3291
3565
  var beforeN = diff.summary.beforeNodeCount || 0;
@@ -3367,7 +3641,7 @@ strings you will paste publicly beyond this temporary link.
3367
3641
  loadProposals();
3368
3642
  loadLogs();
3369
3643
  }
3370
- `}a(Up,"pageClientScriptPartB2Actions");function Bp(){return`
3644
+ `}a(wf,"pageClientScriptPartB2Actions");function bf(){return`
3371
3645
  function renderRequestedVia(p) {
3372
3646
  if (!p.createdBy) return '';
3373
3647
  if (p.createdBy.label) {
@@ -3455,7 +3729,7 @@ strings you will paste publicly beyond this temporary link.
3455
3729
  actions +
3456
3730
  '</article>';
3457
3731
  }
3458
- `}a(Bp,"pageClientScriptPartB2Cards");function qp(){return`
3732
+ `}a(bf,"pageClientScriptPartB2Cards");function vf(){return`
3459
3733
  function renderStaleItem(p) {
3460
3734
  var cached = proposalDiffCache[p.id];
3461
3735
  var title = titleForProposalCard(p);
@@ -3630,7 +3904,7 @@ strings you will paste publicly beyond this temporary link.
3630
3904
  bindProposalActions(list);
3631
3905
  focusProposalFromHash();
3632
3906
  }
3633
- `}a(qp,"pageClientScriptPartB2CardsList");function Wp(){return`
3907
+ `}a(vf,"pageClientScriptPartB2CardsList");function kf(){return`
3634
3908
  async function refreshFlowLabels() {
3635
3909
  try {
3636
3910
  var data = await api('/flows');
@@ -3797,7 +4071,7 @@ strings you will paste publicly beyond this temporary link.
3797
4071
  replaceLocationHash('#proposals');
3798
4072
  return true;
3799
4073
  }
3800
- `}a(Wp,"pageClientScriptPartB2Focus");function Gp(){return`
4074
+ `}a(kf,"pageClientScriptPartB2Focus");function Af(){return`
3801
4075
  function bindDiffModeToggle(host, diff) {
3802
4076
  host.querySelectorAll('[data-diff-mode]').forEach(function (btn) {
3803
4077
  btn.addEventListener('click', function () {
@@ -3909,7 +4183,7 @@ strings you will paste publicly beyond this temporary link.
3909
4183
  esc(e.message || String(e)) + '</p>';
3910
4184
  }
3911
4185
  }
3912
- }`}a(Gp,"pageClientScriptPartB2Apply");function zp(){return`
4186
+ }`}a(Af,"pageClientScriptPartB2Apply");function xf(){return`
3913
4187
 
3914
4188
  function dialogFocusables(dialog) {
3915
4189
  if (!dialog) return [];
@@ -4223,7 +4497,7 @@ strings you will paste publicly beyond this temporary link.
4223
4497
  throw e;
4224
4498
  }
4225
4499
  }
4226
- `}a(zp,"pageClientScriptPartC");function Jp(){return`
4500
+ `}a(xf,"pageClientScriptPartC");function Sf(){return`
4227
4501
  var cachedLogs = [];
4228
4502
 
4229
4503
  function renderLogsTable(logs) {
@@ -4300,7 +4574,7 @@ strings you will paste publicly beyond this temporary link.
4300
4574
  showFlash(flash, e.message || String(e), false);
4301
4575
  }
4302
4576
  }
4303
- `}a(Jp,"pageClientScriptPartCLogs");function Vp(){return`
4577
+ `}a(Sf,"pageClientScriptPartCLogs");function Tf(){return`
4304
4578
  function applyStatus(s) {
4305
4579
  aiOn = !!s.aiFeaturesEnabled;
4306
4580
  canManage = !!(s.capabilities && s.capabilities.manageLinks);
@@ -4340,7 +4614,7 @@ strings you will paste publicly beyond this temporary link.
4340
4614
  var blurb = $('permissions-blurb');
4341
4615
  if (blurb) {
4342
4616
  if (!s.aiFeaturesEnabled) {
4343
- blurb.textContent = 'AI Access is turned off. Enable it in the editor sidebar to create links and review proposals.';
4617
+ blurb.textContent = 'AI Access is turned off. Enable it on the Settings tab (or in the editor sidebar) to create access tokens and review proposals.';
4344
4618
  } else if (canManage && canApprove) {
4345
4619
  blurb.textContent = "You're an admin: you can create access links and approve or reject proposals.";
4346
4620
  } else if (canManage) {
@@ -4461,7 +4735,7 @@ strings you will paste publicly beyond this temporary link.
4461
4735
  });
4462
4736
  }
4463
4737
  }
4464
- `}a(Vp,"pageClientScriptPartD");function Kp(){return`
4738
+ `}a(Tf,"pageClientScriptPartD");function Pf(){return`
4465
4739
  async function boot() {
4466
4740
  accessToken = readEditorToken();
4467
4741
  if (!accessToken) {
@@ -4473,13 +4747,17 @@ strings you will paste publicly beyond this temporary link.
4473
4747
  applyStatus(s);
4474
4748
  $('main-nav').hidden = false;
4475
4749
  $('section-status').hidden = false;
4750
+ $('section-settings').hidden = false;
4476
4751
  $('section-links').hidden = false;
4752
+ $('section-oauth').hidden = false;
4477
4753
  $('section-proposals').hidden = false;
4478
4754
  $('section-logs').hidden = false;
4479
4755
  setupStickyNav();
4480
4756
  // Restore tab from hash (or default #status); keep #proposal=<id> until focus consumes it.
4481
4757
  var initialTab = initialTabFromHash(location.hash || '');
4482
4758
  selectTab(initialTab, { skipHash: !!readProposalHashId() });
4759
+ if (initialTab === 'settings') loadSettings();
4760
+ if (initialTab === 'oauth') loadOAuthTab();
4483
4761
  if (aiOn) {
4484
4762
  if (canManage) loadLinks();
4485
4763
  loadProposals();
@@ -4563,7 +4841,7 @@ strings you will paste publicly beyond this temporary link.
4563
4841
 
4564
4842
  boot();
4565
4843
  })();
4566
- `}a(Kp,"pageClientScriptPartDBoot");function Ea(){return(Lp()+Cp()+Ep()+Ip()+Np()+Fp()+Op()+Mp()+$p()+Dp()+jp()+Hp()+Up()+Bp()+qp()+Wp()+Gp()+zp()+Jp()+Vp()+Kp()).trim()}a(Ea,"pageClientScript");function Xp(){return`
4844
+ `}a(Pf,"pageClientScriptPartDBoot");function Ha(){return(sf()+af()+df()+lf()+cf()+uf()+pf()+ff()+mf()+hf()+gf()+yf()+wf()+bf()+vf()+kf()+Af()+xf()+Sf()+Tf()+ja()+Pf()).trim()}a(Ha,"pageClientScript");function Rf(){return`
4567
4845
  :root {
4568
4846
  color-scheme: light;
4569
4847
  --bg: #eef2f5;
@@ -4577,6 +4855,8 @@ strings you will paste publicly beyond this temporary link.
4577
4855
  --card: #ffffff;
4578
4856
  --line: #cfd8e3;
4579
4857
  --ok: #1b6b4a;
4858
+ --soft: #f4f7fa;
4859
+ --input-bg: #ffffff;
4580
4860
  }
4581
4861
  * { box-sizing: border-box; }
4582
4862
  /* Author display rules (e.g. .ttl-custom-row { display:flex }) must not defeat [hidden]. */
@@ -4631,7 +4911,7 @@ h1 {
4631
4911
  text-decoration: none;
4632
4912
  text-align: center;
4633
4913
  }
4634
- `}a(Xp,"pageStylesPartA");function Yp(){return`
4914
+ `}a(Rf,"pageStylesPartA");function _f(){return`
4635
4915
  nav.tabs {
4636
4916
  position: sticky;
4637
4917
  top: 0;
@@ -4648,7 +4928,7 @@ nav.tabs {
4648
4928
  nav.tabs.is-stuck {
4649
4929
  box-shadow: 0 4px 12px rgba(26, 35, 48, 0.08);
4650
4930
  border-bottom-color: var(--line);
4651
- background: #f3f6f8;
4931
+ background: var(--soft);
4652
4932
  }
4653
4933
  nav.tabs button {
4654
4934
  border: 1px solid var(--line);
@@ -4671,7 +4951,7 @@ nav.tabs button[aria-selected="true"] {
4671
4951
  max-width: 14rem;
4672
4952
  padding: 0.05rem 0.45rem;
4673
4953
  border-radius: 999px;
4674
- background: #fff;
4954
+ background: var(--card);
4675
4955
  color: var(--accent);
4676
4956
  font-size: 0.72rem;
4677
4957
  font-weight: 700;
@@ -4725,7 +5005,7 @@ dd {
4725
5005
  padding: 0.22rem 0.55rem;
4726
5006
  border-radius: 6px;
4727
5007
  cursor: help;
4728
- background: var`}a(Yp,"pageStylesPartA2");function Qp(){return`(--accent-soft);
5008
+ background: var`}a(_f,"pageStylesPartA2");function Lf(){return`(--accent-soft);
4729
5009
  color: var(--accent);
4730
5010
  font-size: 0.82rem;
4731
5011
  font-weight: 650;
@@ -4736,7 +5016,7 @@ dd {
4736
5016
  text-align: left;
4737
5017
  border: 1px dashed var(--line);
4738
5018
  border-radius: 8px;
4739
- background: #f7faf8;
5019
+ background: var(--soft);
4740
5020
  }
4741
5021
  .empty-state .empty-title {
4742
5022
  margin: 0 0 0.25rem;
@@ -4765,7 +5045,7 @@ dd {
4765
5045
  font-size: 0.78rem;
4766
5046
  font-weight: 650;
4767
5047
  white-space: nowrap;
4768
- background: #eef2f0;
5048
+ background: var(--soft);
4769
5049
  color: #3a4a42;
4770
5050
  }
4771
5051
  .kind-badge.kind-get_link { background: #e8f1fb; color: #1a4a7a; }
@@ -4800,7 +5080,7 @@ input, select, textarea, button.action {
4800
5080
  border: 1px solid var(--line);
4801
5081
  border-radius: 7px;
4802
5082
  padding: 0.4rem 0.55rem;
4803
- background: #fff;
5083
+ background: var(--card);
4804
5084
  color: var(--ink);
4805
5085
  }
4806
5086
  textarea { min-height: 4rem; width: min(100%, 28rem); }
@@ -4812,7 +5092,7 @@ button.action {
4812
5092
  font-weight: 600;
4813
5093
  }
4814
5094
  button.action.secondary {
4815
- background: #fff;
5095
+ background: var(--card);
4816
5096
  color: var(--ink);
4817
5097
  border-color: var(--line);
4818
5098
  }
@@ -4837,7 +5117,7 @@ th, td {
4837
5117
  }
4838
5118
  th { color: var(--muted); font-weight: 600; }
4839
5119
  .token-once {
4840
- background: #f4faf7;
5120
+ background: var(--soft);
4841
5121
  border: 1px dashed var(--accent);
4842
5122
  border-radius: 8px;
4843
5123
  padding: 0.75rem 0.9rem;
@@ -4848,7 +5128,7 @@ th { color: var(--muted); font-weight: 600; }
4848
5128
  .token-once .token-copy-block {
4849
5129
  margin: 0.6rem 0;
4850
5130
  padding: 0.55rem 0.65rem;
4851
- background: #fff;
5131
+ background: var(--card);
4852
5132
  border: 1px solid var(--line);
4853
5133
  border-radius: 6px;
4854
5134
  font-size: 0.8rem;
@@ -4857,6 +5137,36 @@ th { color: var(--muted); font-weight: 600; }
4857
5137
  }
4858
5138
  .token-once #token-copy-btn { margin-top: 0.35rem; }
4859
5139
  .token-once #token-docs-hint { margin: 0.45rem 0 0; }
5140
+ .token-usage {
5141
+ background: var(--panel-soft, var(--soft));
5142
+ border: 1px solid var(--line);
5143
+ border-radius: 8px;
5144
+ padding: 0.75rem 0.9rem;
5145
+ margin: 0.75rem 0;
5146
+ }
5147
+ .token-usage .token-kind-label { font-weight: 600; margin: 0 0 0.25rem; }
5148
+ .token-usage .token-copy-block {
5149
+ margin: 0.6rem 0;
5150
+ padding: 0.55rem 0.65rem;
5151
+ background: var(--card);
5152
+ border: 1px solid var(--line);
5153
+ border-radius: 6px;
5154
+ font-size: 0.8rem;
5155
+ white-space: pre-wrap;
5156
+ word-break: break-word;
5157
+ }
5158
+ .token-usage .token-usage-note { font-size: 0.85rem; margin: 0.4rem 0 0; }
5159
+ dialog.token-modal { width: min(720px, 94vw); max-width: min(720px, 94vw); max-height: 90vh; overflow: auto; }
5160
+ .token-modal-head { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; }
5161
+ .token-modal-close { min-width: 2.2rem; font-size: 1.1rem; line-height: 1; }
5162
+ .token-modal-warning {
5163
+ margin: 0.4rem 0 0.6rem;
5164
+ padding: 0.5rem 0.65rem;
5165
+ border: 1px solid #f0c36d;
5166
+ background: #fff8e5;
5167
+ border-radius: 6px;
5168
+ color: #6b4a00;
5169
+ }
4860
5170
  .flash {
4861
5171
  margin: 0.5rem 0 0.85rem;
4862
5172
  padding: 0.55rem 0.7rem;
@@ -4868,7 +5178,7 @@ th { color: var(--muted); font-weight: 600; }
4868
5178
  .proposal-detail {
4869
5179
  margin-top: 0.75rem;
4870
5180
  padding: 0.75rem;
4871
- background: #f7fafc;
5181
+ background: var(--soft);
4872
5182
  border-radius: 8px;
4873
5183
  border: 1px solid var(--line);
4874
5184
  overflow-wrap: anywhere;
@@ -4886,7 +5196,7 @@ th { color: var(--muted); font-weight: 600; }
4886
5196
  color: var(--muted);
4887
5197
  font-size: 0.92rem;
4888
5198
  }
4889
- `}a(Qp,"pageStylesPartB");function Zp(){return`
5199
+ `}a(Lf,"pageStylesPartB");function Cf(){return`
4890
5200
  .logs-toolbar { margin-bottom: 0.65rem; }
4891
5201
  table.audit-table {
4892
5202
  table-layout: fixed;
@@ -4953,7 +5263,7 @@ table.audit-table {
4953
5263
  max-height: 10rem;
4954
5264
  overflow: auto;
4955
5265
  font-size: 0.78rem;
4956
- background: #f4f7fa;
5266
+ background: var(--soft);
4957
5267
  padding: 0.4rem 0.5rem;
4958
5268
  border-radius: 6px;
4959
5269
  }
@@ -4962,11 +5272,11 @@ table.audit-table {
4962
5272
  cursor: help;
4963
5273
  border-bottom: 1px dotted var(--line);
4964
5274
  }
4965
- `}a(Zp,"pageStylesPartC");function ef(){return`
5275
+ `}a(Cf,"pageStylesPartC");function Ef(){return`
4966
5276
  .proposal-diff {
4967
5277
  margin: 0.65rem 0;
4968
5278
  padding: 0.55rem 0.65rem;
4969
- background: #fff;
5279
+ background: var(--card);
4970
5280
  border: 1px solid var(--line);
4971
5281
  border-radius: 7px;
4972
5282
  overflow-wrap: anywhere;
@@ -4984,7 +5294,7 @@ table.audit-table {
4984
5294
  border: 1px solid var(--line);
4985
5295
  border-radius: 7px;
4986
5296
  overflow: hidden;
4987
- background: #f4f7fa;
5297
+ background: var(--soft);
4988
5298
  }
4989
5299
  .diff-mode-toggle button {
4990
5300
  border: 0;
@@ -5039,7 +5349,7 @@ table.audit-table {
5039
5349
  color: var(--muted);
5040
5350
  margin-bottom: 0.15rem;
5041
5351
  }
5042
- `}a(ef,"pageStylesPartD");function tf(){return`
5352
+ `}a(Ef,"pageStylesPartD");function If(){return`
5043
5353
  .diff-block {
5044
5354
  margin-top: 0.25rem;
5045
5355
  font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
@@ -5050,7 +5360,7 @@ table.audit-table {
5050
5360
  max-height: 18rem;
5051
5361
  border: 1px solid var(--line);
5052
5362
  border-radius: 5px;
5053
- background: #fafbfc;
5363
+ background: var(--soft);
5054
5364
  }
5055
5365
  .diff-line {
5056
5366
  display: grid;
@@ -5101,7 +5411,7 @@ table.audit-table {
5101
5411
  font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
5102
5412
  font-size: 0.78rem;
5103
5413
  line-height: 1.4;
5104
- background: #fafbfc;
5414
+ background: var(--soft);
5105
5415
  }
5106
5416
  .diff-split .diff-pane + .diff-pane {
5107
5417
  border-left: 1px solid var(--line);
@@ -5121,7 +5431,7 @@ table.audit-table {
5121
5431
  }
5122
5432
  .diff-before { color: var(--danger); word-break: break-word; }
5123
5433
  .diff-after { color: var(--ok); word-break: break-word; }
5124
- `}a(tf,"pageStylesPartE");function nf(){return`
5434
+ `}a(If,"pageStylesPartE");function Nf(){return`
5125
5435
  .proposal-detail {
5126
5436
  scroll-margin-top: 4.5rem;
5127
5437
  }
@@ -5144,7 +5454,7 @@ table.audit-table {
5144
5454
  margin: 0.35rem 0 0.55rem;
5145
5455
  border: 1px solid var(--line);
5146
5456
  border-radius: 6px;
5147
- background: #fafbfc;
5457
+ background: var(--soft);
5148
5458
  }
5149
5459
  .node-presence > summary {
5150
5460
  cursor: pointer;
@@ -5186,7 +5496,7 @@ table.audit-table {
5186
5496
  padding: 0.15rem 0.55rem;
5187
5497
  border: 1px solid var(--line);
5188
5498
  border-radius: 4px;
5189
- background: #fff;
5499
+ background: var(--card);
5190
5500
  color: var(--ink);
5191
5501
  cursor: pointer;
5192
5502
  }
@@ -5198,7 +5508,7 @@ table.audit-table {
5198
5508
  margin: 0.3rem 0;
5199
5509
  border: 1px solid var(--line);
5200
5510
  border-radius: 5px;
5201
- background: #fff;
5511
+ background: var(--card);
5202
5512
  overflow: hidden;
5203
5513
  }
5204
5514
  .presence-preview-frame {
@@ -5207,7 +5517,7 @@ table.audit-table {
5207
5517
  min-height: 10rem;
5208
5518
  max-height: 18rem;
5209
5519
  border: 0;
5210
- background: #fff;
5520
+ background: var(--card);
5211
5521
  }
5212
5522
  .presence-pre {
5213
5523
  margin: 0.3rem 0 0;
@@ -5217,7 +5527,7 @@ table.audit-table {
5217
5527
  font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
5218
5528
  font-size: 0.78rem;
5219
5529
  line-height: 1.4;
5220
- background: #fff;
5530
+ background: var(--card);
5221
5531
  border: 1px solid var(--line);
5222
5532
  border-radius: 5px;
5223
5533
  padding: 0.4rem 0.5rem;
@@ -5247,7 +5557,7 @@ table.audit-table {
5247
5557
  .audit-table .col-action { width: 5.5rem; min-width: 5.5rem; }
5248
5558
  .audit-table .col-actor { width: 7rem; min-width: 6rem; }
5249
5559
  }
5250
- `}a(nf,"pageStylesPartF");function rf(){return`
5560
+ `}a(Nf,"pageStylesPartF");function Ff(){return`
5251
5561
  .dash-grid {
5252
5562
  display: grid;
5253
5563
  grid-template-columns: repeat(auto-fit, minmax(10.5rem, 1fr));
@@ -5255,7 +5565,7 @@ table.audit-table {
5255
5565
  margin: 0 0 1rem;
5256
5566
  }
5257
5567
  .dash-card {
5258
- background: #f7faf8;
5568
+ background: var(--soft);
5259
5569
  border: 1px solid var(--line);
5260
5570
  border-radius: 9px;
5261
5571
  padding: 0.75rem 0.85rem;
@@ -5289,7 +5599,7 @@ table.audit-table {
5289
5599
  padding: 0.28rem 0.65rem;
5290
5600
  font: inherit;
5291
5601
  font-size: 0.88rem;
5292
- background: #fff;
5602
+ background: var(--card);
5293
5603
  color: var(--ink);
5294
5604
  }
5295
5605
  .proposals-search:focus {
@@ -5319,7 +5629,7 @@ button.id-copy:focus {
5319
5629
  }
5320
5630
  .filter-chip {
5321
5631
  border: 1px solid var(--line);
5322
- background: #fff;
5632
+ background: var(--card);
5323
5633
  color: var(--ink);
5324
5634
  border-radius: 999px;
5325
5635
  padding: 0.28rem 0.75rem;
@@ -5340,7 +5650,7 @@ button.id-copy:focus {
5340
5650
  padding: 0.55rem 0.75rem;
5341
5651
  border: 1px solid var(--line);
5342
5652
  border-radius: 8px;
5343
- background: #fafbfc;
5653
+ background: var(--soft);
5344
5654
  margin-top: 0.45rem;
5345
5655
  cursor: pointer;
5346
5656
  }
@@ -5442,7 +5752,7 @@ button.id-copy:focus {
5442
5752
  .mcp-setup-tab {
5443
5753
  appearance: none;
5444
5754
  border: 1px solid var(--border, #ccd);
5445
- background: var(--surface, #fff);
5755
+ background: var(--surface, var(--card));
5446
5756
  color: inherit;
5447
5757
  border-radius: 4px;
5448
5758
  padding: 0.35rem 0.65rem;
@@ -5461,10 +5771,10 @@ button.id-copy:focus {
5461
5771
  font-size: 0.85rem;
5462
5772
  white-space: pre-wrap;
5463
5773
  word-break: break-word;
5464
- background: var(--code-bg, #f6f8fa);
5774
+ background: var(--code-bg, var(--soft));
5465
5775
  border-radius: 4px;
5466
5776
  }
5467
- `}a(rf,"pageStylesPartG");function of(){return`
5777
+ `}a(Ff,"pageStylesPartG");function Of(){return`
5468
5778
  .stale-group {
5469
5779
  margin: 1rem 0 0;
5470
5780
  border: 1px solid var(--warn-border);
@@ -5483,7 +5793,7 @@ button.id-copy:focus {
5483
5793
  .stale-item {
5484
5794
  margin: 0.55rem 0 0;
5485
5795
  padding: 0.55rem 0.65rem;
5486
- background: #fff;
5796
+ background: var(--card);
5487
5797
  border: 1px solid var(--line);
5488
5798
  border-radius: 7px;
5489
5799
  }
@@ -5628,7 +5938,93 @@ dialog h3 { margin: 0 0 0.4rem; }
5628
5938
  margin-left: auto;
5629
5939
  }
5630
5940
  .debug-toggle input { width: auto; }
5631
- `}a(of,"pageStylesPartH");function Ia(){return(Xp()+Yp()+Qp()+Zp()+ef()+tf()+nf()+rf()+of()).trim()}a(Ia,"pageStyles");function sf(e){return e.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;")}a(sf,"escapeHtml");function af(){return`
5941
+ `}a(Of,"pageStylesPartH");function $f(){return`
5942
+ .card {
5943
+ background: var(--card);
5944
+ border: 1px solid var(--line);
5945
+ border-radius: 10px;
5946
+ padding: 1rem 1.1rem;
5947
+ margin: 0 0 1rem;
5948
+ }
5949
+ .card h3 { margin: 0 0 0.75rem; font-size: 1.05rem; }
5950
+ .card-head { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; margin-bottom: 0.5rem; }
5951
+ .card-head h3 { margin: 0; }
5952
+ .settings-grid {
5953
+ display: grid;
5954
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 420px), 1fr));
5955
+ gap: 1rem;
5956
+ align-items: start;
5957
+ margin-bottom: 1rem;
5958
+ }
5959
+ .settings-grid .card { margin: 0; }
5960
+ .form-row { margin: 0 0 0.9rem; }
5961
+ .form-row:last-child { margin-bottom: 0; }
5962
+ .form-row > label, .form-row > .label { display: block; font-weight: 600; margin: 0 0 0.3rem; }
5963
+ .field-line { display: flex; flex-wrap: nowrap; align-items: center; gap: 0.5rem; min-width: 0; }
5964
+ .field-line .grow { flex: 1 1 auto; min-width: 0; }
5965
+ .field-line input.num { width: 7.5rem; flex: 0 0 auto; }
5966
+ .field-line select { flex: 0 1 auto; width: auto; max-width: 100%; min-width: 0; }
5967
+ .field-line .unit { color: var(--muted); white-space: nowrap; }
5968
+ .field-line code { overflow-wrap: anywhere; padding: 0.35rem 0.5rem; background: var(--soft); border-radius: 6px; }
5969
+ .hint { display: block; color: var(--muted); font-size: 0.88rem; font-weight: 400; margin: 0.25rem 0 0; line-height: 1.4; }
5970
+ .warn-text { color: #8a5a00; }
5971
+ .toggle-row { display: flex; align-items: flex-start; gap: 0.5rem; justify-content: space-between; }
5972
+ .switch-label { display: flex; gap: 0.6rem; align-items: flex-start; cursor: pointer; }
5973
+ .switch-label input { margin-top: 0.25rem; width: 1.05rem; height: 1.05rem; flex: 0 0 auto; }
5974
+ .lock-badge {
5975
+ font-size: 0.8rem; color: var(--muted); border: 1px solid var(--line);
5976
+ border-radius: 999px; padding: 0.1rem 0.5rem; white-space: nowrap;
5977
+ }
5978
+ .notice { padding: 0.6rem 0.8rem; border-radius: 8px; background: var(--soft); border: 1px solid var(--line); }
5979
+ .check-result { margin: 0.6rem 0 0; padding: 0.6rem 0.8rem; border-radius: 8px; font-weight: 600; overflow-wrap: anywhere; }
5980
+ .check-result.is-ok { color: var(--ok); background: #e8f6ef; border: 1px solid #a8d8b4; }
5981
+ .check-result.is-fail { color: var(--danger); background: #fdecec; border: 1px solid #f0c4c4; }
5982
+ .check-result.is-pending { color: var(--muted); font-weight: 400; background: var(--soft); border: 1px solid var(--line); }
5983
+ .steps { margin: 0.5rem 0 0.5rem 1.2rem; padding: 0; }
5984
+ .steps li { margin: 0.2rem 0; }
5985
+ .client-row, .conflict-list li { overflow-wrap: anywhere; }
5986
+ .client-row {
5987
+ display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
5988
+ padding: 0.55rem 0; border-top: 1px solid var(--line);
5989
+ }
5990
+ .client-row:first-child { border-top: 0; }
5991
+ .client-row .client-meta { color: var(--muted); font-size: 0.85rem; }
5992
+ .conflict-list { margin: 0.4rem 0 0 1.2rem; padding: 0; }
5993
+ .conflict-list li { margin: 0.25rem 0; }
5994
+ @media (max-width: 640px) {
5995
+ nav.tabs { overflow-x: auto; flex-wrap: nowrap; }
5996
+ nav.tabs button { flex: 0 0 auto; }
5997
+ .field-line { flex-wrap: wrap; }
5998
+ }
5999
+ @media (prefers-color-scheme: dark) {
6000
+ :root {
6001
+ color-scheme: dark;
6002
+ --bg: #12171d;
6003
+ --ink: #e3e9ef;
6004
+ --muted: #9aa8b6;
6005
+ --accent: #4cc59a;
6006
+ --accent-soft: #1d3a31;
6007
+ --warn-bg: #3a2e1a;
6008
+ --warn-border: #a2772f;
6009
+ --danger: #ff8a8a;
6010
+ --card: #1b222a;
6011
+ --line: #33404d;
6012
+ --ok: #6fd3a5;
6013
+ --soft: #222b35;
6014
+ --input-bg: #11161c;
6015
+ }
6016
+ body { background: var(--bg); }
6017
+ input, select, textarea { background: var(--input-bg); color: var(--ink); border-color: var(--line); }
6018
+ a { color: #7cc4ff; }
6019
+ .flash.error, .check-result.is-fail { background: #3a1f22; border-color: #6b3036; }
6020
+ .flash.ok, .check-result.is-ok { background: #173327; border-color: #2d6149; }
6021
+ .warn-text { color: #f0c36d; }
6022
+ .warning { background: var(--warn-bg); color: var(--ink); }
6023
+ button.action, button.action.danger, nav.tabs button[aria-selected="true"] { color: #0d1a14; }
6024
+ button.action.secondary { background: var(--soft); color: var(--ink); border-color: var(--line); }
6025
+ dialog { background: var(--card); color: var(--ink); }
6026
+ }
6027
+ `}a($f,"pageStylesDashboard");function Ua(){return(Rf()+_f()+Lf()+Cf()+Ef()+If()+Nf()+Ff()+Of()+$f()).trim()}a(Ua,"pageStyles");function Mf(e){return e.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;")}a(Mf,"escapeHtml");function Df(){return`
5632
6028
  <header class="page-header">
5633
6029
  <h1>Node-RED AI Access</h1>
5634
6030
  <p class="lede">
@@ -5646,20 +6042,22 @@ dialog h3 { margin: 0 0 0.4rem; }
5646
6042
  <p>Open the editor, sign in, then return here to manage AI Access.</p>
5647
6043
  <p class="row">
5648
6044
  <a class="action" id="auth-gate-editor" href="/">Open Node-RED editor</a>
5649
- <a class="action secondary" id="auth-gate-return" href="/mcp">Return to AI Access</a>
6045
+ <a class="action secondary" id="auth-gate-return" href="/ai-access">Return to AI Access</a>
5650
6046
  </p>
5651
6047
  </div>
5652
6048
  <div id="toast-host" class="toast-host" aria-live="polite"></div>
5653
6049
  <nav class="tabs" aria-label="Admin views" hidden id="main-nav">
5654
- <button type="button" data-tab="status" aria-selected="true">Status</button>
5655
- <button type="button" data-tab="links" aria-selected="false">Access links</button>
6050
+ <button type="button" data-tab="status" aria-selected="true">Overview</button>
6051
+ <button type="button" data-tab="settings" aria-selected="false">Settings</button>
6052
+ <button type="button" data-tab="links" aria-selected="false">Access tokens</button>
6053
+ <button type="button" data-tab="oauth" aria-selected="false">OAuth</button>
5656
6054
  <button type="button" data-tab="proposals" aria-selected="false">
5657
6055
  Proposals <span class="tab-badge" id="proposals-pending-badge" hidden>0</span>
5658
6056
  </button>
5659
6057
  <button type="button" data-tab="logs" aria-selected="false">Audit log</button>
5660
- </nav>`}a(af,"renderMcpPageHeader");function df(){return`
5661
- <section class="panel active" id="section-status" aria-label="Status" hidden>
5662
- <h2>Status</h2>
6058
+ </nav>`}a(Df,"renderMcpPageHeader");function jf(){return`
6059
+ <section class="panel active" id="section-status" aria-label="Overview" hidden>
6060
+ <h2>Overview</h2>
5663
6061
  <div id="status-flash" class="flash" hidden></div>
5664
6062
  <aside id="status-warning" class="warning" role="status" hidden></aside>
5665
6063
  <div class="dash-grid" id="status-dashboard">
@@ -5701,9 +6099,9 @@ dialog h3 { margin: 0 0 0.4rem; }
5701
6099
  <div class="row" style="margin-top:1rem">
5702
6100
  <button type="button" class="action secondary" id="refresh-status">Refresh</button>
5703
6101
  </div>
5704
- </section>`}a(df,"renderMcpPageStatusSection");function lf(){return`
5705
- <section class="panel" id="section-links" aria-label="Access links" hidden>
5706
- <h2>Access links</h2>
6102
+ </section>`}a(jf,"renderMcpPageStatusSection");function Hf(){return`
6103
+ <section class="panel" id="section-links" aria-label="Access tokens" hidden>
6104
+ <h2>Access tokens</h2>
5707
6105
  <p class="muted" id="links-intro">
5708
6106
  Create a token for the channel your AI uses: a GET link to paste in chat,
5709
6107
  a REST Bearer, or an MCP token. It can read flows and submit proposals \u2014
@@ -5712,7 +6110,193 @@ dialog h3 { margin: 0 0 0.4rem; }
5712
6110
  <div id="links-flash" class="flash" hidden></div>
5713
6111
  <div id="links-manage"></div>
5714
6112
  <div id="links-list"></div>
5715
- </section>`}a(lf,"renderMcpPageLinksSection");function cf(){return`
6113
+ </section>`}a(Hf,"renderMcpPageLinksSection");function Pe(e){return`<span class="lock-badge" id="lock-${e}" hidden>Locked by settings.js</span>`}a(Pe,"lockBadge");function Uf(){return`
6114
+ <section class="panel" id="section-settings" aria-label="Settings" hidden>
6115
+ <h2>Settings</h2>
6116
+ <p class="muted">Changes save automatically and apply immediately (no restart).</p>
6117
+ <div id="settings-flash" class="flash" hidden></div>
6118
+ <p id="settings-readonly" class="notice" hidden>Read-only: full admin permission (*) is required to change these settings.</p>
6119
+ <div class="settings-grid">
6120
+ <div class="card settings-card">
6121
+ <h3>General</h3>
6122
+ <div class="form-row toggle-row" id="row-enabled">
6123
+ <label class="switch-label"><input type="checkbox" id="set-enabled" />
6124
+ <span><strong>Enable AI Access features</strong>
6125
+ <span class="hint">When off, access tokens, proposals, and AI tools are disabled.</span></span></label>
6126
+ ${Pe("enabled")}
6127
+ </div>
6128
+ <div class="form-row toggle-row" id="row-allow-without" hidden>
6129
+ <label class="switch-label"><input type="checkbox" id="set-allow-without" />
6130
+ <span><strong>Allow AI Access without adminAuth (local-only)</strong>
6131
+ <span class="hint danger-text">Not for production. Anyone who can reach the editor admin port can use AI features.</span></span></label>
6132
+ ${Pe("allow-without")}
6133
+ </div>
6134
+ <div class="form-row">
6135
+ <label for="set-deploy">Default deploy type</label>
6136
+ <div class="field-line">
6137
+ <select id="set-deploy">
6138
+ <option value="nodes">Only changed nodes (recommended)</option>
6139
+ <option value="flows">Changed flows</option>
6140
+ <option value="full">Full redeploy</option>
6141
+ </select>
6142
+ ${Pe("deploy")}
6143
+ </div>
6144
+ <p class="hint danger-text" id="set-deploy-full-warn" hidden>Full redeploy restarts the entire runtime.</p>
6145
+ </div>
6146
+ <div class="form-row">
6147
+ <label for="set-ttl">Default access token expiry</label>
6148
+ <div class="field-line">
6149
+ <input type="number" id="set-ttl" min="1" step="1" class="num" />
6150
+ <span class="unit">hours</span>
6151
+ ${Pe("ttl")}
6152
+ </div>
6153
+ </div>
6154
+ <div class="form-row">
6155
+ <label for="set-max-links">Maximum active tokens</label>
6156
+ <div class="field-line">
6157
+ <input type="number" id="set-max-links" min="1" step="1" class="num" />
6158
+ <span class="unit">tokens</span>
6159
+ ${Pe("max-links")}
6160
+ </div>
6161
+ </div>
6162
+ </div>
6163
+
6164
+ <div class="card settings-card">
6165
+ <h3>Public URL</h3>
6166
+ <p class="hint">The HTTPS origin AI clients use to reach Node-RED (used in links and OAuth metadata).</p>
6167
+ <div class="form-row">
6168
+ <label for="set-public-url">Public URL</label>
6169
+ <div class="field-line">
6170
+ <input type="url" id="set-public-url" placeholder="https://nodered.example.com" class="grow" />
6171
+ <button type="button" class="action secondary" id="set-check-public-url">Check</button>
6172
+ ${Pe("public-url")}
6173
+ </div>
6174
+ <div id="set-public-url-warning" class="hint warn-text" role="status" hidden></div>
6175
+ <div id="set-check-result" class="check-result" aria-live="polite" hidden></div>
6176
+ </div>
6177
+ <details class="tech-details">
6178
+ <summary>About the check</summary>
6179
+ <p class="hint">Loopback checks use Node-RED's listening port; behind a reverse proxy, use the internal port. Unix-socket listeners are not supported. HTTPS checks fail with self-signed certificates.</p>
6180
+ <p class="hint" id="set-trust-proxy"></p>
6181
+ </details>
6182
+ </div>
6183
+
6184
+ <div class="card settings-card">
6185
+ <h3>CORS</h3>
6186
+ <div class="form-row">
6187
+ <label for="set-cors">Allowed origins <span class="hint">(comma separated)</span></label>
6188
+ <div class="field-line">
6189
+ <input type="text" id="set-cors" class="grow" placeholder="https://app.example.com" />
6190
+ ${Pe("cors")}
6191
+ </div>
6192
+ </div>
6193
+ </div>
6194
+
6195
+ <div class="card settings-card">
6196
+ <h3>Rate limits</h3>
6197
+ <div class="form-row">
6198
+ <label for="set-rate-window">Window</label>
6199
+ <div class="field-line">
6200
+ <input type="number" id="set-rate-window" min="1" step="1" class="num" />
6201
+ <span class="unit">ms</span>
6202
+ ${Pe("rate-window")}
6203
+ </div>
6204
+ </div>
6205
+ <div class="form-row">
6206
+ <label for="set-rate-ip">Per IP</label>
6207
+ <div class="field-line">
6208
+ <input type="number" id="set-rate-ip" min="1" step="1" class="num" />
6209
+ <span class="unit">requests / window</span>
6210
+ ${Pe("rate-ip")}
6211
+ </div>
6212
+ </div>
6213
+ <div class="form-row">
6214
+ <label for="set-rate-token">Per token</label>
6215
+ <div class="field-line">
6216
+ <input type="number" id="set-rate-token" min="1" step="1" class="num" />
6217
+ <span class="unit">requests / window</span>
6218
+ ${Pe("rate-token")}
6219
+ </div>
6220
+ </div>
6221
+ <p class="hint" id="set-trust-proxy-inline"></p>
6222
+ </div>
6223
+ </div>
6224
+ </section>`}a(Uf,"renderMcpPageSettingsSection");function Bf(){return`
6225
+ <section class="panel" id="section-oauth" aria-label="OAuth" hidden>
6226
+ <h2>MCP OAuth</h2>
6227
+ <p class="muted">MCP clients (ChatGPT, Claude, \u2026) can request read or propose access through OAuth.</p>
6228
+ <div id="oauth-flash" class="flash" hidden></div>
6229
+ <div id="oauth-route-warning" class="warning" role="status" hidden></div>
6230
+ <div class="settings-grid">
6231
+ <div class="card settings-card">
6232
+ <h3>Status</h3>
6233
+ <div class="form-row toggle-row">
6234
+ <label class="switch-label"><input type="checkbox" id="oauth-enabled" />
6235
+ <span><strong>Enable MCP OAuth</strong>
6236
+ <span class="hint">Also available in the editor sidebar (AI Access tab).</span></span></label>
6237
+ ${Pe("oauth")}
6238
+ </div>
6239
+ <div class="form-row">
6240
+ <span class="label">Server URL for MCP clients</span>
6241
+ <div class="field-line"><code id="oauth-server-url" class="grow">\u2014</code>
6242
+ <button type="button" class="action secondary" id="oauth-copy-url">Copy</button></div>
6243
+ </div>
6244
+ <details class="tech-details" open>
6245
+ <summary>How to connect with OAuth</summary>
6246
+ <ol class="hint steps">
6247
+ <li>Set the Public URL on the Settings tab to the HTTPS address clients use, then click Check.</li>
6248
+ <li>Enable MCP OAuth (above, or in the editor sidebar AI Access tab).</li>
6249
+ <li>In your MCP client add the server URL <code>&lt;Public URL&gt;/mcp</code> (shown above).</li>
6250
+ <li>The client registers itself automatically (Dynamic Client Registration).</li>
6251
+ <li>You are sent to the Node-RED login and consent page; approve read or propose access.</li>
6252
+ </ol>
6253
+ <p class="hint">Opening <code>/mcp</code> in a browser redirects to this dashboard; MCP clients use the same URL.</p>
6254
+ </details>
6255
+ </div>
6256
+ <div class="card settings-card">
6257
+ <h3>Token lifetimes</h3>
6258
+ <div class="form-row">
6259
+ <label for="oauth-access-ttl">Access token lifetime</label>
6260
+ <div class="field-line">
6261
+ <input type="number" id="oauth-access-ttl" min="1" step="1" class="num" />
6262
+ <select id="oauth-access-ttl-unit" aria-label="Access token lifetime unit">
6263
+ <option value="minutes">minutes</option><option value="hours">hours</option>
6264
+ </select>
6265
+ </div>
6266
+ </div>
6267
+ <div class="form-row">
6268
+ <label for="oauth-refresh-ttl">Refresh token idle lifetime</label>
6269
+ <div class="field-line">
6270
+ <input type="number" id="oauth-refresh-ttl" min="1" step="1" class="num" />
6271
+ <select id="oauth-refresh-ttl-unit" aria-label="Refresh token idle lifetime unit">
6272
+ <option value="hours">hours</option><option value="days">days</option>
6273
+ </select>
6274
+ </div>
6275
+ </div>
6276
+ <div class="form-row">
6277
+ <label for="oauth-refresh-max">Refresh token max lifetime</label>
6278
+ <div class="field-line">
6279
+ <input type="number" id="oauth-refresh-max" min="0" step="1" class="num" />
6280
+ <span class="unit">days</span>
6281
+ </div>
6282
+ <p class="hint">Default 90 days; 0 = unlimited.</p>
6283
+ </div>
6284
+ <div class="form-row">
6285
+ <label for="oauth-dcr-cap">Maximum registered clients</label>
6286
+ <div class="field-line">
6287
+ <input type="number" id="oauth-dcr-cap" min="1" step="1" class="num" />
6288
+ <span class="unit">clients</span>
6289
+ </div>
6290
+ <p class="hint">Applies to new registrations: existing clients keep working, but new apps are refused while the number of clients is at or above this limit.</p>
6291
+ </div>
6292
+ </div>
6293
+ </div>
6294
+ <div class="card">
6295
+ <div class="card-head"><h3>Registered OAuth clients</h3>
6296
+ <button type="button" class="action secondary" id="oauth-refresh">Refresh</button></div>
6297
+ <div id="oauth-clients" aria-live="polite"><p class="muted">Loading\u2026</p></div>
6298
+ </div>
6299
+ </section>`}a(Bf,"renderMcpPageOAuthSection");function qf(){return`
5716
6300
  <section class="panel" id="section-proposals" aria-label="Proposals" hidden>
5717
6301
  <h2>Proposals</h2>
5718
6302
  <p id="proposals-hint" class="muted"></p>
@@ -5730,7 +6314,7 @@ dialog h3 { margin: 0 0 0.4rem; }
5730
6314
  </div>
5731
6315
  <div id="proposals-flash" class="flash" hidden></div>
5732
6316
  <div id="proposals-list"><p class="muted">Loading\u2026</p></div>
5733
- </section>`}a(cf,"renderMcpPageProposalsSection");function uf(){return`
6317
+ </section>`}a(qf,"renderMcpPageProposalsSection");function Wf(){return`
5734
6318
  <section class="panel" id="section-logs" aria-label="Audit log" hidden>
5735
6319
  <h2>Audit log</h2>
5736
6320
  <div id="logs-flash" class="flash" hidden></div>
@@ -5746,7 +6330,7 @@ dialog h3 { margin: 0 0 0.4rem; }
5746
6330
  </label>
5747
6331
  </div>
5748
6332
  <div id="logs-list"><p class="muted">Loading\u2026</p></div>
5749
- </section>`}a(uf,"renderMcpPageLogsSection");function pf(){return`
6333
+ </section>`}a(Wf,"renderMcpPageLogsSection");function zf(){return`
5750
6334
  <dialog id="reject-dialog">
5751
6335
  <form method="dialog" id="reject-form">
5752
6336
  <h3>Reject proposal</h3>
@@ -5788,7 +6372,7 @@ dialog h3 { margin: 0 0 0.4rem; }
5788
6372
  <button type="submit" value="confirm" class="action danger" id="confirm-action-btn">Confirm</button>
5789
6373
  </div>
5790
6374
  </form>
5791
- </dialog>`}a(pf,"renderMcpPageDialogs");function Na(e){let t={apiBase:e.apiBase,mcpRpc:e.mcpRpc??"/mcp/rpc",editorRoot:e.editorRoot,authTokenKey:e.authTokenKey};return`<!DOCTYPE html>
6375
+ </dialog>`}a(zf,"renderMcpPageDialogs");function Ba(e){let t={apiBase:e.apiBase,mcpRpc:e.mcpRpc??"/mcp/rpc",dashboardPath:e.dashboardPath??"/ai-access",editorRoot:e.editorRoot,authTokenKey:e.authTokenKey};return`<!DOCTYPE html>
5792
6376
  <html lang="en">
5793
6377
  <head>
5794
6378
  <meta charset="utf-8" />
@@ -5796,20 +6380,23 @@ dialog h3 { margin: 0 0 0.4rem; }
5796
6380
  <meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate" />
5797
6381
  <meta http-equiv="Pragma" content="no-cache" />
5798
6382
  <title>Node-RED AI Access</title>
5799
- <style>${Ia()}</style>
6383
+ <style>${Ua()}</style>
5800
6384
  </head>
5801
6385
  <body>
5802
6386
  <main
5803
6387
  id="mcp-root"
5804
- data-config="${sf(JSON.stringify(t))}"
5805
- >${af()}
5806
- ${df()}
5807
- ${lf()}
5808
- ${cf()}
5809
- ${uf()}
5810
- ${pf()}
6388
+ data-config="${Mf(JSON.stringify(t))}"
6389
+ >${Df()}
6390
+ ${jf()}
6391
+ ${Uf()}
6392
+ ${Hf()}
6393
+ ${Bf()}
6394
+ ${qf()}
6395
+ ${Wf()}
6396
+ ${zf()}
5811
6397
  </main>
5812
- <script>${Ea()}</script>
6398
+ <script>${Ha()}</script>
5813
6399
  </body>
5814
- </html>`}a(Na,"renderMcpPage");Ae();ve();Ue();q();tt();G();Ne();Ge();Ae();function vt(e,t,n,r={}){e.setHeader("X-Content-Type-Options","nosniff"),e.setHeader("Content-Type","application/json; charset=utf-8");let o={error:Fe[n]??"Request failed",code:n,...r},s=ie(n,"rest");s&&typeof o.hint!="string"&&(o.hint=s),e.status(t).json(o)}a(vt,"sendSafeJsonError");function Fa(e){let t={kind:"admin"};return typeof e.user?.username=="string"&&(t.username=e.user.username),t}a(Fa,"adminActor");function Oa(e,t,n,r,o){return async(s,i)=>{if(!s.accessLink){i.setHeader("WWW-Authenticate",K),i.status(401).json({error:le,code:Re});return}try{let d=U(s,r),l=oe(s.query),c=await ze(e,t,s.accessLink,s.body??{},n,d,o),u=z(e,c.expectedRev,{flowId:c.payload.flowId}),p=Ee(e,e.get(c.id),s.accessLink.id),f=c.warnings;i.status(201).json({...ce(c,{...d,include:l}),mine:!0,...Ie(p),...X(u),...f&&f.length>0?{warnings:f}:{}})}catch(d){let l=N(d,"rest");if(l){i.status(l.statusCode).json(l.body);return}F(i,d);return}}}a(Oa,"createProposalHandler");function Ma(e,t,n){return async(r,o)=>{let s,i;try{let A=await t.getFlows();s=A.rev,i=ir(A.flows)}catch{}let d={...U(r,n),...i?{liveFlowLabels:i}:{}},l=oe(r.query),c;try{c=pr(r.query)}catch(A){F(o,A);return}let u=s?z(e,s):0,p=r.accessLink;if(p){let A=Ht(e,p,d,c),E=A.proposals.map(D=>{let O=ue(e,p,D,{...d,include:l},{includeConflicts:!1});return O.status!=="pending"||s===void 0?O:{...O,baseRevStale:O.expectedRev!==s}});o.json({proposals:E,...dt({...A,returned:A.proposals.length}),...X(u)});return}let f={};c.flowId&&(f.flowId=c.flowId);let m=Gs(e.list(f).filter(A=>Co(A.status,c.status)),{...d,include:l}),h=c.offset??0,v=(c.limit!==void 0?m.slice(h,h+c.limit):h>0?m.slice(h):m).map(A=>A.status!=="pending"||s===void 0?A:{...A,baseRevStale:A.expectedRev!==s});o.json({proposals:v,...dt({total:m.length,limit:c.limit??null,offset:h,returned:v.length}),...X(u)})}}a(Ma,"listProposalsHandler");function $a(e,t,n){return async(r,o)=>{let s=r.params.id;if(typeof s!="string"||s.length===0){o.status(400).json({error:"Missing proposal id"});return}let i=U(r,t),d=oe(r.query),l;if(n)try{l=(await n.getFlows()).rev}catch{}try{if(r.accessLink){let c=Me(e,r.accessLink,s,i);o.json(ue(e,r.accessLink,c,{...i,include:d},{...l?{liveRev:l}:{}}));return}l&&z(e,l),o.json(ce(e.get(s),{...i,include:d}))}catch(c){if(c instanceof re){vt(o,404,c.code);return}let u=N(c,"rest");if(u){o.status(u.statusCode).json(u.body);return}F(o,c);return}}}a($a,"getProposalHandler");function Da(e,t){return async(n,r)=>{let o=n.params.id;if(typeof o!="string"||o.length===0){r.status(400).json({error:"Missing proposal id"});return}let s=n.body??{},i={};if(s.deploymentType!==void 0){if(s.deploymentType!=="nodes"&&s.deploymentType!=="flows"&&s.deploymentType!=="full"){r.status(400).json({error:"deploymentType must be 'nodes', 'flows', or 'full'"});return}i.deploymentType=s.deploymentType}try{let d=await e.approve(o,Fa(n),i);r.json(se(d,U(n,t)))}catch(d){if(d instanceof re||d instanceof ee||d instanceof bn||d instanceof J){vt(r,d.statusCode,d.code);return}F(r,d);return}}}a(Da,"approveProposalHandler");function ja(e,t){return(n,r)=>{let o=n.params.id;if(typeof o!="string"||o.length===0){r.status(400).json({error:"Missing proposal id"});return}let s=n.body??{},i=typeof s.reason=="string"?s.reason:"Rejected by admin";try{let d=se(e.reject(o,Fa(n),i),U(n,t));r.json(d)}catch(d){if(d instanceof re||d instanceof ee||d instanceof J){vt(r,d.statusCode,d.code);return}F(r,d);return}}}a(ja,"rejectProposalHandler");function Ha(e,t,n){return(r,o)=>{if(!r.accessLink){o.setHeader("WWW-Authenticate",K),o.status(401).json({error:le,code:Re});return}let s=r.params.id;if(typeof s!="string"||s.length===0){o.status(400).json({error:"Missing proposal id",code:"missing_proposal_id"});return}let i=r.body??{},d=U(r,n),l=oe(r.query);try{let c=Ut(e,t,r.accessLink,s,i.reason,d);o.json(ue(e,r.accessLink,c,{...d,include:l}))}catch(c){let u=N(c,"rest");if(u){vt(o,u.statusCode,u.body.code);return}vt(o,500,"internal_error");return}}}a(Ha,"withdrawProposalHandler");function Ua(e,t){return async(n,r)=>{let o=n.params.id;if(typeof o!="string"||o.length===0){r.status(400).json({error:"Missing proposal id"});return}try{let s=n.accessLink?Me(e,n.accessLink,o):e.get(o),i=Vt(n.query.format);if(!i.ok){r.status(400).json({error:"Invalid diff format",code:"invalid_diff_format",hint:"Valid formats: summary, full (default summary)."});return}let d=Kt(await Zt(s,t),i.format);r.json(d)}catch(s){if(s instanceof re){vt(r,404,s.code);return}if(s instanceof B||s instanceof be||s instanceof Le){vt(r,s.statusCode,s.code);return}let i=N(s,"rest");if(i){r.status(i.statusCode).json(i.body);return}F(r,s);return}}}a(Ua,"getProposalDiffHandler");ve();ve();nr();function ff(e){return e.endsWith("/")?e.slice(0,-1):e}a(ff,"trimTrailingSlash");var Br=class{static{a(this,"HttpAdapter")}kind="http";baseUrl;accessToken;fetchImpl;constructor(t){this.baseUrl=ff(t.baseUrl),this.accessToken=t.accessToken,this.fetchImpl=t.fetch??globalThis.fetch.bind(globalThis)}headers(t){let n=new Headers(t);return n.set("Accept","application/json"),n.set("Node-RED-API-Version","v2"),this.accessToken&&n.set("Authorization",`Bearer ${this.accessToken}`),n}async request(t,n={}){return await this.fetchImpl(`${this.baseUrl}${t}`,{...n,headers:this.headers(n.headers)})}async readJson(t,n){let r=await t.text();if(!t.ok){let o=r;try{let s=JSON.parse(r);o=s.message??s.code??r}catch{}throw t.status===404?new B(n):t.status===409?new W("unknown","unknown",`Admin API ${n}`):t.status===401||t.status===403?new Le(Xn):new Error(`Admin API ${n} failed (${t.status}): ${o}`)}if(!r)return null;try{return JSON.parse(r)}catch{throw new Error(`Admin API ${n} returned non-JSON`)}}async getFlows(){let t=await this.request("/flows"),n=await this.readJson(t,"GET /flows");return vn(n,"GET /flows")}async getFlow(t){let n=encodeURIComponent(t),[r,o]=await Promise.all([this.request("/flows"),this.request(`/flow/${n}`)]);if(o.status===404)throw new B(t);let s=await this.readJson(r,"GET /flows"),i=await this.readJson(o,`GET /flow/${t}`);return{rev:vn(s,"GET /flows").rev,flow:Nt(t,i)}}async updateFlow(t){let n=t.deploymentType??"nodes",r=await this.getFlows();if(r.rev!==t.expectedRev)throw new W(t.expectedRev,r.rev,"updateFlow");let o=tr(r.flows,t.flowId,t.flow),s=await this.request("/flows",{method:"POST",headers:{"Content-Type":"application/json","Node-RED-Deployment-Type":n},body:JSON.stringify({flows:o,rev:t.expectedRev})});if(s.status===409){let d=r.rev;try{let l=await s.clone().json();typeof l.current_rev=="string"&&(d=l.current_rev)}catch{}throw new W(t.expectedRev,d,"updateFlow")}let i=await this.readJson(s,"POST /flows");if(!i||typeof i!="object"||typeof i.rev!="string")throw new Error("Admin API POST /flows did not return a rev");return{flowId:t.flowId,rev:i.rev}}};ve();nr();function mf(e){let t=e.runtime?.flows;if(!t?.getFlows||!t.getFlow||!t.setFlows)throw new Error("RED.runtime.flows is missing getFlows, getFlow, or setFlows");return t}a(mf,"requireFlowsApi");function hf(e){if(typeof e=="string")return e;if(typeof e.rev=="string"&&e.rev.length>0)return e.rev;throw new Error("RED.runtime.flows.setFlows did not return a rev")}a(hf,"resolveRev");var qr=class{static{a(this,"RedAdapter")}kind="red";flows;constructor(t){this.flows=mf(t)}async getFlows(){let t=await this.flows.getFlows({});return vn(t,"RED.runtime.flows.getFlows()")}async getFlow(t){let n=await this.getFlows(),r;try{r=await this.flows.getFlow({id:t})}catch(o){throw gf(o)?new B(t):o}return{rev:n.rev,flow:Nt(t,r)}}async updateFlow(t){let n=t.deploymentType??"nodes",r=await this.getFlows();if(r.rev!==t.expectedRev)throw new W(t.expectedRev,r.rev,"updateFlow");let o=tr(r.flows,t.flowId,t.flow),s=await this.flows.setFlows({flows:{flows:o,rev:t.expectedRev},deploymentType:n});return{flowId:t.flowId,rev:hf(s)}}};function Ba(e){let t=e?.runtime?.flows;return!!(t?.getFlows&&t.getFlow&&t.setFlows)}a(Ba,"canUseRedRuntime");function gf(e){if(!e||typeof e!="object")return!1;let t=e;if((typeof t.status=="number"?t.status:t.statusCode)===404||t.code==="not_found"||t.code==="flow_not_found")return!0;let r=typeof t.message=="string"?t.message.toLowerCase():"";return r.includes("not found")||r.includes("unknown flow")}a(gf,"isRuntimeFlowNotFound");var qa=require("module");function Wa(e){if(!e||typeof e!="object")return!1;let t=e;return typeof t.getFlows=="function"&&typeof t.getFlow=="function"&&typeof t.setFlows=="function"}a(Wa,"isFlowsApi");function yf(){if(typeof require=="function")return require;let e=(0,eval)("import.meta.url");return(0,qa.createRequire)(e)}a(yf,"createPluginRequire");function wf(e){try{let t=e("@node-red/runtime/lib/settings");return typeof t.available=="function"&&t.available()===!0}catch{return!1}}a(wf,"isNodeRedRuntimeInitialized");function bf(){try{let e=yf();if(!wf(e))return null;let t=e("@node-red/runtime");return Wa(t.flows)?t.flows:null}catch{return null}}a(bf,"tryRequireInitializedRuntimeFlows");function Ga(e){if(e?.runtime&&Wa(e.runtime.flows))return e;let t=bf();return t?{runtime:{flows:t}}:null}a(Ga,"resolveRuntimeHost");function za(e={}){let t=Ga(e.RED??null);if(t&&Ba(t))return new qr(t);let n=e.adminAuthEnabled===!0;if(e.http?.baseUrl){if(n&&!e.http.accessToken)throw new be("In-process Node-RED flow runtime is unavailable and HTTP Admin API fallback has no credentials while adminAuth is enabled. Ensure the plugin runs inside Node-RED so @node-red/runtime.flows can be used.");return new Br(e.http)}throw new be("No usable Node-RED flow adapter: in-process flows API is unavailable and no HTTP Admin API baseUrl was provided")}a(za,"createFlowAdapter");ve();var Gr=require("fs"),Ja=require("path");Qn();q();var vf=/^[a-z][a-z0-9._-]{0,63}$/;function kf(e){if(e.kind==="system")return{kind:"system"};if(e.kind==="access-link"){let n={kind:"access-link",linkId:String(e.linkId).replace(/\0/g,"").slice(0,200)};return typeof e.label=="string"&&(n.label=e.label.replace(/\0/g,"").slice(0,200)),n}let t={kind:"admin"};return typeof e.username=="string"&&(t.username=e.username.replace(/\0/g,"").slice(0,200)),t}a(kf,"sanitizeActor");var Wr=class{constructor(t,n={}){this.paths=t;this.filePath=(0,Ja.join)(t.logs,"audit.jsonl"),this.now=n.now??(()=>Date.now()),this.maxAppendBytes=n.maxAppendBytes??ss}static{a(this,"AuditLog")}filePath;now;maxAppendBytes;seq=0;append(t,n,r){let o=vf.test(t)?t:"unknown";this.seq+=1;let s={id:`log-${this.now()}-${this.seq}`,at:new Date(this.now()).toISOString(),action:o,actor:kf(n)};r!==void 0&&(s.details=As(r));let i=De(s,this.maxAppendBytes,{kind:"audit"});return as(this.paths.root,this.filePath,i),s}list(t=100){if(!(0,Gr.existsSync)(this.filePath))return[];let n=(0,Gr.readFileSync)(this.filePath,"utf8").split(`
5815
- `).map(o=>o.trim()).filter(Boolean),r=[];for(let o of n)try{r.push(JSON.parse(o))}catch{}return r.slice(-Math.max(1,t)).reverse()}};Ue();Wn();ae();it();ve();Ue();Ge();Ae();var Af="Flow revision conflict: the expectedRev is stale. Ask the AI to re-read flows and resubmit a new proposal.";function Va(e){let t=se(e),n=dr(e)||void 0;return{proposalId:e.id,title:t.title,displayName:t.displayName,flowId:e.payload.flowId,...n?{flowLabel:n}:{}}}a(Va,"auditProposalMeta");var zr=class{constructor(t,n,r,o){this.proposals=t;this.audit=n;this.adapter=r;this.getDeploymentType=o}static{a(this,"ProposalService")}async approve(t,n,r={}){let o=this.proposals.beginApprove(t),s=Va(o),i=r.deploymentType??this.getDeploymentType();try{let d=o.type;if(d!=="replace_flow"){let l=this.proposals.markFailed(t,n,`Unsupported proposal type '${d}'`);return this.audit.append("proposal.failed",n,{...s,reason:l.reason}),l}try{let l=await this.adapter.updateFlow({flowId:o.payload.flowId,flow:o.payload.flow,expectedRev:o.expectedRev,deploymentType:i}),c=this.proposals.markApproved(t,n,l.rev);return this.audit.append("proposal.approved",n,{...s,resultRev:l.rev,flowId:o.payload.flowId}),Vs(this.proposals,o.payload.flowId,t,l.rev),c}catch(l){if(l instanceof W){let p=this.proposals.markRejected(t,n,Af);return this.audit.append("proposal.rejected",n,{...s,reason:p.reason,code:l.code,expectedRev:l.expectedRev,currentRev:l.currentRev}),p}let c=l instanceof Error?l.message:String(l),u=this.proposals.markFailed(t,n,c);return this.audit.append("proposal.failed",n,{...s,reason:c}),u}}finally{this.proposals.releaseLock(t)}}reject(t,n,r){let o=this.proposals.get(t);if(o.status!=="pending")throw new ee(t,o.status);let s=Va(o),i=this.proposals.markRejected(t,n,r.trim()||"Rejected by admin");return this.audit.append("proposal.rejected",n,{...s,reason:i.reason}),i}};_e();function xf(e){let t=a(()=>Promise.reject(new Le(e)),"fail");return{kind:"http",getFlows:t,getFlow:t,updateFlow:t}}a(xf,"misconfiguredAdapter");function Ka(e){let t=e.settings,n=typeof t.uiPort=="number"?t.uiPort:1880,r=t.httpAdminRoot??"/",o=!r||r==="/"?"":r.replace(/\/$/,"");return`http://127.0.0.1:${n}${o}`}a(Ka,"localAdminBaseUrl");function Jr(e,t={}){let n=Hn(e.settings.userDir);ds(n);let r=t.settingsStore??new Tt(n,e.settings.aiAccess,u=>e.log?.warn?.(u));t.bindSettings!==!1&&Gn(r),Jn(r.getEffective().publicBaseUrl);let o=t.linkStore??jr(e);o.configureDefaults({defaultTtlSeconds:r.getEffective().defaultLinkTtlSeconds,maxActiveLinks:r.getEffective().maxActiveLinks});let s=new Zn(n),i=new Wr(n),d;if(t.flowAdapter)d=t.flowAdapter;else try{d=za({RED:e,http:{baseUrl:Ka(e)},adminAuthEnabled:V(e.settings)})}catch(u){let p=u instanceof be?u.message:Xn;e.log?.warn?.(`AI Access flow adapter unavailable: ${p}`),d=xf(p)}let l;if(t.nodeTypesAdapter)l=t.nodeTypesAdapter;else if(t.flowAdapter)l=ho();else try{l=Fs({baseUrl:Ka(e)})}catch(u){let p=u instanceof Error?u.message:String(u);e.log?.warn?.(`AI Access node-types adapter unavailable: ${p}`),l=ho([])}let c=new zr(s,i,d,()=>ge(e.settings).defaultDeploymentType);return{linkStore:o,proposalStore:s,auditLog:i,flowAdapter:d,nodeTypesAdapter:l,proposalService:c,settingsStore:r,...t.catalogueFetcher?{catalogueFetcher:t.catalogueFetcher}:{},...t.catalogueUrl?{catalogueUrl:t.catalogueUrl}:{}}}a(Jr,"createPluginServices");q();ae();Pe();Un();var Xa=dn(require("dns"),1),Ya=dn(require("http"),1),Qa=dn(require("https"),1),Mn=require("net");ae();function Sf(e,t){return e===10||e===172&&t>=16&&t<=31||e===192&&t===168}a(Sf,"isPrivateV4");function Tf(e,t){return e===169&&t===254}a(Tf,"isLinkLocalV4");function _f(e,t,n){return e===0||e===100&&t>=64&&t<=127||e>=224||e===192&&t===0&&(n===0||n===2)||e===198&&(t===18||t===19||t===51&&n===100)||e===203&&t===0&&n===113}a(_f,"isSpecialUseV4");function Pf(e){let[t=0,n=0,r=0]=e.split(".").map(Number);return t===127?"loopback":Sf(t,n)||Tf(t,n)||_f(t,n,r)||e==="168.63.129.16"?"blocked":"public"}a(Pf,"classifyIpv4");function Rf(e){let t=e.split(":"),n=t.indexOf(""),r=[];for(let o of t){if(!o)continue;if(o.includes(".")){let i=o.split(".").map(Number);if(i.length!==4||i.some(d=>!Number.isInteger(d)||d<0||d>255))return;r.push((i[0]??0)<<8|(i[1]??0)),r.push((i[2]??0)<<8|(i[3]??0));continue}let s=Number.parseInt(o,16);if(!/^[\da-f]{1,4}$/i.test(o)||!Number.isInteger(s))return;r.push(s)}if(n>=0){let o=8-r.length;if(o<1)return;let s=t.slice(0,n).filter(Boolean).length;r.splice(s,0,...Array(o).fill(0))}else if(r.length!==8)return;return r.length===8?r:void 0}a(Rf,"expandIpv6");function Lf(e){let t=Rf(e);if(!t)return"blocked";if(t.slice(0,7).every(s=>s===0)&&t[7]===1)return"loopback";let n=t[0]??0,r=t[1]??0;return n===0||n<8192||n>16383||n===8193&&r<=511||n===8193&&r===3512||n===16383&&r<=4095||n===8194?"blocked":"public"}a(Lf,"classifyIpv6");function $n(e){let t=nt(e);return t?(0,Mn.isIP)(t)===4?Pf(t):Lf(t):"blocked"}a($n,"classifyPublicUrlAddress");function Cf(e){let t=e.toLowerCase().replace(/\.$/,"");return["metadata","metadata.google.internal","metadata.google.com","metadata.goog","instance-data","instance-data.ec2.internal","metadata.azure.internal","metadata.aws.internal"].some(n=>t===n||t.endsWith(`.${n}`))}a(Cf,"isMetadataHostname");async function Za(e,t,n){let r=_t(e);if(!r)throw new Error("publicBaseUrl is required for Check");let o=new URL(r),s=o.hostname.replace(/^\[|\]$/g,"");if(Cf(s))throw new Error("Public URL Check cannot target metadata hosts");let i=nt(s),l=(typeof e=="string"?/^https?:\/\/(\[[^\]]+\]|[^/?#:]+)/i.exec(e.trim())?.[1]:void 0)?.replace(/^\[|\]$/g,"");if(i&&l&&l.toLowerCase()!==i.toLowerCase())throw new Error("Public URL Check requires canonical IP address notation");let c=s.toLowerCase().replace(/\.$/,"")==="localhost"||i!==void 0&&$n(i)==="loopback",u=Number(o.port||(o.protocol==="https:"?443:80));if(c&&u!==t)throw new Error("Public URL Check may only target this Node-RED instance's port for loopback URLs");let p=a(g=>{let v=$n(g);if(v==="blocked"||v==="loopback"&&!c||c&&v!=="loopback")throw new Error("Public URL Check cannot target private or non-public addresses")},"validateAddress");if(i)return p(i),{origin:r,address:i,family:(0,Mn.isIP)(i)};let f=await new Promise((g,v)=>{let A=a(()=>v(n.reason instanceof Error?n.reason:new Error(String(n.reason))),"abort");n.throwIfAborted(),n.addEventListener("abort",A,{once:!0}),Xa.default.lookup(s,{all:!0},(E,D)=>{n.removeEventListener("abort",A),E?v(E):g(D)})});if(n.throwIfAborted(),!f.length)throw new Error("Public URL Check hostname has no addresses");for(let g of f)p(g.address);let m=[];for(let g of f){let v=nt(g.address);if(!v)throw new Error("Public URL Check returned an invalid address");m.some(A=>A.address===v)||m.push({address:v,family:(0,Mn.isIP)(v)})}c&&m.sort((g,v)=>g.family-v.family);let h=m[0];if(!h)throw new Error("Public URL Check hostname has no addresses");return{origin:r,...h,addresses:m}}a(Za,"preparePublicUrlCheck");function Ef(e,t,n){let r=e.hostname.replace(/^\[|\]$/g,"");return new Promise((o,s)=>{let d=(e.protocol==="https:"?Qa.default:Ya.default).request(e,{method:"GET",agent:!1,signal:n,headers:{Host:e.host},...(0,Mn.isIP)(r)?{}:{servername:r},lookup:a((l,c,u)=>{c.all?u(null,[{address:t.address,family:t.family}]):u(null,t.address,t.family)},"lookup")},l=>{o(l.statusCode??0),l.destroy()});d.on("error",s),d.end()})}a(Ef,"requestPinned");async function ed(e,t,n){let r=new URL(e.origin+t),o=e.addresses?.length?e.addresses:[{address:e.address,family:e.family}],s;for(let i of o){n.throwIfAborted();try{return await Ef(r,i,n)}catch(d){if(n.aborted)throw d;s=d}}throw s instanceof Error?s:new Error("Public URL Check request failed")}a(ed,"requestPublicUrlCheck");_e();function If(e){let t=ne(e.settings.httpAdminRoot);return t==="/"?"/mcp":`${t}/mcp`}a(If,"mcpPageUrl");function Nf(e){return typeof e=="boolean"||typeof e=="number"||typeof e=="string"?e:Array.isArray(e)?e.join(", "):typeof e=="function"?"custom function":!1}a(Nf,"normalizeTrustProxy");function rd(e){if(!e)return"Set a Public URL before enabling OAuth.";let t=new URL(e),n=nt(t.hostname),r=t.hostname.toLowerCase().replace(/\.$/,"")==="localhost"||n==="::1"||n?.startsWith("127.");return t.protocol==="http:"&&!r?"OAuth Public URL uses HTTP; HTTPS is recommended.":null}a(rd,"oauthPublicUrlWarning");function od(e,t,n){let{sources:r,...o}=t.getEffective(),{permission:s}=zn(e,n);return{ok:!0,adminAuthEnabled:V(e.settings),canEdit:s==="*",permission:s,mcpPageUrl:If(e),apiBase:de(e.settings.httpAdminRoot),settings:o,oauthPublicUrlWarning:rd(o.publicBaseUrl),sources:r,disk:t.getDisk(),trustProxy:Nf(n.app.get("trust proxy")),precedence:["settings.js","disk","default"]}}a(od,"settingsResponse");function sd(e,t){return(n,r)=>{r.json(od(e,t,n))}}a(sd,"getSettingsHandler");function td(e,t,n){let r=e[t];if(r!==void 0){if(typeof r!="boolean")throw new Error(`${t} must be a boolean`);n[t]=r}}a(td,"parseBooleanField");function nd(e,t,n){let r=e[t];if(r!==void 0){if(typeof r!="number"||!Number.isFinite(r)||r<=0)throw new Error(`${t} must be a positive number`);n[t]=Math.floor(r)}}a(nd,"parsePositiveIntField");function Ff(e,t,n){let r=e[t];if(r===void 0)return;let o=t==="rateLimitWindowMs"?100:1,s=t==="rateLimitWindowMs"?36e5:1e6;if(typeof r!="number"||!Number.isInteger(r)||r<o||r>s)throw new Error(`${t} must be an integer between ${o} and ${s}`);n[t]=r}a(Ff,"parseRateLimitField");function Of(e,t){if(e.corsAllowedOrigins===void 0)return;if(!Array.isArray(e.corsAllowedOrigins))throw new Error("corsAllowedOrigins must be a list of HTTP(S) origins");let n=e.corsAllowedOrigins.map(r=>un(r));if(n.some(r=>r===void 0))throw new Error("corsAllowedOrigins must contain only HTTP(S) origins without paths, credentials, queries, or fragments");t.corsAllowedOrigins=[...new Set(n)]}a(Of,"parseCorsField");function id(e,t,n){return(r,o)=>{let s=r.body;if(!s||typeof s!="object"||Array.isArray(s)){o.status(400).json({error:"Request body must be a settings object",code:"invalid_body"});return}let i={...s};for(let[l,c]of Object.entries(t.getEffective().sources))c==="settings.js"&&delete i[l];t.getEffective().sources.oauth==="settings.js"&&delete i.oauth;let d={};try{if(i.oauth!==void 0){if(!i.oauth||typeof i.oauth!="object"||Array.isArray(i.oauth))throw new Error("oauth must be an object");let l=i.oauth,c={};if(l.enabled!==void 0){if(typeof l.enabled!="boolean")throw new Error("oauth.enabled must be a boolean");c.enabled=l.enabled}for(let p of["accessTokenTtlSec","refreshIdleTtlSec","dcrMaxClients"])if(l[p]!==void 0){if(typeof l[p]!="number"||!Number.isInteger(l[p])||l[p]<1||l[p]>31536e3)throw new Error(`oauth.${p} must be a positive integer`);c[p]=l[p]}let u=l.refreshMaxLifetimeSec;if(u!==void 0){if(typeof u!="number"||!Number.isInteger(u)||u<0||u>31536e4)throw new Error("oauth.refreshMaxLifetimeSec must be a non-negative integer");c.refreshMaxLifetimeSec=u}d.oauth=c}td(i,"enabled",d),td(i,"allowWithoutAdminAuth",d),nd(i,"defaultLinkTtlSeconds",d),nd(i,"maxActiveLinks",d)}catch(l){o.status(400).json({error:l.message,code:"invalid_body"});return}if(i.defaultDeploymentType!==void 0){let l=i.defaultDeploymentType;if(l!=="nodes"&&l!=="flows"&&l!=="full"){o.status(400).json({error:"defaultDeploymentType must be 'nodes', 'flows', or 'full'",code:"invalid_body"});return}d.defaultDeploymentType=l}try{i.publicBaseUrl!==void 0&&(d.publicBaseUrl=_t(i.publicBaseUrl)),Of(i,d);for(let l of["rateLimitWindowMs","rateLimitMaxPerIp","rateLimitMaxPerToken"])Ff(i,l,d)}catch(l){o.status(400).json({error:l.message,code:"invalid_body"});return}try{let l=t.patch(d);Jn(l.publicBaseUrl),n.configureDefaults({defaultTtlSeconds:l.defaultLinkTtlSeconds,maxActiveLinks:l.maxActiveLinks});let c=od(e,t,r),u=l.publicBaseUrl?new URL(l.publicBaseUrl):void 0,p=u?nt(u.hostname.replace(/^\[|\]$/g,"")):void 0,f=p?$n(p):void 0,m=[];f!==void 0&&f!=="public"&&m.push("Public URL uses a private or non-public IP address.");let h=rd(l.publicBaseUrl);h&&l.publicBaseUrl&&m.push(h),o.json({...c,...m.length?{warnings:m}:{}})}catch(l){if(l instanceof J){o.status(l.statusCode).json({error:l.message,code:l.code});return}F(o,l);return}}}a(id,"putSettingsHandler");function ad(e){return async(t,n)=>{let r=new AbortController,o=setTimeout(()=>r.abort(),5e3),s;try{let i;try{let c=e.settings,p=e.server?.address(),f=t.body?.publicBaseUrl,m=typeof f=="string"?new URL(f.trim()).hostname:"";if((m.toLowerCase().replace(/^\[|\]$/g,"").replace(/\.$/,"")==="localhost"||$n(m.replace(/^\[|\]$/g,""))==="loopback")&&typeof p=="string")throw new Error("Public URL Check does not support loopback URLs when Node-RED listens on a Unix socket");let g=p&&typeof p=="object"?p.port:c.uiPort??1880;i=await Za(t.body?.publicBaseUrl,g,r.signal)}catch(c){if(r.signal.aborted)throw c;n.status(400).json({error:c.message,code:"invalid_body"});return}s=i.origin+de(e.settings.httpAdminRoot);let d=await ed(i,de(e.settings.httpAdminRoot),r.signal),l={status:d,ok:d>=200&&d<300};n.json({ok:l.ok,status:l.status,url:s,message:l.ok?"Discovery endpoint is reachable":`Discovery endpoint returned HTTP ${l.status}`})}catch{n.json({ok:!1,status:null,url:s,error:r.signal.aborted?"Public URL check timed out after 5 seconds":"Could not reach the discovery endpoint"})}finally{clearTimeout(o)}}}a(ad,"checkPublicUrlHandler");tt();_e();G();Wn();var Mf="com55-ai-access";function $f(e,t,n){e.httpAdmin.get("/mcp",t,(r,o)=>{o.setHeader("Cache-Control","no-store, no-cache, must-revalidate, max-age=0"),o.setHeader("Pragma","no-cache"),o.setHeader("Expires","0"),o.type("html").status(200).send(Na({apiBase:de(e.settings.httpAdminRoot),mcpRpc:je(e.settings.httpAdminRoot),editorRoot:ne(e.settings.httpAdminRoot),authTokenKey:Kn(e.settings.httpAdminRoot)}))}),e.httpAdmin.get("/mcp/api/v1",t,n,$o(e)),e.httpAdmin.get("/mcp/api/v1/",t,n,$o(e)),e.httpAdmin.get("/mcp/api/v1/openapi.yaml",t,n,La()),e.httpAdmin.get("/mcp/api/v1/openapi.json",t,n,Ca()),e.httpAdmin.get("/mcp/editor/ai-access-sidebar.js",t,(r,o)=>{o.setHeader("Cache-Control","no-store, no-cache, must-revalidate, max-age=0"),o.setHeader("Pragma","no-cache"),o.setHeader("Expires","0"),o.setHeader("X-AI-Access-Sidebar-Version",Jt),o.type("application/javascript").status(200).send(hr())})}a($f,"registerMcpShellAndDiscovery");var Df=/^\/mcp\/(?:api\/v1\/proposals|rpc$|a\/[^/]+\/(?:proposals|propose|draft))/;function ld(e,t={}){let n=Jr(e,{...t,linkStore:t.linkStore??jr(e)}),r=Ti(()=>n.settingsStore.getEffective()),{ip:o,token:s}=r,i=new Hr(Hn(e.settings.userDir),()=>n.settingsStore.getEffective().oauth.dcrMaxClients),d=a(()=>n.settingsStore.getEffective().publicBaseUrl,"publicBase"),l=a(()=>`${d().replace(/\/$/,"")}/mcp`,"mcpUrl"),c=new Ur(i,l,()=>n.settingsStore.getEffective().oauth.accessTokenTtlSec,()=>n.settingsStore.getEffective().oauth.refreshIdleTtlSec,()=>n.settingsStore.getEffective().oauth.refreshMaxLifetimeSec),u=Ta(e,c,d,l);cs(e.httpAdmin,ls(()=>n.settingsStore.getEffective().corsAllowedOrigins));let p={authorization:[],metadata:[],httpNodeRootFallback:e.settings.httpNodeRoot===!1||!e.httpNode},f=a(()=>{p.authorization=[...rn(e.httpAdmin,["/mcp/oauth/"]),...rn(e.httpNode,["/mcp/oauth/"])];let y=p.httpNodeRootFallback?["/mcp/.well-known/oauth-"]:["/.well-known/oauth-"];p.metadata=[...rn(e.httpAdmin,y),...rn(e.httpNode,y)];try{e.nodes?.eachNode(k=>{if(k.type!=="http in"||typeof k.url!="string")return;let T=k.url.startsWith("/")?k.url:`/${k.url}`;T.startsWith("/mcp/oauth/")&&p.authorization.push(`http-in node ${T}`),T.startsWith("/.well-known/oauth-")&&p.metadata.push(`http-in node ${T}`)})}catch{}let w=e.settings.httpStatic?[e.settings.httpStatic].flat():[];for(let k of w)for(let T of[".well-known/oauth-authorization-server",".well-known/oauth-protected-resource","mcp/oauth/authorize","mcp/oauth/token","mcp/oauth/register","mcp/oauth/revoke"])(0,dd.existsSync)((0,Vr.join)(k,T))&&(T.startsWith("mcp/")?p.authorization.push(`httpStatic ${(0,Vr.join)(k,T)}`):p.metadata.push(`httpStatic ${(0,Vr.join)(k,T)}`));for(let k of[...p.authorization,...p.metadata])e.log?.warn?.(`AI Access OAuth route collision: ${k}`)},"refreshCollisions");f(),e.events?.on("flows:started",f);let m=Ho({windowMs:6e4,maxPerIp:30}),h=a((y,w,k)=>{if(!n.settingsStore.getEffective().oauth.enabled||p.authorization.length||!y.path.startsWith("/mcp/oauth/")){k();return}let T=y.path.replace(/\/+$/,"").toLowerCase(),_=a(()=>Rt(e)(y,w,()=>u.server(y,w,k)),"run");T==="/mcp/oauth/authorize"?o(y,w,()=>m(y,w,_)):T.startsWith("/mcp/oauth/consent/")?o(y,w,_):_()},"oauthServerMiddleware");h.oauthOwned=!0,e.httpAdmin.use(h);let g=a((y,w,k)=>{if(!n.settingsStore.getEffective().oauth.enabled||p.metadata.length){k();return}u.metadata(y,w,k)},"metadataHandler");g.oauthOwned=!0,p.httpNodeRootFallback?e.httpAdmin.use("/mcp",g):e.httpNode?.use(g),e.httpAdmin.get("/mcp/api/v1/oauth/clients",o,s,we(e,"api"),(y,w)=>w.json({clients:i.data.clients.map(k=>({client_id:k.client_id,client_name:k.client_name,redirect_uris:k.redirect_uris,lastUsedAt:k.lastUsedAt}))})),e.httpAdmin.get("/mcp/api/v1/oauth/status",o,s,we(e,"api"),(y,w)=>w.json({...p,collisions:[...p.authorization,...p.metadata],oauthEnabled:n.settingsStore.getEffective().oauth.enabled})),e.httpAdmin.delete("/mcp/api/v1/oauth/clients/:id",o,s,we(e,"api"),mn(e),(y,w)=>w.json({revoked:i.revokeClient(String(y.params.id))})),e.httpAdmin.use((y,w,k)=>{if(!Df.test(y.path)){k();return}n.flowAdapter.getFlows().then(T=>qs(y,ir(T.flows))).catch(()=>{}).finally(()=>k())}),Xi(e,{linkStore:n.linkStore,proposalStore:n.proposalStore,auditLog:n.auditLog,flowAdapter:n.flowAdapter,nodeTypesAdapter:n.nodeTypesAdapter,...n.catalogueFetcher?{catalogueFetcher:n.catalogueFetcher}:{},...n.catalogueUrl?{catalogueUrl:n.catalogueUrl}:{},rateLimiters:r}),$f(e,o,s),e.httpAdmin.get("/mcp/api/v1/status",o,s,hs(e,y=>n.linkStore.validateResult(y)),P((y,w)=>{w.json(en(e,y))})),e.httpAdmin.get("/mcp/api/v1/settings",o,s,we(e,"api"),P(sd(e,n.settingsStore))),e.httpAdmin.put("/mcp/api/v1/settings",o,s,we(e,"api"),mn(e),Ve("rest"),P(id(e,n.settingsStore,n.linkStore))),e.httpAdmin.post("/mcp/api/v1/settings/check-public-url",o,s,we(e,"api"),mn(e),Ve("rest"),P(ad(e)));let v=[o,s,we(e,"api"),Rt(e),mn(e)];e.httpAdmin.get("/mcp/api/v1/links",...v,P(sa(e,n.linkStore))),e.httpAdmin.post("/mcp/api/v1/links",...v,P(ia(e,n.linkStore,n.auditLog))),e.httpAdmin.delete("/mcp/api/v1/links/:id",...v,P(aa(e,n.linkStore,n.auditLog)));let A=[o,s,Rt(e),ms(e,y=>n.linkStore.validateResult(y))],E=n.flowAdapter,O={nodeTypes:n.nodeTypesAdapter,...n.catalogueFetcher?{catalogueFetcher:n.catalogueFetcher}:{},...n.catalogueUrl?{catalogueUrl:n.catalogueUrl}:{}};e.httpAdmin.get("/mcp/api/v1/flows",...A,P(mi(E))),e.httpAdmin.get("/mcp/api/v1/flows/:id",...A,P(Pr(E))),e.httpAdmin.get("/mcp/api/v1/nodes/:id",...A,P(Rr(E))),e.httpAdmin.get("/mcp/api/v1/nodes/:id/field/:name",...A,P(Lr(E))),e.httpAdmin.get("/mcp/api/v1/search",...A,P(Cr(E))),e.httpAdmin.get("/mcp/api/v1/node-types",...A,P(Er(O))),e.httpAdmin.get("/mcp/api/v1/node-types/:type",...A,P(Ir(O))),e.httpAdmin.get("/mcp/api/v1/palette/search",...A,P(Nr(O)));let te=e.settings.httpAdminRoot;e.httpAdmin.get("/mcp/api/v1/proposals",...A,P(Ma(n.proposalStore,n.flowAdapter,te))),e.httpAdmin.get("/mcp/api/v1/proposals/:id",...A,P($a(n.proposalStore,te,n.flowAdapter))),e.httpAdmin.get("/mcp/api/v1/proposals/:id/diff",...A,P(Ua(n.proposalStore,n.flowAdapter))),e.httpAdmin.post("/mcp/api/v1/proposals",o,s,Ve("rest"),Ct(e,n.linkStore,"rest"),P(Oa(n.proposalStore,n.auditLog,n.flowAdapter,te,n.nodeTypesAdapter))),e.httpAdmin.post("/mcp/api/v1/proposals/:id/approve",...v,Ve("rest"),P(Da(n.proposalService,te))),e.httpAdmin.post("/mcp/api/v1/proposals/:id/reject",...v,Ve("rest"),P(ja(n.proposalService,te))),e.httpAdmin.post("/mcp/api/v1/proposals/:id/withdraw",o,s,Ve("rest"),Ct(e,n.linkStore,"rest"),P(Ha(n.proposalStore,n.auditLog,te)));let me=[o,s,(y,w,k)=>{let T=y.headers.authorization;if(!n.settingsStore.getEffective().oauth.enabled||T?.toLowerCase().startsWith("bearer aia_")){if(n.settingsStore.getEffective().oauth.enabled){let _=w.setHeader.bind(w);w.setHeader=((C,Z)=>{if(C.toLowerCase()==="www-authenticate"&&typeof Z=="string"&&!Z.includes("resource_metadata=")){let he=`${d().replace(/\/$/,"")}${p.httpNodeRootFallback?"/mcp":""}/.well-known/oauth-protected-resource/mcp`;Z+=`, resource_metadata="${he}", scope="read propose"`}return _(C,Z)})}Ct(e,n.linkStore,"mcp")(y,w,k);return}Rt(e)(y,w,()=>u.auth(y,w,k))}];e.httpAdmin.post("/mcp/rpc",...me,Ve("mcp"),P(ha(e,n))),e.httpAdmin.get("/mcp/rpc",...me,Qo),e.httpAdmin.delete("/mcp/rpc",...me,Qo),e.httpAdmin.get("/mcp/api/v1/logs",o,s,we(e,"api"),Rt(e),P(da(n.auditLog,n.linkStore))),e.httpAdmin.use("/mcp/api/v1",ui()),ta(e)}a(ld,"registerRoutes");function jf(e){e.plugins.registerPlugin(Mf,{type:"com55-ai-access",onadd(){ld(e),e.log?.info?.("Node-RED AI Access plugin loaded (/mcp)")}})}a(jf,"registerAiAccessPlugin");var Hf=jf;0&&(module.exports={ACCESS_LINK_INVALID_MESSAGE,ACCESS_LINK_MISSING_MESSAGE,AccessLinkStore,EDITOR_SIDEBAR_ASSET_VERSION,MAX_PROPOSAL_JSON_BYTES,SettingsStore,areAiFeaturesEnabled,bindSettingsStore,createPluginServices,getStorageDir,isAdminAuthEnabled,packageVersion,registerRoutes,renderEditorSidebarJs,renderEditorSidebarLoaderHtml,requireAccessLink,resolveAiAccessSettings});
6400
+ </html>`}a(Ba,"renderMcpPage");Ae();ve();Ge();G();it();K();Fe();Xe();Ae();function Tt(e,t,n,r={}){e.setHeader("X-Content-Type-Options","nosniff"),e.setHeader("Content-Type","application/json; charset=utf-8");let o={error:Oe[n]??"Request failed",code:n,...r},s=se(n,"rest");s&&typeof o.hint!="string"&&(o.hint=s),e.status(t).json(o)}a(Tt,"sendSafeJsonError");function qa(e){let t={kind:"admin"};return typeof e.user?.username=="string"&&(t.username=e.user.username),t}a(qa,"adminActor");function Wa(e,t,n,r,o){return async(s,i)=>{if(!s.accessLink){i.setHeader("WWW-Authenticate",Y),i.status(401).json({error:le,code:Le});return}try{let d=q(s,r),l=re(s.query),c=await Ye(e,t,s.accessLink,s.body??{},n,d,o),u=V(e,c.expectedRev,{flowId:c.payload.flowId}),p=Ie(e,e.get(c.id),s.accessLink.id),f=c.warnings;i.status(201).json({...ce(c,{...d,include:l}),mine:!0,...Ne(p),...Q(u),...f&&f.length>0?{warnings:f}:{}})}catch(d){let l=O(d,"rest");if(l){i.status(l.statusCode).json(l.body);return}$(i,d);return}}}a(Wa,"createProposalHandler");function za(e,t,n){return async(r,o)=>{let s,i;try{let S=await t.getFlows();s=S.rev,i=dr(S.flows)}catch{}let d={...q(r,n),...i?{liveFlowLabels:i}:{}},l=re(r.query),c;try{c=mr(r.query)}catch(S){$(o,S);return}let u=s?V(e,s):0,p=r.accessLink;if(p){let S=qt(e,p,d,c),E=S.proposals.map(I=>{let N=ue(e,p,I,{...d,include:l},{includeConflicts:!1});return N.status!=="pending"||s===void 0?N:{...N,baseRevStale:N.expectedRev!==s}});o.json({proposals:E,...ft({...S,returned:S.proposals.length}),...Q(u)});return}let f={};c.flowId&&(f.flowId=c.flowId);let m=Ys(e.list(f).filter(S=>Co(S.status,c.status)),{...d,include:l}),h=c.offset??0,x=(c.limit!==void 0?m.slice(h,h+c.limit):h>0?m.slice(h):m).map(S=>S.status!=="pending"||s===void 0?S:{...S,baseRevStale:S.expectedRev!==s});o.json({proposals:x,...ft({total:m.length,limit:c.limit??null,offset:h,returned:x.length}),...Q(u)})}}a(za,"listProposalsHandler");function Ga(e,t,n){return async(r,o)=>{let s=r.params.id;if(typeof s!="string"||s.length===0){o.status(400).json({error:"Missing proposal id"});return}let i=q(r,t),d=re(r.query),l;if(n)try{l=(await n.getFlows()).rev}catch{}try{if(r.accessLink){let c=De(e,r.accessLink,s,i);o.json(ue(e,r.accessLink,c,{...i,include:d},{...l?{liveRev:l}:{}}));return}l&&V(e,l),o.json(ce(e.get(s),{...i,include:d}))}catch(c){if(c instanceof ne){Tt(o,404,c.code);return}let u=O(c,"rest");if(u){o.status(u.statusCode).json(u.body);return}$(o,c);return}}}a(Ga,"getProposalHandler");function Ja(e,t){return async(n,r)=>{let o=n.params.id;if(typeof o!="string"||o.length===0){r.status(400).json({error:"Missing proposal id"});return}let s=n.body??{},i={};if(s.deploymentType!==void 0){if(s.deploymentType!=="nodes"&&s.deploymentType!=="flows"&&s.deploymentType!=="full"){r.status(400).json({error:"deploymentType must be 'nodes', 'flows', or 'full'"});return}i.deploymentType=s.deploymentType}try{let d=await e.approve(o,qa(n),i);r.json(oe(d,q(n,t)))}catch(d){if(d instanceof ne||d instanceof te||d instanceof kn||d instanceof X){Tt(r,d.statusCode,d.code);return}$(r,d);return}}}a(Ja,"approveProposalHandler");function Ka(e,t){return(n,r)=>{let o=n.params.id;if(typeof o!="string"||o.length===0){r.status(400).json({error:"Missing proposal id"});return}let s=n.body??{},i=typeof s.reason=="string"?s.reason:"Rejected by admin";try{let d=oe(e.reject(o,qa(n),i),q(n,t));r.json(d)}catch(d){if(d instanceof ne||d instanceof te||d instanceof X){Tt(r,d.statusCode,d.code);return}$(r,d);return}}}a(Ka,"rejectProposalHandler");function Va(e,t,n){return(r,o)=>{if(!r.accessLink){o.setHeader("WWW-Authenticate",Y),o.status(401).json({error:le,code:Le});return}let s=r.params.id;if(typeof s!="string"||s.length===0){o.status(400).json({error:"Missing proposal id",code:"missing_proposal_id"});return}let i=r.body??{},d=q(r,n),l=re(r.query);try{let c=Wt(e,t,r.accessLink,s,i.reason,d);o.json(ue(e,r.accessLink,c,{...d,include:l}))}catch(c){let u=O(c,"rest");if(u){Tt(o,u.statusCode,u.body.code);return}Tt(o,500,"internal_error");return}}}a(Va,"withdrawProposalHandler");function Xa(e,t){return async(n,r)=>{let o=n.params.id;if(typeof o!="string"||o.length===0){r.status(400).json({error:"Missing proposal id"});return}try{let s=n.accessLink?De(e,n.accessLink,o):e.get(o),i=Yt(n.query.format);if(!i.ok){r.status(400).json({error:"Invalid diff format",code:"invalid_diff_format",hint:"Valid formats: summary, full (default summary)."});return}let d=Qt(await nn(s,t),i.format);r.json(d)}catch(s){if(s instanceof ne){Tt(r,404,s.code);return}if(s instanceof z||s instanceof be||s instanceof Ce){Tt(r,s.statusCode,s.code);return}let i=O(s,"rest");if(i){r.status(i.statusCode).json(i.body);return}$(r,s);return}}}a(Xa,"getProposalDiffHandler");ve();ve();or();function Gf(e){return e.endsWith("/")?e.slice(0,-1):e}a(Gf,"trimTrailingSlash");var Wr=class{static{a(this,"HttpAdapter")}kind="http";baseUrl;accessToken;fetchImpl;constructor(t){this.baseUrl=Gf(t.baseUrl),this.accessToken=t.accessToken,this.fetchImpl=t.fetch??globalThis.fetch.bind(globalThis)}headers(t){let n=new Headers(t);return n.set("Accept","application/json"),n.set("Node-RED-API-Version","v2"),this.accessToken&&n.set("Authorization",`Bearer ${this.accessToken}`),n}async request(t,n={}){return await this.fetchImpl(`${this.baseUrl}${t}`,{...n,headers:this.headers(n.headers)})}async readJson(t,n){let r=await t.text();if(!t.ok){let o=r;try{let s=JSON.parse(r);o=s.message??s.code??r}catch{}throw t.status===404?new z(n):t.status===409?new J("unknown","unknown",`Admin API ${n}`):t.status===401||t.status===403?new Ce(Qn):new Error(`Admin API ${n} failed (${t.status}): ${o}`)}if(!r)return null;try{return JSON.parse(r)}catch{throw new Error(`Admin API ${n} returned non-JSON`)}}async getFlows(){let t=await this.request("/flows"),n=await this.readJson(t,"GET /flows");return An(n,"GET /flows")}async getFlow(t){let n=encodeURIComponent(t),[r,o]=await Promise.all([this.request("/flows"),this.request(`/flow/${n}`)]);if(o.status===404)throw new z(t);let s=await this.readJson(r,"GET /flows"),i=await this.readJson(o,`GET /flow/${t}`);return{rev:An(s,"GET /flows").rev,flow:$t(t,i)}}async updateFlow(t){let n=t.deploymentType??"nodes",r=await this.getFlows();if(r.rev!==t.expectedRev)throw new J(t.expectedRev,r.rev,"updateFlow");let o=rr(r.flows,t.flowId,t.flow),s=await this.request("/flows",{method:"POST",headers:{"Content-Type":"application/json","Node-RED-Deployment-Type":n},body:JSON.stringify({flows:o,rev:t.expectedRev})});if(s.status===409){let d=r.rev;try{let l=await s.clone().json();typeof l.current_rev=="string"&&(d=l.current_rev)}catch{}throw new J(t.expectedRev,d,"updateFlow")}let i=await this.readJson(s,"POST /flows");if(!i||typeof i!="object"||typeof i.rev!="string")throw new Error("Admin API POST /flows did not return a rev");return{flowId:t.flowId,rev:i.rev}}};ve();or();function Jf(e){let t=e.runtime?.flows;if(!t?.getFlows||!t.getFlow||!t.setFlows)throw new Error("RED.runtime.flows is missing getFlows, getFlow, or setFlows");return t}a(Jf,"requireFlowsApi");function Kf(e){if(typeof e=="string")return e;if(typeof e.rev=="string"&&e.rev.length>0)return e.rev;throw new Error("RED.runtime.flows.setFlows did not return a rev")}a(Kf,"resolveRev");var zr=class{static{a(this,"RedAdapter")}kind="red";flows;constructor(t){this.flows=Jf(t)}async getFlows(){let t=await this.flows.getFlows({});return An(t,"RED.runtime.flows.getFlows()")}async getFlow(t){let n=await this.getFlows(),r;try{r=await this.flows.getFlow({id:t})}catch(o){throw Vf(o)?new z(t):o}return{rev:n.rev,flow:$t(t,r)}}async updateFlow(t){let n=t.deploymentType??"nodes",r=await this.getFlows();if(r.rev!==t.expectedRev)throw new J(t.expectedRev,r.rev,"updateFlow");let o=rr(r.flows,t.flowId,t.flow),s=await this.flows.setFlows({flows:{flows:o,rev:t.expectedRev},deploymentType:n});return{flowId:t.flowId,rev:Kf(s)}}};function Ya(e){let t=e?.runtime?.flows;return!!(t?.getFlows&&t.getFlow&&t.setFlows)}a(Ya,"canUseRedRuntime");function Vf(e){if(!e||typeof e!="object")return!1;let t=e;if((typeof t.status=="number"?t.status:t.statusCode)===404||t.code==="not_found"||t.code==="flow_not_found")return!0;let r=typeof t.message=="string"?t.message.toLowerCase():"";return r.includes("not found")||r.includes("unknown flow")}a(Vf,"isRuntimeFlowNotFound");var Qa=require("module");function Za(e){if(!e||typeof e!="object")return!1;let t=e;return typeof t.getFlows=="function"&&typeof t.getFlow=="function"&&typeof t.setFlows=="function"}a(Za,"isFlowsApi");function Xf(){if(typeof require=="function")return require;let e=(0,eval)("import.meta.url");return(0,Qa.createRequire)(e)}a(Xf,"createPluginRequire");function Yf(e){try{let t=e("@node-red/runtime/lib/settings");return typeof t.available=="function"&&t.available()===!0}catch{return!1}}a(Yf,"isNodeRedRuntimeInitialized");function Qf(){try{let e=Xf();if(!Yf(e))return null;let t=e("@node-red/runtime");return Za(t.flows)?t.flows:null}catch{return null}}a(Qf,"tryRequireInitializedRuntimeFlows");function ed(e){if(e?.runtime&&Za(e.runtime.flows))return e;let t=Qf();return t?{runtime:{flows:t}}:null}a(ed,"resolveRuntimeHost");function td(e={}){let t=ed(e.RED??null);if(t&&Ya(t))return new zr(t);let n=e.adminAuthEnabled===!0;if(e.http?.baseUrl){if(n&&!e.http.accessToken)throw new be("In-process Node-RED flow runtime is unavailable and HTTP Admin API fallback has no credentials while adminAuth is enabled. Ensure the plugin runs inside Node-RED so @node-red/runtime.flows can be used.");return new Wr(e.http)}throw new be("No usable Node-RED flow adapter: in-process flows API is unavailable and no HTTP Admin API baseUrl was provided")}a(td,"createFlowAdapter");ve();var Jr=require("fs"),nd=require("path");er();G();var Zf=/^[a-z][a-z0-9._-]{0,63}$/;function em(e){if(e.kind==="system")return{kind:"system"};if(e.kind==="access-link"){let n={kind:"access-link",linkId:String(e.linkId).replace(/\0/g,"").slice(0,200)};return typeof e.label=="string"&&(n.label=e.label.replace(/\0/g,"").slice(0,200)),n}let t={kind:"admin"};return typeof e.username=="string"&&(t.username=e.username.replace(/\0/g,"").slice(0,200)),t}a(em,"sanitizeActor");var Gr=class{constructor(t,n={}){this.paths=t;this.filePath=(0,nd.join)(t.logs,"audit.jsonl"),this.now=n.now??(()=>Date.now()),this.maxAppendBytes=n.maxAppendBytes??cs}static{a(this,"AuditLog")}filePath;now;maxAppendBytes;seq=0;append(t,n,r){let o=Zf.test(t)?t:"unknown";this.seq+=1;let s={id:`log-${this.now()}-${this.seq}`,at:new Date(this.now()).toISOString(),action:o,actor:em(n)};r!==void 0&&(s.details=_s(r));let i=Be(s,this.maxAppendBytes,{kind:"audit"});return ps(this.paths.root,this.filePath,i),s}list(t=100){if(!(0,Jr.existsSync)(this.filePath))return[];let n=(0,Jr.readFileSync)(this.filePath,"utf8").split(`
6401
+ `).map(o=>o.trim()).filter(Boolean),r=[];for(let o of n)try{r.push(JSON.parse(o))}catch{}return r.slice(-Math.max(1,t)).reverse()}};Ge();Gn();ie();ut();ve();Ge();Xe();Ae();var tm="Flow revision conflict: the expectedRev is stale. Ask the AI to re-read flows and resubmit a new proposal.";function rd(e){let t=oe(e),n=cr(e)||void 0;return{proposalId:e.id,title:t.title,displayName:t.displayName,flowId:e.payload.flowId,...n?{flowLabel:n}:{}}}a(rd,"auditProposalMeta");var Kr=class{constructor(t,n,r,o){this.proposals=t;this.audit=n;this.adapter=r;this.getDeploymentType=o}static{a(this,"ProposalService")}async approve(t,n,r={}){let o=this.proposals.beginApprove(t),s=rd(o),i=r.deploymentType??this.getDeploymentType();try{let d=o.type;if(d!=="replace_flow"){let l=this.proposals.markFailed(t,n,`Unsupported proposal type '${d}'`);return this.audit.append("proposal.failed",n,{...s,reason:l.reason}),l}try{let l=await this.adapter.updateFlow({flowId:o.payload.flowId,flow:o.payload.flow,expectedRev:o.expectedRev,deploymentType:i}),c=this.proposals.markApproved(t,n,l.rev);return this.audit.append("proposal.approved",n,{...s,resultRev:l.rev,flowId:o.payload.flowId}),ei(this.proposals,o.payload.flowId,t,l.rev),c}catch(l){if(l instanceof J){let p=this.proposals.markRejected(t,n,tm);return this.audit.append("proposal.rejected",n,{...s,reason:p.reason,code:l.code,expectedRev:l.expectedRev,currentRev:l.currentRev}),p}let c=l instanceof Error?l.message:String(l),u=this.proposals.markFailed(t,n,c);return this.audit.append("proposal.failed",n,{...s,reason:c}),u}}finally{this.proposals.releaseLock(t)}}reject(t,n,r){let o=this.proposals.get(t);if(o.status!=="pending")throw new te(t,o.status);let s=rd(o),i=this.proposals.markRejected(t,n,r.trim()||"Rejected by admin");return this.audit.append("proposal.rejected",n,{...s,reason:i.reason}),i}};ye();function nm(e){let t=a(()=>Promise.reject(new Ce(e)),"fail");return{kind:"http",getFlows:t,getFlow:t,updateFlow:t}}a(nm,"misconfiguredAdapter");function od(e){let t=e.settings,n=typeof t.uiPort=="number"?t.uiPort:1880,r=t.httpAdminRoot??"/",o=!r||r==="/"?"":r.replace(/\/$/,"");return`http://127.0.0.1:${n}${o}`}a(od,"localAdminBaseUrl");function Vr(e,t={}){let n=Bn(e.settings.userDir);fs(n);let r=t.settingsStore??new Lt(n,e.settings.aiAccess,u=>e.log?.warn?.(u));t.bindSettings!==!1&&Jn(r),Vn(r.getEffective().publicBaseUrl);let o=t.linkStore??Ur(e);o.configureDefaults({defaultTtlSeconds:r.getEffective().defaultLinkTtlSeconds,maxActiveLinks:r.getEffective().maxActiveLinks});let s=new tr(n),i=new Gr(n),d;if(t.flowAdapter)d=t.flowAdapter;else try{d=td({RED:e,http:{baseUrl:od(e)},adminAuthEnabled:W(e.settings)})}catch(u){let p=u instanceof be?u.message:Qn;e.log?.warn?.(`AI Access flow adapter unavailable: ${p}`),d=nm(p)}let l;if(t.nodeTypesAdapter)l=t.nodeTypesAdapter;else if(t.flowAdapter)l=ho();else try{l=Hs({baseUrl:od(e)})}catch(u){let p=u instanceof Error?u.message:String(u);e.log?.warn?.(`AI Access node-types adapter unavailable: ${p}`),l=ho([])}let c=new Kr(s,i,d,()=>he(e.settings).defaultDeploymentType);return{linkStore:o,proposalStore:s,auditLog:i,flowAdapter:d,nodeTypesAdapter:l,proposalService:c,settingsStore:r,...t.catalogueFetcher?{catalogueFetcher:t.catalogueFetcher}:{},...t.catalogueUrl?{catalogueUrl:t.catalogueUrl}:{}}}a(Vr,"createPluginServices");G();ie();_e();qn();var sd=ln(require("dns"),1),id=ln(require("http"),1),ad=ln(require("https"),1),Dn=require("net");ie();function rm(e,t){return e===10||e===172&&t>=16&&t<=31||e===192&&t===168}a(rm,"isPrivateV4");function om(e,t){return e===169&&t===254}a(om,"isLinkLocalV4");function sm(e,t,n){return e===0||e===100&&t>=64&&t<=127||e>=224||e===192&&t===0&&(n===0||n===2)||e===198&&(t===18||t===19||t===51&&n===100)||e===203&&t===0&&n===113}a(sm,"isSpecialUseV4");function im(e){let[t=0,n=0,r=0]=e.split(".").map(Number);return t===127?"loopback":rm(t,n)||om(t,n)||sm(t,n,r)||e==="168.63.129.16"?"blocked":"public"}a(im,"classifyIpv4");function am(e){let t=e.split(":"),n=t.indexOf(""),r=[];for(let o of t){if(!o)continue;if(o.includes(".")){let i=o.split(".").map(Number);if(i.length!==4||i.some(d=>!Number.isInteger(d)||d<0||d>255))return;r.push((i[0]??0)<<8|(i[1]??0)),r.push((i[2]??0)<<8|(i[3]??0));continue}let s=Number.parseInt(o,16);if(!/^[\da-f]{1,4}$/i.test(o)||!Number.isInteger(s))return;r.push(s)}if(n>=0){let o=8-r.length;if(o<1)return;let s=t.slice(0,n).filter(Boolean).length;r.splice(s,0,...Array(o).fill(0))}else if(r.length!==8)return;return r.length===8?r:void 0}a(am,"expandIpv6");function dm(e){let t=am(e);if(!t)return"blocked";if(t.slice(0,7).every(s=>s===0)&&t[7]===1)return"loopback";let n=t[0]??0,r=t[1]??0;return n===0||n<8192||n>16383||n===8193&&r<=511||n===8193&&r===3512||n===16383&&r<=4095||n===8194?"blocked":"public"}a(dm,"classifyIpv6");function jn(e){let t=at(e);return t?(0,Dn.isIP)(t)===4?im(t):dm(t):"blocked"}a(jn,"classifyPublicUrlAddress");function lm(e){let t=e.toLowerCase().replace(/\.$/,"");return["metadata","metadata.google.internal","metadata.google.com","metadata.goog","instance-data","instance-data.ec2.internal","metadata.azure.internal","metadata.aws.internal"].some(n=>t===n||t.endsWith(`.${n}`))}a(lm,"isMetadataHostname");async function dd(e,t,n){let r=Ct(e);if(!r)throw new Error("publicBaseUrl is required for Check");let o=new URL(r),s=o.hostname.replace(/^\[|\]$/g,"");if(lm(s))throw new Error("Public URL Check cannot target metadata hosts");let i=at(s),l=(typeof e=="string"?/^https?:\/\/(\[[^\]]+\]|[^/?#:]+)/i.exec(e.trim())?.[1]:void 0)?.replace(/^\[|\]$/g,"");if(i&&l&&l.toLowerCase()!==i.toLowerCase())throw new Error("Public URL Check requires canonical IP address notation");let c=s.toLowerCase().replace(/\.$/,"")==="localhost"||i!==void 0&&jn(i)==="loopback",u=Number(o.port||(o.protocol==="https:"?443:80));if(c&&u!==t)throw new Error("Public URL Check may only target this Node-RED instance's port for loopback URLs");let p=a(g=>{let x=jn(g);if(x==="blocked"||x==="loopback"&&!c||c&&x!=="loopback")throw new Error("Public URL Check cannot target private or non-public addresses")},"validateAddress");if(i)return p(i),{origin:r,address:i,family:(0,Dn.isIP)(i)};let f=await new Promise((g,x)=>{let S=a(()=>x(n.reason instanceof Error?n.reason:new Error(String(n.reason))),"abort");n.throwIfAborted(),n.addEventListener("abort",S,{once:!0}),sd.default.lookup(s,{all:!0},(E,I)=>{n.removeEventListener("abort",S),E?x(E):g(I)})});if(n.throwIfAborted(),!f.length)throw new Error("Public URL Check hostname has no addresses");for(let g of f)p(g.address);let m=[];for(let g of f){let x=at(g.address);if(!x)throw new Error("Public URL Check returned an invalid address");m.some(S=>S.address===x)||m.push({address:x,family:(0,Dn.isIP)(x)})}c&&m.sort((g,x)=>g.family-x.family);let h=m[0];if(!h)throw new Error("Public URL Check hostname has no addresses");return{origin:r,...h,addresses:m}}a(dd,"preparePublicUrlCheck");function cm(e,t,n){let r=e.hostname.replace(/^\[|\]$/g,"");return new Promise((o,s)=>{let d=(e.protocol==="https:"?ad.default:id.default).request(e,{method:"GET",agent:!1,signal:n,headers:{Host:e.host},...(0,Dn.isIP)(r)?{}:{servername:r},lookup:a((l,c,u)=>{c.all?u(null,[{address:t.address,family:t.family}]):u(null,t.address,t.family)},"lookup")},l=>{o(l.statusCode??0),l.destroy()});d.on("error",s),d.end()})}a(cm,"requestPinned");async function ld(e,t,n){let r=new URL(e.origin+t),o=e.addresses?.length?e.addresses:[{address:e.address,family:e.family}],s;for(let i of o){n.throwIfAborted();try{return await cm(r,i,n)}catch(d){if(n.aborted)throw d;s=d}}throw s instanceof Error?s:new Error("Public URL Check request failed")}a(ld,"requestPublicUrlCheck");ye();function um(e){return hn(e.settings.httpAdminRoot)}a(um,"mcpPageUrl");function pm(e){return typeof e=="boolean"||typeof e=="number"||typeof e=="string"?e:Array.isArray(e)?e.join(", "):typeof e=="function"?"custom function":!1}a(pm,"normalizeTrustProxy");function pd(e){if(!e)return"Set a Public URL before enabling OAuth.";let t=new URL(e),n=at(t.hostname),r=t.hostname.toLowerCase().replace(/\.$/,"")==="localhost"||n==="::1"||n?.startsWith("127.");return t.protocol==="http:"&&!r?"OAuth Public URL uses HTTP; HTTPS is recommended.":null}a(pd,"oauthPublicUrlWarning");function fd(e,t,n){let{sources:r,...o}=t.getEffective(),{permission:s}=Kn(e,n);return{ok:!0,adminAuthEnabled:W(e.settings),canEdit:s==="*",permission:s,mcpPageUrl:um(e),apiBase:de(e.settings.httpAdminRoot),settings:o,oauthPublicUrlWarning:pd(o.publicBaseUrl),sources:r,disk:t.getDisk(),trustProxy:pm(n.app.get("trust proxy")),precedence:["settings.js","disk","default"]}}a(fd,"settingsResponse");function md(e,t){return(n,r)=>{r.json(fd(e,t,n))}}a(md,"getSettingsHandler");function cd(e,t,n){let r=e[t];if(r!==void 0){if(typeof r!="boolean")throw new Error(`${t} must be a boolean`);n[t]=r}}a(cd,"parseBooleanField");function ud(e,t,n){let r=e[t];if(r!==void 0){if(typeof r!="number"||!Number.isFinite(r)||r<=0)throw new Error(`${t} must be a positive number`);n[t]=Math.floor(r)}}a(ud,"parsePositiveIntField");function fm(e,t,n){let r=e[t];if(r===void 0)return;let o=t==="rateLimitWindowMs"?100:1,s=t==="rateLimitWindowMs"?36e5:1e6;if(typeof r!="number"||!Number.isInteger(r)||r<o||r>s)throw new Error(`${t} must be an integer between ${o} and ${s}`);n[t]=r}a(fm,"parseRateLimitField");function mm(e,t){if(e.corsAllowedOrigins===void 0)return;if(!Array.isArray(e.corsAllowedOrigins))throw new Error("corsAllowedOrigins must be a list of HTTP(S) origins");let n=e.corsAllowedOrigins.map(r=>pn(r));if(n.some(r=>r===void 0))throw new Error("corsAllowedOrigins must contain only HTTP(S) origins without paths, credentials, queries, or fragments");t.corsAllowedOrigins=[...new Set(n)]}a(mm,"parseCorsField");function hd(e,t,n){return(r,o)=>{let s=r.body;if(!s||typeof s!="object"||Array.isArray(s)){o.status(400).json({error:"Request body must be a settings object",code:"invalid_body"});return}let i={...s};for(let[l,c]of Object.entries(t.getEffective().sources))c==="settings.js"&&delete i[l];t.getEffective().sources.oauth==="settings.js"&&delete i.oauth;let d={};try{if(i.oauth!==void 0){if(!i.oauth||typeof i.oauth!="object"||Array.isArray(i.oauth))throw new Error("oauth must be an object");let l=i.oauth,c={};if(l.enabled!==void 0){if(typeof l.enabled!="boolean")throw new Error("oauth.enabled must be a boolean");c.enabled=l.enabled}for(let p of["accessTokenTtlSec","refreshIdleTtlSec","dcrMaxClients"])if(l[p]!==void 0){if(typeof l[p]!="number"||!Number.isInteger(l[p])||l[p]<1||l[p]>31536e3)throw new Error(`oauth.${p} must be a positive integer`);c[p]=l[p]}let u=l.refreshMaxLifetimeSec;if(u!==void 0){if(typeof u!="number"||!Number.isInteger(u)||u<0||u>31536e4)throw new Error("oauth.refreshMaxLifetimeSec must be a non-negative integer");c.refreshMaxLifetimeSec=u}d.oauth=c}cd(i,"enabled",d),cd(i,"allowWithoutAdminAuth",d),ud(i,"defaultLinkTtlSeconds",d),ud(i,"maxActiveLinks",d)}catch(l){o.status(400).json({error:l.message,code:"invalid_body"});return}if(i.defaultDeploymentType!==void 0){let l=i.defaultDeploymentType;if(l!=="nodes"&&l!=="flows"&&l!=="full"){o.status(400).json({error:"defaultDeploymentType must be 'nodes', 'flows', or 'full'",code:"invalid_body"});return}d.defaultDeploymentType=l}try{i.publicBaseUrl!==void 0&&(d.publicBaseUrl=Ct(i.publicBaseUrl)),mm(i,d);for(let l of["rateLimitWindowMs","rateLimitMaxPerIp","rateLimitMaxPerToken"])fm(i,l,d)}catch(l){o.status(400).json({error:l.message,code:"invalid_body"});return}try{let l=t.patch(d);Vn(l.publicBaseUrl),n.configureDefaults({defaultTtlSeconds:l.defaultLinkTtlSeconds,maxActiveLinks:l.maxActiveLinks});let c=fd(e,t,r),u=l.publicBaseUrl?new URL(l.publicBaseUrl):void 0,p=u?at(u.hostname.replace(/^\[|\]$/g,"")):void 0,f=p?jn(p):void 0,m=[];f!==void 0&&f!=="public"&&m.push("Public URL uses a private or non-public IP address.");let h=pd(l.publicBaseUrl);h&&l.publicBaseUrl&&m.push(h),o.json({...c,...m.length?{warnings:m}:{}})}catch(l){if(l instanceof X){o.status(l.statusCode).json({error:l.message,code:l.code});return}$(o,l);return}}}a(hd,"putSettingsHandler");function gd(e){return async(t,n)=>{let r=new AbortController,o=setTimeout(()=>r.abort(),5e3),s;try{let i;try{let c=e.settings,p=e.server?.address(),f=t.body?.publicBaseUrl,m=typeof f=="string"?new URL(f.trim()).hostname:"";if((m.toLowerCase().replace(/^\[|\]$/g,"").replace(/\.$/,"")==="localhost"||jn(m.replace(/^\[|\]$/g,""))==="loopback")&&typeof p=="string")throw new Error("Public URL Check does not support loopback URLs when Node-RED listens on a Unix socket");let g=p&&typeof p=="object"?p.port:c.uiPort??1880;i=await dd(t.body?.publicBaseUrl,g,r.signal)}catch(c){if(r.signal.aborted)throw c;n.status(400).json({error:c.message,code:"invalid_body"});return}s=i.origin+de(e.settings.httpAdminRoot);let d=await ld(i,de(e.settings.httpAdminRoot),r.signal),l={status:d,ok:d>=200&&d<300};n.json({ok:l.ok,status:l.status,url:s,message:l.ok?"Discovery endpoint is reachable":`Discovery endpoint returned HTTP ${l.status}`})}catch{n.json({ok:!1,status:null,url:s,error:r.signal.aborted?"Public URL check timed out after 5 seconds":"Could not reach the discovery endpoint"})}finally{clearTimeout(o)}}}a(gd,"checkPublicUrlHandler");ye();it();ye();K();Gn();var hm="com55-ai-access";function wd(e){let t=new Set;for(let n of String(e??"").split(",")){let[r,...o]=n.split(";").map(d=>d.trim().toLowerCase());if(!r)continue;let s=o.find(d=>d.startsWith("q=")),i=s?Number(s.slice(2)):1;Number.isFinite(i)&&i>0&&t.add(r)}return t}a(wd,"acceptedMediaTypes");function bd(e){let t=wd(e.headers.accept);return t.has("text/event-stream")&&!t.has("text/html")}a(bd,"isMcpStreamRequest");function gm(e,t,n){e.httpAdmin.get("/mcp/api/v1",t,n,Mo(e)),e.httpAdmin.get("/mcp/api/v1/",t,n,Mo(e)),e.httpAdmin.get("/mcp/api/v1/openapi.yaml",t,n,Ma()),e.httpAdmin.get("/mcp/api/v1/openapi.json",t,n,Da()),e.httpAdmin.get("/mcp/editor/ai-access-sidebar.js",t,(r,o)=>{o.setHeader("Cache-Control","no-store, no-cache, must-revalidate, max-age=0"),o.setHeader("Pragma","no-cache"),o.setHeader("Expires","0"),o.setHeader("X-AI-Access-Sidebar-Version",Xt),o.type("application/javascript").status(200).send(yr())})}a(gm,"registerMcpShellAndDiscovery");var ym=/^\/mcp\/(?:api\/v1\/proposals|rpc$|a\/[^/]+\/(?:proposals|propose|draft))/;function vd(e,t={}){let n=Vr(e,{...t,linkStore:t.linkStore??Ur(e)}),r=Ei(()=>n.settingsStore.getEffective()),{ip:o,token:s}=r,i=new Br(Bn(e.settings.userDir),()=>n.settingsStore.getEffective().oauth.dcrMaxClients),d=a(()=>n.settingsStore.getEffective().publicBaseUrl,"publicBase"),l=a(()=>`${d().replace(/\/$/,"")}/mcp`,"mcpUrl"),c=new qr(i,l,()=>n.settingsStore.getEffective().oauth.accessTokenTtlSec,()=>n.settingsStore.getEffective().oauth.refreshIdleTtlSec,()=>n.settingsStore.getEffective().oauth.refreshMaxLifetimeSec),u=Na(e,c,d,l);hs(e.httpAdmin,ms(()=>n.settingsStore.getEffective().corsAllowedOrigins));let p=a(w=>(Array.isArray(w)?w:w?[w]:[]).flatMap(b=>{if(typeof b=="string")return[b];let k=b;return k&&typeof k.path=="string"?[{path:k.path,root:typeof k.root=="string"?k.root:void 0}]:[]}),"staticMounts"),f={authorization:[],metadata:[],details:[],httpNodeRootFallback:e.settings.httpNodeRoot===!1||!e.httpNode},m="",h=a(()=>{let w=Ia({httpAdmin:e.httpAdmin,httpNode:e.httpNode,httpAdminRoot:e.settings.httpAdminRoot,httpNodeRoot:e.settings.httpNodeRoot,metadataFallback:f.httpNodeRootFallback,eachNode:e.nodes?M=>e.nodes?.eachNode(M):void 0,staticDirs:p(e.settings.httpStatic),httpStaticRoot:e.settings.httpStaticRoot,fileExists:yd.existsSync}),b=w.authorization,k=w.metadata;f.authorization=b.map(os),f.metadata=k.map(os),f.details=[...b.map(M=>({...M,kind:"authorization"})),...k.map(M=>({...M,kind:"metadata"}))];let L=[...f.authorization.map(M=>`${M} \u2014 OAuth authorization routes disabled`),...f.metadata.map(M=>`${M} \u2014 OAuth metadata disabled`)],H=L.join(`
6402
+ `);if(H!==m)for(let M of L)e.log?.warn?.(`AI Access OAuth route collision: ${M}`);m=H},"refreshCollisions");h(),e.events?.on("flows:started",h);let g=Ho({windowMs:6e4,maxPerIp:30}),x=a((w,b,k)=>{if(!n.settingsStore.getEffective().oauth.enabled||f.authorization.length||!w.path.startsWith("/mcp/oauth/")){k();return}let L=w.path.replace(/\/+$/,"").toLowerCase(),H=a(()=>Et(e)(w,b,()=>u.server(w,b,k)),"run");L==="/mcp/oauth/authorize"?o(w,b,()=>g(w,b,H)):L.startsWith("/mcp/oauth/consent/")?o(w,b,H):H()},"oauthServerMiddleware");x.oauthOwned=!0,e.httpAdmin.use(x);let S=a((w,b,k)=>{if(!n.settingsStore.getEffective().oauth.enabled||f.metadata.length){k();return}u.metadata(w,b,k)},"metadataHandler");S.oauthOwned=!0,f.httpNodeRootFallback?e.httpAdmin.use("/mcp",S):e.httpNode?.use(S),e.httpAdmin.get("/mcp/api/v1/oauth/clients",o,s,we(e,"api"),(w,b)=>b.json({clients:i.data.clients.map(k=>({client_id:k.client_id,client_name:k.client_name,redirect_uris:k.redirect_uris,lastUsedAt:k.lastUsedAt}))})),e.httpAdmin.get("/mcp/api/v1/oauth/status",o,s,we(e,"api"),(w,b)=>{let k=!W(e.settings)||We(w)==="*";b.json({...f,details:k?f.details:f.details.map(({file:L,...H})=>H),collisions:[...f.authorization,...f.metadata],oauthEnabled:n.settingsStore.getEffective().oauth.enabled})}),e.httpAdmin.delete("/mcp/api/v1/oauth/clients/:id",o,s,we(e,"api"),gn(e),(w,b)=>b.json({revoked:i.revokeClient(String(w.params.id))})),e.httpAdmin.use((w,b,k)=>{if(!ym.test(w.path)){k();return}n.flowAdapter.getFlows().then(L=>Vs(w,dr(L.flows))).catch(()=>{}).finally(()=>k())}),na(e,{linkStore:n.linkStore,proposalStore:n.proposalStore,auditLog:n.auditLog,flowAdapter:n.flowAdapter,nodeTypesAdapter:n.nodeTypesAdapter,...n.catalogueFetcher?{catalogueFetcher:n.catalogueFetcher}:{},...n.catalogueUrl?{catalogueUrl:n.catalogueUrl}:{},rateLimiters:r}),e.httpAdmin.get("/ai-access",o,s,(w,b)=>{b.setHeader("Cache-Control","no-store, no-cache, must-revalidate, max-age=0"),b.setHeader("Pragma","no-cache"),b.setHeader("Expires","0"),b.type("html").status(200).send(Ba({apiBase:de(e.settings.httpAdminRoot),mcpRpc:qe(e.settings.httpAdminRoot),editorRoot:ae(e.settings.httpAdminRoot),authTokenKey:Yn(e.settings.httpAdminRoot),dashboardPath:hn(e.settings.httpAdminRoot)}))}),e.httpAdmin.get("/mcp",o,(w,b,k)=>{if(bd(w)){k();return}b.setHeader("Cache-Control","no-store"),b.redirect(302,hn(e.settings.httpAdminRoot))}),gm(e,o,s),e.httpAdmin.get("/mcp/api/v1/status",o,s,vs(e,w=>n.linkStore.validateResult(w)),R((w,b)=>{b.json(rn(e,w))})),e.httpAdmin.get("/mcp/api/v1/settings",o,s,we(e,"api"),R(md(e,n.settingsStore))),e.httpAdmin.put("/mcp/api/v1/settings",o,s,we(e,"api"),gn(e),je("rest"),R(hd(e,n.settingsStore,n.linkStore))),e.httpAdmin.post("/mcp/api/v1/settings/check-public-url",o,s,we(e,"api"),gn(e),je("rest"),R(gd(e)));let E=[o,s,we(e,"api"),Et(e),gn(e)];e.httpAdmin.get("/mcp/api/v1/links",...E,R(ua(e,n.linkStore))),e.httpAdmin.post("/mcp/api/v1/links",...E,R(pa(e,n.linkStore,n.auditLog))),e.httpAdmin.delete("/mcp/api/v1/links/:id",...E,R(fa(e,n.linkStore,n.auditLog)));let I=[o,s,Et(e),bs(e,w=>n.linkStore.validateResult(w))],N=n.flowAdapter,me={nodeTypes:n.nodeTypesAdapter,...n.catalogueFetcher?{catalogueFetcher:n.catalogueFetcher}:{},...n.catalogueUrl?{catalogueUrl:n.catalogueUrl}:{}};e.httpAdmin.get("/mcp/api/v1/flows",...I,R(vi(N))),e.httpAdmin.get("/mcp/api/v1/flows/:id",...I,R(Lr(N))),e.httpAdmin.get("/mcp/api/v1/nodes/:id",...I,R(Cr(N))),e.httpAdmin.get("/mcp/api/v1/nodes/:id/field/:name",...I,R(Er(N))),e.httpAdmin.get("/mcp/api/v1/search",...I,R(Ir(N))),e.httpAdmin.get("/mcp/api/v1/node-types",...I,R(Nr(me))),e.httpAdmin.get("/mcp/api/v1/node-types/:type",...I,R(Fr(me))),e.httpAdmin.get("/mcp/api/v1/palette/search",...I,R(Or(me)));let A=e.settings.httpAdminRoot;e.httpAdmin.get("/mcp/api/v1/proposals",...I,R(za(n.proposalStore,n.flowAdapter,A))),e.httpAdmin.get("/mcp/api/v1/proposals/:id",...I,R(Ga(n.proposalStore,A,n.flowAdapter))),e.httpAdmin.get("/mcp/api/v1/proposals/:id/diff",...I,R(Xa(n.proposalStore,n.flowAdapter))),e.httpAdmin.post("/mcp/api/v1/proposals",o,s,je("rest"),Nt(e,n.linkStore,"rest"),R(Wa(n.proposalStore,n.auditLog,n.flowAdapter,A,n.nodeTypesAdapter))),e.httpAdmin.post("/mcp/api/v1/proposals/:id/approve",...E,je("rest"),R(Ja(n.proposalService,A))),e.httpAdmin.post("/mcp/api/v1/proposals/:id/reject",...E,je("rest"),R(Ka(n.proposalService,A))),e.httpAdmin.post("/mcp/api/v1/proposals/:id/withdraw",o,s,je("rest"),Nt(e,n.linkStore,"rest"),R(Va(n.proposalStore,n.auditLog,A)));let v=[o,s,(w,b,k)=>{let L=w.headers.authorization;if(!n.settingsStore.getEffective().oauth.enabled||L?.toLowerCase().startsWith("bearer aia_")){if(n.settingsStore.getEffective().oauth.enabled){let H=b.setHeader.bind(b);b.setHeader=((M,Re)=>{if(M.toLowerCase()==="www-authenticate"&&typeof Re=="string"&&!Re.includes("resource_metadata=")){let Ue=`${d().replace(/\/$/,"")}${f.httpNodeRootFallback?"/mcp":""}/.well-known/oauth-protected-resource/mcp`;Re+=`, resource_metadata="${Ue}", scope="read propose"`}return H(M,Re)})}Nt(e,n.linkStore,"mcp")(w,b,k);return}Et(e)(w,b,()=>u.auth(w,b,k))}];e.httpAdmin.post("/mcp/rpc",...v,je("mcp"),R(Qo(e,n))),e.httpAdmin.get("/mcp/rpc",...v,On),e.httpAdmin.delete("/mcp/rpc",...v,On),e.httpAdmin.post("/mcp",...v,je("mcp"),R(Qo(e,n))),e.httpAdmin.get("/mcp",...v,On),e.httpAdmin.delete("/mcp",...v,On),e.httpAdmin.get("/mcp/api/v1/logs",o,s,we(e,"api"),Et(e),R(ma(n.auditLog,n.linkStore))),e.httpAdmin.use("/mcp/api/v1",yi()),ia(e)}a(vd,"registerRoutes");function wm(e){e.plugins.registerPlugin(hm,{type:"com55-ai-access",onadd(){vd(e),e.log?.info?.("Node-RED AI Access plugin loaded (dashboard /ai-access, MCP /mcp)")}})}a(wm,"registerAiAccessPlugin");var bm=wm;0&&(module.exports={ACCESS_LINK_INVALID_MESSAGE,ACCESS_LINK_MISSING_MESSAGE,AccessLinkStore,EDITOR_SIDEBAR_ASSET_VERSION,MAX_PROPOSAL_JSON_BYTES,SettingsStore,acceptedMediaTypes,areAiFeaturesEnabled,bindSettingsStore,createPluginServices,getStorageDir,isAdminAuthEnabled,isMcpStreamRequest,packageVersion,registerRoutes,renderEditorSidebarJs,renderEditorSidebarLoaderHtml,requireAccessLink,resolveAiAccessSettings});