@stkxp/cli 0.1.5 → 0.1.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stkxp/cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Export a Stack Expert Team as an encrypted, standalone-deployable bundle, and redeploy it on any Elasticsearch cluster.",
package/src/export.mjs CHANGED
@@ -7,6 +7,26 @@
7
7
  // dist-server payload used by `stkxp deploy` — that only runs on the TARGET.
8
8
  import { writeFile } from "node:fs/promises";
9
9
 
10
+ // The route validation middleware (server/routes/index.ts) responds with
11
+ // { error: "Invalid body", issues: ZodError.format() } on a 400 — `issues`
12
+ // carries the actually useful detail (e.g. "secret must be at least 8
13
+ // characters") but was previously dropped entirely, leaving the operator
14
+ // with just the generic "Invalid body" and no idea why.
15
+ function formatApiError(body, fallback) {
16
+ const base = body?.error ?? fallback;
17
+ if (!body?.issues) return base;
18
+ const messages = [];
19
+ const walk = (node) => {
20
+ if (!node || typeof node !== "object") return;
21
+ if (Array.isArray(node._errors) && node._errors.length > 0) messages.push(...node._errors);
22
+ for (const [key, value] of Object.entries(node)) {
23
+ if (key !== "_errors") walk(value);
24
+ }
25
+ };
26
+ walk(body.issues);
27
+ return messages.length > 0 ? `${base}: ${messages.join("; ")}` : base;
28
+ }
29
+
10
30
  export async function runExport({ sourceUrl, token, teamId, secret, outFile }) {
11
31
  const url = new URL(`/api/teams/${teamId}/export-encrypted`, sourceUrl);
12
32
  const response = await fetch(url, {
@@ -20,7 +40,7 @@ export async function runExport({ sourceUrl, token, teamId, secret, outFile }) {
20
40
 
21
41
  const body = await response.json();
22
42
  if (!response.ok || !body.success) {
23
- throw new Error(`Export failed (${response.status}): ${body.error ?? response.statusText}`);
43
+ throw new Error(`Export failed (${response.status}): ${formatApiError(body, response.statusText)}`);
24
44
  }
25
45
 
26
46
  // 0600: the bundle carries every one of the team's credentials, encrypted
@@ -53,4 +53,24 @@ describe("runExport", () => {
53
53
  await expect(promise).rejects.toThrow(/Export failed \(403\): not the owner/);
54
54
  await expect(promise).rejects.not.toThrow(/super-secret-pw/);
55
55
  });
56
+
57
+ it("surfaces the Zod validation detail on a 400 Invalid body response instead of just the generic message", async () => {
58
+ // Exact shape server/routes/index.ts sends: { error: "Invalid body", issues: ZodError.format() }
59
+ vi.stubGlobal(
60
+ "fetch",
61
+ vi.fn(async () => ({
62
+ ok: false,
63
+ status: 400,
64
+ statusText: "Bad Request",
65
+ json: async () => ({
66
+ success: false,
67
+ error: "Invalid body",
68
+ issues: { secret: { _errors: ["String must contain at least 8 character(s)"] }, _errors: [] },
69
+ }),
70
+ })),
71
+ );
72
+ const outFile = await tmpOut("bundle.json");
73
+ const promise = runExport({ sourceUrl: "https://app.example.com", token: "jwt", teamId: "team-1", secret: "short", outFile });
74
+ await expect(promise).rejects.toThrow(/Export failed \(400\): Invalid body: String must contain at least 8 character\(s\)/);
75
+ });
56
76
  });
@@ -1 +1 @@
1
- import{randomBytes as p}from"crypto";import{readFileSync as h}from"fs";const w="127.0.0.1";function m(e){return e.esInsecure===!0?{rejectUnauthorized:!1}:e.esCaPath?{rejectUnauthorized:!0,ca:h(e.esCaPath,"utf8")}:{rejectUnauthorized:!0}}async function b(e){process.env.ELASTICSEARCH_HOST=e.esUrl,process.env.ELASTICSEARCH_USER=e.esUser,process.env.ELASTICSEARCH_PASSWORD=e.esPassword,e.esCaPath&&(process.env.ELASTICSEARCH_CA_PATH=e.esCaPath),process.env.ELASTICSEARCH_TLS_REJECT_UNAUTHORIZED=e.esInsecure===!0?"false":"true";const{parseAndDecryptTeamBundle:o}=await import("../services/clone/team-bundle-encrypted"),{closure:n,rootName:r}=o(e.bundle,e.secret),t=n.docs.find(i=>i.key==="team");if(!t)throw new Error("Decrypted bundle has no team document \u2014 cannot determine which team to deploy");const{Client:a}=await import("@elastic/elasticsearch"),d=new a({node:e.esUrl,auth:{username:e.esUser,password:e.esPassword},tls:m(e)}),{bootstrapClosureToEs:u}=await import("../services/clone/team-bundle-bootstrap"),s=await u(d,n),c=await d.get({index:t.index,id:t.esId}).then(i=>i?._source??null).catch(()=>null);if(c?.webhookEnabled&&c?.webhookToken)return{rootName:r,teamId:t.esId,teamIndex:t.index,webhookToken:null,alreadyBootstrapped:!0,indicesCreated:s.indicesCreated,docsIndexed:s.docsIndexed};const l=p(32).toString("hex");return await d.update({index:t.index,id:t.esId,body:{doc:{webhookToken:l,webhookEnabled:!0,webhookCreatedAt:new Date().toISOString()}},refresh:"wait_for"}),{rootName:r,teamId:t.esId,teamIndex:t.index,webhookToken:l,alreadyBootstrapped:!1,indicesCreated:s.indicesCreated,docsIndexed:s.docsIndexed}}async function C(e){const o=await b(e),{buildTeamRunnerApp:n}=await import("./team-runner"),r=n(),t=e.webhookHost??w;return await new Promise(a=>{r.listen(e.webhookPort,t,()=>a())}),{rootName:o.rootName,teamId:o.teamId,webhookToken:o.webhookToken??"(unchanged \u2014 target was already bootstrapped, existing token not re-shown)",webhookUrl:`http://localhost:${e.webhookPort}/api/webhooks/team/${o.teamId}`,indicesCreated:o.indicesCreated,docsIndexed:o.docsIndexed}}export{w as DEFAULT_WEBHOOK_HOST,b as bootstrapTeam,m as buildTargetEsTls,C as runDeploy};
1
+ import{randomBytes as h}from"crypto";import{readFileSync as w}from"fs";const m="127.0.0.1";function b(e){return e.esInsecure===!0?{rejectUnauthorized:!1}:e.esCaPath?{rejectUnauthorized:!0,ca:w(e.esCaPath,"utf8")}:{rejectUnauthorized:!0}}async function I(e){process.env.ELASTICSEARCH_HOST=e.esUrl,process.env.ELASTICSEARCH_USER=e.esUser,process.env.ELASTICSEARCH_PASSWORD=e.esPassword,e.esCaPath&&(process.env.ELASTICSEARCH_CA_PATH=e.esCaPath),process.env.ELASTICSEARCH_TLS_REJECT_UNAUTHORIZED=e.esInsecure===!0?"false":"true";const{parseAndDecryptTeamBundle:o}=await import("../services/clone/team-bundle-encrypted"),{closure:n,rootName:r,indexMappings:a}=o(e.bundle,e.secret),t=n.docs.find(i=>i.key==="team");if(!t)throw new Error("Decrypted bundle has no team document \u2014 cannot determine which team to deploy");const{Client:u}=await import("@elastic/elasticsearch"),d=new u({node:e.esUrl,auth:{username:e.esUser,password:e.esPassword},tls:b(e)}),{bootstrapClosureToEs:p}=await import("../services/clone/team-bundle-bootstrap"),s=await p(d,n,a),c=await d.get({index:t.index,id:t.esId}).then(i=>i?._source??null).catch(()=>null);if(c?.webhookEnabled&&c?.webhookToken)return{rootName:r,teamId:t.esId,teamIndex:t.index,webhookToken:null,alreadyBootstrapped:!0,indicesCreated:s.indicesCreated,docsIndexed:s.docsIndexed};const l=h(32).toString("hex");return await d.update({index:t.index,id:t.esId,body:{doc:{webhookToken:l,webhookEnabled:!0,webhookCreatedAt:new Date().toISOString()}},refresh:"wait_for"}),{rootName:r,teamId:t.esId,teamIndex:t.index,webhookToken:l,alreadyBootstrapped:!1,indicesCreated:s.indicesCreated,docsIndexed:s.docsIndexed}}async function x(e){const o=await I(e),{buildTeamRunnerApp:n}=await import("./team-runner"),r=n(),a=e.webhookHost??m;return await new Promise(t=>{r.listen(e.webhookPort,a,()=>t())}),{rootName:o.rootName,teamId:o.teamId,webhookToken:o.webhookToken??"(unchanged \u2014 target was already bootstrapped, existing token not re-shown)",webhookUrl:`http://localhost:${e.webhookPort}/api/webhooks/team/${o.teamId}`,indicesCreated:o.indicesCreated,docsIndexed:o.docsIndexed}}export{m as DEFAULT_WEBHOOK_HOST,I as bootstrapTeam,b as buildTargetEsTls,x as runDeploy};
@@ -1 +1 @@
1
- import{Client as q}from"@elastic/elasticsearch";import{z as u}from"zod";import{randomBytes as O,randomUUID as K}from"crypto";import{assertOwner as A,getAuthUsername as C,isSuperuser as V}from"../core/utils/ownership";import{makeOwnerScopedClient as Q}from"../core/utils/owner-scope";import{resolveClosure as $,createEsStore as j,PERSONAL_TOOLS_REACHED_VIA as U}from"../services/clone/closure-resolver";import{executeClone as X,createEsCloneWriter as Z}from"../services/clone/clone-executor";import{serializeTeamBundle as J,parseTeamBundle as Y,TeamBundleParseError as ee}from"../services/clone/team-bundle";import{serializeTeamBundleEncrypted as se}from"../services/clone/team-bundle-encrypted";import{buildFullStructureGraph as te}from"../services/graph-canvas/full-structure-builder";import{MultiKibanaService as ae}from"../core/services/multi-kibana-service";import{projectAssistantTokens as ne}from"../core/services/token-projection-service";import{buildMcpAppTesterZip as oe,slugifyTeamName as re}from"../services/mcp-app-tester-package";import{createGatewayGrant as D,redeemGatewayGrant as ie,GATEWAY_UNREACHABLE_PREFIX as N}from"../services/mcp-gateway/gateway-grant-service";import{isGatewayEligiblePlatformType as z}from"../core/services/gateway-platform-types";import{syncRemoteToolsForPlatform as ce}from"../core/services/mcp-sync-service";import{snapshotBeforeUpdate as de}from"../core/services/version-snapshot-service";import{findTeamOwningCommand as le,getChannelCommandOwnership as me}from"../core/services/teams-service";const ue=new ae,F=process.env.ELASTICSEARCH_HOST||"https://51.159.174.230:9205",G=process.env.ELASTICSEARCH_USER||"elastic",B=process.env.ELASTICSEARCH_PASSWORD||"",w=".stkxp_teams",v=".stkxp_assistants",pe=".stkxp_chats",p=Q({node:F,auth:{username:G,password:B},tls:{rejectUnauthorized:!1},requestTimeout:6e4,maxRetries:3}),P=new q({node:F,auth:{username:G,password:B},tls:{rejectUnauthorized:!1},requestTimeout:6e4,maxRetries:3}),L=u.object({enabled:u.string().optional().transform(s=>{if(s!==void 0)return s==="true"}),page:u.string().optional().transform(s=>s?parseInt(s,10):1),pageSize:u.string().optional().transform(s=>s?parseInt(s,10):25),search:u.string().optional().describe("Hybrid lexical + semantic search across name and description"),packageName:u.string().optional(),owners:u.string().optional(),currentUser:u.string().optional(),includeAllSystem:u.string().optional().transform(s=>s==="true"),includeOwned:u.union([u.boolean(),u.string()]).optional().transform(s=>s===!0||s==="true"),assistantId:u.string().optional()});async function ge(){try{await p.indices.create({index:w,body:{mappings:{properties:{name:{type:"keyword",copy_to:"search_semantic"},description:{type:"text",copy_to:"search_semantic"},search_semantic:{type:"semantic_text",inference_id:".multilingual-e5-small-elasticsearch"},owner:{type:"keyword"},enabled:{type:"boolean"},graphId:{type:"keyword"},packageName:{type:"keyword"},assistantIds:{type:"keyword"},assistants:{properties:{id:{type:"keyword"},enabled:{type:"boolean"},condition:{type:"text",index:!1},runAlways:{type:"boolean"}}},sharedContext:{properties:{context:{type:"text"},priority:{type:"keyword"},policy:{properties:{shareToAssistants:{type:"boolean"},shareToDecider:{type:"boolean"},useAssistantProfiles:{type:"boolean"},applyOnDelegation:{type:"boolean"}}}}},webhookToken:{type:"keyword",index:!1},webhookEnabled:{type:"boolean"},webhookCreatedAt:{type:"date"},scheduleCron:{type:"keyword",index:!1},mcpEnabled:{type:"boolean"},mcpSigningSecret:{type:"keyword",index:!1},mcpExposeAssistants:{type:"boolean"},mcpAllowUnsigned:{type:"boolean"},mcpCreatedAt:{type:"date"},channelCommands:{type:"object",dynamic:!0},created_at:{type:"date"},updated_at:{type:"date"}}}}}),console.log("[Teams API] Created index:",w)}catch(s){s.meta?.statusCode!==400&&console.error("[Teams API] Error creating index:",s)}try{await p.indices.putMapping({index:w,body:{properties:{webhookToken:{type:"keyword",index:!1},webhookEnabled:{type:"boolean"},webhookCreatedAt:{type:"date"},scheduleCron:{type:"keyword",index:!1},mcpEnabled:{type:"boolean"},mcpSigningSecret:{type:"keyword",index:!1},mcpExposeAssistants:{type:"boolean"},mcpAllowUnsigned:{type:"boolean"},mcpCreatedAt:{type:"date"},channelCommands:{type:"object",dynamic:!0}}}})}catch(s){String(s?.message||"").includes("illegal_argument_exception")||console.warn("[Teams API] putMapping warning for webhook fields:",s?.message)}try{await p.indices.putMapping({index:w,body:{properties:{search_semantic:{type:"semantic_text",inference_id:".multilingual-e5-small-elasticsearch"},name:{type:"keyword",copy_to:"search_semantic"},description:{type:"text",copy_to:"search_semantic"}}}})}catch(s){String(s?.message||"").includes("illegal_argument_exception")||console.warn("[Teams API] putMapping warning for search_semantic field:",s?.message)}}import{getTeamById as ds,getTeamWebhookConfig as ls,getTeamMcpConfig as ms,getTeamSharedContext as us}from"../core/services/teams-service";async function E(s){if(!s||s.length===0)return[];const t=new Map(s.map(n=>[n.id,n])),e=s.map(n=>n.id);try{return(await p.mget({index:v,body:{ids:e}})).docs.filter(a=>a.found).map(a=>{const o=t.get(a._id);return{id:a._id,name:a._source?.name||"Unknown",topic:a._source?.topic,enabled:o?.enabled??!0,condition:o?.condition??null,runAlways:o?.runAlways??!1}})}catch(n){return console.error("[Teams API] Error joining assistants:",n),e.map(a=>({id:a,name:a,enabled:t.get(a)?.enabled??!0}))}}function fe(s){return s==="clusters"||s==="stack_expert"?["clusters","stack_expert"]:[s]}async function he(s,t){const e=fe(s);console.log(`[Teams API] Package filter: ${s} \u2192 [${e.join(", ")}]${t?` owner: ${t}`:""}`);try{const a=(await p.search({index:".stkxp_platforms",body:{query:{terms:{"config.packageName":e}},size:1e4,_source:["name"]}})).hits.hits.map(g=>g._source?.name).filter(Boolean),o=[...new Set(a)];if(o.includes("stack_expert")&&o.push("clusters"),console.log(`[Teams API] Found ${a.length} package servers for packageName: ${e.join(",")} \u2192 [${o.join(", ")}]`),a.length===0)return new Set;const r={bool:{should:o.map(g=>({term:{"mcp_servers_policy.servers.name.keyword":g}})),minimum_should_match:1}},d=t?{bool:{must:[r,{bool:{should:[{term:{owner:"stkxp"}},{term:{owner:t}}],minimum_should_match:1}}]}}:{bool:{must:[r]}},c=await p.search({index:v,body:{query:d,size:1e4,_source:[]}}),i=new Set(c.hits.hits.map(g=>g._id));return console.log(`[Teams API] Found ${i.size} visible assistants for packageName: ${e.join(",")}`),i}catch(n){return console.error("[Teams API] Error fetching visible assistants for packageName filter:",n),null}}function H(s){return Array.isArray(s.assistants)&&s.assistants.length>0&&s.assistants[0]?.id!==void 0?s.assistants.map(t=>({id:t.id,enabled:t.enabled!==!1,condition:t.condition??null,runAlways:t.runAlways??!1})):(s.assistantIds||[]).map(t=>({id:t,enabled:!0,condition:null,runAlways:!1}))}async function ye(s){try{const[t,e]=await Promise.all([p.search({index:v,body:{query:{term:{owner:s}},size:1e4,_source:[]}}),ue.getAllPackagesFromAllPlatforms(s,!1).catch(()=>({packages:[],errors:[]}))]),n=t.hits.hits.map(d=>d._id),a=[...new Set(e.packages.filter(d=>d.platforms?.some(c=>c.status==="enrolled")).map(d=>d.name).filter(Boolean))];a.includes("stack_expert")&&a.push("clusters"),console.log(`[Teams API] User "${s}": ${a.length} enrolled packages from platforms`,a);let o=[];a.length>0&&(o=(await p.search({index:".stkxp_platforms",body:{query:{terms:{"config.packageName":a}},size:1e4,_source:["name"]}})).hits.hits.map(c=>c._source?.name).filter(Boolean));let r=[];return o.length>0&&(r=(await p.search({index:v,body:{query:{bool:{should:o.map(c=>({term:{"mcp_servers_policy.servers.name.keyword":c}})),minimum_should_match:1}},size:1e4,_source:[]}})).hits.hits.map(c=>c._id)),console.log(`[Teams API] User "${s}": ${a.length} enrolled packages, ${o.length} MCP servers, ${n.length} owned assistants, ${r.length} MCP-linked assistants`),{userMcpPackageNames:a,userOwnedAssistantIds:n,mcpLinkedAssistantIds:r}}catch(t){return console.error("[Teams API] Error fetching user visibility data:",t),{userMcpPackageNames:[],userOwnedAssistantIds:[],mcpLinkedAssistantIds:[]}}}async function we(s,t){try{const e=L.parse(s.query);console.log(`[Teams API] getTeams query: ${JSON.stringify(e)}`);const n=[];if(e.enabled!==void 0&&n.push({term:{enabled:e.enabled}}),e.search){const k=e.search.toLowerCase().trim(),T=k.split("||").map(_=>_.trim()).filter(Boolean),I=T[0]||k,S=[];let R=0,M=5;S.push({multi_match:{query:`*${I}*`,fields:["name^5","description^2"],type:"best_fields",operator:"or",minimum_should_match:"60%",fuzziness:"AUTO",boost:3}}),S.push({match:{description:{query:e.search}}}),S.push({semantic:{field:"search_semantic",query:e.search,boost:2}});for(const _ of T)S.push({match_phrase:{name:{query:_,slop:R++,boost:Math.max(M--,1)}}});n.push({bool:{should:S,minimum_should_match:1}})}if(e.currentUser&&e.includeAllSystem)n.push({bool:{should:[{term:{owner:e.currentUser}},{term:{owner:"stkxp"}}],minimum_should_match:1}});else if(e.currentUser&&e.includeOwned)n.push({term:{owner:e.currentUser}});else if(e.currentUser){const{mcpLinkedAssistantIds:k}=await ye(e.currentUser);n.push({term:{owner:e.currentUser}}),k.length>0?n.push({terms:{assistantIds:k}}):n.push({term:{assistantIds:"__none__"}})}else if(e.owners){const k=e.owners.split(",").map(T=>T.trim()).filter(Boolean);k.length>0&&n.push({terms:{owner:k}})}let a=null;if(e.packageName&&(a=await he(e.packageName,e.currentUser),a!==null)){if(a.size===0)return t.json({success:!0,count:0,total:0,page:e.page||1,pageSize:e.pageSize||25,totalPages:0,teams:[]});n.push({terms:{assistantIds:Array.from(a)}})}e.assistantId&&n.push({term:{assistantIds:e.assistantId}});const o=e.page||1,r=e.pageSize||25,d=(o-1)*r,c={name:"name",updated_at:"updated_at"},i=String(s.query.sortField||"name"),g=c[i]||"name",b=s.query.sortOrder==="desc"?"desc":"asc",m=g==="updated_at"?[{updated_at:{order:b,unmapped_type:"date"}}]:[{[g]:{order:b,unmapped_type:"keyword"}}],h={index:w,body:{track_scores:!0,query:{bool:{must:n.length>0?n:[{match_all:{}}]}},sort:[{_score:{order:"desc"}},...m],from:d,size:r,_source:{excludes:["webhookToken"]}}};console.log("[Teams API] getTeams query:",JSON.stringify(h.body.query));const f=await p.search(h),l=typeof f.hits.total=="number"?f.hits.total:f.hits.total?.value||0,y=await Promise.all(f.hits.hits.map(async k=>{const T=k._source,I=Array.isArray(T.assistants)&&T.assistants.length>0?T.assistants:(T.assistantIds||[]).map(_=>({id:_,enabled:!0})),S=a?I.filter(_=>a.has(_.id)):I,R=e.packageName?S.filter(_=>_.enabled!==!1):S,M=await E(R);return{id:k._id,...T,assistants:M}})),x=e.packageName?y.filter(k=>k.assistants.length>0):y;t.json({success:!0,count:x.length,total:l,page:o,pageSize:r,totalPages:Math.ceil(l/r),teams:x})}catch(e){if(console.error("[Teams API] Error fetching teams:",e),e instanceof u.ZodError)return t.status(400).json({success:!1,error:"Invalid query parameters",details:e.errors});if(e.meta?.statusCode===404)return t.json({success:!0,count:0,total:0,page:1,pageSize:25,totalPages:0,teams:[]});t.status(500).json({success:!1,error:"Failed to fetch teams",message:e.message})}}async function be(s,t){try{const{id:e}=s.params,n=await p.get({index:w,id:e,_source_excludes:["webhookToken"]});if(!n.found)return t.status(404).json({success:!1,error:"Team not found"});const a=n._source,o=Array.isArray(a.assistants)&&a.assistants.length>0?a.assistants:(a.assistantIds||[]).map(d=>({id:d,enabled:!0})),r=await E(o);t.json({success:!0,team:{id:n._id,...a,assistants:r}})}catch(e){if(console.error("[Teams API] Error fetching team:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to fetch team",message:e.message})}}async function ke(s,t){try{await ge();const e=s.body,n=new Date().toISOString(),a=H(e),o=a.map(b=>b.id),d=s.auth?.username||e.owner||"system",c={name:e.name,description:e.description||"",assistantIds:o,assistants:a,owner:d,enabled:e.enabled!==void 0?e.enabled:!0,created_at:n,updated_at:n};e.sharedContext?.context?.trim()&&(c.sharedContext={context:e.sharedContext.context.trim(),priority:e.sharedContext.priority==="after"?"after":"before",...e.sharedContext.policy?{policy:e.sharedContext.policy}:{}}),e.graphId?.trim()&&(c.graphId=e.graphId.trim()),console.log("[Teams API] Creating team:",c.name);const i=await p.index({index:w,body:c,refresh:"wait_for"}),g=await E(a);t.status(201).json({success:!0,team:{id:i._id,...c,assistants:g}})}catch(e){console.error("[Teams API] Error creating team:",e),t.status(500).json({success:!1,error:"Failed to create team",message:e.message})}}async function Ie(s,t){try{const{id:e}=s.params,n=await A(s,t,p,w,e);if(!n)return;await de("team",e,n.source,"manual_edit",C(s)||"system");const a=s.body,o=H(a),r=o.map(g=>g.id),d={name:a.name,description:a.description||"",assistantIds:r,assistants:o,owner:a.owner||"system",enabled:a.enabled!==void 0?a.enabled:!0,updated_at:new Date().toISOString()};a.sharedContext?.context?.trim()?d.sharedContext={context:a.sharedContext.context.trim(),priority:a.sharedContext.priority==="after"?"after":"before",...a.sharedContext.policy?{policy:a.sharedContext.policy}:{}}:d.sharedContext=null,d.graphId=a.graphId?.trim()||null,console.log("[Teams API] Updating team:",e);const c=await p.update({index:w,id:e,body:{doc:d},refresh:"wait_for"}),i=await E(o);t.json({success:!0,team:{id:c._id,...d,assistants:i}})}catch(e){if(console.error("[Teams API] Error updating team:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to update team",message:e.message})}}async function Te(s,t){try{const{id:e}=s.params;if(!await A(s,t,p,w,e))return;console.log("[Teams API] Deleting team:",e),await p.delete({index:w,id:e,refresh:"wait_for"}),t.json({success:!0,message:"Team deleted successfully"})}catch(e){if(console.error("[Teams API] Error deleting team:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to delete team",message:e.message})}}async function Ae(s,t){try{const{id:e}=s.params;if(!await A(s,t,p,w,e))return;const a=s.body?.enabled!==!1,o=O(32).toString("hex"),r=new Date().toISOString();await p.update({index:w,id:e,body:{doc:{webhookToken:o,webhookEnabled:a,webhookCreatedAt:r,updated_at:r}},refresh:"wait_for"}),console.log(`[Teams API] Generated webhook token for team: ${e}`),t.json({success:!0,teamId:e,webhookToken:o,webhookEnabled:a,webhookCreatedAt:r,webhookUrl:`/api/webhooks/team/${e}`})}catch(e){if(console.error("[Teams API] Error generating webhook token:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to generate webhook token",message:e.message})}}async function Ce(s,t){try{const{id:e}=s.params;if(!await A(s,t,p,w,e))return;const a=new Date().toISOString();await p.update({index:w,id:e,body:{doc:{webhookToken:null,webhookEnabled:!1,webhookCreatedAt:null,updated_at:a}},refresh:"wait_for"}),console.log(`[Teams API] Revoked webhook token for team: ${e}`),t.json({success:!0,teamId:e,webhookEnabled:!1})}catch(e){if(console.error("[Teams API] Error revoking webhook token:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to revoke webhook token",message:e.message})}}async function xe(s,t){try{const{id:e}=s.params,n=await A(s,t,p,w,e);if(!n)return;const a=n.source;if(!a.mcpEnabled)return t.status(400).json({success:!1,error:"MCP server is not enabled for this team \u2014 generate/enable it in the MCP App tab first."});const o=a.name||"team",r=`${s.protocol}://${s.get("host")}/api/mcp/team/${e}`,d=await oe({teamName:o,teamId:e,mcpUrl:r,allowUnsigned:a.mcpAllowUnsigned===!0}),c=`mcp-app-tester-${re(o)}.zip`;t.set({"Content-Type":"application/zip","Content-Disposition":`attachment; filename="${c}"`}),t.send(d)}catch(e){console.error("[Teams API] Error generating MCP App tester package:",e),t.status(500).json({success:!1,error:"Failed to generate test client package",message:e.message})}}async function _e(s,t){try{const{id:e}=s.params;if(!await A(s,t,p,w,e))return;const a=s.body?.enabled!==!1,o=s.body?.exposeAssistants!==!1,r=s.body?.allowUnsigned===!0,d=typeof s.body?.signingSecret=="string"?s.body.signingSecret.trim():void 0;if(d!==void 0&&d.length<8)return t.status(400).json({success:!1,error:"signingSecret must be at least 8 characters (use your Slack app Signing Secret to integrate with Slack)"});const c=d||O(32).toString("hex"),i=new Date().toISOString();await p.update({index:w,id:e,body:{doc:{mcpSigningSecret:c,mcpEnabled:a,mcpExposeAssistants:o,mcpAllowUnsigned:r,mcpCreatedAt:i,updated_at:i}},refresh:"wait_for"}),console.log(`[Teams API] Generated MCP signing secret for team: ${e}`),t.json({success:!0,teamId:e,mcpSigningSecret:c,mcpEnabled:a,mcpExposeAssistants:o,mcpAllowUnsigned:r,mcpCreatedAt:i,mcpUrl:`/api/mcp/team/${e}`})}catch(e){if(console.error("[Teams API] Error generating MCP config:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to generate MCP config",message:e.message})}}async function Se(s,t){try{const{id:e}=s.params;if(!await A(s,t,p,w,e))return;const a=new Date().toISOString();await p.update({index:w,id:e,body:{doc:{mcpSigningSecret:null,mcpEnabled:!1,mcpAllowUnsigned:!1,mcpCreatedAt:null,updated_at:a}},refresh:"wait_for"}),console.log(`[Teams API] Revoked MCP config for team: ${e}`),t.json({success:!0,teamId:e,mcpEnabled:!1})}catch(e){if(console.error("[Teams API] Error revoking MCP config:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to revoke MCP config",message:e.message})}}async function Pe(s,t){try{const{id:e,platformId:n}=s.params,a=await A(s,t,p,w,e);if(!a)return;const o=Array.isArray(s.body?.commandIds)?s.body.commandIds:[],r=typeof s.body?.namespace=="string"?s.body.namespace:void 0,d=s.body?.blockKitMode===!0,c=a.source.owner;for(const m of o){const h=await le(c,n,m,e);if(h)return t.status(409).json({success:!1,message:`Command is already assigned to team "${h.teamName}"`})}const i=new Date().toISOString(),b={...a.source.channelCommands??{},[n]:{commandIds:o,namespace:r,blockKitMode:d}};await p.update({index:w,id:e,body:{doc:{channelCommands:b,updated_at:i}},refresh:"wait_for"}),console.log(`[Teams API] Updated channel commands for team ${e} / platform ${n}`),t.json({success:!0,teamId:e,platformId:n,commandIds:o,namespace:r,blockKitMode:d})}catch(e){if(console.error("[Teams API] Error setting channel commands:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to set channel commands",message:e.message})}}async function ve(s,t){try{const{id:e,platformId:n}=s.params,a=await A(s,t,p,w,e);if(!a)return;const o=new Date().toISOString(),r={...a.source.channelCommands??{}};delete r[n],await p.update({index:w,id:e,body:{doc:{channelCommands:r,updated_at:o}},refresh:"wait_for"}),console.log(`[Teams API] Cleared channel commands for team ${e} / platform ${n}`),t.json({success:!0,teamId:e,platformId:n})}catch(e){if(console.error("[Teams API] Error clearing channel commands:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to clear channel commands",message:e.message})}}async function je(s,t){try{const e=C(s);if(!e)return t.status(401).json({success:!1,message:"Unauthorized"});const n=s.query.platformId;if(!n)return t.status(400).json({success:!1,message:"platformId is required"});const a=await me(e,n);t.json({success:!0,body:{ownership:a}})}catch(e){console.error("[Teams API] Error fetching channel command ownership:",e),t.status(500).json({success:!1,message:e.message||"Failed to fetch channel command ownership"})}}async function Ee(s,t){try{const{id:e}=s.params,n=await A(s,t,p,w,e);if(!n)return;const a=n.source.owner||C(s)||"system",o=j(P),r=await $([{index:w,esId:e}],a,o),d=te(r);t.json({success:!0,graph:d})}catch(e){if(console.error("[Teams API] Error building full-structure graph:",e),/Required dependencies/.test(e?.message??""))return t.status(422).json({success:!1,error:e.message});if(e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to build full-structure graph",message:e.message})}}async function Re(s,t){try{const{id:e}=s.params,n=await A(s,t,p,w,e);if(!n)return;const a=n.source.owner||C(s)||"system",o=n.source.name||"(unnamed team)",r=j(P),d=await $([{index:w,esId:e}],a,r),c=J(d,{exportedAt:new Date().toISOString(),rootName:o}),i=d.docs.filter(m=>m.key==="platform"&&z(m.source?.type)&&m.source?.owner===a).map(m=>m.esId),g=d.docs.some(m=>m.key==="tool"&&m.reachedVia?.includes(U)&&m.source?.owner===a);let b;if(i.length>0||g){const m=await D(d,a,e);if(!m)return t.status(422).json({success:!1,error:"Failed to create the MCP gateway grant: could not connect to one or more MCP server platforms in this team."});for(const f of c.closure.docs)f.key==="platform"&&i.includes(f.esId)&&(f.source.config={...f.source.config,serverType:"gateway",url:""});const h=c.closure.docs.filter(f=>f.key==="tool"&&f.source?.type==="mcp_remote"&&(i.includes(f.source?.platformId)||i.includes(f.source?.mcpServerId))).length;h>0&&(c.closure.docs=c.closure.docs.filter(f=>!(f.key==="tool"&&f.source?.type==="mcp_remote"&&(i.includes(f.source?.platformId)||i.includes(f.source?.mcpServerId)))),c.summary.totalDocs-=h,c.summary.byEntity.tool=(c.summary.byEntity.tool??0)-h),c.mcpGateway={grantId:m.grantId,platformIds:m.platformIds},b=m.secret}console.log(`[Teams API] ${C(s)??"?"} exported team "${o}" (${e}): ${c.summary.totalDocs} docs, ${d.unresolved.length} unresolved ref(s)`+(b?`, gateway grant for ${i.length} platform(s)`:"")),t.json({success:!0,bundle:c,gatewaySecret:b,unresolved:d.unresolved})}catch(e){if(console.error("[Teams API] Error exporting team:",e),/Required dependencies/.test(e?.message??""))return t.status(422).json({success:!1,error:e.message});if((e?.message??"").startsWith(N))return t.status(422).json({success:!1,error:e.message});if(e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to export team",message:e.message})}}const W=u.object({secret:u.string().min(8)});async function Me(s,t){const e=W.safeParse(s.body);if(!e.success)return t.status(400).json({success:!1,error:'Body must include a "secret" of at least 8 characters'});const{secret:n}=e.data;try{const{id:a}=s.params,o=await A(s,t,p,w,a);if(!o)return;const r=o.source.owner||C(s)||"system",d=o.source.name||"(unnamed team)",c=j(P),i=await $([{index:w,esId:a}],r,c),g=i.docs.filter(l=>l.key==="platform"&&z(l.source?.type)&&l.source?.owner===r).map(l=>l.esId),b=i.docs.some(l=>l.key==="tool"&&l.reachedVia?.includes(U)&&l.source?.owner===r);let m,h;if(g.length>0||b){const l=await D(i,r,a);if(!l)return t.status(422).json({success:!1,error:"Failed to create the MCP gateway grant: could not connect to one or more MCP server platforms in this team."});for(const y of i.docs)y.key==="platform"&&g.includes(y.esId)&&(y.source.config={...y.source.config,serverType:"gateway",url:""});i.docs=i.docs.filter(y=>!(y.key==="tool"&&y.source?.type==="mcp_remote"&&(g.includes(y.source?.platformId)||g.includes(y.source?.mcpServerId)))),h={grantId:l.grantId,platformIds:l.platformIds},m=l.secret}const f=se(i,{exportedAt:new Date().toISOString(),rootName:d,passphrase:n,gatewaySecret:m});f.mcpGateway=h,console.log(`[Teams API] ${C(s)??"?"} exported (encrypted) team "${d}" (${a}): ${f.summary.totalDocs} docs, ${i.unresolved.length} unresolved ref(s)`),t.json({success:!0,bundle:f,unresolved:i.unresolved})}catch(a){if(console.error("[Teams API] Error exporting team (encrypted):",a),/Required dependencies/.test(a?.message??""))return t.status(422).json({success:!1,error:a.message});if((a?.message??"").startsWith(N))return t.status(422).json({success:!1,error:a.message});if(a.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to export team (encrypted)",message:a.message})}}async function $e(s,t){try{const e=C(s);if(!e)return t.status(401).json({success:!1,error:"Unauthorized"});const n=(s.body&&s.body.bundle)??s.body,a=s.body&&typeof s.body.gatewaySecret=="string"?s.body.gatewaySecret:void 0;let o;try{o=Y(n)}catch(g){const b=g instanceof ee?g.message:"Invalid bundle";return t.status(400).json({success:!1,error:b})}let r;if(o.mcpGateway){if(!a)return t.status(400).json({success:!1,error:"This bundle requires a gateway key to activate its MCP server platform(s). Paste the key you received from the exporting owner."});const g=await ie(o.mcpGateway.grantId,a,e);if("error"in g)return t.status(400).json({success:!1,error:g.error});r=g.gatewayToken}const d=j(P),c=Z(P),i=await X(o.closure,e,d,c,{blankSecrets:!0});if(o.mcpGateway&&r){const g=process.env.MCP_GATEWAY_HOST||"https://mcp.erretegia.com",b=new Date().toISOString();for(const h of i.created){if(h.key!=="platform"||!o.mcpGateway.platformIds.includes(h.oldEsId))continue;const f=`${g}/mcp/gateway:${o.mcpGateway.grantId}:${h.oldEsId}`;await P.update({index:h.index,id:h.newEsId,body:{doc:{managedType:"managed",config:{serverType:"gateway",url:f,authKey:r}}},refresh:!0});try{await ce(h.newEsId,{id:h.newEsId,name:h.name||"MCP Server",type:"MCPServer",managedType:"self-hosted",owner:e,created:b,updated:b,enabled:!0,config:{serverType:"gateway",url:f,authKey:r}},e)}catch(l){console.warn(`[Teams API] Post-import gateway tool sync failed for platform ${h.newEsId}: ${l.message}`)}}const m=`personal:${o.sourceOwner}`;if(o.mcpGateway.platformIds.includes(m)){const h=`${g}/mcp/gateway:${o.mcpGateway.grantId}:${m}`,f=K();await P.index({index:".stkxp_platforms",id:f,document:{id:f,name:"My MCP Tools (gateway)",type:"MCPServer",managedType:"managed",owner:e,enabled:!0,created:b,updated:b,config:{serverType:"gateway",url:h,authKey:r,protocol:"http"}},refresh:!0});const l=`__personal_tools__:${o.sourceOwner}`;for(const y of i.created){if(y.key!=="assistant")continue;const x=o.closure.docs.find(I=>I.key==="assistant"&&I.esId===y.oldEsId),k=x?.source?.mcp_servers_policy?.servers;if(!Array.isArray(k)||!k.some(I=>I?.id===l))continue;const T=k.map(I=>I?.id===l?{...I,id:f}:I);await P.update({index:y.index,id:y.newEsId,body:{doc:{mcp_servers_policy:{...x.source.mcp_servers_policy,servers:T}}},refresh:!0})}}}console.log(`[Teams API] ${e} imported team "${o.rootName}" (from owner "${o.sourceOwner}"): ${i.created.length} created, ${i.skipped.length} reused, ${i.referenced.length} referenced, ${i.droppedRefs.length} refs dropped`),t.status(201).json({success:!0,rootName:o.rootName,report:i,unresolved:o.closure.unresolved})}catch(e){if(console.error("[Teams API] Error importing team:",e),/Required dependencies/.test(e?.message??""))return t.status(422).json({success:!1,error:e.message});t.status(500).json({success:!1,error:"Failed to import team",message:e.message})}}async function Oe(s,t){try{const{id:e}=s.params;if(!C(s))return t.status(401).json({success:!1,error:"Unauthorized"});let a;try{a=(await p.get({index:w,id:e}))._source}catch(m){if(m?.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});throw m}const r=(a?.assistants||[]).filter(m=>m.enabled!==!1).map(m=>m.id);if(r.length===0)return t.json({success:!0,teamId:e,teamName:a?.name,singlePassMax:0,assistants:[]});const c=(await p.mget({index:v,body:{ids:r}})).docs.filter(m=>m.found),i=new Map,g=[];let b=0;for(const m of c){const h=await ne(p,m._id,m._source||{},i);b+=h.singlePassMax,g.push(h)}t.json({success:!0,teamId:e,teamName:a?.name,singlePassMax:b,assistants:g,note:"Single-pass upper bound. Tool loops can exceed it \u2014 multiply by an empirical factor (2-3\xD7) when sizing a budget."})}catch(e){console.error("[Teams API] Error computing token projection:",e),t.status(500).json({success:!1,error:"Failed to compute token projection",message:e.message})}}async function Ue(s,t){try{const{id:e}=s.params,n=C(s);if(!n)return t.status(401).json({success:!1,error:"Unauthorized"});const a=Math.min(parseInt(s.query.limit||"20",10)||20,100),o=[{term:{teamId:e}}];V(s)||o.push({term:{username:n}});const c=((await p.search({index:pe,body:{size:a,sort:[{createdAt:{order:"desc"}}],_source:["id","title","createdAt","metadata.tokenUsage","assistants"],query:{bool:{must:o}}}})).hits.hits||[]).map(l=>{const y=l._source||{},x=y.metadata?.tokenUsage||{};return{chatId:l._id,title:y.title,createdAt:y.createdAt,totalTokens:Number(x.totalTokens||0),inputTokens:Number(x.totalInputTokens||0),outputTokens:Number(x.totalOutputTokens||0),assistantCount:Array.isArray(y.assistants)?y.assistants.length:0}}),i=c.map(l=>l.totalTokens).filter(l=>l>0),g=i.reduce((l,y)=>l+y,0),b=[...i].sort((l,y)=>l-y),m=b.length?b[Math.min(b.length-1,Math.floor(b.length*.95))]:0,h=c.map(l=>l.inputTokens),f=c.map(l=>l.outputTokens);t.json({success:!0,runCount:i.length,avgTotalTokens:i.length?Math.round(g/i.length):0,maxTotalTokens:i.length?Math.max(...i):0,p95TotalTokens:m,avgInputTokens:h.length?Math.round(h.reduce((l,y)=>l+y,0)/h.length):0,avgOutputTokens:f.length?Math.round(f.reduce((l,y)=>l+y,0)/f.length):0,recentRuns:c})}catch(e){console.error("[Teams API] Error fetching token stats:",e),t.status(500).json({success:!1,error:"Failed to fetch token stats",message:e.message})}}const De=u.object({name:u.string().describe("Team name"),description:u.string().optional().describe("Human description"),assistantIds:u.array(u.string()).optional().describe("Ids of assistants in the team"),assistants:u.array(u.any()).optional().describe("Full assistant objects (alternative to assistantIds)"),graphId:u.string().optional().describe("Id of a .stkxp_graphs coordination graph"),enabled:u.boolean().optional().describe("Enabled flag (default true)"),sharedContext:u.any().optional().describe("Shared context object injected into every assistant")}).passthrough(),Ne=u.object({name:u.string().optional().describe("Team name"),description:u.string().optional().describe("Human description"),assistantIds:u.array(u.string()).optional().describe("Ids of assistants in the team"),assistants:u.array(u.any()).optional().describe("Full assistant objects (alternative to assistantIds)"),graphId:u.string().optional().describe("Id of a .stkxp_graphs coordination graph"),enabled:u.boolean().optional().describe("Enabled flag (default true)"),sharedContext:u.any().optional().describe("Shared context object injected into every assistant")}).passthrough(),rs=[{method:"get",path:"/api/teams",handler:we,validate:{query:L},openapi:{summary:"List teams for the caller",description:"Returns all teams owned by the authenticated user. A team groups assistants and optionally references a coordination graph (`graphId`) for multi-assistant orchestration. `search` runs a hybrid lexical + semantic query (semantic_text field `search_semantic`).",tags:["teams"]}},{method:"post",path:"/api/teams/import",handler:$e,openapi:{summary:"Import a team from an export bundle",description:"Reconstructs a team and its full dependency closure under the authenticated caller from a bundle produced by `GET /api/teams/:id/export`. New ids are minted for owned docs (links rebuilt), shared/system docs are referenced in place, same-named docs the caller already owns are reused, and secrets stay blank (re-enter LLM keys / platform authKeys and regenerate the webhook token afterwards).",tags:["teams"],audience:"internal"}},{method:"get",path:"/api/teams/channel-command-ownership",handler:je,openapi:{summary:"Get command\u2192team ownership map for a platform",description:"Query param `platformId`. Returns `{ [commandId]: { teamId, teamName } }` for every command on that platform currently owned by one of the caller's teams \u2014 used by the Team Edit Channels tab to disable already-claimed commands.",tags:["teams","channels"],audience:"internal"}},{method:"get",path:"/api/teams/:id",handler:be,openapi:{summary:"Get a team by id",tags:["teams"]}},{method:"get",path:"/api/teams/:id/export",handler:Re,openapi:{summary:"Export a team as a portable bundle",description:"Resolves the team's full transitive dependency closure (assistants, graphs, sub-graphs, prompts, node templates, routing rules, models, providers, platforms, tools) and returns a secret-free JSON bundle for re-import via `POST /api/teams/import`. Limited to teams the caller owns.",tags:["teams"],audience:"internal"}},{method:"post",path:"/api/teams/:id/export-encrypted",handler:Me,validate:{body:W},openapi:{summary:"Export a team as a fully self-contained, passphrase-encrypted bundle",description:"Like GET /api/teams/:id/export, but instead of blanking secrets (LLM keys, platform creds, webhook token), encrypts them with the caller-supplied passphrase (AES-256-GCM per field). Intended for the `stkxp export` CLI, which produces a bundle deployable standalone via `stkxp deploy` \u2014 see docs/superpowers/specs/2026-09-15-team-standalone-export-design.md.",tags:["teams"],audience:"internal"}},{method:"get",path:"/api/teams/:id/full-structure",handler:Ee,openapi:{summary:"Get a team's full structure as a flattened canvas graph",description:"Resolves the team's full transitive dependency closure (assistants, runtime graphs, nested subgraphs, tools, platforms, models, providers, routing rules) and returns it flattened into nodes/edges/groups for the Team Canvas Overview mode. Read-only. Limited to teams the caller owns.",tags:["teams"],audience:"internal"}},{method:"post",path:"/api/teams",handler:ke,validate:{body:De},openapi:{summary:"Create a team",description:"Creates a team with the supplied `name`, optional `description`, `assistantIds[]`, and optional `graphId` pointing to a `.stkxp_graphs` coordination graph.",tags:["teams"]}},{method:"put",path:"/api/teams/:id",handler:Ie,validate:{body:Ne},openapi:{summary:"Update a team",tags:["teams"]}},{method:"delete",path:"/api/teams/:id",handler:Te,openapi:{summary:"Delete a team",tags:["teams"]}},{method:"get",path:"/api/teams/:id/token-projection",handler:Oe,openapi:{summary:"Theoretical single-pass max token usage for a team",description:"Resolves each enabled assistant graph (`assistant.graphId`), applies `assistant.llm_overrides[node.type]` on top of node-level routing rules, and sums `maxTokens` across nodes with a routing rule. Single-pass upper bound only \u2014 tool loops can exceed it.",tags:["teams"],audience:"internal"}},{method:"get",path:"/api/teams/:id/token-stats",handler:Ue,openapi:{summary:"Aggregate token usage for the caller's recent runs of a team",description:"Reads `.stkxp_chats` filtered by `teamId` (and `username` unless superuser) and returns avg / max / p95 total tokens plus the last N runs. Used by the Team flyout to surface real consumption before setting a future budget.",tags:["teams"],audience:"internal"}},{method:"post",path:"/api/teams/:id/webhook-token",handler:Ae,openapi:{summary:"Generate (or rotate) a webhook token for a team",description:"Creates a new inbound webhook token. Returns the clear-text token **once** \u2014 the UI must surface it immediately to the operator. Subsequent fetches of the team do not expose the token. Optional body `{ enabled?: boolean }` (defaults to true) controls whether the webhook is active.",tags:["teams","webhooks"],audience:"internal"}},{method:"delete",path:"/api/teams/:id/webhook-token",handler:Ce,openapi:{summary:"Revoke the webhook token for a team",description:"Clears the webhook token and disables the webhook. POSTs to `/api/webhooks/team/:id` will be rejected with 401 until a new token is generated.",tags:["teams","webhooks"],audience:"internal"}},{method:"post",path:"/api/teams/:id/mcp-config",handler:_e,openapi:{summary:"Enable (or rotate) the MCP server for a team",description:'Owner-only. Generates a signing secret (returned once) and enables the per-team MCP endpoint at /api/mcp/team/:id. Body `{ enabled?: boolean, exposeAssistants?: boolean, signingSecret?: string }` \u2014 enabled/exposeAssistants default true; if `signingSecret` is provided (\u22658 chars) it is used verbatim (e.g. a Slack app Signing Secret to integrate with Slack), otherwise a random 32-byte secret is minted. `allowUnsigned` (default false) accepts unsigned requests \u2014 needed for Slack\'s MCP client, which sends no signature for auth_type "no_auth"; a signature, when present, is always verified.',tags:["teams","mcp"],audience:"internal"}},{method:"delete",path:"/api/teams/:id/mcp-config",handler:Se,openapi:{summary:"Disable the MCP server for a team",description:"Owner-only. Clears the signing secret and disables the MCP endpoint. Calls to /api/mcp/team/:id return 404 until re-enabled.",tags:["teams","mcp"],audience:"internal"}},{method:"post",path:"/api/teams/:id/channel-commands/:platformId",handler:Pe,openapi:{summary:"Claim one or more commands on a platform for a team",description:"Body `{ commandIds: string[], namespace?: string, blockKitMode?: boolean }`. Rejects with 409 if any commandId is already owned by a different team. A command can be owned by at most one team at a time, scoped to (platformId, commandId).",tags:["teams","channels"],audience:"internal"}},{method:"delete",path:"/api/teams/:id/channel-commands/:platformId",handler:ve,openapi:{summary:"Clear a team's channel-command config for a platform",description:"Removes this team's ownership of any commands claimed on the given platform, freeing them for other teams.",tags:["teams","channels"],audience:"internal"}},{method:"get",path:"/api/teams/:id/mcp-app-tester.zip",handler:xe,openapi:{summary:"Download a pre-configured MCP App test client for this team",description:"Owner-only. Generates a ZIP \u2014 a minimal fork of mcp-apps-tester wired to this team's ask_ tool, with Slack-style HMAC request signing baked in. Requires the team's MCP server to be enabled (mcpEnabled). Ships without the signing secret \u2014 the caller pastes their own copy into the generated .env.",tags:["teams","mcp"],audience:"internal"}}];export{ge as ensureTeamsIndex,ds as getTeamById,ms as getTeamMcpConfig,us as getTeamSharedContext,ls as getTeamWebhookConfig,rs as routes};
1
+ import{Client as q}from"@elastic/elasticsearch";import{z as m}from"zod";import{randomBytes as O,randomUUID as K}from"crypto";import{assertOwner as C,getAuthUsername as x,isSuperuser as V}from"../core/utils/ownership";import{makeOwnerScopedClient as Q}from"../core/utils/owner-scope";import{resolveClosure as $,createEsStore as j,PERSONAL_TOOLS_REACHED_VIA as U}from"../services/clone/closure-resolver";import{executeClone as X,createEsCloneWriter as Z}from"../services/clone/clone-executor";import{serializeTeamBundle as J,parseTeamBundle as Y,TeamBundleParseError as ee}from"../services/clone/team-bundle";import{serializeTeamBundleEncrypted as se}from"../services/clone/team-bundle-encrypted";import{buildFullStructureGraph as te}from"../services/graph-canvas/full-structure-builder";import{MultiKibanaService as ae}from"../core/services/multi-kibana-service";import{projectAssistantTokens as ne}from"../core/services/token-projection-service";import{buildMcpAppTesterZip as oe,slugifyTeamName as re}from"../services/mcp-app-tester-package";import{createGatewayGrant as D,redeemGatewayGrant as ie,GATEWAY_UNREACHABLE_PREFIX as N}from"../services/mcp-gateway/gateway-grant-service";import{isGatewayEligiblePlatformType as z}from"../core/services/gateway-platform-types";import{syncRemoteToolsForPlatform as ce}from"../core/services/mcp-sync-service";import{snapshotBeforeUpdate as de}from"../core/services/version-snapshot-service";import{findTeamOwningCommand as le,getChannelCommandOwnership as me}from"../core/services/teams-service";const ue=new ae,F=process.env.ELASTICSEARCH_HOST||"https://51.159.174.230:9205",G=process.env.ELASTICSEARCH_USER||"elastic",B=process.env.ELASTICSEARCH_PASSWORD||"",f=".stkxp_teams",v=".stkxp_assistants",pe=".stkxp_chats",u=Q({node:F,auth:{username:G,password:B},tls:{rejectUnauthorized:!1},requestTimeout:6e4,maxRetries:3}),S=new q({node:F,auth:{username:G,password:B},tls:{rejectUnauthorized:!1},requestTimeout:6e4,maxRetries:3}),L=m.object({enabled:m.string().optional().transform(s=>{if(s!==void 0)return s==="true"}),page:m.string().optional().transform(s=>s?parseInt(s,10):1),pageSize:m.string().optional().transform(s=>s?parseInt(s,10):25),search:m.string().optional().describe("Hybrid lexical + semantic search across name and description"),packageName:m.string().optional(),owners:m.string().optional(),currentUser:m.string().optional(),includeAllSystem:m.string().optional().transform(s=>s==="true"),includeOwned:m.union([m.boolean(),m.string()]).optional().transform(s=>s===!0||s==="true"),assistantId:m.string().optional()});async function ge(){try{await u.indices.create({index:f,body:{mappings:{properties:{name:{type:"keyword",copy_to:"search_semantic"},description:{type:"text",copy_to:"search_semantic"},search_semantic:{type:"semantic_text",inference_id:".multilingual-e5-small-elasticsearch"},owner:{type:"keyword"},enabled:{type:"boolean"},graphId:{type:"keyword"},packageName:{type:"keyword"},assistantIds:{type:"keyword"},assistants:{properties:{id:{type:"keyword"},enabled:{type:"boolean"},condition:{type:"text",index:!1},runAlways:{type:"boolean"}}},sharedContext:{properties:{context:{type:"text"},priority:{type:"keyword"},policy:{properties:{shareToAssistants:{type:"boolean"},shareToDecider:{type:"boolean"},useAssistantProfiles:{type:"boolean"},applyOnDelegation:{type:"boolean"}}}}},webhookToken:{type:"keyword",index:!1},webhookEnabled:{type:"boolean"},webhookCreatedAt:{type:"date"},scheduleCron:{type:"keyword",index:!1},mcpEnabled:{type:"boolean"},mcpSigningSecret:{type:"keyword",index:!1},mcpExposeAssistants:{type:"boolean"},mcpAllowUnsigned:{type:"boolean"},mcpCreatedAt:{type:"date"},channelCommands:{type:"object",dynamic:!0},created_at:{type:"date"},updated_at:{type:"date"}}}}}),console.log("[Teams API] Created index:",f)}catch(s){s.meta?.statusCode!==400&&console.error("[Teams API] Error creating index:",s)}try{await u.indices.putMapping({index:f,body:{properties:{webhookToken:{type:"keyword",index:!1},webhookEnabled:{type:"boolean"},webhookCreatedAt:{type:"date"},scheduleCron:{type:"keyword",index:!1},mcpEnabled:{type:"boolean"},mcpSigningSecret:{type:"keyword",index:!1},mcpExposeAssistants:{type:"boolean"},mcpAllowUnsigned:{type:"boolean"},mcpCreatedAt:{type:"date"},channelCommands:{type:"object",dynamic:!0}}}})}catch(s){String(s?.message||"").includes("illegal_argument_exception")||console.warn("[Teams API] putMapping warning for webhook fields:",s?.message)}try{await u.indices.putMapping({index:f,body:{properties:{search_semantic:{type:"semantic_text",inference_id:".multilingual-e5-small-elasticsearch"},name:{type:"keyword",copy_to:"search_semantic"},description:{type:"text",copy_to:"search_semantic"}}}})}catch(s){String(s?.message||"").includes("illegal_argument_exception")||console.warn("[Teams API] putMapping warning for search_semantic field:",s?.message)}}import{getTeamById as ds,getTeamWebhookConfig as ls,getTeamMcpConfig as ms,getTeamSharedContext as us}from"../core/services/teams-service";async function E(s){if(!s||s.length===0)return[];const t=new Map(s.map(n=>[n.id,n])),e=s.map(n=>n.id);try{return(await u.mget({index:v,body:{ids:e}})).docs.filter(a=>a.found).map(a=>{const o=t.get(a._id);return{id:a._id,name:a._source?.name||"Unknown",topic:a._source?.topic,enabled:o?.enabled??!0,condition:o?.condition??null,runAlways:o?.runAlways??!1}})}catch(n){return console.error("[Teams API] Error joining assistants:",n),e.map(a=>({id:a,name:a,enabled:t.get(a)?.enabled??!0}))}}function fe(s){return s==="clusters"||s==="stack_expert"?["clusters","stack_expert"]:[s]}async function ye(s,t){const e=fe(s);console.log(`[Teams API] Package filter: ${s} \u2192 [${e.join(", ")}]${t?` owner: ${t}`:""}`);try{const a=(await u.search({index:".stkxp_platforms",body:{query:{terms:{"config.packageName":e}},size:1e4,_source:["name"]}})).hits.hits.map(p=>p._source?.name).filter(Boolean),o=[...new Set(a)];if(o.includes("stack_expert")&&o.push("clusters"),console.log(`[Teams API] Found ${a.length} package servers for packageName: ${e.join(",")} \u2192 [${o.join(", ")}]`),a.length===0)return new Set;const i={bool:{should:o.map(p=>({term:{"mcp_servers_policy.servers.name.keyword":p}})),minimum_should_match:1}},d=t?{bool:{must:[i,{bool:{should:[{term:{owner:"stkxp"}},{term:{owner:t}}],minimum_should_match:1}}]}}:{bool:{must:[i]}},c=await u.search({index:v,body:{query:d,size:1e4,_source:[]}}),r=new Set(c.hits.hits.map(p=>p._id));return console.log(`[Teams API] Found ${r.size} visible assistants for packageName: ${e.join(",")}`),r}catch(n){return console.error("[Teams API] Error fetching visible assistants for packageName filter:",n),null}}function H(s){return Array.isArray(s.assistants)&&s.assistants.length>0&&s.assistants[0]?.id!==void 0?s.assistants.map(t=>({id:t.id,enabled:t.enabled!==!1,condition:t.condition??null,runAlways:t.runAlways??!1})):(s.assistantIds||[]).map(t=>({id:t,enabled:!0,condition:null,runAlways:!1}))}async function he(s){try{const[t,e]=await Promise.all([u.search({index:v,body:{query:{term:{owner:s}},size:1e4,_source:[]}}),ue.getAllPackagesFromAllPlatforms(s,!1).catch(()=>({packages:[],errors:[]}))]),n=t.hits.hits.map(d=>d._id),a=[...new Set(e.packages.filter(d=>d.platforms?.some(c=>c.status==="enrolled")).map(d=>d.name).filter(Boolean))];a.includes("stack_expert")&&a.push("clusters"),console.log(`[Teams API] User "${s}": ${a.length} enrolled packages from platforms`,a);let o=[];a.length>0&&(o=(await u.search({index:".stkxp_platforms",body:{query:{terms:{"config.packageName":a}},size:1e4,_source:["name"]}})).hits.hits.map(c=>c._source?.name).filter(Boolean));let i=[];return o.length>0&&(i=(await u.search({index:v,body:{query:{bool:{should:o.map(c=>({term:{"mcp_servers_policy.servers.name.keyword":c}})),minimum_should_match:1}},size:1e4,_source:[]}})).hits.hits.map(c=>c._id)),console.log(`[Teams API] User "${s}": ${a.length} enrolled packages, ${o.length} MCP servers, ${n.length} owned assistants, ${i.length} MCP-linked assistants`),{userMcpPackageNames:a,userOwnedAssistantIds:n,mcpLinkedAssistantIds:i}}catch(t){return console.error("[Teams API] Error fetching user visibility data:",t),{userMcpPackageNames:[],userOwnedAssistantIds:[],mcpLinkedAssistantIds:[]}}}async function we(s,t){try{const e=L.parse(s.query);console.log(`[Teams API] getTeams query: ${JSON.stringify(e)}`);const n=[];if(e.enabled!==void 0&&n.push({term:{enabled:e.enabled}}),e.search){const w=e.search.toLowerCase().trim(),T=w.split("||").map(_=>_.trim()).filter(Boolean),A=T[0]||w,P=[];let R=0,M=5;P.push({multi_match:{query:`*${A}*`,fields:["name^5","description^2"],type:"best_fields",operator:"or",minimum_should_match:"60%",fuzziness:"AUTO",boost:3}}),P.push({match:{description:{query:e.search}}}),P.push({semantic:{field:"search_semantic",query:e.search,boost:2}});for(const _ of T)P.push({match_phrase:{name:{query:_,slop:R++,boost:Math.max(M--,1)}}});n.push({bool:{should:P,minimum_should_match:1}})}if(e.currentUser&&e.includeAllSystem)n.push({bool:{should:[{term:{owner:e.currentUser}},{term:{owner:"stkxp"}}],minimum_should_match:1}});else if(e.currentUser&&e.includeOwned)n.push({term:{owner:e.currentUser}});else if(e.currentUser){const{mcpLinkedAssistantIds:w}=await he(e.currentUser);n.push({term:{owner:e.currentUser}}),w.length>0?n.push({terms:{assistantIds:w}}):n.push({term:{assistantIds:"__none__"}})}else if(e.owners){const w=e.owners.split(",").map(T=>T.trim()).filter(Boolean);w.length>0&&n.push({terms:{owner:w}})}let a=null;if(e.packageName&&(a=await ye(e.packageName,e.currentUser),a!==null)){if(a.size===0)return t.json({success:!0,count:0,total:0,page:e.page||1,pageSize:e.pageSize||25,totalPages:0,teams:[]});n.push({terms:{assistantIds:Array.from(a)}})}e.assistantId&&n.push({term:{assistantIds:e.assistantId}});const o=e.page||1,i=e.pageSize||25,d=(o-1)*i,c={name:"name",updated_at:"updated_at"},r=String(s.query.sortField||"name"),p=c[r]||"name",b=s.query.sortOrder==="desc"?"desc":"asc",l=p==="updated_at"?[{updated_at:{order:b,unmapped_type:"date"}}]:[{[p]:{order:b,unmapped_type:"keyword"}}],y={index:f,body:{track_scores:!0,query:{bool:{must:n.length>0?n:[{match_all:{}}]}},sort:[{_score:{order:"desc"}},...l],from:d,size:i,_source:{excludes:["webhookToken"]}}};console.log("[Teams API] getTeams query:",JSON.stringify(y.body.query));const g=await u.search(y),h=typeof g.hits.total=="number"?g.hits.total:g.hits.total?.value||0,I=await Promise.all(g.hits.hits.map(async w=>{const T=w._source,A=Array.isArray(T.assistants)&&T.assistants.length>0?T.assistants:(T.assistantIds||[]).map(_=>({id:_,enabled:!0})),P=a?A.filter(_=>a.has(_.id)):A,R=e.packageName?P.filter(_=>_.enabled!==!1):P,M=await E(R);return{id:w._id,...T,assistants:M}})),k=e.packageName?I.filter(w=>w.assistants.length>0):I;t.json({success:!0,count:k.length,total:h,page:o,pageSize:i,totalPages:Math.ceil(h/i),teams:k})}catch(e){if(console.error("[Teams API] Error fetching teams:",e),e instanceof m.ZodError)return t.status(400).json({success:!1,error:"Invalid query parameters",details:e.errors});if(e.meta?.statusCode===404)return t.json({success:!0,count:0,total:0,page:1,pageSize:25,totalPages:0,teams:[]});t.status(500).json({success:!1,error:"Failed to fetch teams",message:e.message})}}async function be(s,t){try{const{id:e}=s.params,n=await u.get({index:f,id:e,_source_excludes:["webhookToken"]});if(!n.found)return t.status(404).json({success:!1,error:"Team not found"});const a=n._source,o=Array.isArray(a.assistants)&&a.assistants.length>0?a.assistants:(a.assistantIds||[]).map(d=>({id:d,enabled:!0})),i=await E(o);t.json({success:!0,team:{id:n._id,...a,assistants:i}})}catch(e){if(console.error("[Teams API] Error fetching team:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to fetch team",message:e.message})}}async function ke(s,t){try{await ge();const e=s.body,n=new Date().toISOString(),a=H(e),o=a.map(b=>b.id),d=s.auth?.username||e.owner||"system",c={name:e.name,description:e.description||"",assistantIds:o,assistants:a,owner:d,enabled:e.enabled!==void 0?e.enabled:!0,created_at:n,updated_at:n};e.sharedContext?.context?.trim()&&(c.sharedContext={context:e.sharedContext.context.trim(),priority:e.sharedContext.priority==="after"?"after":"before",...e.sharedContext.policy?{policy:e.sharedContext.policy}:{}}),e.graphId?.trim()&&(c.graphId=e.graphId.trim()),console.log("[Teams API] Creating team:",c.name);const r=await u.index({index:f,body:c,refresh:"wait_for"}),p=await E(a);t.status(201).json({success:!0,team:{id:r._id,...c,assistants:p}})}catch(e){console.error("[Teams API] Error creating team:",e),t.status(500).json({success:!1,error:"Failed to create team",message:e.message})}}async function Ie(s,t){try{const{id:e}=s.params,n=await C(s,t,u,f,e);if(!n)return;await de("team",e,n.source,"manual_edit",x(s)||"system");const a=s.body,o=H(a),i=o.map(p=>p.id),d={name:a.name,description:a.description||"",assistantIds:i,assistants:o,owner:a.owner||"system",enabled:a.enabled!==void 0?a.enabled:!0,updated_at:new Date().toISOString()};a.sharedContext?.context?.trim()?d.sharedContext={context:a.sharedContext.context.trim(),priority:a.sharedContext.priority==="after"?"after":"before",...a.sharedContext.policy?{policy:a.sharedContext.policy}:{}}:d.sharedContext=null,d.graphId=a.graphId?.trim()||null,console.log("[Teams API] Updating team:",e);const c=await u.update({index:f,id:e,body:{doc:d},refresh:"wait_for"}),r=await E(o);t.json({success:!0,team:{id:c._id,...d,assistants:r}})}catch(e){if(console.error("[Teams API] Error updating team:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to update team",message:e.message})}}async function Te(s,t){try{const{id:e}=s.params;if(!await C(s,t,u,f,e))return;console.log("[Teams API] Deleting team:",e),await u.delete({index:f,id:e,refresh:"wait_for"}),t.json({success:!0,message:"Team deleted successfully"})}catch(e){if(console.error("[Teams API] Error deleting team:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to delete team",message:e.message})}}async function Ae(s,t){try{const{id:e}=s.params;if(!await C(s,t,u,f,e))return;const a=s.body?.enabled!==!1,o=O(32).toString("hex"),i=new Date().toISOString();await u.update({index:f,id:e,body:{doc:{webhookToken:o,webhookEnabled:a,webhookCreatedAt:i,updated_at:i}},refresh:"wait_for"}),console.log(`[Teams API] Generated webhook token for team: ${e}`),t.json({success:!0,teamId:e,webhookToken:o,webhookEnabled:a,webhookCreatedAt:i,webhookUrl:`/api/webhooks/team/${e}`})}catch(e){if(console.error("[Teams API] Error generating webhook token:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to generate webhook token",message:e.message})}}async function Ce(s,t){try{const{id:e}=s.params;if(!await C(s,t,u,f,e))return;const a=new Date().toISOString();await u.update({index:f,id:e,body:{doc:{webhookToken:null,webhookEnabled:!1,webhookCreatedAt:null,updated_at:a}},refresh:"wait_for"}),console.log(`[Teams API] Revoked webhook token for team: ${e}`),t.json({success:!0,teamId:e,webhookEnabled:!1})}catch(e){if(console.error("[Teams API] Error revoking webhook token:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to revoke webhook token",message:e.message})}}async function xe(s,t){try{const{id:e}=s.params,n=await C(s,t,u,f,e);if(!n)return;const a=n.source;if(!a.mcpEnabled)return t.status(400).json({success:!1,error:"MCP server is not enabled for this team \u2014 generate/enable it in the MCP App tab first."});const o=a.name||"team",i=`${s.protocol}://${s.get("host")}/api/mcp/team/${e}`,d=await oe({teamName:o,teamId:e,mcpUrl:i,allowUnsigned:a.mcpAllowUnsigned===!0}),c=`mcp-app-tester-${re(o)}.zip`;t.set({"Content-Type":"application/zip","Content-Disposition":`attachment; filename="${c}"`}),t.send(d)}catch(e){console.error("[Teams API] Error generating MCP App tester package:",e),t.status(500).json({success:!1,error:"Failed to generate test client package",message:e.message})}}async function _e(s,t){try{const{id:e}=s.params;if(!await C(s,t,u,f,e))return;const a=s.body?.enabled!==!1,o=s.body?.exposeAssistants!==!1,i=s.body?.allowUnsigned===!0,d=typeof s.body?.signingSecret=="string"?s.body.signingSecret.trim():void 0;if(d!==void 0&&d.length<8)return t.status(400).json({success:!1,error:"signingSecret must be at least 8 characters (use your Slack app Signing Secret to integrate with Slack)"});const c=d||O(32).toString("hex"),r=new Date().toISOString();await u.update({index:f,id:e,body:{doc:{mcpSigningSecret:c,mcpEnabled:a,mcpExposeAssistants:o,mcpAllowUnsigned:i,mcpCreatedAt:r,updated_at:r}},refresh:"wait_for"}),console.log(`[Teams API] Generated MCP signing secret for team: ${e}`),t.json({success:!0,teamId:e,mcpSigningSecret:c,mcpEnabled:a,mcpExposeAssistants:o,mcpAllowUnsigned:i,mcpCreatedAt:r,mcpUrl:`/api/mcp/team/${e}`})}catch(e){if(console.error("[Teams API] Error generating MCP config:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to generate MCP config",message:e.message})}}async function Se(s,t){try{const{id:e}=s.params;if(!await C(s,t,u,f,e))return;const a=new Date().toISOString();await u.update({index:f,id:e,body:{doc:{mcpSigningSecret:null,mcpEnabled:!1,mcpAllowUnsigned:!1,mcpCreatedAt:null,updated_at:a}},refresh:"wait_for"}),console.log(`[Teams API] Revoked MCP config for team: ${e}`),t.json({success:!0,teamId:e,mcpEnabled:!1})}catch(e){if(console.error("[Teams API] Error revoking MCP config:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to revoke MCP config",message:e.message})}}async function Pe(s,t){try{const{id:e,platformId:n}=s.params,a=await C(s,t,u,f,e);if(!a)return;const o=Array.isArray(s.body?.commandIds)?s.body.commandIds:[],i=typeof s.body?.namespace=="string"?s.body.namespace:void 0,d=s.body?.blockKitMode===!0,c=a.source.owner;for(const l of o){const y=await le(c,n,l,e);if(y)return t.status(409).json({success:!1,message:`Command is already assigned to team "${y.teamName}"`})}const r=new Date().toISOString(),b={...a.source.channelCommands??{},[n]:{commandIds:o,namespace:i,blockKitMode:d}};await u.update({index:f,id:e,body:{doc:{channelCommands:b,updated_at:r}},refresh:"wait_for"}),console.log(`[Teams API] Updated channel commands for team ${e} / platform ${n}`),t.json({success:!0,teamId:e,platformId:n,commandIds:o,namespace:i,blockKitMode:d})}catch(e){if(console.error("[Teams API] Error setting channel commands:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to set channel commands",message:e.message})}}async function ve(s,t){try{const{id:e,platformId:n}=s.params,a=await C(s,t,u,f,e);if(!a)return;const o=new Date().toISOString(),i={...a.source.channelCommands??{}};delete i[n],await u.update({index:f,id:e,body:{doc:{channelCommands:i,updated_at:o}},refresh:"wait_for"}),console.log(`[Teams API] Cleared channel commands for team ${e} / platform ${n}`),t.json({success:!0,teamId:e,platformId:n})}catch(e){if(console.error("[Teams API] Error clearing channel commands:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to clear channel commands",message:e.message})}}async function je(s,t){try{const e=x(s);if(!e)return t.status(401).json({success:!1,message:"Unauthorized"});const n=s.query.platformId;if(!n)return t.status(400).json({success:!1,message:"platformId is required"});const a=await me(e,n);t.json({success:!0,body:{ownership:a}})}catch(e){console.error("[Teams API] Error fetching channel command ownership:",e),t.status(500).json({success:!1,message:e.message||"Failed to fetch channel command ownership"})}}async function Ee(s,t){try{const{id:e}=s.params,n=await C(s,t,u,f,e);if(!n)return;const a=n.source.owner||x(s)||"system",o=j(S),i=await $([{index:f,esId:e}],a,o),d=te(i);t.json({success:!0,graph:d})}catch(e){if(console.error("[Teams API] Error building full-structure graph:",e),/Required dependencies/.test(e?.message??""))return t.status(422).json({success:!1,error:e.message});if(e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to build full-structure graph",message:e.message})}}async function Re(s,t){try{const{id:e}=s.params,n=await C(s,t,u,f,e);if(!n)return;const a=n.source.owner||x(s)||"system",o=n.source.name||"(unnamed team)",i=j(S),d=await $([{index:f,esId:e}],a,i),c=J(d,{exportedAt:new Date().toISOString(),rootName:o}),r=d.docs.filter(l=>l.key==="platform"&&z(l.source?.type)&&l.source?.owner===a).map(l=>l.esId),p=d.docs.some(l=>l.key==="tool"&&l.reachedVia?.includes(U)&&l.source?.owner===a);let b;if(r.length>0||p){const l=await D(d,a,e);if(!l)return t.status(422).json({success:!1,error:"Failed to create the MCP gateway grant: could not connect to one or more MCP server platforms in this team."});for(const g of c.closure.docs)g.key==="platform"&&r.includes(g.esId)&&(g.source.config={...g.source.config,serverType:"gateway",url:""});const y=c.closure.docs.filter(g=>g.key==="tool"&&g.source?.type==="mcp_remote"&&(r.includes(g.source?.platformId)||r.includes(g.source?.mcpServerId))).length;y>0&&(c.closure.docs=c.closure.docs.filter(g=>!(g.key==="tool"&&g.source?.type==="mcp_remote"&&(r.includes(g.source?.platformId)||r.includes(g.source?.mcpServerId)))),c.summary.totalDocs-=y,c.summary.byEntity.tool=(c.summary.byEntity.tool??0)-y),c.mcpGateway={grantId:l.grantId,platformIds:l.platformIds},b=l.secret}console.log(`[Teams API] ${x(s)??"?"} exported team "${o}" (${e}): ${c.summary.totalDocs} docs, ${d.unresolved.length} unresolved ref(s)`+(b?`, gateway grant for ${r.length} platform(s)`:"")),t.json({success:!0,bundle:c,gatewaySecret:b,unresolved:d.unresolved})}catch(e){if(console.error("[Teams API] Error exporting team:",e),/Required dependencies/.test(e?.message??""))return t.status(422).json({success:!1,error:e.message});if((e?.message??"").startsWith(N))return t.status(422).json({success:!1,error:e.message});if(e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to export team",message:e.message})}}const W=m.object({secret:m.string().min(8)});async function Me(s,t){const e=W.safeParse(s.body);if(!e.success)return t.status(400).json({success:!1,error:'Body must include a "secret" of at least 8 characters'});const{secret:n}=e.data;try{const{id:a}=s.params,o=await C(s,t,u,f,a);if(!o)return;const i=o.source.owner||x(s)||"system",d=o.source.name||"(unnamed team)",c=j(S),r=await $([{index:f,esId:a}],i,c),p=[...new Set(r.docs.map(k=>k.index))],b={};if(p.length>0){const k=await S.indices.getMapping({index:p});for(const[w,T]of Object.entries(k))b[w]=T.mappings}const l=r.docs.filter(k=>k.key==="platform"&&z(k.source?.type)&&k.source?.owner===i).map(k=>k.esId),y=r.docs.some(k=>k.key==="tool"&&k.reachedVia?.includes(U)&&k.source?.owner===i);let g,h;if(l.length>0||y){const k=await D(r,i,a);if(!k)return t.status(422).json({success:!1,error:"Failed to create the MCP gateway grant: could not connect to one or more MCP server platforms in this team."});for(const w of r.docs)w.key==="platform"&&l.includes(w.esId)&&(w.source.config={...w.source.config,serverType:"gateway",url:""});r.docs=r.docs.filter(w=>!(w.key==="tool"&&w.source?.type==="mcp_remote"&&(l.includes(w.source?.platformId)||l.includes(w.source?.mcpServerId)))),h={grantId:k.grantId,platformIds:k.platformIds},g=k.secret}const I=se(r,{exportedAt:new Date().toISOString(),rootName:d,passphrase:n,gatewaySecret:g,indexMappings:b});I.mcpGateway=h,console.log(`[Teams API] ${x(s)??"?"} exported (encrypted) team "${d}" (${a}): ${I.summary.totalDocs} docs, ${r.unresolved.length} unresolved ref(s)`),t.json({success:!0,bundle:I,unresolved:r.unresolved})}catch(a){if(console.error("[Teams API] Error exporting team (encrypted):",a),/Required dependencies/.test(a?.message??""))return t.status(422).json({success:!1,error:a.message});if((a?.message??"").startsWith(N))return t.status(422).json({success:!1,error:a.message});if(a.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to export team (encrypted)",message:a.message})}}async function $e(s,t){try{const e=x(s);if(!e)return t.status(401).json({success:!1,error:"Unauthorized"});const n=(s.body&&s.body.bundle)??s.body,a=s.body&&typeof s.body.gatewaySecret=="string"?s.body.gatewaySecret:void 0;let o;try{o=Y(n)}catch(p){const b=p instanceof ee?p.message:"Invalid bundle";return t.status(400).json({success:!1,error:b})}let i;if(o.mcpGateway){if(!a)return t.status(400).json({success:!1,error:"This bundle requires a gateway key to activate its MCP server platform(s). Paste the key you received from the exporting owner."});const p=await ie(o.mcpGateway.grantId,a,e);if("error"in p)return t.status(400).json({success:!1,error:p.error});i=p.gatewayToken}const d=j(S),c=Z(S),r=await X(o.closure,e,d,c,{blankSecrets:!0});if(o.mcpGateway&&i){const p=process.env.MCP_GATEWAY_HOST||"https://mcp.erretegia.com",b=new Date().toISOString();for(const y of r.created){if(y.key!=="platform"||!o.mcpGateway.platformIds.includes(y.oldEsId))continue;const g=`${p}/mcp/gateway:${o.mcpGateway.grantId}:${y.oldEsId}`;await S.update({index:y.index,id:y.newEsId,body:{doc:{managedType:"managed",config:{serverType:"gateway",url:g,authKey:i}}},refresh:!0});try{await ce(y.newEsId,{id:y.newEsId,name:y.name||"MCP Server",type:"MCPServer",managedType:"self-hosted",owner:e,created:b,updated:b,enabled:!0,config:{serverType:"gateway",url:g,authKey:i}},e)}catch(h){console.warn(`[Teams API] Post-import gateway tool sync failed for platform ${y.newEsId}: ${h.message}`)}}const l=`personal:${o.sourceOwner}`;if(o.mcpGateway.platformIds.includes(l)){const y=`${p}/mcp/gateway:${o.mcpGateway.grantId}:${l}`,g=K();await S.index({index:".stkxp_platforms",id:g,document:{id:g,name:"My MCP Tools (gateway)",type:"MCPServer",managedType:"managed",owner:e,enabled:!0,created:b,updated:b,config:{serverType:"gateway",url:y,authKey:i,protocol:"http"}},refresh:!0});const h=`__personal_tools__:${o.sourceOwner}`;for(const I of r.created){if(I.key!=="assistant")continue;const k=o.closure.docs.find(A=>A.key==="assistant"&&A.esId===I.oldEsId),w=k?.source?.mcp_servers_policy?.servers;if(!Array.isArray(w)||!w.some(A=>A?.id===h))continue;const T=w.map(A=>A?.id===h?{...A,id:g}:A);await S.update({index:I.index,id:I.newEsId,body:{doc:{mcp_servers_policy:{...k.source.mcp_servers_policy,servers:T}}},refresh:!0})}}}console.log(`[Teams API] ${e} imported team "${o.rootName}" (from owner "${o.sourceOwner}"): ${r.created.length} created, ${r.skipped.length} reused, ${r.referenced.length} referenced, ${r.droppedRefs.length} refs dropped`),t.status(201).json({success:!0,rootName:o.rootName,report:r,unresolved:o.closure.unresolved})}catch(e){if(console.error("[Teams API] Error importing team:",e),/Required dependencies/.test(e?.message??""))return t.status(422).json({success:!1,error:e.message});t.status(500).json({success:!1,error:"Failed to import team",message:e.message})}}async function Oe(s,t){try{const{id:e}=s.params;if(!x(s))return t.status(401).json({success:!1,error:"Unauthorized"});let a;try{a=(await u.get({index:f,id:e}))._source}catch(l){if(l?.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});throw l}const i=(a?.assistants||[]).filter(l=>l.enabled!==!1).map(l=>l.id);if(i.length===0)return t.json({success:!0,teamId:e,teamName:a?.name,singlePassMax:0,assistants:[]});const c=(await u.mget({index:v,body:{ids:i}})).docs.filter(l=>l.found),r=new Map,p=[];let b=0;for(const l of c){const y=await ne(u,l._id,l._source||{},r);b+=y.singlePassMax,p.push(y)}t.json({success:!0,teamId:e,teamName:a?.name,singlePassMax:b,assistants:p,note:"Single-pass upper bound. Tool loops can exceed it \u2014 multiply by an empirical factor (2-3\xD7) when sizing a budget."})}catch(e){console.error("[Teams API] Error computing token projection:",e),t.status(500).json({success:!1,error:"Failed to compute token projection",message:e.message})}}async function Ue(s,t){try{const{id:e}=s.params,n=x(s);if(!n)return t.status(401).json({success:!1,error:"Unauthorized"});const a=Math.min(parseInt(s.query.limit||"20",10)||20,100),o=[{term:{teamId:e}}];V(s)||o.push({term:{username:n}});const c=((await u.search({index:pe,body:{size:a,sort:[{createdAt:{order:"desc"}}],_source:["id","title","createdAt","metadata.tokenUsage","assistants"],query:{bool:{must:o}}}})).hits.hits||[]).map(h=>{const I=h._source||{},k=I.metadata?.tokenUsage||{};return{chatId:h._id,title:I.title,createdAt:I.createdAt,totalTokens:Number(k.totalTokens||0),inputTokens:Number(k.totalInputTokens||0),outputTokens:Number(k.totalOutputTokens||0),assistantCount:Array.isArray(I.assistants)?I.assistants.length:0}}),r=c.map(h=>h.totalTokens).filter(h=>h>0),p=r.reduce((h,I)=>h+I,0),b=[...r].sort((h,I)=>h-I),l=b.length?b[Math.min(b.length-1,Math.floor(b.length*.95))]:0,y=c.map(h=>h.inputTokens),g=c.map(h=>h.outputTokens);t.json({success:!0,runCount:r.length,avgTotalTokens:r.length?Math.round(p/r.length):0,maxTotalTokens:r.length?Math.max(...r):0,p95TotalTokens:l,avgInputTokens:y.length?Math.round(y.reduce((h,I)=>h+I,0)/y.length):0,avgOutputTokens:g.length?Math.round(g.reduce((h,I)=>h+I,0)/g.length):0,recentRuns:c})}catch(e){console.error("[Teams API] Error fetching token stats:",e),t.status(500).json({success:!1,error:"Failed to fetch token stats",message:e.message})}}const De=m.object({name:m.string().describe("Team name"),description:m.string().optional().describe("Human description"),assistantIds:m.array(m.string()).optional().describe("Ids of assistants in the team"),assistants:m.array(m.any()).optional().describe("Full assistant objects (alternative to assistantIds)"),graphId:m.string().optional().describe("Id of a .stkxp_graphs coordination graph"),enabled:m.boolean().optional().describe("Enabled flag (default true)"),sharedContext:m.any().optional().describe("Shared context object injected into every assistant")}).passthrough(),Ne=m.object({name:m.string().optional().describe("Team name"),description:m.string().optional().describe("Human description"),assistantIds:m.array(m.string()).optional().describe("Ids of assistants in the team"),assistants:m.array(m.any()).optional().describe("Full assistant objects (alternative to assistantIds)"),graphId:m.string().optional().describe("Id of a .stkxp_graphs coordination graph"),enabled:m.boolean().optional().describe("Enabled flag (default true)"),sharedContext:m.any().optional().describe("Shared context object injected into every assistant")}).passthrough(),rs=[{method:"get",path:"/api/teams",handler:we,validate:{query:L},openapi:{summary:"List teams for the caller",description:"Returns all teams owned by the authenticated user. A team groups assistants and optionally references a coordination graph (`graphId`) for multi-assistant orchestration. `search` runs a hybrid lexical + semantic query (semantic_text field `search_semantic`).",tags:["teams"]}},{method:"post",path:"/api/teams/import",handler:$e,openapi:{summary:"Import a team from an export bundle",description:"Reconstructs a team and its full dependency closure under the authenticated caller from a bundle produced by `GET /api/teams/:id/export`. New ids are minted for owned docs (links rebuilt), shared/system docs are referenced in place, same-named docs the caller already owns are reused, and secrets stay blank (re-enter LLM keys / platform authKeys and regenerate the webhook token afterwards).",tags:["teams"],audience:"internal"}},{method:"get",path:"/api/teams/channel-command-ownership",handler:je,openapi:{summary:"Get command\u2192team ownership map for a platform",description:"Query param `platformId`. Returns `{ [commandId]: { teamId, teamName } }` for every command on that platform currently owned by one of the caller's teams \u2014 used by the Team Edit Channels tab to disable already-claimed commands.",tags:["teams","channels"],audience:"internal"}},{method:"get",path:"/api/teams/:id",handler:be,openapi:{summary:"Get a team by id",tags:["teams"]}},{method:"get",path:"/api/teams/:id/export",handler:Re,openapi:{summary:"Export a team as a portable bundle",description:"Resolves the team's full transitive dependency closure (assistants, graphs, sub-graphs, prompts, node templates, routing rules, models, providers, platforms, tools) and returns a secret-free JSON bundle for re-import via `POST /api/teams/import`. Limited to teams the caller owns.",tags:["teams"],audience:"internal"}},{method:"post",path:"/api/teams/:id/export-encrypted",handler:Me,validate:{body:W},openapi:{summary:"Export a team as a fully self-contained, passphrase-encrypted bundle",description:"Like GET /api/teams/:id/export, but instead of blanking secrets (LLM keys, platform creds, webhook token), encrypts them with the caller-supplied passphrase (AES-256-GCM per field). Intended for the `stkxp export` CLI, which produces a bundle deployable standalone via `stkxp deploy` \u2014 see docs/superpowers/specs/2026-09-15-team-standalone-export-design.md.",tags:["teams"],audience:"internal"}},{method:"get",path:"/api/teams/:id/full-structure",handler:Ee,openapi:{summary:"Get a team's full structure as a flattened canvas graph",description:"Resolves the team's full transitive dependency closure (assistants, runtime graphs, nested subgraphs, tools, platforms, models, providers, routing rules) and returns it flattened into nodes/edges/groups for the Team Canvas Overview mode. Read-only. Limited to teams the caller owns.",tags:["teams"],audience:"internal"}},{method:"post",path:"/api/teams",handler:ke,validate:{body:De},openapi:{summary:"Create a team",description:"Creates a team with the supplied `name`, optional `description`, `assistantIds[]`, and optional `graphId` pointing to a `.stkxp_graphs` coordination graph.",tags:["teams"]}},{method:"put",path:"/api/teams/:id",handler:Ie,validate:{body:Ne},openapi:{summary:"Update a team",tags:["teams"]}},{method:"delete",path:"/api/teams/:id",handler:Te,openapi:{summary:"Delete a team",tags:["teams"]}},{method:"get",path:"/api/teams/:id/token-projection",handler:Oe,openapi:{summary:"Theoretical single-pass max token usage for a team",description:"Resolves each enabled assistant graph (`assistant.graphId`), applies `assistant.llm_overrides[node.type]` on top of node-level routing rules, and sums `maxTokens` across nodes with a routing rule. Single-pass upper bound only \u2014 tool loops can exceed it.",tags:["teams"],audience:"internal"}},{method:"get",path:"/api/teams/:id/token-stats",handler:Ue,openapi:{summary:"Aggregate token usage for the caller's recent runs of a team",description:"Reads `.stkxp_chats` filtered by `teamId` (and `username` unless superuser) and returns avg / max / p95 total tokens plus the last N runs. Used by the Team flyout to surface real consumption before setting a future budget.",tags:["teams"],audience:"internal"}},{method:"post",path:"/api/teams/:id/webhook-token",handler:Ae,openapi:{summary:"Generate (or rotate) a webhook token for a team",description:"Creates a new inbound webhook token. Returns the clear-text token **once** \u2014 the UI must surface it immediately to the operator. Subsequent fetches of the team do not expose the token. Optional body `{ enabled?: boolean }` (defaults to true) controls whether the webhook is active.",tags:["teams","webhooks"],audience:"internal"}},{method:"delete",path:"/api/teams/:id/webhook-token",handler:Ce,openapi:{summary:"Revoke the webhook token for a team",description:"Clears the webhook token and disables the webhook. POSTs to `/api/webhooks/team/:id` will be rejected with 401 until a new token is generated.",tags:["teams","webhooks"],audience:"internal"}},{method:"post",path:"/api/teams/:id/mcp-config",handler:_e,openapi:{summary:"Enable (or rotate) the MCP server for a team",description:'Owner-only. Generates a signing secret (returned once) and enables the per-team MCP endpoint at /api/mcp/team/:id. Body `{ enabled?: boolean, exposeAssistants?: boolean, signingSecret?: string }` \u2014 enabled/exposeAssistants default true; if `signingSecret` is provided (\u22658 chars) it is used verbatim (e.g. a Slack app Signing Secret to integrate with Slack), otherwise a random 32-byte secret is minted. `allowUnsigned` (default false) accepts unsigned requests \u2014 needed for Slack\'s MCP client, which sends no signature for auth_type "no_auth"; a signature, when present, is always verified.',tags:["teams","mcp"],audience:"internal"}},{method:"delete",path:"/api/teams/:id/mcp-config",handler:Se,openapi:{summary:"Disable the MCP server for a team",description:"Owner-only. Clears the signing secret and disables the MCP endpoint. Calls to /api/mcp/team/:id return 404 until re-enabled.",tags:["teams","mcp"],audience:"internal"}},{method:"post",path:"/api/teams/:id/channel-commands/:platformId",handler:Pe,openapi:{summary:"Claim one or more commands on a platform for a team",description:"Body `{ commandIds: string[], namespace?: string, blockKitMode?: boolean }`. Rejects with 409 if any commandId is already owned by a different team. A command can be owned by at most one team at a time, scoped to (platformId, commandId).",tags:["teams","channels"],audience:"internal"}},{method:"delete",path:"/api/teams/:id/channel-commands/:platformId",handler:ve,openapi:{summary:"Clear a team's channel-command config for a platform",description:"Removes this team's ownership of any commands claimed on the given platform, freeing them for other teams.",tags:["teams","channels"],audience:"internal"}},{method:"get",path:"/api/teams/:id/mcp-app-tester.zip",handler:xe,openapi:{summary:"Download a pre-configured MCP App test client for this team",description:"Owner-only. Generates a ZIP \u2014 a minimal fork of mcp-apps-tester wired to this team's ask_ tool, with Slack-style HMAC request signing baked in. Requires the team's MCP server to be enabled (mcpEnabled). Ships without the signing secret \u2014 the caller pastes their own copy into the generated .env.",tags:["teams","mcp"],audience:"internal"}}];export{ge as ensureTeamsIndex,ds as getTeamById,ms as getTeamMcpConfig,us as getTeamSharedContext,ls as getTeamWebhookConfig,rs as routes};
@@ -1 +1 @@
1
- function t(n){return n?.body??n}async function c(n,s){const a=[...new Set(s.docs.map(e=>e.index))],o=[];for(const e of a)t(await n.indices.exists({index:e}))||(await n.indices.create({index:e}),o.push(e));if(s.docs.length===0)return{indicesCreated:o,docsIndexed:0};const d=s.docs.flatMap(e=>[{index:{_index:e.index,_id:e.esId}},e.source]),i=t(await n.bulk({operations:d,refresh:"wait_for"}));if(i.errors){const e=(i.items??[]).filter(r=>r.index?.error).map(r=>`${r.index._id}: ${r.index.error.reason}`);throw new Error(`Bulk index failed for ${e.length} doc(s): ${e.join("; ")}`)}return{indicesCreated:o,docsIndexed:s.docs.length}}export{c as bootstrapClosureToEs};
1
+ function a(n){return n?.body??n}async function f(n,s,d){const c=[...new Set(s.docs.map(e=>e.index))],o=[];for(const e of c)if(!a(await n.indices.exists({index:e}))){const t=d?.[e];await n.indices.create(t?{index:e,mappings:t}:{index:e}),o.push(e)}if(s.docs.length===0)return{indicesCreated:o,docsIndexed:0};const x=s.docs.flatMap(e=>[{index:{_index:e.index,_id:e.esId}},e.source]),i=a(await n.bulk({operations:x,refresh:"wait_for"}));if(i.errors){const e=(i.items??[]).filter(r=>r.index?.error).map(r=>`${r.index._id}: ${r.index.error.reason}`);throw new Error(`Bulk index failed for ${e.length} doc(s): ${e.join("; ")}`)}return{indicesCreated:o,docsIndexed:s.docs.length}}export{f as bootstrapClosureToEs};
@@ -1 +1 @@
1
- import{getEntitySpec as f}from"./entity-graph";import{encryptValue as p,decryptValue as g,deriveBundleKey as m,generateBundleSalt as k,BundleDecryptError as N}from"./team-bundle-crypto";const u="stkxp-team-bundle-encrypted",w=1;function E(r,n){const t=n.split(".");let e=r;for(let c=0;c<t.length-1;c++){if(e==null||typeof e!="object")return null;e=e[t[c]]}if(e==null||typeof e!="object")return null;const a=t[t.length-1];return a in e?{parent:e,leaf:a}:null}function S(r,n,t){const e=E(r,n);e&&(e.parent[e.leaf]=p(e.parent[e.leaf],t))}function b(r,n,t){const e=E(r,n);!e||typeof e.parent[e.leaf]!="string"||(e.parent[e.leaf]=g(e.parent[e.leaf],t))}function B(r,n){const t=JSON.parse(JSON.stringify(r)),e={},a=k(),c=m(n.passphrase,a);for(const s of t.docs){e[s.key]=(e[s.key]??0)+1;const o=f(s.key);for(const l of o.secretFields??[])S(s.source,l,c);s.key==="team"&&(s.source.webhookEnabled=!1,delete s.source.webhookCreatedAt)}return{kind:u,bundleVersion:w,exportedAt:n.exportedAt,sourceOwner:r.sourceOwner,rootName:n.rootName,summary:{totalDocs:t.docs.length,byEntity:e},kdfSalt:a,closure:t,encryptedGatewaySecret:n.gatewaySecret?p(n.gatewaySecret,c):void 0}}class i extends Error{}function D(r,n){if(r==null||typeof r!="object")throw new i("Bundle is not a JSON object");const t=r;if(t.kind!==u)throw new i(`Not an encrypted team bundle (expected kind "${u}", got "${t.kind}")`);if(t.bundleVersion!==w)throw new i(`Unsupported bundle version ${t.bundleVersion}`);const e=t.closure;if(!e||typeof e!="object"||!Array.isArray(e.docs))throw new i("Bundle is missing a well-formed closure");if(typeof t.kdfSalt!="string"||t.kdfSalt.length===0)throw new i("Bundle is missing its key-derivation salt (kdfSalt)");let a;try{a=m(n,t.kdfSalt)}catch(o){throw new i(`Bundle is missing a usable key-derivation salt: ${o instanceof Error?o.message:"unknown error"}`)}const c=JSON.parse(JSON.stringify(e));for(const o of c.docs){const l=f(o.key);for(const y of l.secretFields??[])try{b(o.source,y,a)}catch(d){throw d instanceof N?new i(`Failed to decrypt ${o.key}.${y}: ${d.message}`):d}}let s;if(typeof t.encryptedGatewaySecret=="string")try{s=g(t.encryptedGatewaySecret,a)}catch(o){throw new i(`Failed to decrypt gateway secret: ${o instanceof Error?o.message:"unknown error"}`)}return{closure:c,rootName:typeof t.rootName=="string"?t.rootName:"(unnamed team)",sourceOwner:typeof t.sourceOwner=="string"?t.sourceOwner:"",mcpGateway:t.mcpGateway,gatewaySecret:s}}export{u as ENCRYPTED_BUNDLE_KIND,w as ENCRYPTED_BUNDLE_VERSION,i as TeamBundleDecryptError,D as parseAndDecryptTeamBundle,B as serializeTeamBundleEncrypted};
1
+ import{getEntitySpec as y}from"./entity-graph";import{encryptValue as f,decryptValue as g,deriveBundleKey as m,generateBundleSalt as k,BundleDecryptError as N}from"./team-bundle-crypto";const p="stkxp-team-bundle-encrypted",w=1;function E(n,r){const e=r.split(".");let t=n;for(let i=0;i<e.length-1;i++){if(t==null||typeof t!="object")return null;t=t[e[i]]}if(t==null||typeof t!="object")return null;const a=e[e.length-1];return a in t?{parent:t,leaf:a}:null}function S(n,r,e){const t=E(n,r);t&&(t.parent[t.leaf]=f(t.parent[t.leaf],e))}function b(n,r,e){const t=E(n,r);!t||typeof t.parent[t.leaf]!="string"||(t.parent[t.leaf]=g(t.parent[t.leaf],e))}function C(n,r){const e=JSON.parse(JSON.stringify(n)),t={},a=k(),i=m(r.passphrase,a);for(const s of e.docs){t[s.key]=(t[s.key]??0)+1;const o=y(s.key);for(const d of o.secretFields??[])S(s.source,d,i);s.key==="team"&&(s.source.webhookEnabled=!1,delete s.source.webhookCreatedAt)}return{kind:p,bundleVersion:w,exportedAt:r.exportedAt,sourceOwner:n.sourceOwner,rootName:r.rootName,summary:{totalDocs:e.docs.length,byEntity:t},kdfSalt:a,closure:e,encryptedGatewaySecret:r.gatewaySecret?f(r.gatewaySecret,i):void 0,indexMappings:r.indexMappings}}class c extends Error{}function B(n,r){if(n==null||typeof n!="object")throw new c("Bundle is not a JSON object");const e=n;if(e.kind!==p)throw new c(`Not an encrypted team bundle (expected kind "${p}", got "${e.kind}")`);if(e.bundleVersion!==w)throw new c(`Unsupported bundle version ${e.bundleVersion}`);const t=e.closure;if(!t||typeof t!="object"||!Array.isArray(t.docs))throw new c("Bundle is missing a well-formed closure");if(typeof e.kdfSalt!="string"||e.kdfSalt.length===0)throw new c("Bundle is missing its key-derivation salt (kdfSalt)");let a;try{a=m(r,e.kdfSalt)}catch(o){throw new c(`Bundle is missing a usable key-derivation salt: ${o instanceof Error?o.message:"unknown error"}`)}const i=JSON.parse(JSON.stringify(t));for(const o of i.docs){const d=y(o.key);for(const u of d.secretFields??[])try{b(o.source,u,a)}catch(l){throw l instanceof N?new c(`Failed to decrypt ${o.key}.${u}: ${l.message}`):l}}let s;if(typeof e.encryptedGatewaySecret=="string")try{s=g(e.encryptedGatewaySecret,a)}catch(o){throw new c(`Failed to decrypt gateway secret: ${o instanceof Error?o.message:"unknown error"}`)}return{closure:i,rootName:typeof e.rootName=="string"?e.rootName:"(unnamed team)",sourceOwner:typeof e.sourceOwner=="string"?e.sourceOwner:"",mcpGateway:e.mcpGateway,gatewaySecret:s,indexMappings:e.indexMappings&&typeof e.indexMappings=="object"?e.indexMappings:void 0}}export{p as ENCRYPTED_BUNDLE_KIND,w as ENCRYPTED_BUNDLE_VERSION,c as TeamBundleDecryptError,B as parseAndDecryptTeamBundle,C as serializeTeamBundleEncrypted};