@zhama/tego-plugin-sdk 1.36.1 → 1.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +50 -3
  2. package/agent/RULES.md +123 -0
  3. package/agent/guides/access.md +39 -0
  4. package/agent/guides/backend.md +76 -0
  5. package/agent/guides/claw.md +18 -0
  6. package/agent/guides/desktop-web.md +48 -0
  7. package/agent/guides/frontend.md +83 -0
  8. package/agent/guides/host-api-versions.md +34 -0
  9. package/agent/guides/i18n.md +17 -0
  10. package/agent/guides/manifest.md +26 -0
  11. package/agent/guides/migrations.md +17 -0
  12. package/agent/guides/storage.md +18 -0
  13. package/agent/guides/testing.md +17 -0
  14. package/agent/guides/ui.md +22 -0
  15. package/agent/reference/plugin-sdk-api.md +561 -0
  16. package/bin/tego-plugin-pack.mjs +1 -1
  17. package/bin/tego-plugin.mjs +4 -2
  18. package/dist/chunk-2NTAWYCG.js +1 -0
  19. package/dist/chunk-2P6UEAPV.cjs +1 -0
  20. package/dist/chunk-45Y4CR4R.cjs +1 -0
  21. package/dist/{chunk-THMWRUAT.cjs → chunk-4LKRXDYY.cjs} +13 -12
  22. package/dist/{chunk-7XHF3YSK.js → chunk-56QOZQTY.js} +1 -1
  23. package/dist/{chunk-C2OCOPZQ.cjs → chunk-6HNG5OZI.cjs} +1 -1
  24. package/dist/chunk-DEGQDJF6.js +1 -0
  25. package/dist/chunk-EIVA5BR6.js +1 -0
  26. package/dist/chunk-FCYAAOHC.js +20 -0
  27. package/dist/chunk-HZ4YWRYX.cjs +5 -0
  28. package/dist/chunk-I5Z3ECC4.js +5 -0
  29. package/dist/chunk-INCA3GFW.js +1 -0
  30. package/dist/chunk-IVJECPGG.js +1 -0
  31. package/dist/chunk-MWMGJJ74.cjs +1 -0
  32. package/dist/chunk-N4SXZZPY.cjs +1 -0
  33. package/dist/chunk-SKNPNPJP.cjs +1 -0
  34. package/dist/dev.cjs +358 -0
  35. package/dist/dev.d.cts +144 -0
  36. package/dist/dev.d.ts +144 -0
  37. package/dist/dev.js +358 -0
  38. package/dist/doctor.cjs +8 -0
  39. package/dist/doctor.d.cts +62 -0
  40. package/dist/doctor.d.ts +62 -0
  41. package/dist/doctor.js +8 -0
  42. package/dist/frontend.d.cts +33 -1
  43. package/dist/frontend.d.ts +33 -1
  44. package/dist/hono.cjs +1 -0
  45. package/dist/hono.d.cts +84 -0
  46. package/dist/hono.d.ts +84 -0
  47. package/dist/hono.js +1 -0
  48. package/dist/index.cjs +1 -1
  49. package/dist/index.d.cts +8 -3
  50. package/dist/index.d.ts +8 -3
  51. package/dist/index.js +1 -1
  52. package/dist/{manifest-types-DQARCQJt.d.cts → manifest-types-CUTNfJTa.d.cts} +1 -1
  53. package/dist/{manifest-types-DQARCQJt.d.ts → manifest-types-CUTNfJTa.d.ts} +1 -1
  54. package/dist/manifest.cjs +1 -1
  55. package/dist/manifest.d.cts +55 -2
  56. package/dist/manifest.d.ts +55 -2
  57. package/dist/manifest.js +1 -1
  58. package/dist/migrations-sync.cjs +1 -0
  59. package/dist/migrations-sync.d.cts +20 -0
  60. package/dist/migrations-sync.d.ts +20 -0
  61. package/dist/migrations-sync.js +1 -0
  62. package/dist/pack.cjs +1 -1
  63. package/dist/pack.d.cts +8 -2
  64. package/dist/pack.d.ts +8 -2
  65. package/dist/pack.js +1 -1
  66. package/dist/publish.cjs +1 -1
  67. package/dist/publish.d.cts +1 -1
  68. package/dist/publish.d.ts +1 -1
  69. package/dist/publish.js +1 -1
  70. package/dist/react.cjs +1 -0
  71. package/dist/react.d.cts +101 -0
  72. package/dist/react.d.ts +101 -0
  73. package/dist/react.js +1 -0
  74. package/dist/scaffold.cjs +672 -17
  75. package/dist/scaffold.d.cts +42 -18
  76. package/dist/scaffold.d.ts +42 -18
  77. package/dist/scaffold.js +674 -19
  78. package/dist/server-runtime.cjs +1 -1
  79. package/dist/server-runtime.d.cts +27 -2
  80. package/dist/server-runtime.d.ts +27 -2
  81. package/dist/server-runtime.js +1 -1
  82. package/dist/testing.cjs +6 -2
  83. package/dist/testing.d.cts +19 -1
  84. package/dist/testing.d.ts +19 -1
  85. package/dist/testing.js +6 -2
  86. package/dist/verify-FF2RD2MV.cjs +1 -0
  87. package/dist/verify-W7ZTUCO4.js +1 -0
  88. package/dist/vite.cjs +1 -1
  89. package/dist/vite.d.cts +27 -1
  90. package/dist/vite.d.ts +27 -1
  91. package/dist/vite.js +1 -1
  92. package/package.json +37 -3
  93. package/dist/chunk-3KUOBGJN.js +0 -19
  94. package/dist/chunk-3WA3M347.cjs +0 -1
  95. package/dist/chunk-5CU33JVY.js +0 -1
  96. package/dist/chunk-7K37PVGV.js +0 -5
  97. package/dist/chunk-BTHXQWHT.js +0 -1
  98. package/dist/chunk-ROFUQLOZ.cjs +0 -1
  99. package/dist/chunk-W7C357A6.cjs +0 -5
@@ -1 +1 @@
1
- "use strict";var S=Object.defineProperty;var n=(a,e)=>S(a,"name",{value:e,configurable:!0});var chunkIMAVYQ4U_cjs=require("./chunk-IMAVYQ4U.cjs"),chunkBN6TGHSB_cjs=require("./chunk-BN6TGHSB.cjs"),http=require("http");function toArrayBuffer(a){const e=new ArrayBuffer(a.byteLength);return new Uint8Array(e).set(a),e}n(toArrayBuffer,"toArrayBuffer");function readEnv(a){const e=n(s=>{const c=a[s];if(!c)throw new Error(`${s} is required but was not set`);return c},"require_");return{pluginId:e("TEGO_PLUGIN_ID"),tenantId:e("TEGO_TENANT_ID"),port:Number(e("TEGO_PLUGIN_PORT")),healthPath:a.TEGO_PLUGIN_HEALTH_PATH??"/healthz",hostApiUrl:e("TEGO_PLUGIN_HOST_API_URL"),hostApiToken:e("TEGO_PLUGIN_HOST_API_TOKEN"),databaseUrl:e("DATABASE_URL"),gatewayKey:e("TEGO_PLUGIN_GATEWAY_KEY")}}n(readEnv,"readEnv");function readUserContext(a,e){const s=n(h=>{const A=a[h],I=Array.isArray(A)?A[0]:A;return I&&I.length>0?I:null},"read"),c=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.userId),d=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.tenantId);if(!c||!d)return null;const p=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.signature),w=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.issuedAt);if(!p||!w)return null;const y=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.avatarId),m=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.roles),i=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.delegation),u={userId:c,tenantId:d,rolesHeader:m??"",avatarId:y??"",issuedAt:w,...i?{delegationHeader:i}:{}};if(!chunkIMAVYQ4U_cjs.verifyPluginContext({context:u,signature:p,key:e.key,nowMs:e.nowMs}))return null;let T=[];if(m)try{const h=JSON.parse(m);if(!Array.isArray(h))return null;T=h.filter(A=>typeof A=="string")}catch{return null}let E;if(i){if(!y)return null;try{const h=JSON.parse(i);if(!h||typeof h.id!="string"||!h.id||!Array.isArray(h.actions)||h.actions.some(A=>A!=="records.read"&&A!=="work.prepare"))return null;E={id:h.id,actions:h.actions}}catch{return null}}return{userId:c,tenantId:d,roles:T,...y?{avatarId:y}:{},...E?{delegation:E}:{}}}n(readUserContext,"readUserContext");function readPublicContext(a,e){const s=n(T=>{const E=a[T],h=Array.isArray(E)?E[0]:E;return h&&h.length>0?h:null},"read"),c=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.tenantId),d=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.installId),p=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.publicRoute),w=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.clientIp),y=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.avatarId),m=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.issuedAt),i=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.signature);if(!c||!d||!p||!w||!m||!i)return null;const u={tenantId:c,installId:d,route:p,clientIp:w,...y?{avatarId:y}:{}};return chunkIMAVYQ4U_cjs.verifyPluginPublicContext({context:{...u,issuedAt:m},signature:i,key:e.key,nowMs:e.nowMs})?u:null}n(readPublicContext,"readPublicContext");function createHostClient(a){const e=n(async(t,o)=>{const r=await fetch(`${a.hostApiUrl}${t}`,{method:"POST",headers:{"content-type":"application/json",authorization:`Bearer ${a.hostApiToken}`},body:JSON.stringify(o),...t.startsWith("/knowledge/collections/")||t.startsWith("/knowledge/admin/")?{signal:AbortSignal.timeout(12e4)}:t==="/mail/send"?{signal:AbortSignal.timeout(6e4)}:t.startsWith("/directory/")||t.startsWith("/knowledge/")||t.startsWith("/delegations/")||t.startsWith("/proactive/")||t==="/mail/status"||t==="/mail/unsubscribes"||t==="/events"?{signal:AbortSignal.timeout(1e4)}:{}});if(!r.ok){const l=await r.text();let g;try{if(g=JSON.parse(l)?.error?.details,typeof g=="string")try{g=JSON.parse(g)}catch{}}catch{}throw Object.assign(new Error(`Tego host API ${t} failed: ${r.status} ${l}`),{status:r.status,details:g,retryAfter:r.headers.get("retry-after")})}return r},"call"),s={debug:n((t,o)=>void c("debug",t,o),"debug"),info:n((t,o)=>void c("info",t,o),"info"),warn:n((t,o)=>void c("warn",t,o),"warn"),error:n((t,o)=>void c("error",t,o),"error")};async function c(t,o,r){try{await e("/log",{level:t,message:o,fields:r})}catch{}}n(c,"emitLog");const d=n((t,o)=>`${a.hostApiUrl}/storage/${t}?key=${encodeURIComponent(o)}`,"storageUrl"),p={copy:n(async(t,o)=>{const l=await(await e("/storage/copy",{sourceKey:t,destinationKey:o})).json();if(!l.data)throw new Error("Tego host API returned no copied object");return l.data},"copy"),getStream:n(async t=>{const o=await fetch(d("stream",t),{headers:{authorization:`Bearer ${a.hostApiToken}`}});if(o.status===404)return null;if(!o.ok)throw new Error(`Tego storage stream failed: ${o.status}`);return o.body},"getStream"),putStream:n(async(t,o,r)=>{const l={method:"POST",duplex:"half",body:o,headers:{authorization:`Bearer ${a.hostApiToken}`,"content-length":String(r.sizeBytes),"content-type":r.contentType??"application/octet-stream"}},g=await fetch(d("stream",t),l);if(!g.ok)throw new Error(`Tego storage stream write failed: ${g.status}`)},"putStream"),put:n(async(t,o,r)=>{const l=await fetch(d("put",t),{method:"POST",headers:{authorization:`Bearer ${a.hostApiToken}`,"content-type":r??"application/octet-stream"},body:toArrayBuffer(o)});if(!l.ok)throw new Error(`Tego storage put failed: ${l.status}`)},"put"),get:n(async(t,o)=>{const r=o?.firstBytes,l=d("get",t),g=await fetch(r===void 0?l:`${l}&bytes=${r}`,{headers:{authorization:`Bearer ${a.hostApiToken}`}});if(g.status===404)return null;if(!g.ok)throw new Error(`Tego storage get failed: ${g.status}`);return new Uint8Array(await g.arrayBuffer())},"get"),head:n(async t=>{const o=await fetch(`${a.hostApiUrl}/storage/head`,{method:"POST",headers:{"content-type":"application/json",authorization:`Bearer ${a.hostApiToken}`},body:JSON.stringify({key:t})});if(o.status===404)return null;if(!o.ok)throw new Error(`Tego storage head failed: ${o.status}`);const r=await o.json();if(!r.data)throw new Error("Tego host API returned no object head");return r.data},"head"),delete:n(async t=>{await e("/storage/delete",{key:t})},"delete"),signedUrl:n(async(t,o)=>{const g=(await(await e("/storage/signed-url",{key:t,ttlSeconds:o})).json()).data?.url;if(!g)throw new Error("Tego host API returned no signed url");return g},"signedUrl"),signedPath:n(async(t,o)=>{const g=(await(await e("/storage/signed-url",{key:t,ttlSeconds:o})).json()).data?.path;if(!g)throw new Error("Tego host API returned no signed path");return g},"signedPath"),managedPath:n(async(t,o,r)=>{const g=await(await e("/storage/signed-url",{key:t,policy:o,ttlSeconds:r})).json();if(!g.data?.path)throw new Error("Tego host API returned no managed path");return g.data.path},"managedPath"),uploadPath:n(async(t,o)=>{const l=await(await e("/storage/upload-url",{key:t,maxBytes:o?.maxBytes,ttlSeconds:o?.ttlSeconds})).json();if(!l.data?.path)throw new Error("Tego host API returned no upload path");return l.data},"uploadPath")},w={publish:n(async(t,o)=>{await e("/events",{name:t,payload:o})},"publish")},y={send:n(async t=>{const r=await(await e("/notifications",t)).json();if(!r.data?.id)throw new Error("Tego host API returned no notification id");return{id:r.data.id}},"send")},m={async emit(t){const r=await(await e("/proactive/emit",t)).json();if(!r.data)throw new Error("Tego proactive response is missing data");return r.data}},i=n(async(t,o)=>{const l=await(await e(t,o)).json();if(!l.data)throw new Error("Tego attention response is missing data");return l.data},"attentionStatus"),u={async request(t){const r=await(await e("/attention/requests",t)).json();if(!r.data?.itemId)throw new Error("Tego host API returned no attention item");return r.data},get:n(t=>i("/attention/requests/get",{itemId:t}),"get"),cancel:n(t=>i("/attention/requests/cancel",{itemId:t}),"cancel")},T={async send(t){const r=await(await e("/mail/send",t)).json();if(!r.data)throw new Error("Tego mail response is missing data");return r.data},async status(t){const r=await(await e("/mail/status",{userId:t})).json();if(!r.data)throw new Error("Tego mail response is missing data");return r.data},async unsubscribes(t={}){const r=await(await e("/mail/unsubscribes",t)).json();if(!r.data)throw new Error("Tego mail response is missing data");return r.data}};let E={};const A={current:n(()=>E,"current"),refresh:n(async()=>{const t=await fetch(`${a.hostApiUrl}/config`,{headers:{authorization:`Bearer ${a.hostApiToken}`}});if(!t.ok)throw new Error(`Tego host API /config failed: ${t.status} ${await t.text()}`);return E=(await t.json()).data?.values??{},E},"refresh")},I={async read(t,o){return(await(await e("/sources/read",{id:t,...o})).json()).data}},v={async list(t){const r=await(await e("/delegations/list",{userId:t})).json();if(!r.data)throw new Error("Tego delegation response is missing data");return r.data}},P={async verify(t){const r=await(await e("/knowledge/verify",t)).json();if(!r.data)throw new Error("Tego knowledge response is missing data");return r.data},collections:{ensure:n(t=>f("ensure",t),"ensure"),permissions:n(t=>f("permissions",t),"permissions"),move:n(t=>f("move",t),"move"),access:n(t=>f("access",t),"access"),import:n(t=>f("import",t),"import"),importFiles:n(t=>f("import-files",t),"importFiles"),status:n(t=>f("status",t),"status"),delete:n(t=>f("delete",t),"delete"),documentsStatus:n(t=>f("documents-status",t),"documentsStatus"),documentContent:n(t=>f("document-content",t),"documentContent"),search:n(t=>f("search",t),"search"),remove:n(t=>f("remove",t),"remove")}};async function f(t,o){const l=await(await e(`/knowledge/collections/${t}`,o)).json();if(!l.data)throw new Error("Tego knowledge collection response is missing data");return l.data}n(f,"collectionCall");const _={async identity(t){const r=await(await e("/directory/identity",{userId:t})).json();if(!r.data)throw new Error("Tego directory response is missing data");return r.data},async validate(t){const r=await(await e("/directory/validate",{principals:t})).json();if(!r.data)throw new Error("Tego directory response is missing data");return r.data},async list(t){const r=await(await e("/directory/list",t)).json();if(!r.data)throw new Error("Tego directory response is missing data");return r.data},async match(t){const r=await(await e("/directory/match",t)).json();if(!r.data)throw new Error("Tego directory response is missing data");return r.data}},k={provisionUser:n(t=>b("/accounts/provision",t),"provisionUser"),resetPassword:n(t=>b("/accounts/password/reset",t),"resetPassword"),inviteUser:n(t=>b("/accounts/invite",t),"inviteUser"),getInvitation:n(t=>b("/accounts/get",{invitationId:t}),"getInvitation"),resendInvitation:n(t=>b("/accounts/resend",{invitationId:t}),"resendInvitation")};async function b(t,o){const l=await(await e(t,o)).json();if(!l.data)throw new Error("Tego account response is missing data");return l.data}return n(b,"collectionEnvelope"),{accounts:k,log:s,storage:p,diagrams:{status:n(async()=>{const o=await(await e("/diagrams/status",{})).json();if(!o.data)throw new Error("Tego host API returned no renderer status");return o.data},"status"),render:n(async t=>{const r=await(await e("/diagrams/render",t)).json();if(!r.data?.key)throw new Error("Tego host API returned no diagram artifact");return r.data},"render")},events:w,notifications:y,proactive:m,attention:u,mail:T,config:A,sources:I,directory:_,knowledge:P,knowledgeAdmin:{listManageableBases:n(t=>b("/knowledge/admin/list",t),"listManageableBases"),previewCreateBase:n(t=>b("/knowledge/admin/create-preview",t),"previewCreateBase"),createApprovedBase:n(t=>b("/knowledge/admin/create",t),"createApprovedBase"),prepareDocumentContent:n(t=>b("/knowledge/admin/prepare",t),"prepareDocumentContent"),previewDocumentBatch:n(t=>b("/knowledge/admin/preview",t),"previewDocumentBatch"),stageApprovedDocumentBatch:n(t=>b("/knowledge/admin/import",t),"stageApprovedDocumentBatch"),getTask:n(t=>b("/knowledge/admin/task",t),"getTask")},delegations:v}}n(createHostClient,"createHostClient");var ACQUIRE_TIMEOUT_MS=5e3,IDLE_IN_TRANSACTION_TIMEOUT_MS=15e3;function createScopedDb(a){let e=null;async function s(){if(e)return e;const c=await import("pg"),d=c.default?.Pool??c.Pool;if(!d)throw new Error('The "pg" module resolved without a Pool constructor. The plugin runtime expects the driver the runner image provides.');return e=new d({connectionString:a,connectionTimeoutMillis:ACQUIRE_TIMEOUT_MS}),e}return n(s,"ensurePool"),{query:n(async(c,d)=>(await(await s()).query(c,d)).rows,"query"),transaction:n(async c=>{const p=await(await s()).connect();let w=!0,y=!1;const m={query:n(async(i,u)=>{if(y)throw new Error("This transaction has already ended; its `tx` cannot be used afterwards.");return(await p.query(i,u)).rows},"query")};try{await p.query("BEGIN"),await p.query(`SET LOCAL idle_in_transaction_session_timeout = ${IDLE_IN_TRANSACTION_TIMEOUT_MS}`);const i=await c(m);return await p.query("COMMIT"),i}catch(i){try{await p.query("ROLLBACK")}catch{w=!1}throw i}finally{y=!0,p.release(w?void 0:!0)}},"transaction")}}n(createScopedDb,"createScopedDb");function toWebRequest(a,e){const s=`http://localhost${a.url??"/"}`,c=new Headers;for(const[p,w]of Object.entries(a.headers))w!==void 0&&c.set(p,Array.isArray(w)?w.join(", "):w);const d=a.method??"GET";return new Request(s,{method:d,headers:c,...d==="GET"||d==="HEAD"?{}:{body:e}})}n(toWebRequest,"toWebRequest");async function writeWebResponse(a,e){const s={};e.headers.forEach((c,d)=>{s[d]=c}),a.writeHead(e.status,s),a.end(Buffer.from(await e.arrayBuffer()))}n(writeWebResponse,"writeWebResponse");function readBody(a,e){return new Promise((s,c)=>{const d=[];let p=0;a.on("data",w=>{if(p+=w.length,e!==void 0&&p>e){c(new Error("Policy request body too large")),a.destroy();return}d.push(w)}),a.on("end",()=>{s(toArrayBuffer(Buffer.concat(d)))}),a.on("error",c)})}n(readBody,"readBody");function serveBackend(a){const e=readEnv(process.env),s={pluginId:e.pluginId,tenantId:e.tenantId,db:createScopedDb(e.databaseUrl),...createHostClient(e)},c=http.createServer((i,u)=>{(async()=>{try{const T=(i.url??"/").split("?")[0];if(T===e.healthPath){u.writeHead(200,{"content-type":"application/json"}),u.end('{"status":"ok"}');return}if(i.headers["x-tego-plugin-storage-policy"]==="1"){const I=await readBody(i,16384),v=n(f=>typeof i.headers[f]=="string"?i.headers[f]:"","header"),P={installId:v("x-tego-plugin-install-id"),tenantId:v("x-tego-plugin-tenant-id"),issuedAt:v("x-tego-plugin-issued-at"),bodyHash:v("x-tego-plugin-body-hash")};if(T!==chunkIMAVYQ4U_cjs.PLUGIN_STORAGE_POLICY_PATH||i.method!=="POST"||!P.installId||!P.tenantId||!chunkIMAVYQ4U_cjs.verifyPluginStoragePolicy({context:P,signature:v("x-tego-plugin-signature"),key:e.gatewayKey,nowMs:Date.now(),body:Buffer.from(I).toString("utf8")})){u.writeHead(401),u.end();return}if(!a.authorizeStorage){u.writeHead(404),u.end();return}await writeWebResponse(u,await a.authorizeStorage(toWebRequest(i,I),P,s));return}if(i.headers[chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.public]==="1"){const I=readPublicContext(i.headers,{key:e.gatewayKey,nowMs:Date.now()});if(!I){u.writeHead(401,{"content-type":"application/json"}),u.end('{"error":"missing or unverifiable tego plugin context"}');return}if(!a.fetchPublic){u.writeHead(404,{"content-type":"application/json"}),u.end('{"error":"not found"}');return}const v=await readBody(i);await writeWebResponse(u,await a.fetchPublic(toWebRequest(i,v),I,s));return}const E=readUserContext(i.headers,{key:e.gatewayKey,nowMs:Date.now()});if(!E){u.writeHead(401,{"content-type":"application/json"}),u.end('{"error":"missing or unverifiable tego plugin context"}');return}const h=await readBody(i),A=await a.fetch(toWebRequest(i,h),E,s);await writeWebResponse(u,A)}catch(T){s.log.error("Unhandled error in plugin backend",{error:T instanceof Error?T.message:String(T)}),u.headersSent?u.end():(u.writeHead(500,{"content-type":"application/json"}),u.end('{"error":"plugin backend failed"}'))}})()});let d=!1,p=!1,w;const y=n(()=>(w??=Promise.resolve().then(()=>a.stop?.()),w),"stopModule"),m=(async()=>{const i=await startUp(s,e);if(i)throw new Error(i);d||(p=!0,await a.start?.(s),!d&&c.listen(e.port,()=>{s.log.info(`Plugin backend listening on ${e.port}`)}))})();m.catch(async i=>{console.error("Plugin backend startup failed",i);try{p&&await y()}catch(u){console.error("Plugin backend shutdown failed",u)}process.exit(1)}),process.on("SIGTERM",()=>{d||(d=!0,Promise.all([new Promise(i=>c.close(()=>i())),m.then(async()=>{p&&await y()})]).then(()=>process.exit(0)).catch(i=>{console.error("Plugin backend shutdown failed",i),process.exit(1)}))})}n(serveBackend,"serveBackend");async function startUp(a,e){const s=await checkDatabaseReachable(a.db,e.databaseUrl);if(s)return s;try{await a.config.refresh()}catch(c){return`Plugin backend cannot read its configuration from the host API: ${c instanceof Error?c.message:String(c)}`}return null}n(startUp,"startUp");async function checkDatabaseReachable(a,e){try{return await a.query("select 1"),null}catch(s){return`Plugin backend cannot reach its database at ${describeDatabaseTarget(e)}: ${s instanceof Error?s.message:String(s)}`}}n(checkDatabaseReachable,"checkDatabaseReachable");function describeDatabaseTarget(a){try{const e=new URL(a);return`${e.host}${e.pathname}`}catch{return"the address in DATABASE_URL"}}n(describeDatabaseTarget,"describeDatabaseTarget"),exports.checkDatabaseReachable=checkDatabaseReachable,exports.createScopedDb=createScopedDb,exports.readPublicContext=readPublicContext,exports.readUserContext=readUserContext,exports.serveBackend=serveBackend;
1
+ "use strict";var S=Object.defineProperty;var n=(a,e)=>S(a,"name",{value:e,configurable:!0});var chunkIMAVYQ4U_cjs=require("./chunk-IMAVYQ4U.cjs"),chunkBN6TGHSB_cjs=require("./chunk-BN6TGHSB.cjs"),http=require("http");function toArrayBuffer(a){const e=new ArrayBuffer(a.byteLength);return new Uint8Array(e).set(a),e}n(toArrayBuffer,"toArrayBuffer");function readEnv(a){const e=n(s=>{const c=a[s];if(!c)throw new Error(`${s} is required but was not set`);return c},"require_");return{pluginId:e("TEGO_PLUGIN_ID"),tenantId:e("TEGO_TENANT_ID"),port:Number(e("TEGO_PLUGIN_PORT")),healthPath:a.TEGO_PLUGIN_HEALTH_PATH??"/healthz",hostApiUrl:e("TEGO_PLUGIN_HOST_API_URL"),hostApiToken:e("TEGO_PLUGIN_HOST_API_TOKEN"),databaseUrl:e("DATABASE_URL"),gatewayKey:e("TEGO_PLUGIN_GATEWAY_KEY")}}n(readEnv,"readEnv");function readUserContext(a,e){const s=n(f=>{const A=a[f],v=Array.isArray(A)?A[0]:A;return v&&v.length>0?v:null},"read"),c=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.userId),d=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.tenantId);if(!c||!d)return null;const l=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.signature),w=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.issuedAt);if(!l||!w)return null;const h=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.avatarId),y=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.roles),i=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.delegation),u={userId:c,tenantId:d,rolesHeader:y??"",avatarId:h??"",issuedAt:w,...i?{delegationHeader:i}:{}};if(!chunkIMAVYQ4U_cjs.verifyPluginContext({context:u,signature:l,key:e.key,nowMs:e.nowMs}))return null;let E=[];if(y)try{const f=JSON.parse(y);if(!Array.isArray(f))return null;E=f.filter(A=>typeof A=="string")}catch{return null}let m;if(i){if(!h)return null;try{const f=JSON.parse(i);if(!f||typeof f.id!="string"||!f.id||!Array.isArray(f.actions)||f.actions.some(A=>A!=="records.read"&&A!=="work.prepare"))return null;m={id:f.id,actions:f.actions}}catch{return null}}return{userId:c,tenantId:d,roles:E,...h?{avatarId:h}:{},...m?{delegation:m}:{}}}n(readUserContext,"readUserContext");function readPublicContext(a,e){const s=n(E=>{const m=a[E],f=Array.isArray(m)?m[0]:m;return f&&f.length>0?f:null},"read"),c=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.tenantId),d=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.installId),l=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.publicRoute),w=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.clientIp),h=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.avatarId),y=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.issuedAt),i=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.signature);if(!c||!d||!l||!w||!y||!i)return null;const u={tenantId:c,installId:d,route:l,clientIp:w,...h?{avatarId:h}:{}};return chunkIMAVYQ4U_cjs.verifyPluginPublicContext({context:{...u,issuedAt:y},signature:i,key:e.key,nowMs:e.nowMs})?u:null}n(readPublicContext,"readPublicContext");function createHostClient(a){const e=n(async(t,o)=>{const r=await fetch(`${a.hostApiUrl}${t}`,{method:"POST",headers:{"content-type":"application/json",authorization:`Bearer ${a.hostApiToken}`},body:JSON.stringify(o),...t.startsWith("/knowledge/collections/")||t.startsWith("/knowledge/admin/")?{signal:AbortSignal.timeout(12e4)}:t==="/mail/send"?{signal:AbortSignal.timeout(6e4)}:t.startsWith("/directory/")||t.startsWith("/knowledge/")||t.startsWith("/delegations/")||t.startsWith("/proactive/")||t==="/mail/status"||t==="/mail/unsubscribes"||t==="/events"?{signal:AbortSignal.timeout(1e4)}:{}});if(!r.ok){const p=await r.text();let g;try{if(g=JSON.parse(p)?.error?.details,typeof g=="string")try{g=JSON.parse(g)}catch{}}catch{}throw Object.assign(new Error(`Tego host API ${t} failed: ${r.status} ${p}`),{status:r.status,details:g,retryAfter:r.headers.get("retry-after")})}return r},"call"),s={debug:n((t,o)=>void c("debug",t,o),"debug"),info:n((t,o)=>void c("info",t,o),"info"),warn:n((t,o)=>void c("warn",t,o),"warn"),error:n((t,o)=>void c("error",t,o),"error")};async function c(t,o,r){try{await e("/log",{level:t,message:o,fields:r})}catch{}}n(c,"emitLog");const d=n((t,o)=>`${a.hostApiUrl}/storage/${t}?key=${encodeURIComponent(o)}`,"storageUrl"),l={copy:n(async(t,o)=>{const p=await(await e("/storage/copy",{sourceKey:t,destinationKey:o})).json();if(!p.data)throw new Error("Tego host API returned no copied object");return p.data},"copy"),getStream:n(async t=>{const o=await fetch(d("stream",t),{headers:{authorization:`Bearer ${a.hostApiToken}`}});if(o.status===404)return null;if(!o.ok)throw new Error(`Tego storage stream failed: ${o.status}`);return o.body},"getStream"),putStream:n(async(t,o,r)=>{const p={method:"POST",duplex:"half",body:o,headers:{authorization:`Bearer ${a.hostApiToken}`,"content-length":String(r.sizeBytes),"content-type":r.contentType??"application/octet-stream"}},g=await fetch(d("stream",t),p);if(!g.ok)throw new Error(`Tego storage stream write failed: ${g.status}`)},"putStream"),put:n(async(t,o,r)=>{const p=await fetch(d("put",t),{method:"POST",headers:{authorization:`Bearer ${a.hostApiToken}`,"content-type":r??"application/octet-stream"},body:toArrayBuffer(o)});if(!p.ok)throw new Error(`Tego storage put failed: ${p.status}`)},"put"),get:n(async(t,o)=>{const r=o?.firstBytes,p=d("get",t),g=await fetch(r===void 0?p:`${p}&bytes=${r}`,{headers:{authorization:`Bearer ${a.hostApiToken}`}});if(g.status===404)return null;if(!g.ok)throw new Error(`Tego storage get failed: ${g.status}`);return new Uint8Array(await g.arrayBuffer())},"get"),head:n(async t=>{const o=await fetch(`${a.hostApiUrl}/storage/head`,{method:"POST",headers:{"content-type":"application/json",authorization:`Bearer ${a.hostApiToken}`},body:JSON.stringify({key:t})});if(o.status===404)return null;if(!o.ok)throw new Error(`Tego storage head failed: ${o.status}`);const r=await o.json();if(!r.data)throw new Error("Tego host API returned no object head");return r.data},"head"),delete:n(async t=>{await e("/storage/delete",{key:t})},"delete"),signedUrl:n(async(t,o)=>{const g=(await(await e("/storage/signed-url",{key:t,ttlSeconds:o})).json()).data?.url;if(!g)throw new Error("Tego host API returned no signed url");return g},"signedUrl"),signedPath:n(async(t,o)=>{const g=(await(await e("/storage/signed-url",{key:t,ttlSeconds:o})).json()).data?.path;if(!g)throw new Error("Tego host API returned no signed path");return g},"signedPath"),managedPath:n(async(t,o,r)=>{const g=await(await e("/storage/signed-url",{key:t,policy:o,ttlSeconds:r})).json();if(!g.data?.path)throw new Error("Tego host API returned no managed path");return g.data.path},"managedPath"),uploadPath:n(async(t,o)=>{const p=await(await e("/storage/upload-url",{key:t,maxBytes:o?.maxBytes,ttlSeconds:o?.ttlSeconds})).json();if(!p.data?.path)throw new Error("Tego host API returned no upload path");return p.data},"uploadPath")},w={publish:n(async(t,o)=>{await e("/events",{name:t,payload:o})},"publish")},h={send:n(async t=>{const r=await(await e("/notifications",t)).json();if(!r.data?.id)throw new Error("Tego host API returned no notification id");return{id:r.data.id}},"send")},y={async emit(t){const r=await(await e("/proactive/emit",t)).json();if(!r.data)throw new Error("Tego proactive response is missing data");return r.data}},i=n(async(t,o)=>{const p=await(await e(t,o)).json();if(!p.data)throw new Error("Tego attention response is missing data");return p.data},"attentionStatus"),u={async request(t){const r=await(await e("/attention/requests",t)).json();if(!r.data?.itemId)throw new Error("Tego host API returned no attention item");return r.data},get:n(t=>i("/attention/requests/get",{itemId:t}),"get"),cancel:n(t=>i("/attention/requests/cancel",{itemId:t}),"cancel")},E={async send(t){const r=await(await e("/mail/send",t)).json();if(!r.data)throw new Error("Tego mail response is missing data");return r.data},async status(t){const r=await(await e("/mail/status",{userId:t})).json();if(!r.data)throw new Error("Tego mail response is missing data");return r.data},async unsubscribes(t={}){const r=await(await e("/mail/unsubscribes",t)).json();if(!r.data)throw new Error("Tego mail response is missing data");return r.data}};let m={};const A={current:n(()=>m,"current"),refresh:n(async()=>{const t=await fetch(`${a.hostApiUrl}/config`,{headers:{authorization:`Bearer ${a.hostApiToken}`}});if(!t.ok)throw new Error(`Tego host API /config failed: ${t.status} ${await t.text()}`);return m=(await t.json()).data?.values??{},m},"refresh")},v={async read(t,o){return(await(await e("/sources/read",{id:t,...o})).json()).data}},I={async list(t){const r=await(await e("/delegations/list",{userId:t})).json();if(!r.data)throw new Error("Tego delegation response is missing data");return r.data}},P={async verify(t){const r=await(await e("/knowledge/verify",t)).json();if(!r.data)throw new Error("Tego knowledge response is missing data");return r.data},collections:{ensure:n(t=>T("ensure",t),"ensure"),permissions:n(t=>T("permissions",t),"permissions"),move:n(t=>T("move",t),"move"),access:n(t=>T("access",t),"access"),import:n(t=>T("import",t),"import"),importFiles:n(t=>T("import-files",t),"importFiles"),status:n(t=>T("status",t),"status"),delete:n(t=>T("delete",t),"delete"),documentsStatus:n(t=>T("documents-status",t),"documentsStatus"),documentContent:n(t=>T("document-content",t),"documentContent"),search:n(t=>T("search",t),"search"),remove:n(t=>T("remove",t),"remove")}};async function T(t,o){const p=await(await e(`/knowledge/collections/${t}`,o)).json();if(!p.data)throw new Error("Tego knowledge collection response is missing data");return p.data}n(T,"collectionCall");const _={async identity(t){const r=await(await e("/directory/identity",{userId:t})).json();if(!r.data)throw new Error("Tego directory response is missing data");return r.data},async validate(t){const r=await(await e("/directory/validate",{principals:t})).json();if(!r.data)throw new Error("Tego directory response is missing data");return r.data},async list(t){const r=await(await e("/directory/list",t)).json();if(!r.data)throw new Error("Tego directory response is missing data");return r.data},async match(t){const r=await(await e("/directory/match",t)).json();if(!r.data)throw new Error("Tego directory response is missing data");return r.data}},k={provisionUser:n(t=>b("/accounts/provision",t),"provisionUser"),resetPassword:n(t=>b("/accounts/password/reset",t),"resetPassword"),inviteUser:n(t=>b("/accounts/invite",t),"inviteUser"),getInvitation:n(t=>b("/accounts/get",{invitationId:t}),"getInvitation"),resendInvitation:n(t=>b("/accounts/resend",{invitationId:t}),"resendInvitation")};async function b(t,o){const p=await(await e(t,o)).json();if(!p.data)throw new Error("Tego account response is missing data");return p.data}return n(b,"collectionEnvelope"),{accounts:k,log:s,storage:l,diagrams:{status:n(async()=>{const o=await(await e("/diagrams/status",{})).json();if(!o.data)throw new Error("Tego host API returned no renderer status");return o.data},"status"),render:n(async t=>{const r=await(await e("/diagrams/render",t)).json();if(!r.data?.key)throw new Error("Tego host API returned no diagram artifact");return r.data},"render")},events:w,notifications:h,proactive:y,attention:u,mail:E,config:A,sources:v,directory:_,knowledge:P,knowledgeAdmin:{listManageableBases:n(t=>b("/knowledge/admin/list",t),"listManageableBases"),previewCreateBase:n(t=>b("/knowledge/admin/create-preview",t),"previewCreateBase"),createApprovedBase:n(t=>b("/knowledge/admin/create",t),"createApprovedBase"),prepareDocumentContent:n(t=>b("/knowledge/admin/prepare",t),"prepareDocumentContent"),previewDocumentBatch:n(t=>b("/knowledge/admin/preview",t),"previewDocumentBatch"),stageApprovedDocumentBatch:n(t=>b("/knowledge/admin/import",t),"stageApprovedDocumentBatch"),getTask:n(t=>b("/knowledge/admin/task",t),"getTask")},delegations:I}}n(createHostClient,"createHostClient");var ACQUIRE_TIMEOUT_MS=5e3,IDLE_IN_TRANSACTION_TIMEOUT_MS=15e3;function createScopedDb(a){let e=null;async function s(){if(e)return e;const c=await import("pg"),d=c.default?.Pool??c.Pool;if(!d)throw new Error('The "pg" module resolved without a Pool constructor. The plugin runtime expects the driver the runner image provides.');return e=new d({connectionString:a,connectionTimeoutMillis:ACQUIRE_TIMEOUT_MS}),e}return n(s,"ensurePool"),{query:n(async(c,d)=>(await(await s()).query(c,d)).rows,"query"),transaction:n(async c=>{const l=await(await s()).connect();let w=!0,h=!1;const y={query:n(async(i,u)=>{if(h)throw new Error("This transaction has already ended; its `tx` cannot be used afterwards.");return(await l.query(i,u)).rows},"query")};try{await l.query("BEGIN"),await l.query(`SET LOCAL idle_in_transaction_session_timeout = ${IDLE_IN_TRANSACTION_TIMEOUT_MS}`);const i=await c(y);return await l.query("COMMIT"),i}catch(i){try{await l.query("ROLLBACK")}catch{w=!1}throw i}finally{h=!0,l.release(w?void 0:!0)}},"transaction")}}n(createScopedDb,"createScopedDb");function toWebRequest(a,e){const s=`http://localhost${a.url??"/"}`,c=new Headers;for(const[l,w]of Object.entries(a.headers))w!==void 0&&c.set(l,Array.isArray(w)?w.join(", "):w);const d=a.method??"GET";return new Request(s,{method:d,headers:c,...d==="GET"||d==="HEAD"?{}:{body:e}})}n(toWebRequest,"toWebRequest");async function writeWebResponse(a,e){const s={};e.headers.forEach((c,d)=>{s[d]=c}),a.writeHead(e.status,s),a.end(Buffer.from(await e.arrayBuffer()))}n(writeWebResponse,"writeWebResponse");function readBody(a,e){return new Promise((s,c)=>{const d=[];let l=0;a.on("data",w=>{if(l+=w.length,e!==void 0&&l>e){c(new Error("Policy request body too large")),a.destroy();return}d.push(w)}),a.on("end",()=>{s(toArrayBuffer(Buffer.concat(d)))}),a.on("error",c)})}n(readBody,"readBody");function serveBackend(a){const e=readEnv(process.env),s={pluginId:e.pluginId,tenantId:e.tenantId,db:createScopedDb(e.databaseUrl),...createHostClient(e)},c=http.createServer((i,u)=>{(async()=>{try{const E=(i.url??"/").split("?")[0];if(E===e.healthPath){u.writeHead(200,{"content-type":"application/json"}),u.end('{"status":"ok"}');return}if(i.headers["x-tego-plugin-storage-policy"]==="1"){const v=await readBody(i,16384),I=n(T=>typeof i.headers[T]=="string"?i.headers[T]:"","header"),P={installId:I("x-tego-plugin-install-id"),tenantId:I("x-tego-plugin-tenant-id"),issuedAt:I("x-tego-plugin-issued-at"),bodyHash:I("x-tego-plugin-body-hash")};if(E!==chunkIMAVYQ4U_cjs.PLUGIN_STORAGE_POLICY_PATH||i.method!=="POST"||!P.installId||!P.tenantId||!chunkIMAVYQ4U_cjs.verifyPluginStoragePolicy({context:P,signature:I("x-tego-plugin-signature"),key:e.gatewayKey,nowMs:Date.now(),body:Buffer.from(v).toString("utf8")})){u.writeHead(401),u.end();return}if(!a.authorizeStorage){u.writeHead(404),u.end();return}await writeWebResponse(u,await a.authorizeStorage(toWebRequest(i,v),P,s));return}if(i.headers[chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.public]==="1"){const v=readPublicContext(i.headers,{key:e.gatewayKey,nowMs:Date.now()});if(!v){u.writeHead(401,{"content-type":"application/json"}),u.end('{"error":"missing or unverifiable tego plugin context"}');return}if(!a.fetchPublic){u.writeHead(404,{"content-type":"application/json"}),u.end('{"error":"not found"}');return}const I=await readBody(i);await writeWebResponse(u,await a.fetchPublic(toWebRequest(i,I),v,s));return}const m=readUserContext(i.headers,{key:e.gatewayKey,nowMs:Date.now()});if(!m){u.writeHead(401,{"content-type":"application/json"}),u.end('{"error":"missing or unverifiable tego plugin context"}');return}const f=await readBody(i),A=await a.fetch(toWebRequest(i,f),m,s);await writeWebResponse(u,A)}catch(E){s.log.error("Unhandled error in plugin backend",{error:E instanceof Error?E.message:String(E)}),u.headersSent?u.end():(u.writeHead(500,{"content-type":"application/json"}),u.end('{"error":"plugin backend failed"}'))}})()});let d=!1,l=!1,w;const h=n(()=>(w??=Promise.resolve().then(()=>a.stop?.()),w),"stopModule"),y=(async()=>{const i=await startUp(s,e);if(i)throw new Error(i);d||(l=!0,await a.start?.(s),!d&&c.listen(e.port,()=>{s.log.info(`Plugin backend listening on ${e.port}`)}))})();y.catch(async i=>{console.error("Plugin backend startup failed",i);try{l&&await h()}catch(u){console.error("Plugin backend shutdown failed",u)}process.exit(1)}),process.on("SIGTERM",()=>{d||(d=!0,Promise.all([new Promise(i=>c.close(()=>i())),y.then(async()=>{l&&await h()})]).then(()=>process.exit(0)).catch(i=>{console.error("Plugin backend shutdown failed",i),process.exit(1)}))})}n(serveBackend,"serveBackend");async function startUp(a,e){const s=await checkDatabaseReachable(a.db,e.databaseUrl);if(s)return s;try{await a.config.refresh()}catch(c){return`Plugin backend cannot read its configuration from the host API: ${c instanceof Error?c.message:String(c)}`}return null}n(startUp,"startUp");async function checkDatabaseReachable(a,e){try{return await a.query("select 1"),null}catch(s){return`Plugin backend cannot reach its database at ${describeDatabaseTarget(e)}: ${s instanceof Error?s.message:String(s)}`}}n(checkDatabaseReachable,"checkDatabaseReachable");function describeDatabaseTarget(a){try{const e=new URL(a);return`${e.host}${e.pathname}`}catch{return"the address in DATABASE_URL"}}n(describeDatabaseTarget,"describeDatabaseTarget");function createMaintenance(a,e={}){const s=e.intervalMs??3e4,c=[];let d,l,w=!0;const h=n(i=>{w||(d&&clearTimeout(d),d=setTimeout(()=>void y(),i))},"schedule"),y=n(async()=>{if(d=void 0,l)return h(s);l=(async()=>{for(const i of c){if(w)break;try{await i.run()}catch(u){a.log.error(`maintenance step ${i.name} failed`,{error:u instanceof Error?u.message:String(u)})}}})(),await l,l=void 0,h(s)},"tick");return{step(i,u){c.push({name:i,run:u})},start(){w&&(w=!1,h(0))},wake(){h(0)},async stop(){w=!0,d&&clearTimeout(d),d=void 0,await l}}}n(createMaintenance,"createMaintenance"),exports.checkDatabaseReachable=checkDatabaseReachable,exports.createMaintenance=createMaintenance,exports.createScopedDb=createScopedDb,exports.readPublicContext=readPublicContext,exports.readUserContext=readUserContext,exports.serveBackend=serveBackend;
@@ -1,5 +1,5 @@
1
1
  import { PluginUserContext } from './frontend.cjs';
2
- import { PluginScopedDb, PluginPublicContext, PluginBackendModule } from './server.cjs';
2
+ import { PluginScopedDb, PluginHostApi, PluginPublicContext, PluginBackendModule } from './server.cjs';
3
3
  import 'zod';
4
4
 
5
5
  /**
@@ -59,5 +59,30 @@ declare function serveBackend(module: PluginBackendModule): void;
59
59
  * stays at the one call site that owns the process.
60
60
  */
61
61
  declare function checkDatabaseReachable(db: Pick<PluginScopedDb, 'query'>, databaseUrl: string): Promise<string | null>;
62
+ /**
63
+ * A background loop for `start()`: named steps run every `intervalMs`, each
64
+ * isolated so one failure is logged and the others still run, never two runs
65
+ * of the loop at once, and `stop()` waits for the run in flight.
66
+ *
67
+ * Steps are trusted system code: they never act as a user. Work a person asked
68
+ * for arrives through a request (or claw's `fetch` with a delegation).
69
+ *
70
+ * ```ts
71
+ * const maintenance = createMaintenance(host, { intervalMs: 30_000 });
72
+ * maintenance.step('expire-drafts', () => expireDrafts(host.db));
73
+ * export const backend = definePluginBackend({
74
+ * fetch, start: () => maintenance.start(), stop: () => maintenance.stop(),
75
+ * });
76
+ * ```
77
+ */
78
+ declare function createMaintenance(host: Pick<PluginHostApi, 'log'>, options?: {
79
+ intervalMs?: number;
80
+ }): {
81
+ step(name: string, run: () => Promise<void> | void): void;
82
+ start(): void;
83
+ /** Runs the steps soon (after a write that created work), without waiting for the interval. */
84
+ wake(): void;
85
+ stop(): Promise<void>;
86
+ };
62
87
 
63
- export { checkDatabaseReachable, createScopedDb, readPublicContext, readUserContext, serveBackend };
88
+ export { checkDatabaseReachable, createMaintenance, createScopedDb, readPublicContext, readUserContext, serveBackend };
@@ -1,5 +1,5 @@
1
1
  import { PluginUserContext } from './frontend.js';
2
- import { PluginScopedDb, PluginPublicContext, PluginBackendModule } from './server.js';
2
+ import { PluginScopedDb, PluginHostApi, PluginPublicContext, PluginBackendModule } from './server.js';
3
3
  import 'zod';
4
4
 
5
5
  /**
@@ -59,5 +59,30 @@ declare function serveBackend(module: PluginBackendModule): void;
59
59
  * stays at the one call site that owns the process.
60
60
  */
61
61
  declare function checkDatabaseReachable(db: Pick<PluginScopedDb, 'query'>, databaseUrl: string): Promise<string | null>;
62
+ /**
63
+ * A background loop for `start()`: named steps run every `intervalMs`, each
64
+ * isolated so one failure is logged and the others still run, never two runs
65
+ * of the loop at once, and `stop()` waits for the run in flight.
66
+ *
67
+ * Steps are trusted system code: they never act as a user. Work a person asked
68
+ * for arrives through a request (or claw's `fetch` with a delegation).
69
+ *
70
+ * ```ts
71
+ * const maintenance = createMaintenance(host, { intervalMs: 30_000 });
72
+ * maintenance.step('expire-drafts', () => expireDrafts(host.db));
73
+ * export const backend = definePluginBackend({
74
+ * fetch, start: () => maintenance.start(), stop: () => maintenance.stop(),
75
+ * });
76
+ * ```
77
+ */
78
+ declare function createMaintenance(host: Pick<PluginHostApi, 'log'>, options?: {
79
+ intervalMs?: number;
80
+ }): {
81
+ step(name: string, run: () => Promise<void> | void): void;
82
+ start(): void;
83
+ /** Runs the steps soon (after a write that created work), without waiting for the interval. */
84
+ wake(): void;
85
+ stop(): Promise<void>;
86
+ };
62
87
 
63
- export { checkDatabaseReachable, createScopedDb, readPublicContext, readUserContext, serveBackend };
88
+ export { checkDatabaseReachable, createMaintenance, createScopedDb, readPublicContext, readUserContext, serveBackend };
@@ -1 +1 @@
1
- var H=Object.defineProperty;var n=(a,e)=>H(a,"name",{value:e,configurable:!0});import{verifyPluginContext as L,verifyPluginPublicContext as C,PLUGIN_STORAGE_POLICY_PATH as R,verifyPluginStoragePolicy as M}from"./chunk-WLTASJF6.js";import{PLUGIN_CONTEXT_HEADERS as m}from"./chunk-QKSPPMVN.js";import{createServer as D}from"http";function $(a){const e=new ArrayBuffer(a.byteLength);return new Uint8Array(e).set(a),e}n($,"toArrayBuffer");function z(a){const e=n(s=>{const c=a[s];if(!c)throw new Error(`${s} is required but was not set`);return c},"require_");return{pluginId:e("TEGO_PLUGIN_ID"),tenantId:e("TEGO_TENANT_ID"),port:Number(e("TEGO_PLUGIN_PORT")),healthPath:a.TEGO_PLUGIN_HEALTH_PATH??"/healthz",hostApiUrl:e("TEGO_PLUGIN_HOST_API_URL"),hostApiToken:e("TEGO_PLUGIN_HOST_API_TOKEN"),databaseUrl:e("DATABASE_URL"),gatewayKey:e("TEGO_PLUGIN_GATEWAY_KEY")}}n(z,"readEnv");function O(a,e){const s=n(h=>{const v=a[h],I=Array.isArray(v)?v[0]:v;return I&&I.length>0?I:null},"read"),c=s(m.userId),d=s(m.tenantId);if(!c||!d)return null;const p=s(m.signature),w=s(m.issuedAt);if(!p||!w)return null;const f=s(m.avatarId),A=s(m.roles),i=s(m.delegation),u={userId:c,tenantId:d,rolesHeader:A??"",avatarId:f??"",issuedAt:w,...i?{delegationHeader:i}:{}};if(!L({context:u,signature:p,key:e.key,nowMs:e.nowMs}))return null;let T=[];if(A)try{const h=JSON.parse(A);if(!Array.isArray(h))return null;T=h.filter(v=>typeof v=="string")}catch{return null}let b;if(i){if(!f)return null;try{const h=JSON.parse(i);if(!h||typeof h.id!="string"||!h.id||!Array.isArray(h.actions)||h.actions.some(v=>v!=="records.read"&&v!=="work.prepare"))return null;b={id:h.id,actions:h.actions}}catch{return null}}return{userId:c,tenantId:d,roles:T,...f?{avatarId:f}:{},...b?{delegation:b}:{}}}n(O,"readUserContext");function U(a,e){const s=n(T=>{const b=a[T],h=Array.isArray(b)?b[0]:b;return h&&h.length>0?h:null},"read"),c=s(m.tenantId),d=s(m.installId),p=s(m.publicRoute),w=s(m.clientIp),f=s(m.avatarId),A=s(m.issuedAt),i=s(m.signature);if(!c||!d||!p||!w||!A||!i)return null;const u={tenantId:c,installId:d,route:p,clientIp:w,...f?{avatarId:f}:{}};return C({context:{...u,issuedAt:A},signature:i,key:e.key,nowMs:e.nowMs})?u:null}n(U,"readPublicContext");function W(a){const e=n(async(t,o)=>{const r=await fetch(`${a.hostApiUrl}${t}`,{method:"POST",headers:{"content-type":"application/json",authorization:`Bearer ${a.hostApiToken}`},body:JSON.stringify(o),...t.startsWith("/knowledge/collections/")||t.startsWith("/knowledge/admin/")?{signal:AbortSignal.timeout(12e4)}:t==="/mail/send"?{signal:AbortSignal.timeout(6e4)}:t.startsWith("/directory/")||t.startsWith("/knowledge/")||t.startsWith("/delegations/")||t.startsWith("/proactive/")||t==="/mail/status"||t==="/mail/unsubscribes"||t==="/events"?{signal:AbortSignal.timeout(1e4)}:{}});if(!r.ok){const l=await r.text();let g;try{if(g=JSON.parse(l)?.error?.details,typeof g=="string")try{g=JSON.parse(g)}catch{}}catch{}throw Object.assign(new Error(`Tego host API ${t} failed: ${r.status} ${l}`),{status:r.status,details:g,retryAfter:r.headers.get("retry-after")})}return r},"call"),s={debug:n((t,o)=>void c("debug",t,o),"debug"),info:n((t,o)=>void c("info",t,o),"info"),warn:n((t,o)=>void c("warn",t,o),"warn"),error:n((t,o)=>void c("error",t,o),"error")};async function c(t,o,r){try{await e("/log",{level:t,message:o,fields:r})}catch{}}n(c,"emitLog");const d=n((t,o)=>`${a.hostApiUrl}/storage/${t}?key=${encodeURIComponent(o)}`,"storageUrl"),p={copy:n(async(t,o)=>{const l=await(await e("/storage/copy",{sourceKey:t,destinationKey:o})).json();if(!l.data)throw new Error("Tego host API returned no copied object");return l.data},"copy"),getStream:n(async t=>{const o=await fetch(d("stream",t),{headers:{authorization:`Bearer ${a.hostApiToken}`}});if(o.status===404)return null;if(!o.ok)throw new Error(`Tego storage stream failed: ${o.status}`);return o.body},"getStream"),putStream:n(async(t,o,r)=>{const l={method:"POST",duplex:"half",body:o,headers:{authorization:`Bearer ${a.hostApiToken}`,"content-length":String(r.sizeBytes),"content-type":r.contentType??"application/octet-stream"}},g=await fetch(d("stream",t),l);if(!g.ok)throw new Error(`Tego storage stream write failed: ${g.status}`)},"putStream"),put:n(async(t,o,r)=>{const l=await fetch(d("put",t),{method:"POST",headers:{authorization:`Bearer ${a.hostApiToken}`,"content-type":r??"application/octet-stream"},body:$(o)});if(!l.ok)throw new Error(`Tego storage put failed: ${l.status}`)},"put"),get:n(async(t,o)=>{const r=o?.firstBytes,l=d("get",t),g=await fetch(r===void 0?l:`${l}&bytes=${r}`,{headers:{authorization:`Bearer ${a.hostApiToken}`}});if(g.status===404)return null;if(!g.ok)throw new Error(`Tego storage get failed: ${g.status}`);return new Uint8Array(await g.arrayBuffer())},"get"),head:n(async t=>{const o=await fetch(`${a.hostApiUrl}/storage/head`,{method:"POST",headers:{"content-type":"application/json",authorization:`Bearer ${a.hostApiToken}`},body:JSON.stringify({key:t})});if(o.status===404)return null;if(!o.ok)throw new Error(`Tego storage head failed: ${o.status}`);const r=await o.json();if(!r.data)throw new Error("Tego host API returned no object head");return r.data},"head"),delete:n(async t=>{await e("/storage/delete",{key:t})},"delete"),signedUrl:n(async(t,o)=>{const g=(await(await e("/storage/signed-url",{key:t,ttlSeconds:o})).json()).data?.url;if(!g)throw new Error("Tego host API returned no signed url");return g},"signedUrl"),signedPath:n(async(t,o)=>{const g=(await(await e("/storage/signed-url",{key:t,ttlSeconds:o})).json()).data?.path;if(!g)throw new Error("Tego host API returned no signed path");return g},"signedPath"),managedPath:n(async(t,o,r)=>{const g=await(await e("/storage/signed-url",{key:t,policy:o,ttlSeconds:r})).json();if(!g.data?.path)throw new Error("Tego host API returned no managed path");return g.data.path},"managedPath"),uploadPath:n(async(t,o)=>{const l=await(await e("/storage/upload-url",{key:t,maxBytes:o?.maxBytes,ttlSeconds:o?.ttlSeconds})).json();if(!l.data?.path)throw new Error("Tego host API returned no upload path");return l.data},"uploadPath")},w={publish:n(async(t,o)=>{await e("/events",{name:t,payload:o})},"publish")},f={send:n(async t=>{const r=await(await e("/notifications",t)).json();if(!r.data?.id)throw new Error("Tego host API returned no notification id");return{id:r.data.id}},"send")},A={async emit(t){const r=await(await e("/proactive/emit",t)).json();if(!r.data)throw new Error("Tego proactive response is missing data");return r.data}},i=n(async(t,o)=>{const l=await(await e(t,o)).json();if(!l.data)throw new Error("Tego attention response is missing data");return l.data},"attentionStatus"),u={async request(t){const r=await(await e("/attention/requests",t)).json();if(!r.data?.itemId)throw new Error("Tego host API returned no attention item");return r.data},get:n(t=>i("/attention/requests/get",{itemId:t}),"get"),cancel:n(t=>i("/attention/requests/cancel",{itemId:t}),"cancel")},T={async send(t){const r=await(await e("/mail/send",t)).json();if(!r.data)throw new Error("Tego mail response is missing data");return r.data},async status(t){const r=await(await e("/mail/status",{userId:t})).json();if(!r.data)throw new Error("Tego mail response is missing data");return r.data},async unsubscribes(t={}){const r=await(await e("/mail/unsubscribes",t)).json();if(!r.data)throw new Error("Tego mail response is missing data");return r.data}};let b={};const v={current:n(()=>b,"current"),refresh:n(async()=>{const t=await fetch(`${a.hostApiUrl}/config`,{headers:{authorization:`Bearer ${a.hostApiToken}`}});if(!t.ok)throw new Error(`Tego host API /config failed: ${t.status} ${await t.text()}`);return b=(await t.json()).data?.values??{},b},"refresh")},I={async read(t,o){return(await(await e("/sources/read",{id:t,...o})).json()).data}},P={async list(t){const r=await(await e("/delegations/list",{userId:t})).json();if(!r.data)throw new Error("Tego delegation response is missing data");return r.data}},k={async verify(t){const r=await(await e("/knowledge/verify",t)).json();if(!r.data)throw new Error("Tego knowledge response is missing data");return r.data},collections:{ensure:n(t=>y("ensure",t),"ensure"),permissions:n(t=>y("permissions",t),"permissions"),move:n(t=>y("move",t),"move"),access:n(t=>y("access",t),"access"),import:n(t=>y("import",t),"import"),importFiles:n(t=>y("import-files",t),"importFiles"),status:n(t=>y("status",t),"status"),delete:n(t=>y("delete",t),"delete"),documentsStatus:n(t=>y("documents-status",t),"documentsStatus"),documentContent:n(t=>y("document-content",t),"documentContent"),search:n(t=>y("search",t),"search"),remove:n(t=>y("remove",t),"remove")}};async function y(t,o){const l=await(await e(`/knowledge/collections/${t}`,o)).json();if(!l.data)throw new Error("Tego knowledge collection response is missing data");return l.data}n(y,"collectionCall");const N={async identity(t){const r=await(await e("/directory/identity",{userId:t})).json();if(!r.data)throw new Error("Tego directory response is missing data");return r.data},async validate(t){const r=await(await e("/directory/validate",{principals:t})).json();if(!r.data)throw new Error("Tego directory response is missing data");return r.data},async list(t){const r=await(await e("/directory/list",t)).json();if(!r.data)throw new Error("Tego directory response is missing data");return r.data},async match(t){const r=await(await e("/directory/match",t)).json();if(!r.data)throw new Error("Tego directory response is missing data");return r.data}},G={provisionUser:n(t=>E("/accounts/provision",t),"provisionUser"),resetPassword:n(t=>E("/accounts/password/reset",t),"resetPassword"),inviteUser:n(t=>E("/accounts/invite",t),"inviteUser"),getInvitation:n(t=>E("/accounts/get",{invitationId:t}),"getInvitation"),resendInvitation:n(t=>E("/accounts/resend",{invitationId:t}),"resendInvitation")};async function E(t,o){const l=await(await e(t,o)).json();if(!l.data)throw new Error("Tego account response is missing data");return l.data}return n(E,"collectionEnvelope"),{accounts:G,log:s,storage:p,diagrams:{status:n(async()=>{const o=await(await e("/diagrams/status",{})).json();if(!o.data)throw new Error("Tego host API returned no renderer status");return o.data},"status"),render:n(async t=>{const r=await(await e("/diagrams/render",t)).json();if(!r.data?.key)throw new Error("Tego host API returned no diagram artifact");return r.data},"render")},events:w,notifications:f,proactive:A,attention:u,mail:T,config:v,sources:I,directory:N,knowledge:k,knowledgeAdmin:{listManageableBases:n(t=>E("/knowledge/admin/list",t),"listManageableBases"),previewCreateBase:n(t=>E("/knowledge/admin/create-preview",t),"previewCreateBase"),createApprovedBase:n(t=>E("/knowledge/admin/create",t),"createApprovedBase"),prepareDocumentContent:n(t=>E("/knowledge/admin/prepare",t),"prepareDocumentContent"),previewDocumentBatch:n(t=>E("/knowledge/admin/preview",t),"previewDocumentBatch"),stageApprovedDocumentBatch:n(t=>E("/knowledge/admin/import",t),"stageApprovedDocumentBatch"),getTask:n(t=>E("/knowledge/admin/task",t),"getTask")},delegations:P}}n(W,"createHostClient");var K=5e3,J=15e3;function x(a){let e=null;async function s(){if(e)return e;const c=await import("pg"),d=c.default?.Pool??c.Pool;if(!d)throw new Error('The "pg" module resolved without a Pool constructor. The plugin runtime expects the driver the runner image provides.');return e=new d({connectionString:a,connectionTimeoutMillis:K}),e}return n(s,"ensurePool"),{query:n(async(c,d)=>(await(await s()).query(c,d)).rows,"query"),transaction:n(async c=>{const p=await(await s()).connect();let w=!0,f=!1;const A={query:n(async(i,u)=>{if(f)throw new Error("This transaction has already ended; its `tx` cannot be used afterwards.");return(await p.query(i,u)).rows},"query")};try{await p.query("BEGIN"),await p.query(`SET LOCAL idle_in_transaction_session_timeout = ${J}`);const i=await c(A);return await p.query("COMMIT"),i}catch(i){try{await p.query("ROLLBACK")}catch{w=!1}throw i}finally{f=!0,p.release(w?void 0:!0)}},"transaction")}}n(x,"createScopedDb");function S(a,e){const s=`http://localhost${a.url??"/"}`,c=new Headers;for(const[p,w]of Object.entries(a.headers))w!==void 0&&c.set(p,Array.isArray(w)?w.join(", "):w);const d=a.method??"GET";return new Request(s,{method:d,headers:c,...d==="GET"||d==="HEAD"?{}:{body:e}})}n(S,"toWebRequest");async function _(a,e){const s={};e.headers.forEach((c,d)=>{s[d]=c}),a.writeHead(e.status,s),a.end(Buffer.from(await e.arrayBuffer()))}n(_,"writeWebResponse");function j(a,e){return new Promise((s,c)=>{const d=[];let p=0;a.on("data",w=>{if(p+=w.length,e!==void 0&&p>e){c(new Error("Policy request body too large")),a.destroy();return}d.push(w)}),a.on("end",()=>{s($(Buffer.concat(d)))}),a.on("error",c)})}n(j,"readBody");function Y(a){const e=z(process.env),s={pluginId:e.pluginId,tenantId:e.tenantId,db:x(e.databaseUrl),...W(e)},c=D((i,u)=>{(async()=>{try{const T=(i.url??"/").split("?")[0];if(T===e.healthPath){u.writeHead(200,{"content-type":"application/json"}),u.end('{"status":"ok"}');return}if(i.headers["x-tego-plugin-storage-policy"]==="1"){const I=await j(i,16384),P=n(y=>typeof i.headers[y]=="string"?i.headers[y]:"","header"),k={installId:P("x-tego-plugin-install-id"),tenantId:P("x-tego-plugin-tenant-id"),issuedAt:P("x-tego-plugin-issued-at"),bodyHash:P("x-tego-plugin-body-hash")};if(T!==R||i.method!=="POST"||!k.installId||!k.tenantId||!M({context:k,signature:P("x-tego-plugin-signature"),key:e.gatewayKey,nowMs:Date.now(),body:Buffer.from(I).toString("utf8")})){u.writeHead(401),u.end();return}if(!a.authorizeStorage){u.writeHead(404),u.end();return}await _(u,await a.authorizeStorage(S(i,I),k,s));return}if(i.headers[m.public]==="1"){const I=U(i.headers,{key:e.gatewayKey,nowMs:Date.now()});if(!I){u.writeHead(401,{"content-type":"application/json"}),u.end('{"error":"missing or unverifiable tego plugin context"}');return}if(!a.fetchPublic){u.writeHead(404,{"content-type":"application/json"}),u.end('{"error":"not found"}');return}const P=await j(i);await _(u,await a.fetchPublic(S(i,P),I,s));return}const b=O(i.headers,{key:e.gatewayKey,nowMs:Date.now()});if(!b){u.writeHead(401,{"content-type":"application/json"}),u.end('{"error":"missing or unverifiable tego plugin context"}');return}const h=await j(i),v=await a.fetch(S(i,h),b,s);await _(u,v)}catch(T){s.log.error("Unhandled error in plugin backend",{error:T instanceof Error?T.message:String(T)}),u.headersSent?u.end():(u.writeHead(500,{"content-type":"application/json"}),u.end('{"error":"plugin backend failed"}'))}})()});let d=!1,p=!1,w;const f=n(()=>(w??=Promise.resolve().then(()=>a.stop?.()),w),"stopModule"),A=(async()=>{const i=await F(s,e);if(i)throw new Error(i);d||(p=!0,await a.start?.(s),!d&&c.listen(e.port,()=>{s.log.info(`Plugin backend listening on ${e.port}`)}))})();A.catch(async i=>{console.error("Plugin backend startup failed",i);try{p&&await f()}catch(u){console.error("Plugin backend shutdown failed",u)}process.exit(1)}),process.on("SIGTERM",()=>{d||(d=!0,Promise.all([new Promise(i=>c.close(()=>i())),A.then(async()=>{p&&await f()})]).then(()=>process.exit(0)).catch(i=>{console.error("Plugin backend shutdown failed",i),process.exit(1)}))})}n(Y,"serveBackend");async function F(a,e){const s=await B(a.db,e.databaseUrl);if(s)return s;try{await a.config.refresh()}catch(c){return`Plugin backend cannot read its configuration from the host API: ${c instanceof Error?c.message:String(c)}`}return null}n(F,"startUp");async function B(a,e){try{return await a.query("select 1"),null}catch(s){return`Plugin backend cannot reach its database at ${Q(e)}: ${s instanceof Error?s.message:String(s)}`}}n(B,"checkDatabaseReachable");function Q(a){try{const e=new URL(a);return`${e.host}${e.pathname}`}catch{return"the address in DATABASE_URL"}}n(Q,"describeDatabaseTarget");export{B as checkDatabaseReachable,x as createScopedDb,U as readPublicContext,O as readUserContext,Y as serveBackend};
1
+ var H=Object.defineProperty;var n=(a,e)=>H(a,"name",{value:e,configurable:!0});import{verifyPluginContext as L,verifyPluginPublicContext as M,PLUGIN_STORAGE_POLICY_PATH as C,verifyPluginStoragePolicy as R}from"./chunk-WLTASJF6.js";import{PLUGIN_CONTEXT_HEADERS as T}from"./chunk-QKSPPMVN.js";import{createServer as D}from"http";function j(a){const e=new ArrayBuffer(a.byteLength);return new Uint8Array(e).set(a),e}n(j,"toArrayBuffer");function z(a){const e=n(s=>{const c=a[s];if(!c)throw new Error(`${s} is required but was not set`);return c},"require_");return{pluginId:e("TEGO_PLUGIN_ID"),tenantId:e("TEGO_TENANT_ID"),port:Number(e("TEGO_PLUGIN_PORT")),healthPath:a.TEGO_PLUGIN_HEALTH_PATH??"/healthz",hostApiUrl:e("TEGO_PLUGIN_HOST_API_URL"),hostApiToken:e("TEGO_PLUGIN_HOST_API_TOKEN"),databaseUrl:e("DATABASE_URL"),gatewayKey:e("TEGO_PLUGIN_GATEWAY_KEY")}}n(z,"readEnv");function O(a,e){const s=n(f=>{const A=a[f],I=Array.isArray(A)?A[0]:A;return I&&I.length>0?I:null},"read"),c=s(T.userId),d=s(T.tenantId);if(!c||!d)return null;const l=s(T.signature),w=s(T.issuedAt);if(!l||!w)return null;const h=s(T.avatarId),y=s(T.roles),i=s(T.delegation),u={userId:c,tenantId:d,rolesHeader:y??"",avatarId:h??"",issuedAt:w,...i?{delegationHeader:i}:{}};if(!L({context:u,signature:l,key:e.key,nowMs:e.nowMs}))return null;let b=[];if(y)try{const f=JSON.parse(y);if(!Array.isArray(f))return null;b=f.filter(A=>typeof A=="string")}catch{return null}let v;if(i){if(!h)return null;try{const f=JSON.parse(i);if(!f||typeof f.id!="string"||!f.id||!Array.isArray(f.actions)||f.actions.some(A=>A!=="records.read"&&A!=="work.prepare"))return null;v={id:f.id,actions:f.actions}}catch{return null}}return{userId:c,tenantId:d,roles:b,...h?{avatarId:h}:{},...v?{delegation:v}:{}}}n(O,"readUserContext");function U(a,e){const s=n(b=>{const v=a[b],f=Array.isArray(v)?v[0]:v;return f&&f.length>0?f:null},"read"),c=s(T.tenantId),d=s(T.installId),l=s(T.publicRoute),w=s(T.clientIp),h=s(T.avatarId),y=s(T.issuedAt),i=s(T.signature);if(!c||!d||!l||!w||!y||!i)return null;const u={tenantId:c,installId:d,route:l,clientIp:w,...h?{avatarId:h}:{}};return M({context:{...u,issuedAt:y},signature:i,key:e.key,nowMs:e.nowMs})?u:null}n(U,"readPublicContext");function W(a){const e=n(async(t,o)=>{const r=await fetch(`${a.hostApiUrl}${t}`,{method:"POST",headers:{"content-type":"application/json",authorization:`Bearer ${a.hostApiToken}`},body:JSON.stringify(o),...t.startsWith("/knowledge/collections/")||t.startsWith("/knowledge/admin/")?{signal:AbortSignal.timeout(12e4)}:t==="/mail/send"?{signal:AbortSignal.timeout(6e4)}:t.startsWith("/directory/")||t.startsWith("/knowledge/")||t.startsWith("/delegations/")||t.startsWith("/proactive/")||t==="/mail/status"||t==="/mail/unsubscribes"||t==="/events"?{signal:AbortSignal.timeout(1e4)}:{}});if(!r.ok){const p=await r.text();let g;try{if(g=JSON.parse(p)?.error?.details,typeof g=="string")try{g=JSON.parse(g)}catch{}}catch{}throw Object.assign(new Error(`Tego host API ${t} failed: ${r.status} ${p}`),{status:r.status,details:g,retryAfter:r.headers.get("retry-after")})}return r},"call"),s={debug:n((t,o)=>void c("debug",t,o),"debug"),info:n((t,o)=>void c("info",t,o),"info"),warn:n((t,o)=>void c("warn",t,o),"warn"),error:n((t,o)=>void c("error",t,o),"error")};async function c(t,o,r){try{await e("/log",{level:t,message:o,fields:r})}catch{}}n(c,"emitLog");const d=n((t,o)=>`${a.hostApiUrl}/storage/${t}?key=${encodeURIComponent(o)}`,"storageUrl"),l={copy:n(async(t,o)=>{const p=await(await e("/storage/copy",{sourceKey:t,destinationKey:o})).json();if(!p.data)throw new Error("Tego host API returned no copied object");return p.data},"copy"),getStream:n(async t=>{const o=await fetch(d("stream",t),{headers:{authorization:`Bearer ${a.hostApiToken}`}});if(o.status===404)return null;if(!o.ok)throw new Error(`Tego storage stream failed: ${o.status}`);return o.body},"getStream"),putStream:n(async(t,o,r)=>{const p={method:"POST",duplex:"half",body:o,headers:{authorization:`Bearer ${a.hostApiToken}`,"content-length":String(r.sizeBytes),"content-type":r.contentType??"application/octet-stream"}},g=await fetch(d("stream",t),p);if(!g.ok)throw new Error(`Tego storage stream write failed: ${g.status}`)},"putStream"),put:n(async(t,o,r)=>{const p=await fetch(d("put",t),{method:"POST",headers:{authorization:`Bearer ${a.hostApiToken}`,"content-type":r??"application/octet-stream"},body:j(o)});if(!p.ok)throw new Error(`Tego storage put failed: ${p.status}`)},"put"),get:n(async(t,o)=>{const r=o?.firstBytes,p=d("get",t),g=await fetch(r===void 0?p:`${p}&bytes=${r}`,{headers:{authorization:`Bearer ${a.hostApiToken}`}});if(g.status===404)return null;if(!g.ok)throw new Error(`Tego storage get failed: ${g.status}`);return new Uint8Array(await g.arrayBuffer())},"get"),head:n(async t=>{const o=await fetch(`${a.hostApiUrl}/storage/head`,{method:"POST",headers:{"content-type":"application/json",authorization:`Bearer ${a.hostApiToken}`},body:JSON.stringify({key:t})});if(o.status===404)return null;if(!o.ok)throw new Error(`Tego storage head failed: ${o.status}`);const r=await o.json();if(!r.data)throw new Error("Tego host API returned no object head");return r.data},"head"),delete:n(async t=>{await e("/storage/delete",{key:t})},"delete"),signedUrl:n(async(t,o)=>{const g=(await(await e("/storage/signed-url",{key:t,ttlSeconds:o})).json()).data?.url;if(!g)throw new Error("Tego host API returned no signed url");return g},"signedUrl"),signedPath:n(async(t,o)=>{const g=(await(await e("/storage/signed-url",{key:t,ttlSeconds:o})).json()).data?.path;if(!g)throw new Error("Tego host API returned no signed path");return g},"signedPath"),managedPath:n(async(t,o,r)=>{const g=await(await e("/storage/signed-url",{key:t,policy:o,ttlSeconds:r})).json();if(!g.data?.path)throw new Error("Tego host API returned no managed path");return g.data.path},"managedPath"),uploadPath:n(async(t,o)=>{const p=await(await e("/storage/upload-url",{key:t,maxBytes:o?.maxBytes,ttlSeconds:o?.ttlSeconds})).json();if(!p.data?.path)throw new Error("Tego host API returned no upload path");return p.data},"uploadPath")},w={publish:n(async(t,o)=>{await e("/events",{name:t,payload:o})},"publish")},h={send:n(async t=>{const r=await(await e("/notifications",t)).json();if(!r.data?.id)throw new Error("Tego host API returned no notification id");return{id:r.data.id}},"send")},y={async emit(t){const r=await(await e("/proactive/emit",t)).json();if(!r.data)throw new Error("Tego proactive response is missing data");return r.data}},i=n(async(t,o)=>{const p=await(await e(t,o)).json();if(!p.data)throw new Error("Tego attention response is missing data");return p.data},"attentionStatus"),u={async request(t){const r=await(await e("/attention/requests",t)).json();if(!r.data?.itemId)throw new Error("Tego host API returned no attention item");return r.data},get:n(t=>i("/attention/requests/get",{itemId:t}),"get"),cancel:n(t=>i("/attention/requests/cancel",{itemId:t}),"cancel")},b={async send(t){const r=await(await e("/mail/send",t)).json();if(!r.data)throw new Error("Tego mail response is missing data");return r.data},async status(t){const r=await(await e("/mail/status",{userId:t})).json();if(!r.data)throw new Error("Tego mail response is missing data");return r.data},async unsubscribes(t={}){const r=await(await e("/mail/unsubscribes",t)).json();if(!r.data)throw new Error("Tego mail response is missing data");return r.data}};let v={};const A={current:n(()=>v,"current"),refresh:n(async()=>{const t=await fetch(`${a.hostApiUrl}/config`,{headers:{authorization:`Bearer ${a.hostApiToken}`}});if(!t.ok)throw new Error(`Tego host API /config failed: ${t.status} ${await t.text()}`);return v=(await t.json()).data?.values??{},v},"refresh")},I={async read(t,o){return(await(await e("/sources/read",{id:t,...o})).json()).data}},P={async list(t){const r=await(await e("/delegations/list",{userId:t})).json();if(!r.data)throw new Error("Tego delegation response is missing data");return r.data}},k={async verify(t){const r=await(await e("/knowledge/verify",t)).json();if(!r.data)throw new Error("Tego knowledge response is missing data");return r.data},collections:{ensure:n(t=>m("ensure",t),"ensure"),permissions:n(t=>m("permissions",t),"permissions"),move:n(t=>m("move",t),"move"),access:n(t=>m("access",t),"access"),import:n(t=>m("import",t),"import"),importFiles:n(t=>m("import-files",t),"importFiles"),status:n(t=>m("status",t),"status"),delete:n(t=>m("delete",t),"delete"),documentsStatus:n(t=>m("documents-status",t),"documentsStatus"),documentContent:n(t=>m("document-content",t),"documentContent"),search:n(t=>m("search",t),"search"),remove:n(t=>m("remove",t),"remove")}};async function m(t,o){const p=await(await e(`/knowledge/collections/${t}`,o)).json();if(!p.data)throw new Error("Tego knowledge collection response is missing data");return p.data}n(m,"collectionCall");const N={async identity(t){const r=await(await e("/directory/identity",{userId:t})).json();if(!r.data)throw new Error("Tego directory response is missing data");return r.data},async validate(t){const r=await(await e("/directory/validate",{principals:t})).json();if(!r.data)throw new Error("Tego directory response is missing data");return r.data},async list(t){const r=await(await e("/directory/list",t)).json();if(!r.data)throw new Error("Tego directory response is missing data");return r.data},async match(t){const r=await(await e("/directory/match",t)).json();if(!r.data)throw new Error("Tego directory response is missing data");return r.data}},G={provisionUser:n(t=>E("/accounts/provision",t),"provisionUser"),resetPassword:n(t=>E("/accounts/password/reset",t),"resetPassword"),inviteUser:n(t=>E("/accounts/invite",t),"inviteUser"),getInvitation:n(t=>E("/accounts/get",{invitationId:t}),"getInvitation"),resendInvitation:n(t=>E("/accounts/resend",{invitationId:t}),"resendInvitation")};async function E(t,o){const p=await(await e(t,o)).json();if(!p.data)throw new Error("Tego account response is missing data");return p.data}return n(E,"collectionEnvelope"),{accounts:G,log:s,storage:l,diagrams:{status:n(async()=>{const o=await(await e("/diagrams/status",{})).json();if(!o.data)throw new Error("Tego host API returned no renderer status");return o.data},"status"),render:n(async t=>{const r=await(await e("/diagrams/render",t)).json();if(!r.data?.key)throw new Error("Tego host API returned no diagram artifact");return r.data},"render")},events:w,notifications:h,proactive:y,attention:u,mail:b,config:A,sources:I,directory:N,knowledge:k,knowledgeAdmin:{listManageableBases:n(t=>E("/knowledge/admin/list",t),"listManageableBases"),previewCreateBase:n(t=>E("/knowledge/admin/create-preview",t),"previewCreateBase"),createApprovedBase:n(t=>E("/knowledge/admin/create",t),"createApprovedBase"),prepareDocumentContent:n(t=>E("/knowledge/admin/prepare",t),"prepareDocumentContent"),previewDocumentBatch:n(t=>E("/knowledge/admin/preview",t),"previewDocumentBatch"),stageApprovedDocumentBatch:n(t=>E("/knowledge/admin/import",t),"stageApprovedDocumentBatch"),getTask:n(t=>E("/knowledge/admin/task",t),"getTask")},delegations:P}}n(W,"createHostClient");var K=5e3,J=15e3;function x(a){let e=null;async function s(){if(e)return e;const c=await import("pg"),d=c.default?.Pool??c.Pool;if(!d)throw new Error('The "pg" module resolved without a Pool constructor. The plugin runtime expects the driver the runner image provides.');return e=new d({connectionString:a,connectionTimeoutMillis:K}),e}return n(s,"ensurePool"),{query:n(async(c,d)=>(await(await s()).query(c,d)).rows,"query"),transaction:n(async c=>{const l=await(await s()).connect();let w=!0,h=!1;const y={query:n(async(i,u)=>{if(h)throw new Error("This transaction has already ended; its `tx` cannot be used afterwards.");return(await l.query(i,u)).rows},"query")};try{await l.query("BEGIN"),await l.query(`SET LOCAL idle_in_transaction_session_timeout = ${J}`);const i=await c(y);return await l.query("COMMIT"),i}catch(i){try{await l.query("ROLLBACK")}catch{w=!1}throw i}finally{h=!0,l.release(w?void 0:!0)}},"transaction")}}n(x,"createScopedDb");function S(a,e){const s=`http://localhost${a.url??"/"}`,c=new Headers;for(const[l,w]of Object.entries(a.headers))w!==void 0&&c.set(l,Array.isArray(w)?w.join(", "):w);const d=a.method??"GET";return new Request(s,{method:d,headers:c,...d==="GET"||d==="HEAD"?{}:{body:e}})}n(S,"toWebRequest");async function _(a,e){const s={};e.headers.forEach((c,d)=>{s[d]=c}),a.writeHead(e.status,s),a.end(Buffer.from(await e.arrayBuffer()))}n(_,"writeWebResponse");function $(a,e){return new Promise((s,c)=>{const d=[];let l=0;a.on("data",w=>{if(l+=w.length,e!==void 0&&l>e){c(new Error("Policy request body too large")),a.destroy();return}d.push(w)}),a.on("end",()=>{s(j(Buffer.concat(d)))}),a.on("error",c)})}n($,"readBody");function Y(a){const e=z(process.env),s={pluginId:e.pluginId,tenantId:e.tenantId,db:x(e.databaseUrl),...W(e)},c=D((i,u)=>{(async()=>{try{const b=(i.url??"/").split("?")[0];if(b===e.healthPath){u.writeHead(200,{"content-type":"application/json"}),u.end('{"status":"ok"}');return}if(i.headers["x-tego-plugin-storage-policy"]==="1"){const I=await $(i,16384),P=n(m=>typeof i.headers[m]=="string"?i.headers[m]:"","header"),k={installId:P("x-tego-plugin-install-id"),tenantId:P("x-tego-plugin-tenant-id"),issuedAt:P("x-tego-plugin-issued-at"),bodyHash:P("x-tego-plugin-body-hash")};if(b!==C||i.method!=="POST"||!k.installId||!k.tenantId||!R({context:k,signature:P("x-tego-plugin-signature"),key:e.gatewayKey,nowMs:Date.now(),body:Buffer.from(I).toString("utf8")})){u.writeHead(401),u.end();return}if(!a.authorizeStorage){u.writeHead(404),u.end();return}await _(u,await a.authorizeStorage(S(i,I),k,s));return}if(i.headers[T.public]==="1"){const I=U(i.headers,{key:e.gatewayKey,nowMs:Date.now()});if(!I){u.writeHead(401,{"content-type":"application/json"}),u.end('{"error":"missing or unverifiable tego plugin context"}');return}if(!a.fetchPublic){u.writeHead(404,{"content-type":"application/json"}),u.end('{"error":"not found"}');return}const P=await $(i);await _(u,await a.fetchPublic(S(i,P),I,s));return}const v=O(i.headers,{key:e.gatewayKey,nowMs:Date.now()});if(!v){u.writeHead(401,{"content-type":"application/json"}),u.end('{"error":"missing or unverifiable tego plugin context"}');return}const f=await $(i),A=await a.fetch(S(i,f),v,s);await _(u,A)}catch(b){s.log.error("Unhandled error in plugin backend",{error:b instanceof Error?b.message:String(b)}),u.headersSent?u.end():(u.writeHead(500,{"content-type":"application/json"}),u.end('{"error":"plugin backend failed"}'))}})()});let d=!1,l=!1,w;const h=n(()=>(w??=Promise.resolve().then(()=>a.stop?.()),w),"stopModule"),y=(async()=>{const i=await F(s,e);if(i)throw new Error(i);d||(l=!0,await a.start?.(s),!d&&c.listen(e.port,()=>{s.log.info(`Plugin backend listening on ${e.port}`)}))})();y.catch(async i=>{console.error("Plugin backend startup failed",i);try{l&&await h()}catch(u){console.error("Plugin backend shutdown failed",u)}process.exit(1)}),process.on("SIGTERM",()=>{d||(d=!0,Promise.all([new Promise(i=>c.close(()=>i())),y.then(async()=>{l&&await h()})]).then(()=>process.exit(0)).catch(i=>{console.error("Plugin backend shutdown failed",i),process.exit(1)}))})}n(Y,"serveBackend");async function F(a,e){const s=await B(a.db,e.databaseUrl);if(s)return s;try{await a.config.refresh()}catch(c){return`Plugin backend cannot read its configuration from the host API: ${c instanceof Error?c.message:String(c)}`}return null}n(F,"startUp");async function B(a,e){try{return await a.query("select 1"),null}catch(s){return`Plugin backend cannot reach its database at ${Q(e)}: ${s instanceof Error?s.message:String(s)}`}}n(B,"checkDatabaseReachable");function Q(a){try{const e=new URL(a);return`${e.host}${e.pathname}`}catch{return"the address in DATABASE_URL"}}n(Q,"describeDatabaseTarget");function X(a,e={}){const s=e.intervalMs??3e4,c=[];let d,l,w=!0;const h=n(i=>{w||(d&&clearTimeout(d),d=setTimeout(()=>void y(),i))},"schedule"),y=n(async()=>{if(d=void 0,l)return h(s);l=(async()=>{for(const i of c){if(w)break;try{await i.run()}catch(u){a.log.error(`maintenance step ${i.name} failed`,{error:u instanceof Error?u.message:String(u)})}}})(),await l,l=void 0,h(s)},"tick");return{step(i,u){c.push({name:i,run:u})},start(){w&&(w=!1,h(0))},wake(){h(0)},async stop(){w=!0,d&&clearTimeout(d),d=void 0,await l}}}n(X,"createMaintenance");export{B as checkDatabaseReachable,X as createMaintenance,x as createScopedDb,U as readPublicContext,O as readUserContext,Y as serveBackend};
package/dist/testing.cjs CHANGED
@@ -1,2 +1,6 @@
1
- "use strict";var A=Object.defineProperty;var a=(s,l)=>A(s,"name",{value:l,configurable:!0});var crypto=require("crypto"),promises=require("fs/promises"),path=require("path"),DEFAULT_DATABASE_URL=process.env.TEST_DATABASE_URL??"postgresql://zhama:zhama123321@localhost:15432/zhama",TEST_TENANT_ID="00000000-0000-4000-8000-00000000a001";async function createTestPluginHost(s){const{Pool:l}=await import("pg"),c=s.databaseUrl??DEFAULT_DATABASE_URL,u=`plugintest_${(s.label??s.pluginId).replace(/[^a-z0-9]+/gi,"_")}_${Math.random().toString(36).slice(2,8)}`,w=new l({connectionString:c});await w.query(`CREATE SCHEMA "${u}"`);const p=await w.connect();try{await p.query(`SET search_path TO "${u}"`);for(const e of await migrationFiles(s.migrationsDir))await p.query(await promises.readFile(e,"utf8"))}finally{p.release()}await w.end();const f=new l({connectionString:c,options:`-c search_path=${u}`}),m=[],b=[],r=new Map,o=new Map;let T=!1,h={...s.config??{}};const E={query:a(async(e,t)=>(await f.query(e,t)).rows,"query"),transaction:a(async e=>{const t=await f.connect();try{await t.query("BEGIN");const n=await e({query:a(async(i,g)=>(await t.query(i,g)).rows,"query")});return await t.query("COMMIT"),n}catch(n){throw await t.query("ROLLBACK"),n}finally{t.release()}},"transaction")};return{host:{pluginId:s.pluginId,tenantId:s.tenantId??TEST_TENANT_ID,db:E,storage:{put:a(async(e,t,n)=>{r.set(e,t),o.set(e,n??"application/octet-stream")},"put"),copy:a(async(e,t)=>{const n=r.get(e);if(!n)throw new Error("Object not found");r.set(t,n.slice());const i=o.get(e)??"application/octet-stream";return o.set(t,i),{size:n.byteLength,contentType:i}},"copy"),getStream:a(async e=>{const t=r.get(e);return t?new ReadableStream({start(n){n.enqueue(t.slice()),n.close()}}):null},"getStream"),putStream:a(async(e,t,n)=>{const i=t.getReader(),g=[];let d=0;try{for(;;){const y=await i.read();if(y.done)break;if(g.push(y.value),d+=y.value.byteLength,d>n.sizeBytes)throw new Error("Stream exceeds declared size")}}finally{i.releaseLock()}if(d!==n.sizeBytes)throw new Error("Stream size mismatch");const q=new Uint8Array(d);let S=0;for(const y of g)q.set(y,S),S+=y.byteLength;r.set(e,q),o.set(e,n.contentType??"application/octet-stream")},"putStream"),get:a(async(e,t)=>{const n=r.get(e);return n?t?.firstBytes===void 0?n:n.subarray(0,t.firstBytes):null},"get"),head:a(async e=>{const t=r.get(e);return t?{size:t.byteLength,contentType:o.get(e)??null,versionTag:crypto.createHash("sha256").update(t).digest("hex")}:null},"head"),delete:a(async e=>{r.delete(e),o.delete(e)},"delete"),signedUrl:a(async e=>`https://storage.test/${e}`,"signedUrl"),signedPath:a(async e=>`/storage.test/${e}`,"signedPath"),managedPath:a(async e=>`/storage.test/${e}`,"managedPath"),uploadPath:a(async(e,t)=>({path:`/storage.test/upload/${e}`,maxBytes:t?.maxBytes??512*1024*1024,expiresAt:Date.now()+(t?.ttlSeconds??300)*1e3}),"uploadPath")},log:{debug:a(()=>{},"debug"),info:a(()=>{},"info"),warn:a(()=>{},"warn"),error:a((e,t)=>{b.push({message:e,...t?{fields:t}:{}})},"error")},events:{publish:a(async(e,t)=>{if(T)throw new Error("the event bus is down");m.push({topic:e,payload:t})},"publish")},notifications:{send:a(async()=>({id:crypto.randomUUID()}),"send")},config:{current:a(()=>h,"current"),refresh:a(async()=>h,"refresh")}},schema:u,publishedEvents:m,loggedErrors:b,storedObjects:r,failPublish:a((e=!0)=>{T=e},"failPublish"),setConfig:a(e=>{h={...e}},"setConfig"),reset:a(async()=>{const e=await E.query(`select quote_ident(tablename) as name
2
- from pg_tables where schemaname = $1`,[u]);e.length>0&&await E.query(`truncate table ${e.map(t=>t.name).join(", ")} restart identity cascade`),m.length=0,b.length=0,r.clear(),o.clear(),T=!1,h={...s.config??{}}},"reset"),close:a(async()=>{await f.end();const e=new l({connectionString:c});await e.query(`DROP SCHEMA "${u}" CASCADE`),await e.end()},"close")}}a(createTestPluginHost,"createTestPluginHost");async function migrationFiles(s){return(await promises.readdir(s)).filter(c=>c.endsWith(".sql")).sort().map(c=>path.join(s,c))}a(migrationFiles,"migrationFiles"),exports.TEST_TENANT_ID=TEST_TENANT_ID,exports.createTestPluginHost=createTestPluginHost;
1
+ "use strict";var _=Object.defineProperty;var e=(s,d)=>_(s,"name",{value:d,configurable:!0});var crypto=require("crypto"),promises=require("fs/promises"),path=require("path"),DEFAULT_DATABASE_URL=process.env.TEST_DATABASE_URL??"postgresql://zhama:zhama123321@localhost:15432/zhama",TEST_TENANT_ID="00000000-0000-4000-8000-00000000a001";async function createTestPluginHost(s){const{Pool:d}=await import("pg"),y=s.databaseUrl??DEFAULT_DATABASE_URL,w=`plugintest_${(s.label??s.pluginId).replace(/[^a-z0-9]+/gi,"_")}_${Math.random().toString(36).slice(2,8)}`,h=new d({connectionString:y});await h.query(`CREATE SCHEMA "${w}"`);const f=await h.connect();try{await f.query(`SET search_path TO "${w}"`);for(const t of await migrationFiles(s.migrationsDir))await f.query(await promises.readFile(t,"utf8"))}finally{f.release()}await h.end();const g=new d({connectionString:y,options:`-c search_path=${w}`}),p=[],c=[],o=new Map,l=new Map;let n=!1,u={...s.config??{}};const i={query:e(async(t,a)=>(await g.query(t,a)).rows,"query"),transaction:e(async t=>{const a=await g.connect();try{await a.query("BEGIN");const r=await t({query:e(async(b,E)=>(await a.query(b,E)).rows,"query")});return await a.query("COMMIT"),r}catch(r){throw await a.query("ROLLBACK"),r}finally{a.release()}},"transaction")};return{host:{pluginId:s.pluginId,tenantId:s.tenantId??TEST_TENANT_ID,db:i,storage:{put:e(async(t,a,r)=>{o.set(t,a),l.set(t,r??"application/octet-stream")},"put"),copy:e(async(t,a)=>{const r=o.get(t);if(!r)throw new Error("Object not found");o.set(a,r.slice());const b=l.get(t)??"application/octet-stream";return l.set(a,b),{size:r.byteLength,contentType:b}},"copy"),getStream:e(async t=>{const a=o.get(t);return a?new ReadableStream({start(r){r.enqueue(a.slice()),r.close()}}):null},"getStream"),putStream:e(async(t,a,r)=>{const b=a.getReader(),E=[];let S=0;try{for(;;){const $=await b.read();if($.done)break;if(E.push($.value),S+=$.value.byteLength,S>r.sizeBytes)throw new Error("Stream exceeds declared size")}}finally{b.releaseLock()}if(S!==r.sizeBytes)throw new Error("Stream size mismatch");const A=new Uint8Array(S);let q=0;for(const $ of E)A.set($,q),q+=$.byteLength;o.set(t,A),l.set(t,r.contentType??"application/octet-stream")},"putStream"),get:e(async(t,a)=>{const r=o.get(t);return r?a?.firstBytes===void 0?r:r.subarray(0,a.firstBytes):null},"get"),head:e(async t=>{const a=o.get(t);return a?{size:a.byteLength,contentType:l.get(t)??null,versionTag:crypto.createHash("sha256").update(a).digest("hex")}:null},"head"),delete:e(async t=>{o.delete(t),l.delete(t)},"delete"),signedUrl:e(async t=>`https://storage.test/${t}`,"signedUrl"),signedPath:e(async t=>`/storage.test/${t}`,"signedPath"),managedPath:e(async t=>`/storage.test/${t}`,"managedPath"),uploadPath:e(async(t,a)=>({path:`/storage.test/upload/${t}`,maxBytes:a?.maxBytes??512*1024*1024,expiresAt:Date.now()+(a?.ttlSeconds??300)*1e3}),"uploadPath")},log:{debug:e(()=>{},"debug"),info:e(()=>{},"info"),warn:e(()=>{},"warn"),error:e((t,a)=>{c.push({message:t,...a?{fields:a}:{}})},"error")},events:{publish:e(async(t,a)=>{if(n)throw new Error("the event bus is down");p.push({topic:t,payload:a})},"publish")},notifications:{send:e(async()=>({id:crypto.randomUUID()}),"send")},config:{current:e(()=>u,"current"),refresh:e(async()=>u,"refresh")}},schema:w,publishedEvents:p,loggedErrors:c,storedObjects:o,failPublish:e((t=!0)=>{n=t},"failPublish"),setConfig:e(t=>{u={...t}},"setConfig"),reset:e(async()=>{const t=await i.query(`select quote_ident(tablename) as name
2
+ from pg_tables where schemaname = $1`,[w]);t.length>0&&await i.query(`truncate table ${t.map(a=>a.name).join(", ")} restart identity cascade`),p.length=0,c.length=0,o.clear(),l.clear(),n=!1,u={...s.config??{}}},"reset"),close:e(async()=>{await g.end();const t=new d({connectionString:y});await t.query(`DROP SCHEMA "${w}" CASCADE`),await t.end()},"close")}}e(createTestPluginHost,"createTestPluginHost");async function migrationFiles(s){return(await promises.readdir(s)).filter(y=>y.endsWith(".sql")).sort().map(y=>path.join(s,y))}e(migrationFiles,"migrationFiles");async function assertLocaleParity(s){const{readdir:d,readFile:y}=await import("fs/promises"),{verifyFrontendLocales:T}=await import("./verify-FF2RD2MV.cjs"),{join:w}=await import("path"),h={list:e(async()=>(await d(s).catch(()=>[])).filter(n=>n.endsWith(".json")),"list"),readText:e(n=>y(w(s,n.split("/").at(-1)),"utf8").catch(()=>null),"readText")},{bundles:f,errors:g}=await T(h,{i18nDirPath:"i18n",nav:[]}),p=[...g],c=e((n,u="",i=new Map)=>{if(typeof n!="object"||n===null||Array.isArray(n))i.set(u,n);else for(const[m,t]of Object.entries(n))c(t,u?`${u}.${m}`:m,i);return i},"leaves"),o=e(n=>[...typeof n=="string"?n.matchAll(/\{\{\s*([\w.-]+)\s*\}\}/g):[]].map(u=>u[1]).sort().join(","),"placeholders"),l=c(f.get("zh-CN"));for(const[n,u]of f)for(const[i,m]of c(u))typeof m=="string"&&m.trim()===""&&p.push(`${n}.json: "${i}" is empty`),n!=="zh-CN"&&l.has(i)&&o(m)!==o(l.get(i))&&p.push(`${n}.json: "${i}" has placeholders {${o(m)}} but zh-CN has {${o(l.get(i))}}`);if(p.length>0)throw new Error(`Locale bundles in ${s} are inconsistent:
3
+ - ${p.join(`
4
+ - `)}`)}e(assertLocaleParity,"assertLocaleParity");async function assertClawSkills(s,d){const{readdir:y,readFile:T}=await import("fs/promises"),{join:w}=await import("path"),h=[],f=new Set(d),g=new Set(d.map(c=>c.split("_")[0])),p=await y(s,{withFileTypes:!0}).catch(()=>[]);for(const c of p){if(!c.isDirectory())continue;const o=await T(w(s,c.name,"SKILL.md"),"utf8").catch(()=>null);if(!o){h.push(`${c.name}: no SKILL.md`);continue}const l=/^---\n([\s\S]*?)\n---/.exec(o)?.[1]??"",n=/^name:\s*["']?([^"'\n]+)["']?\s*$/m.exec(l)?.[1]?.trim();n!==c.name&&h.push(`${c.name}: frontmatter name is "${n??"(none)"}"`),/^description:\s*\S/m.test(l)||h.push(`${c.name}: no description`);for(const u of o.matchAll(/`([a-z][a-z0-9]*_[a-z0-9_]+)`/g)){const i=u[1];g.has(i.split("_")[0])&&!f.has(i)&&h.push(`${c.name}: mentions \`${i}\`, which the claw part does not register`)}}if(h.length>0)throw new Error(`Claw skills in ${s} are inconsistent:
5
+ - ${h.join(`
6
+ - `)}`)}e(assertClawSkills,"assertClawSkills"),exports.TEST_TENANT_ID=TEST_TENANT_ID,exports.assertClawSkills=assertClawSkills,exports.assertLocaleParity=assertLocaleParity,exports.createTestPluginHost=createTestPluginHost;
@@ -88,5 +88,23 @@ interface TestPluginHost {
88
88
  * ```
89
89
  */
90
90
  declare function createTestPluginHost(options: TestPluginHostOptions): Promise<TestPluginHost>;
91
+ /**
92
+ * Throws (with every problem listed) unless the plugin's locale bundles pass
93
+ * what upload and `tego-plugin doctor` check: all ten locales, the keys of
94
+ * zh-CN everywhere, the same `{{placeholders}}`, no empty values. Use it in a
95
+ * frontend test so `pnpm test` catches a half-translated change:
96
+ *
97
+ * ```ts
98
+ * it('[I18N-001] ships complete translations', () =>
99
+ * assertLocaleParity(fileURLToPath(new URL('../i18n', import.meta.url))));
100
+ * ```
101
+ */
102
+ declare function assertLocaleParity(i18nDir: string): Promise<void>;
103
+ /**
104
+ * Throws unless every `<skillsDir>/<name>/SKILL.md` declares `name: <name>`
105
+ * and a description, and every `toolNames` entry a skill mentions in
106
+ * backticks exists. Pass the tools the claw part registers.
107
+ */
108
+ declare function assertClawSkills(skillsDir: string, toolNames: readonly string[]): Promise<void>;
91
109
 
92
- export { TEST_TENANT_ID, type TestPluginHost, type TestPluginHostOptions, createTestPluginHost };
110
+ export { TEST_TENANT_ID, type TestPluginHost, type TestPluginHostOptions, assertClawSkills, assertLocaleParity, createTestPluginHost };
package/dist/testing.d.ts CHANGED
@@ -88,5 +88,23 @@ interface TestPluginHost {
88
88
  * ```
89
89
  */
90
90
  declare function createTestPluginHost(options: TestPluginHostOptions): Promise<TestPluginHost>;
91
+ /**
92
+ * Throws (with every problem listed) unless the plugin's locale bundles pass
93
+ * what upload and `tego-plugin doctor` check: all ten locales, the keys of
94
+ * zh-CN everywhere, the same `{{placeholders}}`, no empty values. Use it in a
95
+ * frontend test so `pnpm test` catches a half-translated change:
96
+ *
97
+ * ```ts
98
+ * it('[I18N-001] ships complete translations', () =>
99
+ * assertLocaleParity(fileURLToPath(new URL('../i18n', import.meta.url))));
100
+ * ```
101
+ */
102
+ declare function assertLocaleParity(i18nDir: string): Promise<void>;
103
+ /**
104
+ * Throws unless every `<skillsDir>/<name>/SKILL.md` declares `name: <name>`
105
+ * and a description, and every `toolNames` entry a skill mentions in
106
+ * backticks exists. Pass the tools the claw part registers.
107
+ */
108
+ declare function assertClawSkills(skillsDir: string, toolNames: readonly string[]): Promise<void>;
91
109
 
92
- export { TEST_TENANT_ID, type TestPluginHost, type TestPluginHostOptions, createTestPluginHost };
110
+ export { TEST_TENANT_ID, type TestPluginHost, type TestPluginHostOptions, assertClawSkills, assertLocaleParity, createTestPluginHost };
package/dist/testing.js CHANGED
@@ -1,2 +1,6 @@
1
- var _=Object.defineProperty;var a=(s,l)=>_(s,"name",{value:l,configurable:!0});import{randomUUID as $,createHash as z}from"crypto";import{readFile as v,readdir as B}from"fs/promises";import{join as L}from"path";var D=process.env.TEST_DATABASE_URL??"postgresql://zhama:zhama123321@localhost:15432/zhama",A="00000000-0000-4000-8000-00000000a001";async function I(s){const{Pool:l}=await import("pg"),c=s.databaseUrl??D,u=`plugintest_${(s.label??s.pluginId).replace(/[^a-z0-9]+/gi,"_")}_${Math.random().toString(36).slice(2,8)}`,w=new l({connectionString:c});await w.query(`CREATE SCHEMA "${u}"`);const m=await w.connect();try{await m.query(`SET search_path TO "${u}"`);for(const e of await U(s.migrationsDir))await m.query(await v(e,"utf8"))}finally{m.release()}await w.end();const f=new l({connectionString:c,options:`-c search_path=${u}`}),p=[],b=[],r=new Map,o=new Map;let E=!1,h={...s.config??{}};const T={query:a(async(e,t)=>(await f.query(e,t)).rows,"query"),transaction:a(async e=>{const t=await f.connect();try{await t.query("BEGIN");const n=await e({query:a(async(i,g)=>(await t.query(i,g)).rows,"query")});return await t.query("COMMIT"),n}catch(n){throw await t.query("ROLLBACK"),n}finally{t.release()}},"transaction")};return{host:{pluginId:s.pluginId,tenantId:s.tenantId??A,db:T,storage:{put:a(async(e,t,n)=>{r.set(e,t),o.set(e,n??"application/octet-stream")},"put"),copy:a(async(e,t)=>{const n=r.get(e);if(!n)throw new Error("Object not found");r.set(t,n.slice());const i=o.get(e)??"application/octet-stream";return o.set(t,i),{size:n.byteLength,contentType:i}},"copy"),getStream:a(async e=>{const t=r.get(e);return t?new ReadableStream({start(n){n.enqueue(t.slice()),n.close()}}):null},"getStream"),putStream:a(async(e,t,n)=>{const i=t.getReader(),g=[];let d=0;try{for(;;){const y=await i.read();if(y.done)break;if(g.push(y.value),d+=y.value.byteLength,d>n.sizeBytes)throw new Error("Stream exceeds declared size")}}finally{i.releaseLock()}if(d!==n.sizeBytes)throw new Error("Stream size mismatch");const S=new Uint8Array(d);let q=0;for(const y of g)S.set(y,q),q+=y.byteLength;r.set(e,S),o.set(e,n.contentType??"application/octet-stream")},"putStream"),get:a(async(e,t)=>{const n=r.get(e);return n?t?.firstBytes===void 0?n:n.subarray(0,t.firstBytes):null},"get"),head:a(async e=>{const t=r.get(e);return t?{size:t.byteLength,contentType:o.get(e)??null,versionTag:z("sha256").update(t).digest("hex")}:null},"head"),delete:a(async e=>{r.delete(e),o.delete(e)},"delete"),signedUrl:a(async e=>`https://storage.test/${e}`,"signedUrl"),signedPath:a(async e=>`/storage.test/${e}`,"signedPath"),managedPath:a(async e=>`/storage.test/${e}`,"managedPath"),uploadPath:a(async(e,t)=>({path:`/storage.test/upload/${e}`,maxBytes:t?.maxBytes??512*1024*1024,expiresAt:Date.now()+(t?.ttlSeconds??300)*1e3}),"uploadPath")},log:{debug:a(()=>{},"debug"),info:a(()=>{},"info"),warn:a(()=>{},"warn"),error:a((e,t)=>{b.push({message:e,...t?{fields:t}:{}})},"error")},events:{publish:a(async(e,t)=>{if(E)throw new Error("the event bus is down");p.push({topic:e,payload:t})},"publish")},notifications:{send:a(async()=>({id:$()}),"send")},config:{current:a(()=>h,"current"),refresh:a(async()=>h,"refresh")}},schema:u,publishedEvents:p,loggedErrors:b,storedObjects:r,failPublish:a((e=!0)=>{E=e},"failPublish"),setConfig:a(e=>{h={...e}},"setConfig"),reset:a(async()=>{const e=await T.query(`select quote_ident(tablename) as name
2
- from pg_tables where schemaname = $1`,[u]);e.length>0&&await T.query(`truncate table ${e.map(t=>t.name).join(", ")} restart identity cascade`),p.length=0,b.length=0,r.clear(),o.clear(),E=!1,h={...s.config??{}}},"reset"),close:a(async()=>{await f.end();const e=new l({connectionString:c});await e.query(`DROP SCHEMA "${u}" CASCADE`),await e.end()},"close")}}a(I,"createTestPluginHost");async function U(s){return(await B(s)).filter(c=>c.endsWith(".sql")).sort().map(c=>L(s,c))}a(U,"migrationFiles");export{A as TEST_TENANT_ID,I as createTestPluginHost};
1
+ var L=Object.defineProperty;var e=(s,d)=>L(s,"name",{value:d,configurable:!0});import{randomUUID as _,createHash as j}from"crypto";import{readFile as C,readdir as I}from"fs/promises";import{join as P}from"path";var x=process.env.TEST_DATABASE_URL??"postgresql://zhama:zhama123321@localhost:15432/zhama",z="00000000-0000-4000-8000-00000000a001";async function B(s){const{Pool:d}=await import("pg"),y=s.databaseUrl??x,w=`plugintest_${(s.label??s.pluginId).replace(/[^a-z0-9]+/gi,"_")}_${Math.random().toString(36).slice(2,8)}`,u=new d({connectionString:y});await u.query(`CREATE SCHEMA "${w}"`);const m=await u.connect();try{await m.query(`SET search_path TO "${w}"`);for(const t of await v(s.migrationsDir))await m.query(await C(t,"utf8"))}finally{m.release()}await u.end();const g=new d({connectionString:y,options:`-c search_path=${w}`}),f=[],c=[],o=new Map,l=new Map;let a=!1,h={...s.config??{}};const i={query:e(async(t,n)=>(await g.query(t,n)).rows,"query"),transaction:e(async t=>{const n=await g.connect();try{await n.query("BEGIN");const r=await t({query:e(async(b,T)=>(await n.query(b,T)).rows,"query")});return await n.query("COMMIT"),r}catch(r){throw await n.query("ROLLBACK"),r}finally{n.release()}},"transaction")};return{host:{pluginId:s.pluginId,tenantId:s.tenantId??z,db:i,storage:{put:e(async(t,n,r)=>{o.set(t,n),l.set(t,r??"application/octet-stream")},"put"),copy:e(async(t,n)=>{const r=o.get(t);if(!r)throw new Error("Object not found");o.set(n,r.slice());const b=l.get(t)??"application/octet-stream";return l.set(n,b),{size:r.byteLength,contentType:b}},"copy"),getStream:e(async t=>{const n=o.get(t);return n?new ReadableStream({start(r){r.enqueue(n.slice()),r.close()}}):null},"getStream"),putStream:e(async(t,n,r)=>{const b=n.getReader(),T=[];let S=0;try{for(;;){const $=await b.read();if($.done)break;if(T.push($.value),S+=$.value.byteLength,S>r.sizeBytes)throw new Error("Stream exceeds declared size")}}finally{b.releaseLock()}if(S!==r.sizeBytes)throw new Error("Stream size mismatch");const A=new Uint8Array(S);let q=0;for(const $ of T)A.set($,q),q+=$.byteLength;o.set(t,A),l.set(t,r.contentType??"application/octet-stream")},"putStream"),get:e(async(t,n)=>{const r=o.get(t);return r?n?.firstBytes===void 0?r:r.subarray(0,n.firstBytes):null},"get"),head:e(async t=>{const n=o.get(t);return n?{size:n.byteLength,contentType:l.get(t)??null,versionTag:j("sha256").update(n).digest("hex")}:null},"head"),delete:e(async t=>{o.delete(t),l.delete(t)},"delete"),signedUrl:e(async t=>`https://storage.test/${t}`,"signedUrl"),signedPath:e(async t=>`/storage.test/${t}`,"signedPath"),managedPath:e(async t=>`/storage.test/${t}`,"managedPath"),uploadPath:e(async(t,n)=>({path:`/storage.test/upload/${t}`,maxBytes:n?.maxBytes??512*1024*1024,expiresAt:Date.now()+(n?.ttlSeconds??300)*1e3}),"uploadPath")},log:{debug:e(()=>{},"debug"),info:e(()=>{},"info"),warn:e(()=>{},"warn"),error:e((t,n)=>{c.push({message:t,...n?{fields:n}:{}})},"error")},events:{publish:e(async(t,n)=>{if(a)throw new Error("the event bus is down");f.push({topic:t,payload:n})},"publish")},notifications:{send:e(async()=>({id:_()}),"send")},config:{current:e(()=>h,"current"),refresh:e(async()=>h,"refresh")}},schema:w,publishedEvents:f,loggedErrors:c,storedObjects:o,failPublish:e((t=!0)=>{a=t},"failPublish"),setConfig:e(t=>{h={...t}},"setConfig"),reset:e(async()=>{const t=await i.query(`select quote_ident(tablename) as name
2
+ from pg_tables where schemaname = $1`,[w]);t.length>0&&await i.query(`truncate table ${t.map(n=>n.name).join(", ")} restart identity cascade`),f.length=0,c.length=0,o.clear(),l.clear(),a=!1,h={...s.config??{}}},"reset"),close:e(async()=>{await g.end();const t=new d({connectionString:y});await t.query(`DROP SCHEMA "${w}" CASCADE`),await t.end()},"close")}}e(B,"createTestPluginHost");async function v(s){return(await I(s)).filter(y=>y.endsWith(".sql")).sort().map(y=>P(s,y))}e(v,"migrationFiles");async function U(s){const{readdir:d,readFile:y}=await import("fs/promises"),{verifyFrontendLocales:E}=await import("./verify-W7ZTUCO4.js"),{join:w}=await import("path"),u={list:e(async()=>(await d(s).catch(()=>[])).filter(a=>a.endsWith(".json")),"list"),readText:e(a=>y(w(s,a.split("/").at(-1)),"utf8").catch(()=>null),"readText")},{bundles:m,errors:g}=await E(u,{i18nDirPath:"i18n",nav:[]}),f=[...g],c=e((a,h="",i=new Map)=>{if(typeof a!="object"||a===null||Array.isArray(a))i.set(h,a);else for(const[p,t]of Object.entries(a))c(t,h?`${h}.${p}`:p,i);return i},"leaves"),o=e(a=>[...typeof a=="string"?a.matchAll(/\{\{\s*([\w.-]+)\s*\}\}/g):[]].map(h=>h[1]).sort().join(","),"placeholders"),l=c(m.get("zh-CN"));for(const[a,h]of m)for(const[i,p]of c(h))typeof p=="string"&&p.trim()===""&&f.push(`${a}.json: "${i}" is empty`),a!=="zh-CN"&&l.has(i)&&o(p)!==o(l.get(i))&&f.push(`${a}.json: "${i}" has placeholders {${o(p)}} but zh-CN has {${o(l.get(i))}}`);if(f.length>0)throw new Error(`Locale bundles in ${s} are inconsistent:
3
+ - ${f.join(`
4
+ - `)}`)}e(U,"assertLocaleParity");async function F(s,d){const{readdir:y,readFile:E}=await import("fs/promises"),{join:w}=await import("path"),u=[],m=new Set(d),g=new Set(d.map(c=>c.split("_")[0])),f=await y(s,{withFileTypes:!0}).catch(()=>[]);for(const c of f){if(!c.isDirectory())continue;const o=await E(w(s,c.name,"SKILL.md"),"utf8").catch(()=>null);if(!o){u.push(`${c.name}: no SKILL.md`);continue}const l=/^---\n([\s\S]*?)\n---/.exec(o)?.[1]??"",a=/^name:\s*["']?([^"'\n]+)["']?\s*$/m.exec(l)?.[1]?.trim();a!==c.name&&u.push(`${c.name}: frontmatter name is "${a??"(none)"}"`),/^description:\s*\S/m.test(l)||u.push(`${c.name}: no description`);for(const h of o.matchAll(/`([a-z][a-z0-9]*_[a-z0-9_]+)`/g)){const i=h[1];g.has(i.split("_")[0])&&!m.has(i)&&u.push(`${c.name}: mentions \`${i}\`, which the claw part does not register`)}}if(u.length>0)throw new Error(`Claw skills in ${s} are inconsistent:
5
+ - ${u.join(`
6
+ - `)}`)}e(F,"assertClawSkills");export{z as TEST_TENANT_ID,F as assertClawSkills,U as assertLocaleParity,B as createTestPluginHost};
@@ -0,0 +1 @@
1
+ "use strict";var n=Object.defineProperty;var e=(r,t)=>n(r,"name",{value:t,configurable:!0});var chunk45Y4CR4R_cjs=require("./chunk-45Y4CR4R.cjs");require("./chunk-N4SXZZPY.cjs"),Object.defineProperty(exports,"LOCALE_FILENAME_PATTERN",{enumerable:!0,get:e(function(){return chunk45Y4CR4R_cjs.LOCALE_FILENAME_PATTERN},"get")}),Object.defineProperty(exports,"MIGRATION_FILENAME_PATTERN",{enumerable:!0,get:e(function(){return chunk45Y4CR4R_cjs.MIGRATION_FILENAME_PATTERN},"get")}),Object.defineProperty(exports,"flattenLocaleKeys",{enumerable:!0,get:e(function(){return chunk45Y4CR4R_cjs.flattenLocaleKeys},"get")}),Object.defineProperty(exports,"verifyFrontendLocales",{enumerable:!0,get:e(function(){return chunk45Y4CR4R_cjs.verifyFrontendLocales},"get")}),Object.defineProperty(exports,"verifyMigrationNames",{enumerable:!0,get:e(function(){return chunk45Y4CR4R_cjs.verifyMigrationNames},"get")});
@@ -0,0 +1 @@
1
+ export{LOCALE_FILENAME_PATTERN,MIGRATION_FILENAME_PATTERN,flattenLocaleKeys,verifyFrontendLocales,verifyMigrationNames}from"./chunk-2NTAWYCG.js";import"./chunk-INCA3GFW.js";
package/dist/vite.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";var n=Object.defineProperty;var e=(r,t)=>n(r,"name",{value:t,configurable:!0});var chunkW7C357A6_cjs=require("./chunk-W7C357A6.cjs");require("./chunk-C2OCOPZQ.cjs"),Object.defineProperty(exports,"TEGO_PLUGIN_DEV_OVERRIDES",{enumerable:!0,get:e(function(){return chunkW7C357A6_cjs.TEGO_PLUGIN_DEV_OVERRIDES},"get")}),Object.defineProperty(exports,"tegoPluginFrontend",{enumerable:!0,get:e(function(){return chunkW7C357A6_cjs.tegoPluginFrontend},"get")}),Object.defineProperty(exports,"tegoPluginTailwind",{enumerable:!0,get:e(function(){return chunkW7C357A6_cjs.tegoPluginTailwind},"get")});
1
+ "use strict";var n=Object.defineProperty;var e=(t,r)=>n(t,"name",{value:r,configurable:!0});var chunkHZ4YWRYX_cjs=require("./chunk-HZ4YWRYX.cjs");require("./chunk-6HNG5OZI.cjs"),Object.defineProperty(exports,"TEGO_PLUGIN_DEV_OVERRIDES",{enumerable:!0,get:e(function(){return chunkHZ4YWRYX_cjs.TEGO_PLUGIN_DEV_OVERRIDES},"get")}),Object.defineProperty(exports,"scopeRootForm",{enumerable:!0,get:e(function(){return chunkHZ4YWRYX_cjs.scopeRootForm},"get")}),Object.defineProperty(exports,"tegoPluginFrontend",{enumerable:!0,get:e(function(){return chunkHZ4YWRYX_cjs.tegoPluginFrontend},"get")}),Object.defineProperty(exports,"tegoPluginScopeRoot",{enumerable:!0,get:e(function(){return chunkHZ4YWRYX_cjs.tegoPluginScopeRoot},"get")}),Object.defineProperty(exports,"tegoPluginTailwind",{enumerable:!0,get:e(function(){return chunkHZ4YWRYX_cjs.tegoPluginTailwind},"get")});
package/dist/vite.d.cts CHANGED
@@ -1,5 +1,31 @@
1
1
  import { UserConfig } from 'vite';
2
2
 
3
+ /**
4
+ * Build-time PostCSS step: makes the plugin's scoped utilities also apply on
5
+ * the scope element itself.
6
+ *
7
+ * `tegoPluginTailwind` scopes every utility to *descendants* of
8
+ * `.<pluginId>`. Dialogs, popovers and selects are portalled under `body` and
9
+ * carry the scope class on their own root, which a descendant selector
10
+ * misses: a width class there does nothing. Each scoped rule also gets the
11
+ * compound form `.x.<pluginId>`, of the same specificity.
12
+ *
13
+ * ```ts
14
+ * config.css = { postcss: { plugins: [tailwindcss(tegoPluginTailwind(…)), tegoPluginScopeRoot('acme-crm')] } };
15
+ * ```
16
+ */
17
+ /** The selector that also matches the scope element itself, or null when `selector` is not scoped. */
18
+ declare function scopeRootForm(selector: string, scope: string): string | null;
19
+ interface Rules {
20
+ walkRules(visit: (rule: {
21
+ selectors: string[];
22
+ }) => void): void;
23
+ }
24
+ declare function tegoPluginScopeRoot(pluginId: string): {
25
+ postcssPlugin: string;
26
+ OnceExit(root: Rules): void;
27
+ };
28
+
3
29
  /**
4
30
  * The Tailwind config a plugin frontend compiles its own utilities with.
5
31
  *
@@ -130,4 +156,4 @@ declare const TEGO_PLUGIN_DEV_OVERRIDES: {
130
156
  */
131
157
  declare function tegoPluginFrontend(options: TegoPluginFrontendOptions): UserConfig;
132
158
 
133
- export { TEGO_PLUGIN_DEV_OVERRIDES, type TegoPluginFrontendOptions, type TegoPluginTailwindConfig, type TegoPluginTailwindOptions, tegoPluginFrontend, tegoPluginTailwind };
159
+ export { TEGO_PLUGIN_DEV_OVERRIDES, type TegoPluginFrontendOptions, type TegoPluginTailwindConfig, type TegoPluginTailwindOptions, scopeRootForm, tegoPluginFrontend, tegoPluginScopeRoot, tegoPluginTailwind };
package/dist/vite.d.ts CHANGED
@@ -1,5 +1,31 @@
1
1
  import { UserConfig } from 'vite';
2
2
 
3
+ /**
4
+ * Build-time PostCSS step: makes the plugin's scoped utilities also apply on
5
+ * the scope element itself.
6
+ *
7
+ * `tegoPluginTailwind` scopes every utility to *descendants* of
8
+ * `.<pluginId>`. Dialogs, popovers and selects are portalled under `body` and
9
+ * carry the scope class on their own root, which a descendant selector
10
+ * misses: a width class there does nothing. Each scoped rule also gets the
11
+ * compound form `.x.<pluginId>`, of the same specificity.
12
+ *
13
+ * ```ts
14
+ * config.css = { postcss: { plugins: [tailwindcss(tegoPluginTailwind(…)), tegoPluginScopeRoot('acme-crm')] } };
15
+ * ```
16
+ */
17
+ /** The selector that also matches the scope element itself, or null when `selector` is not scoped. */
18
+ declare function scopeRootForm(selector: string, scope: string): string | null;
19
+ interface Rules {
20
+ walkRules(visit: (rule: {
21
+ selectors: string[];
22
+ }) => void): void;
23
+ }
24
+ declare function tegoPluginScopeRoot(pluginId: string): {
25
+ postcssPlugin: string;
26
+ OnceExit(root: Rules): void;
27
+ };
28
+
3
29
  /**
4
30
  * The Tailwind config a plugin frontend compiles its own utilities with.
5
31
  *
@@ -130,4 +156,4 @@ declare const TEGO_PLUGIN_DEV_OVERRIDES: {
130
156
  */
131
157
  declare function tegoPluginFrontend(options: TegoPluginFrontendOptions): UserConfig;
132
158
 
133
- export { TEGO_PLUGIN_DEV_OVERRIDES, type TegoPluginFrontendOptions, type TegoPluginTailwindConfig, type TegoPluginTailwindOptions, tegoPluginFrontend, tegoPluginTailwind };
159
+ export { TEGO_PLUGIN_DEV_OVERRIDES, type TegoPluginFrontendOptions, type TegoPluginTailwindConfig, type TegoPluginTailwindOptions, scopeRootForm, tegoPluginFrontend, tegoPluginScopeRoot, tegoPluginTailwind };
package/dist/vite.js CHANGED
@@ -1 +1 @@
1
- export{TEGO_PLUGIN_DEV_OVERRIDES,tegoPluginFrontend,tegoPluginTailwind}from"./chunk-7K37PVGV.js";import"./chunk-7XHF3YSK.js";
1
+ export{TEGO_PLUGIN_DEV_OVERRIDES,scopeRootForm,tegoPluginFrontend,tegoPluginScopeRoot,tegoPluginTailwind}from"./chunk-I5Z3ECC4.js";import"./chunk-56QOZQTY.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhama/tego-plugin-sdk",
3
- "version": "1.36.1",
3
+ "version": "1.37.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",
@@ -103,6 +103,33 @@
103
103
  "import": "./dist/publish.js",
104
104
  "default": "./dist/publish.js"
105
105
  },
106
+ "./react": {
107
+ "types": "./dist/react.d.ts",
108
+ "import": "./dist/react.js",
109
+ "require": "./dist/react.cjs",
110
+ "default": "./dist/react.js"
111
+ },
112
+ "./hono": {
113
+ "types": "./dist/hono.d.ts",
114
+ "import": "./dist/hono.js",
115
+ "require": "./dist/hono.cjs",
116
+ "default": "./dist/hono.js"
117
+ },
118
+ "./migrations-sync": {
119
+ "types": "./dist/migrations-sync.d.ts",
120
+ "import": "./dist/migrations-sync.js",
121
+ "default": "./dist/migrations-sync.js"
122
+ },
123
+ "./doctor": {
124
+ "types": "./dist/doctor.d.ts",
125
+ "import": "./dist/doctor.js",
126
+ "default": "./dist/doctor.js"
127
+ },
128
+ "./dev": {
129
+ "types": "./dist/dev.d.ts",
130
+ "import": "./dist/dev.js",
131
+ "default": "./dist/dev.js"
132
+ },
106
133
  "./testing": {
107
134
  "types": "./dist/testing.d.ts",
108
135
  "import": "./dist/testing.js",
@@ -118,12 +145,15 @@
118
145
  "bin",
119
146
  "sql",
120
147
  "access-fixtures",
121
- "access-catalog.schema.json"
148
+ "access-catalog.schema.json",
149
+ "agent"
122
150
  ],
123
151
  "dependencies": {
124
152
  "esbuild": "^0.25.12",
125
153
  "jszip": "^3.10.1",
126
154
  "magic-string": "^0.30.21",
155
+ "prettier": "^3.7.4",
156
+ "tailwindcss": "^3.4.18",
127
157
  "zod": "^3.24.1",
128
158
  "zod-to-json-schema": "^3.25.2"
129
159
  },
@@ -131,7 +161,8 @@
131
161
  "pg": "^8.13.1",
132
162
  "vite": "^6.0.0",
133
163
  "hono": "^4.10.7",
134
- "drizzle-orm": "^0.38.3"
164
+ "drizzle-orm": "^0.38.3",
165
+ "react": "^19.0.0"
135
166
  },
136
167
  "peerDependenciesMeta": {
137
168
  "pg": {
@@ -145,6 +176,9 @@
145
176
  },
146
177
  "drizzle-orm": {
147
178
  "optional": true
179
+ },
180
+ "react": {
181
+ "optional": true
148
182
  }
149
183
  },
150
184
  "description": "Create, build, package and test Tego plugins: tego-plugin create, manifest, frontend/backend APIs, access control and tego-plugin-pack",