@nexa26/nexa 0.1.2 → 0.1.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.
Files changed (2) hide show
  1. package/dist/plugin.js +1 -1
  2. package/package.json +1 -1
package/dist/plugin.js CHANGED
@@ -1 +1 @@
1
- import{URL as e}from"node:url";import{Buffer as t}from"node:buffer";import{createDecipheriv as n,scryptSync as r}from"node:crypto";import{createRequire as o}from"node:module";import{DEFAULT_ACCOUNT_ID as a,DEFAULT_CHANNEL_TYPE as s,NEXA_OPENCLAW_CHANNEL_ID as c,listAccountIds as l,resolveControlPlane as i,resolveAccount as d}from"./config.js";import{normalizeWebhookEvent as u,resolveOutboundTarget as g}from"./normalize.js";import{createInboundAdapter as p}from"./inbound-adapter.js";import{createDefaultInboundHandler as h}from"./default-inbound-handler.js";import{createRuntimeBridge as f}from"./runtime-bridge.js";import{authenticateControlPlane as m,fetchControlPlaneAgents as w,fetchControlPlaneBindings as I,fetchControlPlaneConfig as y,exchangeBootstrap as b,isUnauthorizedControlPlaneError as k,sendHeartbeat as v,sendReply as x}from"./server-client.js";import{createServerDispatch as S}from"./server-dispatch.js";import{createServerSocket as C}from"./server-socket.js";import{computeNextHeartbeatDelay as M,computeNextPollDelay as T}from"./transport-timing.js";import{mergeConfigWithPluginState as A}from"./plugin-state.js";const P=o(import.meta.url),{version:_}=P("../package.json");async function U(e,t,n,r){const{sendTextToStream:o}=await import("./stream-client.js");return o(e,t,n,r)}async function $(e,t,n){const{verifyStreamWebhook:r}=await import("./stream-client.js");return r(e,t,n)}async function O(e,t,n){const{ensureStreamUser:r}=await import("./stream-client.js");return r(e,t,n)}function j(e,t,n){return e.statusCode=t,e.setHeader("content-type","application/json"),e.end(JSON.stringify(n)),!0}function R(e,t){return t?.transport?.preferredTransport||e?.transport?.preferredTransport||"long-poll"}function E(t,n,r){const o=r?.transport?.websocketUrl||n?.transport?.websocketUrl;if(!o)return null;if(o.startsWith("ws://")||o.startsWith("wss://"))return o;const a=new e(t.serverUrl),s="https:"===a.protocol?"wss:":"ws:";return new e(o,`${s}//${a.host}`).toString()}export function createNexaOpenClawPlugin({store:o,inboundHandler:P,verifyWebhook:B=$,sendText:N=U,ensureUser:W=O,createServerSocketClient:D=C,stateStore:G={path:null,load:async()=>null,save:async()=>{throw new Error("nexa_state_store_not_configured")},clear:async()=>{}}}={}){const V=new Map,L=new Map,J=new Map,q=new Map,H=new Map,K=new Map,z=new Map,F=new Map;let Q=null;function X(e){const t=e||a,n=K.get(t);if(n)return n;const r={timer:null,attempt:0,connecting:!1,stopped:!1};return K.set(t,r),r}function Y(e){const t=K.get(e||a);t?.timer&&(clearTimeout(t.timer),t.timer=null)}async function Z(e,t){const n=e||a,r=F.get(n);if(r){r.controller.abort(),await r.promise.catch(()=>{}),F.delete(n);try{await oe.gateway.stopAccount({accountId:n,log:t})}catch{}}}function ee(e){const t=e||a,n=L.get(t);n&&(n.stopped=!0,n.timer&&clearTimeout(n.timer),L.delete(t))}function te(e,t,n,r){const o=e||a;if(L.has(o))return;const s={stopped:!1,timer:null,emptyStreak:0,failureStreak:0};L.set(o,s);const c=e=>{s.stopped||(s.timer=setTimeout(l,Math.max(0,e)))},l=async()=>{if(s.stopped)return;if(H.get(o))return void c(n.pollIntervalMs);H.set(o,!0);let e=n.pollIntervalMs,a=0;try{a=await t.pollOnce(),s.failureStreak=0,s.emptyStreak=a>0?0:s.emptyStreak+1}catch(e){if(s.failureStreak+=1,k(e)){const e=z.get(o);try{await(e?.refreshRemoteState?.())}catch(e){r?.error?.(`[nexa] poll re-auth failed: ${e?.message||e}`)}}r?.error?.(`[nexa] inbox poll failed: ${e?.message||e}`)}finally{e=T({baseMs:n.pollIntervalMs,idleMinMs:n.pollIdleMinMs,idleMaxMs:n.pollIdleMaxMs,emptyStreak:s.emptyStreak,failureStreak:s.failureStreak,deliveredCount:a,jitterRatio:n.pollJitterRatio}),H.set(o,!1)}s.stopped||L.get(o)!==s||c(e)};c(0)}async function ne(e){const{accountId:t,websocketUrl:n,token:r,logger:o,serverDispatch:a,controlPlane:s}=e;if(!n||J.has(t))return!1;const c=D({websocketUrl:n,token:r,logger:o,dispatch:a,onDisconnect:()=>{J.delete(t),!K.get(t)?.stopped&&z.has(t)&&(te(t,a,s,o),re(t))}});await c.connect(),J.set(t,c),ee(t);const l=X(t);return l.attempt=0,l.connecting=!1,Y(t),!0}function re(e){const t=e||a,n=z.get(t);if(!n?.shouldUseWebSocket)return;const r=X(t);if(r.stopped||r.timer||r.connecting||J.has(t))return;r.attempt+=1;const o=Math.min(n.controlPlane.websocketReconnectMaxMs,n.controlPlane.websocketReconnectBaseMs*2**Math.max(r.attempt-1,0));r.timer=setTimeout(async()=>{if(r.timer=null,!r.stopped&&!J.has(t)){r.connecting=!0;try{await(n.refreshRemoteState?.()),await ne(n)}catch(e){n.logger?.error?.(`[nexa] websocket reconnect failed: ${e?.message||e}`)}finally{r.connecting=!1,r.stopped||J.has(t)||re(t)}}},o)}const oe={id:c,meta:{id:c,label:"Nexa",selectionLabel:"Nexa OpenClaw",detailLabel:"Nexa OpenClaw Channel",docsPath:"/channels/nexa",blurb:"Nexa native channel plugin for OpenClaw groups.",aliases:["nexa","getstream"]},capabilities:{chatTypes:["direct","group"]},config:{listAccountIds:l,resolveAccount:d},outbound:{deliveryMode:"direct",sendText:async e=>{const t=d(e?.config||{},e?.accountId||a),n=g(e,t.defaultChannelType||s);return N(t,n,e?.text||"",{userId:e?.userId,custom:e?.custom})}},gateway:{startAccount:async({accountId:e,cfg:t,log:n,abortSignal:r})=>{try{const c=await G.load(),l=A(t,c),u=d(l,e||a),g=i(l),p=Q,h=p?.logger||n||console;if(h?.info?.(`[nexa] startAccount accountId=${u.accountId} controlPlaneEnabled=${g.enabled} serverUrl=${g.serverUrl||"missing"} channelKeyPresent=${!!g.channelKey} channelSecretPresent=${!!g.channelSecret}`),g.enabled){let e=null,t=null,r=null,s=[],c=[],l=null;const i=(e=[])=>{o?.replaceBindings?.(e.map(e=>({accountId:u.accountId,channelType:e.externalChannelType||u.defaultChannelType,channelId:e.externalChannelId,route:{...e.routeConfig||{},nexaAgentId:e.agentId,openclawAgentId:e.routeConfig?.openclawAgentId||e.openclawAgentId||null,agentId:e.routeConfig?.openclawAgentId||e.openclawAgentId||e.routeConfig?.agentId||e.agentId,replyUserId:e.routeConfig?.replyUserId||e.providerBotUserId},metadata:{requireMention:e.requireMention,providerBotUserId:e.providerBotUserId}})))},d=async()=>l||(l=(async()=>{h?.info?.(`[nexa] authenticating control plane accountId=${u.accountId} serverUrl=${g.serverUrl}`),e=await m({...g,pluginVersion:_}),t=e.token,[r,s,c]=await Promise.all([y(g,t),w(g,t),I(g,t)]),o?.setRemoteState?.({token:t,controlPlane:g,auth:e,config:r,agents:s,bindings:c}),i(c||[]);const n=z.get(u.accountId);return n&&(n.token=t,n.websocketUrl=E(g,e,r)),h?.info?.(`[nexa] control plane ready accountId=${u.accountId} preferredTransport=${R(e,r)} websocketUrlPresent=${!!E(g,e,r)} bindings=${c?.length||0} agents=${s?.length||0}`),{auth:e,token:t,remoteConfig:r,remoteAgents:s,remoteBindings:c}})().finally(()=>{l=null}),l);await d();const b=async(e,n,r,o={})=>{const a={channelType:n.channelType,channelId:n.channelId,text:r,userId:o.userId,userName:o.userName,userImage:o.userImage,custom:o.custom};try{const e=await x(g,t,a);return{ok:!0,messageId:e.messageId,createdAt:e.createdAt}}catch(e){if(!k(e))throw e;await d();const n=await x(g,t,a);return{ok:!0,messageId:n.messageId,createdAt:n.createdAt}}},C=async()=>({ok:!0}),T=f({api:p,logger:p?.logger||h||console,sendText:b,ensureUser:C}),A=S({controlPlane:g,getToken:()=>t,store:o,runtimeBridge:T,logger:p?.logger||h||console});!function({accountId:e,controlPlane:t,logger:n,refreshRemoteState:r,getToken:o,getRemoteConfig:s}){const c=e||a;if(V.has(c))return;const l={stopped:!1,timer:null,failureStreak:0};V.set(c,l);const i=e=>{l.stopped||(l.timer=setTimeout(d,Math.max(0,e)))},d=async()=>{if(l.stopped)return;if(q.get(c))return void i(t.heartbeatIntervalMs);q.set(c,!0);try{const e=s(),n=await v(t,o(),{pluginVersion:_,configVersion:e?.configVersion,bindingVersion:e?.bindingVersion});l.failureStreak=0,n?.configVersion&&n.configVersion!==e?.configVersion&&await(r?.())}catch(e){if(l.failureStreak+=1,k(e))try{await(r?.())}catch(e){n?.error?.(`[nexa] heartbeat re-auth failed: ${e?.message||e}`)}n?.error?.(`[nexa] heartbeat failed: ${e?.message||e}`)}finally{q.set(c,!1)}const e=M({baseMs:t.heartbeatIntervalMs,failureStreak:l.failureStreak,maxMs:t.heartbeatFailureMaxMs,jitterRatio:t.heartbeatJitterRatio});l.stopped||V.get(c)!==l||i(e)};i(t.heartbeatIntervalMs)}({accountId:u.accountId,controlPlane:g,logger:p?.logger||n||console,refreshRemoteState:d,getToken:()=>t,getRemoteConfig:()=>r});const P=R(e,r),U=E(g,e,r);if(X(u.accountId).stopped=!1,z.set(u.accountId,{accountId:u.accountId,controlPlane:g,token:t,websocketUrl:U,serverDispatch:A,logger:p?.logger||h||console,shouldUseWebSocket:"websocket"===P&&!!U,refreshRemoteState:d}),h?.info?.(`[nexa] transport selected accountId=${u.accountId} preferred=${P} websocketUrlPresent=${!!U}`),"websocket"===P&&U)try{await ne({accountId:u.accountId,controlPlane:g,token:t,websocketUrl:U,serverDispatch:A,logger:p?.logger||h||console})}catch(e){n?.error?.(`[nexa] websocket transport unavailable, falling back to poll: ${e?.message||e}`),te(u.accountId,A,g,n),re(u.accountId)}else te(u.accountId,A,g,n)}return n?.info?.(`[nexa] account ready: ${u.accountId}`),await(s=r,s?s.aborted?Promise.resolve():new Promise(e=>{s.addEventListener("abort",e,{once:!0})}):new Promise(()=>{})),{ok:!0,accountId:u.accountId,stopped:!0}}catch(e){throw n?.error?.(`[nexa] startAccount failed: ${e?.stack||e?.message||e}`),e}var s},stopAccount:async({accountId:e,log:t})=>{const n=e||a;X(n).stopped=!0,Y(n),z.delete(n),V.get(n)&&function(e){const t=e||a,n=V.get(t);n&&(n.stopped=!0,n.timer&&clearTimeout(n.timer),V.delete(t))}(n),q.delete(n),ee(n),H.delete(n);const r=J.get(n);return r&&(r.close?.(),J.delete(n)),K.delete(n),t?.info?.(`[nexa] account stopped: ${n}`),{ok:!0,accountId:n}}},register(i){Q=i,i.registerHttpRoute({path:"/nexa/webhook",auth:"plugin",match:"exact",handler:async(n,r)=>{if("POST"!==n.method)return j(r,405,{ok:!1,error:"method_not_allowed"});const s=function(t){const n=t.headers.host?`http://${t.headers.host}`:"http://localhost";return new e(t.url||"/",n).searchParams.get("accountId")||a}(n),c=d(i.config||{},s),l=await async function(e){return"string"==typeof e.rawBody?e.rawBody:await new Promise((n,r)=>{const o=[];e.on("data",e=>o.push(t.isBuffer(e)?e:t.from(e))),e.on("end",()=>n(t.concat(o).toString("utf8"))),e.on("error",r)})}(n),g=c.webhookSignatureHeader||"x-signature",m=n.headers?.[g],w=Array.isArray(m)?m[0]:m;if(!w||!await B(c,l,w))return j(r,401,{ok:!1,error:"invalid_signature"});const I=JSON.parse(l),y=u(I),b=y.channelId?o?.getBinding?.({accountId:s,channelType:y.channelType,channelId:y.channelId}):null;o?.setLastWebhook?.({receivedAt:(new Date).toISOString(),accountId:s,account:c,normalized:y,binding:b});const k=f({api:i,logger:i.logger||console,sendText:N,ensureUser:W}),v=P||h({logger:i.logger||console,runtimeBridge:k}),x=p({inboundHandler:v,store:o,logger:i.logger||console}),S=await x.dispatch({accountId:s,account:c,binding:b,normalized:y,raw:I});return i.logger?.info?.(`[nexa] webhook accepted type=${y.eventType} channel=${y.channelType}:${y.channelId||"unknown"}`),j(r,200,{ok:!0,accountId:s,accepted:!0,eventType:y.eventType,channelId:y.channelId,delivered:S.delivered,dispatchReason:S.reason})}}),i.registerGatewayMethod("nexa.bindChannel",({params:e,respond:t})=>{const n=o?.bindChannel?.({accountId:e?.accountId||a,channelType:e?.channelType||s,channelId:e?.channelId,route:e?.route||{},metadata:e?.metadata||null});t(!0,{ok:!0,binding:n})}),i.registerGatewayMethod("nexa.unbindChannel",({params:e,respond:t})=>{const n=o?.unbindChannel?.({accountId:e?.accountId||a,channelType:e?.channelType||s,channelId:e?.channelId});t(!0,{ok:!0,removed:n})}),i.registerGatewayMethod("nexa.bindings",({respond:e})=>{e(!0,{ok:!0,bindings:o?.getBindings?.()||[]})}),i.registerGatewayMethod("nexa.login",async({params:e,respond:s})=>{try{const c=e?.code,l=e?.bootstrap,d=c?function(e){if("string"!=typeof e||!e.trim())throw new Error("code_required");if(!e.startsWith("ocb_"))throw new Error("invalid_code_prefix");const o=t.from(e.slice(4),"base64url");if(o.length<=28)throw new Error("invalid_code_payload");const a=o.subarray(0,12),s=o.subarray(12,28),c=o.subarray(28),l=n("aes-256-gcm",r(process.env.OPENCLAW_BOOTSTRAP_CODE_SECRET||"nexa-opaque-code-v1","openclaw-nexa-bootstrap-code",32),a);l.setAuthTag(s);const i=t.concat([l.update(c),l.final()]).toString("utf8"),d=JSON.parse(i);if(!d||"object"!=typeof d)throw new Error("invalid_code_payload");return d}(c):function(e){if("string"!=typeof e||!e.trim())throw new Error("bootstrap_required");const n=e.split(".");if(n.length<2)throw new Error("invalid_bootstrap");const r=JSON.parse(function(e){const n=String(e||"").replace(/-/g,"+").replace(/_/g,"/"),r=n.padEnd(4*Math.ceil(n.length/4),"=");return t.from(r,"base64").toString("utf8")}(n[1]));if(!r||"object"!=typeof r)throw new Error("invalid_bootstrap_payload");return r}(l);if(!d.bootstrapServer)throw new Error("bootstrap_server_missing");if(!d.exp||1e3*Number(d.exp)<=Date.now())throw new Error(c?"code_expired":"bootstrap_expired");if(c&&(!d.exchangeCode||!d.jti))throw new Error("invalid_code_exchange_payload");if(!c&&"openclaw-plugin"!==d.aud)throw new Error("invalid_bootstrap_audience");const u=await b(d.bootstrapServer,c?{exchangeCode:d.exchangeCode,jti:d.jti}:{bootstrap:l}),g=await G.save({controlPlane:{enabled:!0,serverUrl:u.serverUrl,channelKey:u.channelKey,channelSecret:u.channelSecret},defaultOpenClawAgentId:u.defaultOpenClawAgentId||"main",agentChannelId:u.agentChannelId||d.agentChannelId||null,environment:u.environment||d.env||null,issuedAt:(new Date).toISOString()});o?.setRemoteState?.(null),o?.replaceBindings?.([]);const p=A(i.config||{},g);await Z(a,i.logger||console),function(e,t,n){const r=e||a,o=new AbortController,s=oe.gateway.startAccount({accountId:r,cfg:t,log:n,abortSignal:o.signal}).catch(e=>{n?.error?.(`[nexa] managed account start failed: ${e?.message||e}`)}).finally(()=>{const e=F.get(r);e?.controller===o&&F.delete(r)});F.set(r,{controller:o,promise:s})}(a,p,i.logger||console),s(!0,{ok:!0,environment:g.environment,defaultOpenClawAgentId:g.defaultOpenClawAgentId,agentChannelId:g.agentChannelId,serverUrl:g.controlPlane.serverUrl,statePath:G.path||null})}catch(e){s(!1,{ok:!1,error:e?.message||String(e)})}}),i.registerGatewayMethod("nexa.logout",async({respond:e})=>{try{await Z(a,i.logger||console),await G.clear(),o?.setRemoteState?.(null),o?.replaceBindings?.([]),e(!0,{ok:!0})}catch(t){e(!1,{ok:!1,error:t?.message||String(t)})}}),i.registerGatewayMethod("nexa.whoami",async({respond:e})=>{try{const t=await G.load(),n=o?.getRemoteState?.()||null;e(!0,{ok:!0,loggedIn:!!t,environment:t?.environment||null,defaultOpenClawAgentId:t?.defaultOpenClawAgentId||n?.defaultOpenClawAgentId||null,agentChannelId:t?.agentChannelId||null,serverUrl:t?.controlPlane?.serverUrl||null,statePath:G.path||null})}catch(t){e(!1,{ok:!1,error:t?.message||String(t)})}}),i.registerGatewayMethod("nexa.status",({respond:e})=>{e(!0,{ok:!0,channelId:c,accounts:l(i.config||{}),bindings:o?.getBindings?.()||[],controlPlane:o?.getRemoteState?.()||null})}),i.registerGatewayMethod("nexa.lastWebhook",({respond:e})=>{e(!0,{ok:!0,webhook:o?.getLastWebhook?.()||null})}),i.registerGatewayMethod("nexa.lastDispatch",({respond:e})=>{e(!0,{ok:!0,dispatch:o?.getLastDispatch?.()||null})}),i.registerChannel({plugin:this})}};return oe}export const createStreamChatPlugin=createNexaOpenClawPlugin;
1
+ import{URL as e}from"node:url";import{Buffer as t}from"node:buffer";import{createDecipheriv as n,scryptSync as r}from"node:crypto";import{createRequire as o}from"node:module";import{DEFAULT_ACCOUNT_ID as a,DEFAULT_CHANNEL_TYPE as s,NEXA_OPENCLAW_CHANNEL_ID as c,listAccountIds as l,resolveControlPlane as i,resolveAccount as d}from"./config.js";import{normalizeWebhookEvent as u,resolveOutboundTarget as g}from"./normalize.js";import{createInboundAdapter as p}from"./inbound-adapter.js";import{createDefaultInboundHandler as h}from"./default-inbound-handler.js";import{createRuntimeBridge as f}from"./runtime-bridge.js";import{authenticateControlPlane as m,fetchControlPlaneAgents as w,fetchControlPlaneBindings as I,fetchControlPlaneConfig as y,exchangeBootstrap as b,isUnauthorizedControlPlaneError as k,sendHeartbeat as v,sendReply as x}from"./server-client.js";import{createServerDispatch as S}from"./server-dispatch.js";import{createServerSocket as M}from"./server-socket.js";import{computeNextHeartbeatDelay as C,computeNextPollDelay as T}from"./transport-timing.js";import{mergeConfigWithPluginState as A}from"./plugin-state.js";const P=o(import.meta.url),{version:U}=P("../package.json");async function $(e,t,n,r){const{sendTextToStream:o}=await import("./stream-client.js");return o(e,t,n,r)}async function _(e,t,n){const{verifyStreamWebhook:r}=await import("./stream-client.js");return r(e,t,n)}async function j(e,t,n){const{ensureStreamUser:r}=await import("./stream-client.js");return r(e,t,n)}function O(e,t,n){return e.statusCode=t,e.setHeader("content-type","application/json"),e.end(JSON.stringify(n)),!0}function R(e,t){return t?.transport?.preferredTransport||e?.transport?.preferredTransport||"long-poll"}function B(t,n,r){const o=r?.transport?.websocketUrl||n?.transport?.websocketUrl;if(!o)return null;if(o.startsWith("ws://")||o.startsWith("wss://"))return o;const a=new e(t.serverUrl),s="https:"===a.protocol?"wss:":"ws:";return new e(o,`${s}//${a.host}`).toString()}export function createNexaOpenClawPlugin({store:o,inboundHandler:P,verifyWebhook:E=_,sendText:N=$,ensureUser:W=j,createServerSocketClient:D=M,stateStore:G={path:null,load:async()=>null,save:async()=>{throw new Error("nexa_state_store_not_configured")},clear:async()=>{}}}={}){const V=new Map,J=new Map,L=new Map,q=new Map,H=new Map,K=new Map,z=new Map,F=new Map;let Q=null;function X(e){const t=e||a,n=K.get(t);if(n)return n;const r={timer:null,attempt:0,connecting:!1,stopped:!1};return K.set(t,r),r}function Y(e){const t=K.get(e||a);t?.timer&&(clearTimeout(t.timer),t.timer=null)}async function Z(e,t){const n=e||a,r=F.get(n);if(r){r.controller.abort(),await r.promise.catch(()=>{}),F.delete(n);try{await oe.gateway.stopAccount({accountId:n,log:t})}catch{}}}function ee(e){const t=e||a,n=J.get(t);n&&(n.stopped=!0,n.timer&&clearTimeout(n.timer),J.delete(t))}function te(e,t,n,r){const o=e||a;if(J.has(o))return;const s={stopped:!1,timer:null,emptyStreak:0,failureStreak:0};J.set(o,s);const c=e=>{s.stopped||(s.timer=setTimeout(l,Math.max(0,e)))},l=async()=>{if(s.stopped)return;if(H.get(o))return void c(n.pollIntervalMs);H.set(o,!0);let e=n.pollIntervalMs,a=0;try{a=await t.pollOnce(),s.failureStreak=0,s.emptyStreak=a>0?0:s.emptyStreak+1}catch(e){if(s.failureStreak+=1,k(e)){const e=z.get(o);try{await(e?.refreshRemoteState?.())}catch(e){r?.error?.(`[nexa] poll re-auth failed: ${e?.message||e}`)}}r?.error?.(`[nexa] inbox poll failed: ${e?.message||e}`)}finally{e=T({baseMs:n.pollIntervalMs,idleMinMs:n.pollIdleMinMs,idleMaxMs:n.pollIdleMaxMs,emptyStreak:s.emptyStreak,failureStreak:s.failureStreak,deliveredCount:a,jitterRatio:n.pollJitterRatio}),H.set(o,!1)}s.stopped||J.get(o)!==s||c(e)};c(0)}async function ne(e){const{accountId:t,websocketUrl:n,token:r,logger:o,serverDispatch:a,controlPlane:s}=e;if(!n||L.has(t))return!1;const c=D({websocketUrl:n,token:r,logger:o,dispatch:a,onDisconnect:()=>{L.delete(t),!K.get(t)?.stopped&&z.has(t)&&(te(t,a,s,o),re(t))}});await c.connect(),L.set(t,c),ee(t);const l=X(t);return l.attempt=0,l.connecting=!1,Y(t),!0}function re(e){const t=e||a,n=z.get(t);if(!n?.shouldUseWebSocket)return;const r=X(t);if(r.stopped||r.timer||r.connecting||L.has(t))return;r.attempt+=1;const o=Math.min(n.controlPlane.websocketReconnectMaxMs,n.controlPlane.websocketReconnectBaseMs*2**Math.max(r.attempt-1,0));r.timer=setTimeout(async()=>{if(r.timer=null,!r.stopped&&!L.has(t)){r.connecting=!0;try{await(n.refreshRemoteState?.()),await ne(n)}catch(e){n.logger?.error?.(`[nexa] websocket reconnect failed: ${e?.message||e}`)}finally{r.connecting=!1,r.stopped||L.has(t)||re(t)}}},o)}const oe={id:c,meta:{id:c,label:"Nexa",selectionLabel:"Nexa OpenClaw",detailLabel:"Nexa OpenClaw Channel",docsPath:"/channels/nexa",blurb:"Nexa native channel plugin for OpenClaw groups.",aliases:["nexa","getstream"]},capabilities:{chatTypes:["direct","group"]},config:{listAccountIds:l,resolveAccount:d},outbound:{deliveryMode:"direct",sendText:async e=>{const t=d(e?.config||{},e?.accountId||a),n=g(e,t.defaultChannelType||s);return N(t,n,e?.text||"",{userId:e?.userId,custom:e?.custom})}},gateway:{startAccount:async({accountId:e,cfg:t,log:n,abortSignal:r})=>{try{const c=await G.load(),l=A(t,c),u=d(l,e||a),g=i(l),p=Q,h=p?.logger||n||console;if(h?.info?.(`[nexa] startAccount accountId=${u.accountId} controlPlaneEnabled=${g.enabled} serverUrl=${g.serverUrl||"missing"} channelKeyPresent=${!!g.channelKey} channelSecretPresent=${!!g.channelSecret}`),g.enabled){let e=null,t=null,r=null,s=[],c=[],l=null;const i=(e=[])=>{o?.replaceBindings?.(e.map(e=>({accountId:u.accountId,channelType:e.externalChannelType||u.defaultChannelType,channelId:e.externalChannelId,route:{...e.routeConfig||{},nexaAgentId:e.agentId,openclawAgentId:e.routeConfig?.openclawAgentId||e.openclawAgentId||null,agentId:e.routeConfig?.openclawAgentId||e.openclawAgentId||e.routeConfig?.agentId||e.agentId,replyUserId:e.routeConfig?.replyUserId||e.providerBotUserId},metadata:{requireMention:e.requireMention,providerBotUserId:e.providerBotUserId}})))},d=async()=>l||(l=(async()=>{h?.info?.(`[nexa] authenticating control plane accountId=${u.accountId} serverUrl=${g.serverUrl}`),e=await m({...g,pluginVersion:U}),t=e.token,[r,s,c]=await Promise.all([y(g,t),w(g,t),I(g,t)]),o?.setRemoteState?.({token:t,controlPlane:g,auth:e,config:r,agents:s,bindings:c}),i(c||[]);const n=z.get(u.accountId);return n&&(n.token=t,n.websocketUrl=B(g,e,r)),h?.info?.(`[nexa] control plane ready accountId=${u.accountId} preferredTransport=${R(e,r)} websocketUrlPresent=${!!B(g,e,r)} bindings=${c?.length||0} agents=${s?.length||0}`),{auth:e,token:t,remoteConfig:r,remoteAgents:s,remoteBindings:c}})().finally(()=>{l=null}),l);await d();const b=async(e,n,r,o={})=>{const a={channelType:n.channelType,channelId:n.channelId,text:r,userId:o.userId,userName:o.userName,userImage:o.userImage,custom:o.custom};try{const e=await x(g,t,a);return{ok:!0,messageId:e.messageId,createdAt:e.createdAt}}catch(e){if(!k(e))throw e;await d();const n=await x(g,t,a);return{ok:!0,messageId:n.messageId,createdAt:n.createdAt}}},M=async()=>({ok:!0}),T=f({api:p,logger:p?.logger||h||console,sendText:b,ensureUser:M}),A=S({controlPlane:g,getToken:()=>t,store:o,runtimeBridge:T,logger:p?.logger||h||console});!function({accountId:e,controlPlane:t,logger:n,refreshRemoteState:r,getToken:o,getRemoteConfig:s}){const c=e||a;if(V.has(c))return;const l={stopped:!1,timer:null,failureStreak:0};V.set(c,l);const i=e=>{l.stopped||(l.timer=setTimeout(d,Math.max(0,e)))},d=async()=>{if(l.stopped)return;if(q.get(c))return void i(t.heartbeatIntervalMs);q.set(c,!0);try{const e=s(),n=await v(t,o(),{pluginVersion:U,configVersion:e?.configVersion,bindingVersion:e?.bindingVersion});l.failureStreak=0,n?.configVersion&&n.configVersion!==e?.configVersion&&await(r?.())}catch(e){if(l.failureStreak+=1,k(e))try{await(r?.())}catch(e){n?.error?.(`[nexa] heartbeat re-auth failed: ${e?.message||e}`)}n?.error?.(`[nexa] heartbeat failed: ${e?.message||e}`)}finally{q.set(c,!1)}const e=C({baseMs:t.heartbeatIntervalMs,failureStreak:l.failureStreak,maxMs:t.heartbeatFailureMaxMs,jitterRatio:t.heartbeatJitterRatio});l.stopped||V.get(c)!==l||i(e)};i(t.heartbeatIntervalMs)}({accountId:u.accountId,controlPlane:g,logger:p?.logger||n||console,refreshRemoteState:d,getToken:()=>t,getRemoteConfig:()=>r});const P=R(e,r),$=B(g,e,r);if(X(u.accountId).stopped=!1,z.set(u.accountId,{accountId:u.accountId,controlPlane:g,token:t,websocketUrl:$,serverDispatch:A,logger:p?.logger||h||console,shouldUseWebSocket:"websocket"===P&&!!$,refreshRemoteState:d}),h?.info?.(`[nexa] transport selected accountId=${u.accountId} preferred=${P} websocketUrlPresent=${!!$}`),"websocket"===P&&$)try{await ne({accountId:u.accountId,controlPlane:g,token:t,websocketUrl:$,serverDispatch:A,logger:p?.logger||h||console})}catch(e){n?.error?.(`[nexa] websocket transport unavailable, falling back to poll: ${e?.message||e}`),te(u.accountId,A,g,n),re(u.accountId)}else te(u.accountId,A,g,n)}return n?.info?.(`[nexa] account ready: ${u.accountId}`),await(s=r,s?s.aborted?Promise.resolve():new Promise(e=>{s.addEventListener("abort",e,{once:!0})}):new Promise(()=>{})),{ok:!0,accountId:u.accountId,stopped:!0}}catch(e){throw n?.error?.(`[nexa] startAccount failed: ${e?.stack||e?.message||e}`),e}var s},stopAccount:async({accountId:e,log:t})=>{const n=e||a;X(n).stopped=!0,Y(n),z.delete(n),V.get(n)&&function(e){const t=e||a,n=V.get(t);n&&(n.stopped=!0,n.timer&&clearTimeout(n.timer),V.delete(t))}(n),q.delete(n),ee(n),H.delete(n);const r=L.get(n);return r&&(r.close?.(),L.delete(n)),K.delete(n),t?.info?.(`[nexa] account stopped: ${n}`),{ok:!0,accountId:n}}},register(i){Q=i,i.registerHttpRoute({path:"/nexa/webhook",auth:"plugin",match:"exact",handler:async(n,r)=>{if("POST"!==n.method)return O(r,405,{ok:!1,error:"method_not_allowed"});const s=function(t){const n=t.headers.host?`http://${t.headers.host}`:"http://localhost";return new e(t.url||"/",n).searchParams.get("accountId")||a}(n),c=d(i.config||{},s),l=await async function(e){return"string"==typeof e.rawBody?e.rawBody:await new Promise((n,r)=>{const o=[];e.on("data",e=>o.push(t.isBuffer(e)?e:t.from(e))),e.on("end",()=>n(t.concat(o).toString("utf8"))),e.on("error",r)})}(n),g=c.webhookSignatureHeader||"x-signature",m=n.headers?.[g],w=Array.isArray(m)?m[0]:m;if(!w||!await E(c,l,w))return O(r,401,{ok:!1,error:"invalid_signature"});const I=JSON.parse(l),y=u(I),b=y.channelId?o?.getBinding?.({accountId:s,channelType:y.channelType,channelId:y.channelId}):null;o?.setLastWebhook?.({receivedAt:(new Date).toISOString(),accountId:s,account:c,normalized:y,binding:b});const k=f({api:i,logger:i.logger||console,sendText:N,ensureUser:W}),v=P||h({logger:i.logger||console,runtimeBridge:k}),x=p({inboundHandler:v,store:o,logger:i.logger||console}),S=await x.dispatch({accountId:s,account:c,binding:b,normalized:y,raw:I});return i.logger?.info?.(`[nexa] webhook accepted type=${y.eventType} channel=${y.channelType}:${y.channelId||"unknown"}`),O(r,200,{ok:!0,accountId:s,accepted:!0,eventType:y.eventType,channelId:y.channelId,delivered:S.delivered,dispatchReason:S.reason})}}),i.registerGatewayMethod("nexa.bindChannel",({params:e,respond:t})=>{const n=o?.bindChannel?.({accountId:e?.accountId||a,channelType:e?.channelType||s,channelId:e?.channelId,route:e?.route||{},metadata:e?.metadata||null});t(!0,{ok:!0,binding:n})}),i.registerGatewayMethod("nexa.unbindChannel",({params:e,respond:t})=>{const n=o?.unbindChannel?.({accountId:e?.accountId||a,channelType:e?.channelType||s,channelId:e?.channelId});t(!0,{ok:!0,removed:n})}),i.registerGatewayMethod("nexa.bindings",({respond:e})=>{e(!0,{ok:!0,bindings:o?.getBindings?.()||[]})}),i.registerGatewayMethod("nexa.login",async({params:e,respond:s})=>{try{const c=e?.code,l=e?.bootstrap,d=c?function(e){if("string"!=typeof e||!e.trim())throw new Error("code_required");if(!e.startsWith("ocb_"))throw new Error("invalid_code_prefix");const o=t.from(e.slice(4),"base64url");if(o.length<=28)throw new Error("invalid_code_payload");const a=o.subarray(0,12),s=o.subarray(12,28),c=o.subarray(28),l=n("aes-256-gcm",r("nexa-opaque-code-v1","openclaw-nexa-bootstrap-code",32),a);l.setAuthTag(s);const i=t.concat([l.update(c),l.final()]).toString("utf8"),d=JSON.parse(i);if(!d||"object"!=typeof d)throw new Error("invalid_code_payload");return d}(c):function(e){if("string"!=typeof e||!e.trim())throw new Error("bootstrap_required");const n=e.split(".");if(n.length<2)throw new Error("invalid_bootstrap");const r=JSON.parse(function(e){const n=String(e||"").replace(/-/g,"+").replace(/_/g,"/"),r=n.padEnd(4*Math.ceil(n.length/4),"=");return t.from(r,"base64").toString("utf8")}(n[1]));if(!r||"object"!=typeof r)throw new Error("invalid_bootstrap_payload");return r}(l);if(!d.bootstrapServer)throw new Error("bootstrap_server_missing");if(!d.exp||1e3*Number(d.exp)<=Date.now())throw new Error(c?"code_expired":"bootstrap_expired");if(c&&(!d.exchangeCode||!d.jti))throw new Error("invalid_code_exchange_payload");if(!c&&"openclaw-plugin"!==d.aud)throw new Error("invalid_bootstrap_audience");const u=await b(d.bootstrapServer,c?{exchangeCode:d.exchangeCode,jti:d.jti}:{bootstrap:l}),g=await G.save({controlPlane:{enabled:!0,serverUrl:u.serverUrl,channelKey:u.channelKey,channelSecret:u.channelSecret},defaultOpenClawAgentId:u.defaultOpenClawAgentId||"main",agentChannelId:u.agentChannelId||d.agentChannelId||null,environment:u.environment||d.env||null,issuedAt:(new Date).toISOString()});o?.setRemoteState?.(null),o?.replaceBindings?.([]);const p=A(i.config||{},g);await Z(a,i.logger||console),function(e,t,n){const r=e||a,o=new AbortController,s=oe.gateway.startAccount({accountId:r,cfg:t,log:n,abortSignal:o.signal}).catch(e=>{n?.error?.(`[nexa] managed account start failed: ${e?.message||e}`)}).finally(()=>{const e=F.get(r);e?.controller===o&&F.delete(r)});F.set(r,{controller:o,promise:s})}(a,p,i.logger||console),s(!0,{ok:!0,environment:g.environment,defaultOpenClawAgentId:g.defaultOpenClawAgentId,agentChannelId:g.agentChannelId,serverUrl:g.controlPlane.serverUrl,statePath:G.path||null})}catch(e){s(!1,{ok:!1,error:e?.message||String(e)})}}),i.registerGatewayMethod("nexa.logout",async({respond:e})=>{try{await Z(a,i.logger||console),await G.clear(),o?.setRemoteState?.(null),o?.replaceBindings?.([]),e(!0,{ok:!0})}catch(t){e(!1,{ok:!1,error:t?.message||String(t)})}}),i.registerGatewayMethod("nexa.whoami",async({respond:e})=>{try{const t=await G.load(),n=o?.getRemoteState?.()||null;e(!0,{ok:!0,loggedIn:!!t,environment:t?.environment||null,defaultOpenClawAgentId:t?.defaultOpenClawAgentId||n?.defaultOpenClawAgentId||null,agentChannelId:t?.agentChannelId||null,serverUrl:t?.controlPlane?.serverUrl||null,statePath:G.path||null})}catch(t){e(!1,{ok:!1,error:t?.message||String(t)})}}),i.registerGatewayMethod("nexa.status",({respond:e})=>{e(!0,{ok:!0,channelId:c,accounts:l(i.config||{}),bindings:o?.getBindings?.()||[],controlPlane:o?.getRemoteState?.()||null})}),i.registerGatewayMethod("nexa.lastWebhook",({respond:e})=>{e(!0,{ok:!0,webhook:o?.getLastWebhook?.()||null})}),i.registerGatewayMethod("nexa.lastDispatch",({respond:e})=>{e(!0,{ok:!0,dispatch:o?.getLastDispatch?.()||null})}),i.registerChannel({plugin:this})}};return oe}export const createStreamChatPlugin=createNexaOpenClawPlugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexa26/nexa",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Nexa OpenClaw native channel plugin",