@zhama/tego-plugin-sdk 1.36.2 → 1.37.1

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 (93) hide show
  1. package/agent/RULES.md +123 -0
  2. package/agent/guides/access.md +39 -0
  3. package/agent/guides/backend.md +76 -0
  4. package/agent/guides/claw.md +18 -0
  5. package/agent/guides/desktop-web.md +48 -0
  6. package/agent/guides/frontend.md +83 -0
  7. package/agent/guides/host-api-versions.md +34 -0
  8. package/agent/guides/i18n.md +17 -0
  9. package/agent/guides/manifest.md +26 -0
  10. package/agent/guides/migrations.md +17 -0
  11. package/agent/guides/storage.md +18 -0
  12. package/agent/guides/testing.md +17 -0
  13. package/agent/guides/ui.md +22 -0
  14. package/agent/reference/plugin-sdk-api.md +561 -0
  15. package/bin/tego-plugin-pack.mjs +1 -1
  16. package/bin/tego-plugin.mjs +4 -2
  17. package/dist/chunk-2NTAWYCG.js +1 -0
  18. package/dist/chunk-2P6UEAPV.cjs +1 -0
  19. package/dist/chunk-45Y4CR4R.cjs +1 -0
  20. package/dist/{chunk-PWSTCQRN.cjs → chunk-4LKRXDYY.cjs} +13 -12
  21. package/dist/{chunk-7XHF3YSK.js → chunk-56QOZQTY.js} +1 -1
  22. package/dist/{chunk-C2OCOPZQ.cjs → chunk-6HNG5OZI.cjs} +1 -1
  23. package/dist/{chunk-2ORO36BY.js → chunk-DEGQDJF6.js} +1 -1
  24. package/dist/chunk-EIVA5BR6.js +1 -0
  25. package/dist/{chunk-P2Q2AKWP.js → chunk-FCYAAOHC.js} +17 -16
  26. package/dist/chunk-HZ4YWRYX.cjs +5 -0
  27. package/dist/chunk-I5Z3ECC4.js +5 -0
  28. package/dist/chunk-INCA3GFW.js +1 -0
  29. package/dist/chunk-IVJECPGG.js +1 -0
  30. package/dist/{chunk-KMN5RHLX.cjs → chunk-MWMGJJ74.cjs} +1 -1
  31. package/dist/chunk-N4SXZZPY.cjs +1 -0
  32. package/dist/chunk-SKNPNPJP.cjs +1 -0
  33. package/dist/dev.cjs +165 -59
  34. package/dist/dev.d.cts +37 -1
  35. package/dist/dev.d.ts +37 -1
  36. package/dist/dev.js +168 -62
  37. package/dist/doctor.cjs +8 -0
  38. package/dist/doctor.d.cts +62 -0
  39. package/dist/doctor.d.ts +62 -0
  40. package/dist/doctor.js +8 -0
  41. package/dist/frontend.d.cts +33 -1
  42. package/dist/frontend.d.ts +33 -1
  43. package/dist/hono.cjs +1 -0
  44. package/dist/hono.d.cts +84 -0
  45. package/dist/hono.d.ts +84 -0
  46. package/dist/hono.js +1 -0
  47. package/dist/index.cjs +1 -1
  48. package/dist/index.d.cts +6 -1
  49. package/dist/index.d.ts +6 -1
  50. package/dist/index.js +1 -1
  51. package/dist/manifest.cjs +1 -1
  52. package/dist/manifest.d.cts +55 -2
  53. package/dist/manifest.d.ts +55 -2
  54. package/dist/manifest.js +1 -1
  55. package/dist/migrations-sync.cjs +1 -0
  56. package/dist/migrations-sync.d.cts +20 -0
  57. package/dist/migrations-sync.d.ts +20 -0
  58. package/dist/migrations-sync.js +1 -0
  59. package/dist/pack.cjs +1 -1
  60. package/dist/pack.d.cts +5 -0
  61. package/dist/pack.d.ts +5 -0
  62. package/dist/pack.js +1 -1
  63. package/dist/publish.cjs +1 -1
  64. package/dist/publish.js +1 -1
  65. package/dist/react.cjs +1 -0
  66. package/dist/react.d.cts +101 -0
  67. package/dist/react.d.ts +101 -0
  68. package/dist/react.js +1 -0
  69. package/dist/scaffold.cjs +671 -17
  70. package/dist/scaffold.d.cts +42 -18
  71. package/dist/scaffold.d.ts +42 -18
  72. package/dist/scaffold.js +673 -19
  73. package/dist/server-runtime.cjs +1 -1
  74. package/dist/server-runtime.d.cts +27 -2
  75. package/dist/server-runtime.d.ts +27 -2
  76. package/dist/server-runtime.js +1 -1
  77. package/dist/testing.cjs +6 -2
  78. package/dist/testing.d.cts +19 -1
  79. package/dist/testing.d.ts +19 -1
  80. package/dist/testing.js +6 -2
  81. package/dist/verify-FF2RD2MV.cjs +1 -0
  82. package/dist/verify-W7ZTUCO4.js +1 -0
  83. package/dist/vite.cjs +1 -1
  84. package/dist/vite.d.cts +27 -1
  85. package/dist/vite.d.ts +27 -1
  86. package/dist/vite.js +1 -1
  87. package/package.json +31 -3
  88. package/dist/chunk-3WA3M347.cjs +0 -1
  89. package/dist/chunk-5CU33JVY.js +0 -1
  90. package/dist/chunk-7K37PVGV.js +0 -5
  91. package/dist/chunk-BTHXQWHT.js +0 -1
  92. package/dist/chunk-ROFUQLOZ.cjs +0 -1
  93. package/dist/chunk-W7C357A6.cjs +0 -5
package/dist/doctor.js ADDED
@@ -0,0 +1,8 @@
1
+ var z=Object.defineProperty;var d=(e,t)=>z(e,"name",{value:t,configurable:!0});import{parseTegoPluginManifest as F}from"./chunk-EIVA5BR6.js";import{verifyFrontendLocales as G,flattenLocaleKeys as H,verifyMigrationNames as Z}from"./chunk-2NTAWYCG.js";import{resolvePluginParts as q}from"./chunk-IVJECPGG.js";import{pluginI18nNamespace as J}from"./chunk-INCA3GFW.js";import{isValidHostApiRange as Q,hostApiRangeMinimum as V}from"./chunk-56QOZQTY.js";import"./chunk-VMCGDDGP.js";import"./chunk-SNUCO2MK.js";import{execFile as X}from"child_process";import{stat as Y,readFile as x,readdir as C}from"fs/promises";import{join as P,relative as ee,extname as I,sep as te}from"path";var oe="ui-colors-allow",re="slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose",se="bg|text|border(?:-[trblxyse])?|ring(?:-offset)?|from|via|to|fill|stroke|outline|divide|placeholder|caret|accent|decoration|shadow",ne=[new RegExp(`(?<![\\w-])(?:${se})-(?:(?:${re})-(?:50|[1-9]00|950)|white|black)(?:\\/\\d{1,3})?(?![\\w-])`,"g"),/-\[(?:#[0-9a-fA-F]{3,8}|(?:rgba?|hsla?)\(\s*\d+)/g,/(?<![a-zA-Z0-9[-])(?:rgba?|hsla?)\(\s*\d+/g],ae=/['"`]#[0-9a-fA-F]{3,8}['"`]/g,ie=/(?<![\w&-])#[0-9a-fA-F]{3,8}(?![\w-])/g,j=/(?<![\w-])dark:(?=\S)/g,ce=/\.dark(?![\w-])/g;function B(e){return e.replace(/(^|[\s{(,;])\/\*[\s\S]*?\*\//g,(t,o)=>o+t.slice(o.length).replace(/[^\n]/g," ")).replace(/(^|\s)\/\/.*$/gm,(t,o)=>o)}d(B,"stripComments");function le(e){const t=new Set;return e.forEach((o,r)=>{o.includes(oe)&&(t.add(r),/^\s*(?:\/\/|\/\*|\*|\{\/\*)/.test(o)&&t.add(r+1))}),t}d(le,"allowedLines");function L(e,t){const o=t===".css",r=le(e.split(`
2
+ `)),c=B(e).split(`
3
+ `),i=[...ne,o?ie:ae],s=o?[j,ce]:[j],p=[];return c.forEach((m,n)=>{if(r.has(n))return;const u=[];for(const[a,l]of[["color",i],["dark",s]])for(const b of l)for(const v of m.matchAll(b))u.push({kind:a,match:v[0],at:v.index??0});u.sort((a,l)=>a.kind.localeCompare(l.kind)||a.at-l.at).forEach(({kind:a,match:l})=>p.push({line:n+1,kind:a,match:l}))}),p}d(L,"findColorIssues");var h=d((e,t,o,r)=>({id:e,severity:t,guide:o,title:r}),"rule"),N=[h("TP-MAN-001","error","manifest.md","tego.plugin.json is valid"),h("TP-MAN-002","error","host-api-versions.md","hostApiRange admits every host capability the code uses"),h("TP-PKG-001","error","manifest.md","SDK packages come from npm aliases or workspace links, never a plain @tego/* name"),h("TP-PKG-002","warning","testing.md","package.json has the lint, type-check, test and build gates"),h("TP-CLAW-002","warning","claw.md","Each claw skill's frontmatter name matches its folder"),h("TP-DESK-001","error","desktop-web.md","No eval or new Function (the desktop CSP refuses them)"),h("TP-DESK-002","error","desktop-web.md","No addresses built from location.origin (tauri://localhost on desktop)"),h("TP-DESK-003","error","desktop-web.md","External links open with ctx.openExternal, not window.open"),h("TP-DESK-004","error","desktop-web.md","Files are saved with ctx.download, not <a download> or a.download"),h("TP-DESK-005","warning","desktop-web.md","No streamed responses or EventSource from the frontend (the desktop proxy buffers; use ctx.events)"),h("TP-DESK-006","warning","desktop-web.md","Clipboard writes go through ctx.copyText"),h("TP-WEB-001","error","desktop-web.md","The frontend calls no third-party URL (the web CSP blocks it); call the backend, which declares egress"),h("TP-WEB-002","error","desktop-web.md","No remote or injected scripts; everything ships in the bundle"),h("TP-UI-001","error","ui.md","Colors come from token classes: no hex, rgb(), palette classes or dark: overrides"),h("TP-UI-002","error","ui.md","No direct @radix-ui/* imports; use @tego/ui"),h("TP-UI-003","error","ui.md","The plugin's root element carries its scope class (.<pluginId>) when it compiles its own Tailwind"),h("TP-UI-004","error","frontend.md","The frontend has its own error boundary"),h("TP-I18N-001","error","i18n.md","All ten locales, with the same keys as zh-CN, and every nav label defined"),h("TP-I18N-002","error","i18n.md","Every locale value keeps the {{placeholders}} of zh-CN and is not empty"),h("TP-I18N-003","error","i18n.md","Every literal t('\u2026') key exists in zh-CN"),h("TP-I18N-004","warning","i18n.md","Current terminology: \u6570\u5B57\u5458\u5DE5 / Employee, never \u6570\u5B57\u5206\u8EAB / digital avatar"),h("TP-BE-001","error","backend.md","Identity comes from the verified ctx, never from the raw x-tego-plugin-* headers"),h("TP-BE-002","error","access.md","No host role names in business code; ask the access engine"),h("TP-BE-003","error","migrations.md","Migrations are <ordinal>_<name>.sql with unique ordinals"),h("TP-BE-004","error","migrations.md","Committed migrations are never edited or deleted"),h("TP-BE-005","warning","backend.md","Backend errors are logged through host.log, not console"),h("TP-ACC-001","error","access.md","With an access catalog, a test asserts every route has a policy (assertRouteCoverage)"),h("TP-ACC-002","error","access.md","With an access catalog, start() reconciles it (reconcile(host))"),h("TP-TEST-001","warning","testing.md","The plugin has tests")],de=new Map(N.map(e=>[e.id,e]));function _(e){return de.get(e)}d(_,"doctorRule");var ue=[{pattern:/\.openExternal\s*\(/,since:"1.37.0",name:"ctx.openExternal"},{pattern:/\.copyText\s*\(/,since:"1.37.0",name:"ctx.copyText"},{pattern:/\b(?:ctx|host|context)\.download\s*\(/,since:"1.37.0",name:"ctx.download"},{pattern:/\b(?:ctx|host|context)\.platform\b/,since:"1.37.0",name:"ctx.platform"}],pe=new Set([".ts",".tsx",".mts",".cts",".js",".jsx",".mjs",".css"]),he=new Set(["node_modules","dist","build",".tego-dev",".turbo",".drizzle","__fixtures__","coverage"]),fe=/\.(?:test|spec)\.[cm]?[jt]sx?$|(?:^|[\\/])test[\\/]|(?:^|[\\/])test-[\w-]+\.[cm]?[jt]s$|-double\.[cm]?[jt]s$|\.d\.ts$/,me=/tego-doctor-ignore\s+([A-Z0-9-]+(?:\s*,\s*[A-Z0-9-]+)*)/,ge=/\{\{\s*([\w.-]+)\s*\}\}/g,we=["lint","type-check","test","build"];function Te(e){return e.split(te).join("/")}d(Te,"toPosix");async function K(e,t,o){let r;try{r=await C(P(e,t),{withFileTypes:!0})}catch{return}for(const c of r){const i=t?`${t}/${c.name}`:c.name;c.isDirectory()?!he.has(c.name)&&!c.name.startsWith(".")&&await K(e,i,o):pe.has(I(c.name))&&o.push(i)}}d(K,"walk");function O(e){return{async list(t){try{return(await C(P(e,t),{withFileTypes:!0})).filter(r=>r.isFile()).map(r=>r.name)}catch{return[]}},readText:d(t=>x(P(e,t),"utf8").catch(()=>null),"readText")}}d(O,"folderReader");function M(e,t){return new Promise(o=>{X("git",["-C",e,...t],{maxBuffer:8*1024*1024},(r,c)=>o(r?null:c))})}d(M,"runGit");function R(e){const t=new Map;return e.split(`
4
+ `).forEach((o,r)=>{const c=me.exec(o);if(!c?.[1])return;const i=new Set(c[1].split(",").map(p=>p.trim())),s=d(p=>t.set(p,new Set([...t.get(p)??[],...i])),"add");s(r),/^\s*(?:\/\/|\/\*|\*|\{\/\*)/.test(o)&&s(r+1)}),t}d(R,"ignoredLines");var be=class{static{d(this,"Findings")}list=[];add(e,t,o={}){const{severity:r,guide:c}=_(e);this.list.push({rule:e,severity:r,message:t,guide:c,...o})}};function w(e,t,o,r){const c=R(e.text);e.code.split(`
5
+ `).forEach((i,s)=>{if(c.get(s)?.has(t))return;const p=o.exec(i);p&&r(s+1,p[0])})}d(w,"eachMatch");function Pe(e,t){const o=d(r=>!!r&&(e===r||e.startsWith(`${r}/`)),"inside");return o(t.frontend?.dir)||o(t.publicFrontend?.dir)?"frontend":o(t.backend?.dir)?"backend":o(t.claw?.dir)?"claw":o("shared")?"shared":"other"}d(Pe,"partOf");async function ye(e){try{return JSON.parse(await x(e,"utf8"))}catch{return}}d(ye,"readJson");function ke(e,t){if(!t)return;for(const c of["dependencies","devDependencies","peerDependencies"]){const i=t[c];if(!(!i||typeof i!="object"))for(const[s,p]of Object.entries(i))!s.startsWith("@tego/")||typeof p!="string"||/^(?:npm:@zhama\/tego-|workspace:|file:|link:|portal:)/.test(p)||e.add("TP-PKG-001",`${c}.${s} is "${p}": on npm @tego/* belongs to another project. Install it as "npm:@zhama/tego-${s.slice(6)}@^<sdk version>".`,{file:"package.json"})}const o=t.scripts??{},r=we.filter(c=>typeof o[c]!="string");r.length>0&&e.add("TP-PKG-002",`package.json has no ${r.map(c=>`"${c}"`).join(", ")} script; the platform's gates are lint, type-check, test and build.`,{file:"package.json"})}d(ke,"checkPackageJson");async function Ee(e,t,o,r,c){const i=r.frontend;if(!i)return;const{bundles:s,errors:p}=await G(O(t),i);for(const f of p)e.add("TP-I18N-001",f,{file:i.i18nDirPath});const m=s.get("zh-CN");if(!m)return;const n=d((f,g="",y=new Map)=>{if(typeof f!="object"||f===null||Array.isArray(f))return y.set(g,f),y;for(const[E,k]of Object.entries(f))n(k,g?`${g}.${E}`:E,y);return y},"leaves"),u=d(f=>new Set(typeof f=="string"?[...f.matchAll(ge)].map(g=>g[1]):[]),"placeholders"),a=n(m);for(const[f,g]of s){const y=`${i.i18nDirPath}/${f}.json`;let E=0;for(const[k,T]of n(g)){if(E>=10)break;if(typeof T=="string"&&T.trim()===""){e.add("TP-I18N-002",`"${k}" is empty in ${f}.json.`,{file:y}),E++;continue}if(f==="zh-CN"||!a.has(k))continue;const S=u(a.get(k)),$=u(T),U=[...S].filter(A=>!$.has(A)),W=[...$].filter(A=>!S.has(A));(U.length||W.length)&&(e.add("TP-I18N-002",`"${k}" in ${f}.json has placeholders {${[...$].join(", ")}} but zh-CN has {${[...S].join(", ")}}.`,{file:y}),E++)}for(const[k,T]of n(g))if(typeof T=="string"&&/数字分身|digital avatar/i.test(T)){e.add("TP-I18N-004",`"${k}" in ${f}.json says "${/数字分身/.test(T)?"\u6570\u5B57\u5206\u8EAB":"digital avatar"}"; the platform calls them \u6570\u5B57\u5458\u5DE5 / Employee ({{avatarTerm}}).`,{file:y});break}}const l=H(m),b=new Set(Object.keys(m)),v=d(f=>l.has(f)||["_one","_other","_zero","_few","_many","_two"].some(g=>l.has(f+g))||[...l].some(g=>g.startsWith(`${f}.`)),"exists");for(const f of c){if(f.part!=="frontend"||f.test)continue;const g=R(f.text);f.code.split(`
6
+ `).forEach((y,E)=>{if(!g.get(E)?.has("TP-I18N-003"))for(const k of y.matchAll(/\bt\(\s*['"]([A-Za-z0-9_.-]+)['"]/g)){const T=k[1],S=T.split(".")[0];!T.includes(".")||!b.has(S)||v(T)||e.add("TP-I18N-003",`t('${T}') has no entry in zh-CN.json (namespace ${J(o.id)}).`,{file:f.path,line:E+1})}})}}d(Ee,"checkLocales");async function ve(e,t,o){const r=o.backend;if(!r)return;const{errors:c}=await Z(O(t),r.migrationsDirPath);for(const p of c)e.add("TP-BE-003",p,{file:r.migrationsDirPath});const i=await M(t,["diff","--name-status","HEAD","--",r.migrationsDirPath]);if(!i)return;const s=(await M(t,["rev-parse","--show-prefix"]))?.trim()??"";for(const p of i.split(`
7
+ `)){const[m,n]=p.split(" ");if(!n||m!=="M"&&m!=="D")continue;const u=n.startsWith(s)?n.slice(s.length):n;e.add("TP-BE-004",`${u} is a committed migration and was ${m==="M"?"edited":"deleted"}; an install may have applied it already. Revert it and add a new migration instead.`,{file:u})}}d(ve,"checkMigrations");async function xe(e,t,o){const r=o.claw;if(!r)return;const c=P(t,r.dir,"skills");let i=[];try{i=(await C(c,{withFileTypes:!0})).filter(s=>s.isDirectory()).map(s=>s.name)}catch{return}for(const s of i){const p=await x(P(c,s,"SKILL.md"),"utf8").catch(()=>null),m=`${r.dir}/skills/${s}/SKILL.md`,n=p?/^---[\s\S]*?^name:\s*["']?([^"'\n]+)["']?\s*$/m.exec(p)?.[1]?.trim():void 0;n!==s&&e.add("TP-CLAW-002",p?`${m} declares name "${n??"(none)"}"; it must equal the folder name "${s}".`:`${r.dir}/skills/${s} has no SKILL.md.`,{file:m})}}d(xe,"checkClaw");function Se(e,t,o,r,c,i){const s=t.hostApiRange&&Q(t.hostApiRange)?V(t.hostApiRange):void 0,p=d(n=>{if(!s)return!1;const[u,a,l]=s.split(".").map(Number),[b,v,f]=n.split(".").map(Number);return u!==b?u<b:a!==v?a<v:l<f},"below");for(const n of r){if(!c(n)||n.test)continue;const u=d(a=>({file:n.path,line:a}),"at");if(n.part==="frontend"){if(n.path.endsWith(".css")||n.path.endsWith(".tsx")||n.path.endsWith(".ts")){const a=R(n.text);for(const l of L(n.text,I(n.path)))a.get(l.line-1)?.has("TP-UI-001")||e.add("TP-UI-001",l.kind==="dark"?`"${l.match}" is a hand-written dark override; tokens already carry both themes.`:`"${l.match}" is a literal color; use a token class (bg-card, text-muted-foreground, bg-primary\u2026).`,u(l.line))}w(n,"TP-DESK-001",/\beval\s*\(|\bnew\s+Function\s*\(/,a=>e.add("TP-DESK-001","eval / new Function: the desktop app's CSP (script-src 'self') refuses code compiled from strings.",u(a))),w(n,"TP-DESK-002",/\b(?:window\.|document\.)?location\.origin\b/,a=>e.add("TP-DESK-002","location.origin is tauri://localhost in the desktop app; build host addresses with ctx.hostUrl(path) and share links from ctx.publicPagesUrl.",u(a))),w(n,"TP-DESK-003",/\bwindow\.open\s*\(/,a=>e.add("TP-DESK-003","window.open does not reliably open links in the desktop app; use ctx.openExternal(url) (hostApiRange ^1.37.0), or ctx.download for a file.",u(a))),w(n,"TP-DESK-004",/\.download\s*=(?!=)|<a\b[^>]*\sdownload(?:[\s=>]|$)/,a=>e.add("TP-DESK-004","An <a download> save is not handled by the desktop app; use ctx.download({ path | blob, fileName }) (hostApiRange ^1.37.0).",u(a))),w(n,"TP-DESK-005",/\bnew\s+EventSource\s*\(|\.body\s*(?:\?\.|!\.|\.)\s*getReader\s*\(/,a=>e.add("TP-DESK-005","The desktop app buffers ctx.api responses whole (and caps them at 100 MB), so streams arrive at the end; subscribe to ctx.events for pushed updates.",u(a))),w(n,"TP-DESK-006",/\bnavigator\.clipboard\./,a=>e.add("TP-DESK-006","navigator.clipboard is not available in every shell; use ctx.copyText(text) (hostApiRange ^1.37.0).",u(a))),w(n,"TP-WEB-001",/\bfetch\s*\(\s*['"`]https?:\/\//,a=>e.add("TP-WEB-001","The web app's CSP allows connections to the platform only; call your backend through ctx.api and let it reach the service (declare it in parts.backend.egress).",u(a))),w(n,"TP-WEB-002",/\bimport\s*\(\s*['"`]https?:|createElement\(\s*['"]script['"]\s*\)|<script\b[^>]*\bsrc=/,a=>e.add("TP-WEB-002","Remote or injected scripts are refused by both shells' CSP; bundle the code.",u(a))),w(n,"TP-UI-002",/from\s+['"]@radix-ui\//,a=>e.add("TP-UI-002","Import the @tego/ui wrapper instead of @radix-ui/* (the shell shares @tego/ui; a second Radix copy breaks focus and portals).",u(a)));for(const a of ue){if(!p(a.since))continue;const l=a.name.split(".").pop();new RegExp(`\\.${l}\\s*(?:\\)|\\?(?!\\.)|&&)`).test(n.code)||w(n,"TP-MAN-002",a.pattern,b=>e.add("TP-MAN-002",`${a.name} needs host API ${a.since}, but hostApiRange is "${t.hostApiRange}"; raise it to ^${a.since}.`,u(b)))}}n.part==="backend"&&(w(n,"TP-BE-001",/\bheaders?\s*(?:\.get)?\(\s*['"]x-tego-plugin-(?:user-id|tenant-id|roles|avatar-id|delegation)['"]|\bheaders\s*\[\s*['"]x-tego-plugin-(?:user-id|tenant-id|roles|avatar-id|delegation)['"]/i,a=>e.add("TP-BE-001","Take the caller from the verified ctx (ctx.userId, ctx.tenantId, ctx.roles) that serveBackend hands you; the raw x-tego-plugin-* headers are unverified here.",u(a))),w(n,"TP-BE-005",/\bconsole\.(?:error|warn)\s*\(/,a=>e.add("TP-BE-005","Log through host.log so the message reaches the platform's logs; console output from a plugin container is not collected.",u(a)))),n.part!=="other"&&w(n,"TP-BE-002",/\b(?:roles|roleNames)\s*\??\.\s*(?:includes|indexOf)\(\s*['"`](?:admin|owner|super_?admin|tenant_?admin|member|manager)['"`]/i,a=>e.add("TP-BE-002","Do not decide by host role names; declare permissions in the access catalog (hostRoleBindings) and ask the engine (authorize / useAccess).",u(a)))}const m=r.filter(n=>n.part==="frontend"&&!n.test);if(o.frontend&&m.length>0&&(m.some(n=>/getDerivedStateFromError|componentDidCatch|PluginErrorBoundary/.test(n.code))||e.add("TP-UI-004","No error boundary in the frontend: the shell's boundary cannot see into your React root, so a render error leaves a blank page. Wrap the root in PluginErrorBoundary (@tego/plugin-sdk/react) or your own.",{file:o.frontend.dir}),i?.includes("tegoPluginTailwind"))){const n=new RegExp(`(?:className|class)=\\{?\\s*(?:cn\\()?\\s*['"\`][^'"\`]*(?<![\\w-])${t.id.replace(/[.]/g,"\\.")}(?![\\w-])`);m.some(u=>n.test(u.code))||e.add("TP-UI-003",`tegoPluginTailwind scopes every utility under .${t.id}, but no element carries className="${t.id}": none of the plugin's own classes will apply. Put it on the root element (and on portalled content, or use tegoPluginScopeRoot).`,{file:o.frontend.dir})}}d(Se,"checkSources");function $e(e,t,o,r){const c=r.filter(s=>s.test);if(c.length===0&&e.add("TP-TEST-001","The plugin has no *.test.ts files; backend routes and pure frontend logic need tests (createTestPluginHost for SQL)."),!t.access||!o.backend)return;c.some(s=>s.text.includes("assertRouteCoverage"))||e.add("TP-ACC-001","The manifest declares an access catalog but no test calls assertRouteCoverage(app): a route without a policy would be open to everyone who can open the plugin.",{file:o.backend.dir}),r.filter(s=>(s.part==="backend"||s.part==="shared")&&!s.test).some(s=>/\breconcile\s*\(/.test(s.code))||e.add("TP-ACC-002","The manifest declares an access catalog but start() never calls reconcile(host): built-in roles and catalog changes never reach the database.",{file:o.backend.dir})}d($e,"checkAccessAndTests");async function Ae(e){const t=e.root;await Y(P(t,"tego.plugin.json")).catch(()=>{throw new Error(`No tego.plugin.json in ${t}; run doctor in the plugin folder.`)});const o=new be;let r;try{r=JSON.parse(await x(P(t,"tego.plugin.json"),"utf8"))}catch(l){return o.add("TP-MAN-001",`tego.plugin.json is not valid JSON: ${l instanceof Error?l.message:String(l)}`,{file:"tego.plugin.json"}),D(o,void 0)}const c=F(r);if(!c.ok||!c.manifest){for(const l of c.errors)o.add("TP-MAN-001",l,{file:"tego.plugin.json"});return D(o,r.id)}const i=c.manifest,s=q(i),p=[];await K(t,"",p);const m=await Promise.all(p.map(async l=>{const b=await x(P(t,l),"utf8");return{path:l,part:Pe(l,s),text:b,code:B(b),test:fe.test(l)}})),n=e.files?.map(l=>Te(ee(t,P(t,l)))),u=d(l=>!n||n.includes(l.path),"scanned"),a=s.frontend?await x(P(t,s.frontend.dir,"vite.config.ts"),"utf8").catch(()=>null):null;return ke(o,await ye(P(t,"package.json"))),await Ee(o,t,i,s,m.filter(u)),await ve(o,t,s),await xe(o,t,s),Se(o,i,s,m,u,a),$e(o,i,s,m),D(o,i.id)}d(Ae,"runDoctor");function D(e,t){const o={errors:e.list.filter(i=>i.severity==="error").length,warnings:e.list.filter(i=>i.severity==="warning").length},r=new Map(N.map((i,s)=>[i.id,s])),c=[...e.list].sort((i,s)=>(i.severity===s.severity?0:i.severity==="error"?-1:1)||(r.get(i.rule)??0)-(r.get(s.rule)??0)||(i.file??"").localeCompare(s.file??"")||(i.line??0)-(s.line??0));return{...t?{pluginId:t}:{},findings:c,...o}}d(D,"summarize");var Ce=["TP-MAN-001","TP-I18N-001","TP-BE-003"];function Ne(e){const t=e.findings.map(o=>{const r=o.file?`${o.file}${o.line?`:${o.line}`:""} `:"";return`${o.severity==="error"?"\u2716":"\u26A0"} ${o.rule} ${r}${o.message}`});return t.push(e.findings.length===0?"\u2705 doctor: no problems":`${e.errors} error(s), ${e.warnings} warning(s). Each rule is explained in node_modules/@tego/plugin-sdk/agent/RULES.md.`),t.join(`
8
+ `)}d(Ne,"formatDoctorReport");export{N as DOCTOR_RULES,Ce as UPLOAD_BLOCKING_RULES,_ as doctorRule,L as findColorIssues,Ne as formatDoctorReport,Ae as runDoctor};
@@ -306,7 +306,39 @@ interface PluginHostContext {
306
306
  * catalog (`@tego/plugin-sdk/access`).
307
307
  */
308
308
  can(navKey: string): boolean;
309
+ /**
310
+ * Which shell runs the page (host API 1.37). Write code that is correct in
311
+ * both rather than branching on this; it is for wording ("Open in browser")
312
+ * and diagnostics. Absent on older hosts.
313
+ */
314
+ platform?: 'web' | 'desktop';
315
+ /**
316
+ * Opens an external http(s) URL in the system browser (desktop) or a new tab
317
+ * (web). `window.open` is unreliable inside the desktop webview. Host API 1.37.
318
+ */
319
+ openExternal?(url: string): Promise<void>;
320
+ /**
321
+ * Saves a file: a host path the backend issued (`signedPath`, `managedPath`)
322
+ * or content the page generated. The desktop app shows a native save
323
+ * dialog; `<a download>` does nothing there. Resolves once the save started
324
+ * (web) or finished (desktop); rejects on failure, not on a cancelled
325
+ * dialog. Host API 1.37.
326
+ */
327
+ download?(request: PluginDownloadRequest): Promise<void>;
328
+ /** Copies text to the clipboard; `false` when the shell refused. Host API 1.37. */
329
+ copyText?(text: string): Promise<boolean>;
309
330
  }
331
+ /** What `PluginHostContext.download` saves. */
332
+ type PluginDownloadRequest = {
333
+ /** A root-relative host path from the backend (`signedPath`, `managedPath`). */
334
+ path: string;
335
+ /** Suggested name; defaults to what the server sends. */
336
+ fileName?: string;
337
+ } | {
338
+ /** Content generated in the page (a CSV, a template). */
339
+ blob: Blob;
340
+ fileName: string;
341
+ };
310
342
  /** Metadata produced by the plugin after applying its document and pricing permissions. */
311
343
  interface PluginOfficeArtifact {
312
344
  fileName: string;
@@ -350,4 +382,4 @@ interface PluginFrontendModule {
350
382
  /** Identity helper for inference, mirroring claw's `defineToolPlugin`. */
351
383
  declare function definePluginFrontend(module: PluginFrontendModule): PluginFrontendModule;
352
384
 
353
- export { type PluginAiDraftErrorCode, type PluginAiDraftRequest, type PluginAiFieldValue, type PluginAiForm, type PluginAiFormField, type PluginAiFormRegistry, type PluginAiFormSnapshot, type PluginApiClient, type PluginDelegationState, type PluginDelegationStore, type PluginEvent, type PluginEventStream, type PluginFrontendModule, type PluginFrontendUserContext, type PluginHostContext, type PluginOfficeArtifact, type PluginRoute, type PluginRouteStore, type PluginUserContext, buildPluginAiFormPrompt, createPluginAiFormRegistry, definePluginFrontend, resolvePluginAiFormPatch };
385
+ export { type PluginAiDraftErrorCode, type PluginAiDraftRequest, type PluginAiFieldValue, type PluginAiForm, type PluginAiFormField, type PluginAiFormRegistry, type PluginAiFormSnapshot, type PluginApiClient, type PluginDelegationState, type PluginDelegationStore, type PluginDownloadRequest, type PluginEvent, type PluginEventStream, type PluginFrontendModule, type PluginFrontendUserContext, type PluginHostContext, type PluginOfficeArtifact, type PluginRoute, type PluginRouteStore, type PluginUserContext, buildPluginAiFormPrompt, createPluginAiFormRegistry, definePluginFrontend, resolvePluginAiFormPatch };
@@ -306,7 +306,39 @@ interface PluginHostContext {
306
306
  * catalog (`@tego/plugin-sdk/access`).
307
307
  */
308
308
  can(navKey: string): boolean;
309
+ /**
310
+ * Which shell runs the page (host API 1.37). Write code that is correct in
311
+ * both rather than branching on this; it is for wording ("Open in browser")
312
+ * and diagnostics. Absent on older hosts.
313
+ */
314
+ platform?: 'web' | 'desktop';
315
+ /**
316
+ * Opens an external http(s) URL in the system browser (desktop) or a new tab
317
+ * (web). `window.open` is unreliable inside the desktop webview. Host API 1.37.
318
+ */
319
+ openExternal?(url: string): Promise<void>;
320
+ /**
321
+ * Saves a file: a host path the backend issued (`signedPath`, `managedPath`)
322
+ * or content the page generated. The desktop app shows a native save
323
+ * dialog; `<a download>` does nothing there. Resolves once the save started
324
+ * (web) or finished (desktop); rejects on failure, not on a cancelled
325
+ * dialog. Host API 1.37.
326
+ */
327
+ download?(request: PluginDownloadRequest): Promise<void>;
328
+ /** Copies text to the clipboard; `false` when the shell refused. Host API 1.37. */
329
+ copyText?(text: string): Promise<boolean>;
309
330
  }
331
+ /** What `PluginHostContext.download` saves. */
332
+ type PluginDownloadRequest = {
333
+ /** A root-relative host path from the backend (`signedPath`, `managedPath`). */
334
+ path: string;
335
+ /** Suggested name; defaults to what the server sends. */
336
+ fileName?: string;
337
+ } | {
338
+ /** Content generated in the page (a CSV, a template). */
339
+ blob: Blob;
340
+ fileName: string;
341
+ };
310
342
  /** Metadata produced by the plugin after applying its document and pricing permissions. */
311
343
  interface PluginOfficeArtifact {
312
344
  fileName: string;
@@ -350,4 +382,4 @@ interface PluginFrontendModule {
350
382
  /** Identity helper for inference, mirroring claw's `defineToolPlugin`. */
351
383
  declare function definePluginFrontend(module: PluginFrontendModule): PluginFrontendModule;
352
384
 
353
- export { type PluginAiDraftErrorCode, type PluginAiDraftRequest, type PluginAiFieldValue, type PluginAiForm, type PluginAiFormField, type PluginAiFormRegistry, type PluginAiFormSnapshot, type PluginApiClient, type PluginDelegationState, type PluginDelegationStore, type PluginEvent, type PluginEventStream, type PluginFrontendModule, type PluginFrontendUserContext, type PluginHostContext, type PluginOfficeArtifact, type PluginRoute, type PluginRouteStore, type PluginUserContext, buildPluginAiFormPrompt, createPluginAiFormRegistry, definePluginFrontend, resolvePluginAiFormPatch };
385
+ export { type PluginAiDraftErrorCode, type PluginAiDraftRequest, type PluginAiFieldValue, type PluginAiForm, type PluginAiFormField, type PluginAiFormRegistry, type PluginAiFormSnapshot, type PluginApiClient, type PluginDelegationState, type PluginDelegationStore, type PluginDownloadRequest, type PluginEvent, type PluginEventStream, type PluginFrontendModule, type PluginFrontendUserContext, type PluginHostContext, type PluginOfficeArtifact, type PluginRoute, type PluginRouteStore, type PluginUserContext, buildPluginAiFormPrompt, createPluginAiFormRegistry, definePluginFrontend, resolvePluginAiFormPatch };
package/dist/hono.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";var u=Object.defineProperty;var n=(s,e)=>u(s,"name",{value:e,configurable:!0});var HttpError=class extends Error{static{n(this,"HttpError")}constructor(s,e,t,r){super(t),this.status=s,this.code=e,this.detail=r,this.name="HttpError"}status;code;detail},badRequest=n((s,e,t)=>new HttpError(400,s,e,t),"badRequest"),forbidden=n((s,e)=>new HttpError(403,s,e),"forbidden"),notFound=n(s=>new HttpError(404,"not_found",`${s} was not found or is not visible to you.`),"notFound"),conflict=n((s,e,t)=>new HttpError(409,s,e,t),"conflict");function firstIssue(s,e){const t=s.issues[0],r=t?.path.map(String).join(".")??"",o=r.length>0?r:e;return{path:o,message:`${o}: ${t?.message??"invalid"}`}}n(firstIssue,"firstIssue");async function readBody(s,e){let t;try{t=await s.req.json()}catch{throw badRequest("invalid_json","The request body is not valid JSON.")}const r=e.safeParse(t);if(r.success)return r.data;const{path:o,message:a}=firstIssue(r.error,"body");throw badRequest("invalid_body",a,{path:o})}n(readBody,"readBody");function readQuery(s,e){const t=e.safeParse(s.req.query());if(t.success)return t.data;const{path:r,message:o}=firstIssue(t.error,"query");throw badRequest("invalid_query",o,{path:r})}n(readQuery,"readQuery");var jsonNotFound=n(s=>s.json({error:{code:"no_route",message:`No such route: ${s.req.method} ${new URL(s.req.url).pathname}`}},404),"jsonNotFound");function accessError(s){if(s instanceof Error&&!(s.name!=="AccessDeniedError"&&s.name!=="AccessAdminError"))return s}n(accessError,"accessError");function pluginErrorHandler(s={}){return(e,t)=>{const r=accessError(e);if(r)return r.name==="AccessAdminError"?t.json({error:{code:r.code??"forbidden",message:r.message}},r.status):r.status===404?t.json({error:{code:"not_found",message:"Not found"}},404):t.json({error:{code:"permission_required",message:r.message,permission:r.decision?.permission,reason:r.decision?.reason}},403);if(e instanceof HttpError)return t.json({error:{code:e.code,message:e.message,...e.detail?{detail:e.detail}:{}}},e.status);if(e.status===429&&e.message.startsWith("Tego host API ")){const i=e.retryAfter;return typeof i=="string"&&t.header("Retry-After",i),t.json({error:{code:"rate_limited",message:"Too many requests; try again shortly."}},429)}const a=t.env;return(s.log?.(t)??a?.host?.log)?.error("request failed",{method:t.req.method,path:new URL(t.req.url).pathname,error:e instanceof Error?e.message:String(e)}),t.json({error:{code:"internal",message:s.message??"The request could not be completed."}},500)}}n(pluginErrorHandler,"pluginErrorHandler");function changedEventsMiddleware(s={}){return async(e,t)=>{await t();const r=e.req.method;if(r==="GET"||r==="HEAD"||r==="OPTIONS"||e.res.status>=400)return;const o=new URL(e.req.url).pathname;if(s.skip?.(o))return;const a=e.env,d=a?.host;if(d)try{await d.events.publish(s.topic??"changed",{method:r,path:o,byAgent:!!a?.ctx?.avatarId})}catch(i){d.log.warn("change notice not published",{error:i instanceof Error?i.message:String(i)})}}}n(changedEventsMiddleware,"changedEventsMiddleware"),exports.HttpError=HttpError,exports.badRequest=badRequest,exports.changedEventsMiddleware=changedEventsMiddleware,exports.conflict=conflict,exports.forbidden=forbidden,exports.jsonNotFound=jsonNotFound,exports.notFound=notFound,exports.pluginErrorHandler=pluginErrorHandler,exports.readBody=readBody,exports.readQuery=readQuery;
@@ -0,0 +1,84 @@
1
+ import { MiddlewareHandler, NotFoundHandler, Context, ErrorHandler } from 'hono';
2
+ import { PluginHostApi } from './server.cjs';
3
+ import 'zod';
4
+ import './frontend.cjs';
5
+
6
+ /**
7
+ * Hono helpers for a plugin backend: refusals with a code the frontend can
8
+ * translate, validated bodies, and one error handler that turns everything
9
+ * else into JSON without leaking SQL or stack traces.
10
+ *
11
+ * `hono` stays the plugin's own dependency; schemas are duck-typed
12
+ * (`safeParse`) so any zod version works.
13
+ */
14
+
15
+ type HttpErrorStatus = 400 | 401 | 403 | 404 | 409 | 413 | 422 | 429;
16
+ /** A refusal the caller can act on: a status, a stable code, a sentence. */
17
+ declare class HttpError extends Error {
18
+ readonly status: HttpErrorStatus;
19
+ readonly code: string;
20
+ readonly detail?: Record<string, unknown> | undefined;
21
+ constructor(status: HttpErrorStatus, code: string, message: string, detail?: Record<string, unknown> | undefined);
22
+ }
23
+ declare const badRequest: (code: string, message: string, detail?: Record<string, unknown>) => HttpError;
24
+ declare const forbidden: (code: string, message: string) => HttpError;
25
+ declare const notFound: (what: string) => HttpError;
26
+ declare const conflict: (code: string, message: string, detail?: Record<string, unknown>) => HttpError;
27
+ /** Anything with zod's `safeParse` shape. */
28
+ interface ParseSchema<T> {
29
+ safeParse(input: unknown): {
30
+ success: true;
31
+ data: T;
32
+ } | {
33
+ success: false;
34
+ error: {
35
+ issues: {
36
+ path: PropertyKey[];
37
+ message: string;
38
+ }[];
39
+ };
40
+ };
41
+ }
42
+ /**
43
+ * The JSON body, validated, or a 400 naming the field. A truncated body would
44
+ * otherwise be a SyntaxError and a 500 — "the service broke" when the request did.
45
+ */
46
+ declare function readBody<T>(c: Pick<Context, 'req'> | {
47
+ req: {
48
+ json(): Promise<unknown>;
49
+ };
50
+ }, schema: ParseSchema<T>): Promise<T>;
51
+ /** The query string, validated, or a 400 naming the parameter. */
52
+ declare function readQuery<T>(c: {
53
+ req: {
54
+ query(): Record<string, string>;
55
+ };
56
+ }, schema: ParseSchema<T>): T;
57
+ /** `{ error: { code: 'no_route', … } }` instead of Hono's plain-text 404. */
58
+ declare const jsonNotFound: NotFoundHandler;
59
+ /**
60
+ * `app.onError(pluginErrorHandler())`:
61
+ *
62
+ * - access refusals → 403 `permission_required` (or the concealing 404), admin
63
+ * refusals with their code;
64
+ * - `HttpError` → its status, code, message and detail;
65
+ * - the host API's 429 → 429 `rate_limited` with its `Retry-After`;
66
+ * - anything else → logged through `host.log.error` (from `c.env.host`, or
67
+ * `options.log`) and a generic 500 that names nothing internal.
68
+ */
69
+ declare function pluginErrorHandler(options?: {
70
+ /** The sentence users see for an unexpected failure. */
71
+ message?: string;
72
+ log?: (c: Context) => Pick<PluginHostApi['log'], 'error'> | undefined;
73
+ }): ErrorHandler;
74
+ /**
75
+ * Publishes `changed` (`{ method, path, byAgent }`, no record data — the stream
76
+ * is tenant-wide) after every successful write, so open pages refresh. Failures
77
+ * to publish are logged, never surfaced to the caller.
78
+ */
79
+ declare function changedEventsMiddleware(options?: {
80
+ topic?: string;
81
+ skip?: (path: string) => boolean;
82
+ }): MiddlewareHandler;
83
+
84
+ export { HttpError, type HttpErrorStatus, type ParseSchema, badRequest, changedEventsMiddleware, conflict, forbidden, jsonNotFound, notFound, pluginErrorHandler, readBody, readQuery };
package/dist/hono.d.ts ADDED
@@ -0,0 +1,84 @@
1
+ import { MiddlewareHandler, NotFoundHandler, Context, ErrorHandler } from 'hono';
2
+ import { PluginHostApi } from './server.js';
3
+ import 'zod';
4
+ import './frontend.js';
5
+
6
+ /**
7
+ * Hono helpers for a plugin backend: refusals with a code the frontend can
8
+ * translate, validated bodies, and one error handler that turns everything
9
+ * else into JSON without leaking SQL or stack traces.
10
+ *
11
+ * `hono` stays the plugin's own dependency; schemas are duck-typed
12
+ * (`safeParse`) so any zod version works.
13
+ */
14
+
15
+ type HttpErrorStatus = 400 | 401 | 403 | 404 | 409 | 413 | 422 | 429;
16
+ /** A refusal the caller can act on: a status, a stable code, a sentence. */
17
+ declare class HttpError extends Error {
18
+ readonly status: HttpErrorStatus;
19
+ readonly code: string;
20
+ readonly detail?: Record<string, unknown> | undefined;
21
+ constructor(status: HttpErrorStatus, code: string, message: string, detail?: Record<string, unknown> | undefined);
22
+ }
23
+ declare const badRequest: (code: string, message: string, detail?: Record<string, unknown>) => HttpError;
24
+ declare const forbidden: (code: string, message: string) => HttpError;
25
+ declare const notFound: (what: string) => HttpError;
26
+ declare const conflict: (code: string, message: string, detail?: Record<string, unknown>) => HttpError;
27
+ /** Anything with zod's `safeParse` shape. */
28
+ interface ParseSchema<T> {
29
+ safeParse(input: unknown): {
30
+ success: true;
31
+ data: T;
32
+ } | {
33
+ success: false;
34
+ error: {
35
+ issues: {
36
+ path: PropertyKey[];
37
+ message: string;
38
+ }[];
39
+ };
40
+ };
41
+ }
42
+ /**
43
+ * The JSON body, validated, or a 400 naming the field. A truncated body would
44
+ * otherwise be a SyntaxError and a 500 — "the service broke" when the request did.
45
+ */
46
+ declare function readBody<T>(c: Pick<Context, 'req'> | {
47
+ req: {
48
+ json(): Promise<unknown>;
49
+ };
50
+ }, schema: ParseSchema<T>): Promise<T>;
51
+ /** The query string, validated, or a 400 naming the parameter. */
52
+ declare function readQuery<T>(c: {
53
+ req: {
54
+ query(): Record<string, string>;
55
+ };
56
+ }, schema: ParseSchema<T>): T;
57
+ /** `{ error: { code: 'no_route', … } }` instead of Hono's plain-text 404. */
58
+ declare const jsonNotFound: NotFoundHandler;
59
+ /**
60
+ * `app.onError(pluginErrorHandler())`:
61
+ *
62
+ * - access refusals → 403 `permission_required` (or the concealing 404), admin
63
+ * refusals with their code;
64
+ * - `HttpError` → its status, code, message and detail;
65
+ * - the host API's 429 → 429 `rate_limited` with its `Retry-After`;
66
+ * - anything else → logged through `host.log.error` (from `c.env.host`, or
67
+ * `options.log`) and a generic 500 that names nothing internal.
68
+ */
69
+ declare function pluginErrorHandler(options?: {
70
+ /** The sentence users see for an unexpected failure. */
71
+ message?: string;
72
+ log?: (c: Context) => Pick<PluginHostApi['log'], 'error'> | undefined;
73
+ }): ErrorHandler;
74
+ /**
75
+ * Publishes `changed` (`{ method, path, byAgent }`, no record data — the stream
76
+ * is tenant-wide) after every successful write, so open pages refresh. Failures
77
+ * to publish are logged, never surfaced to the caller.
78
+ */
79
+ declare function changedEventsMiddleware(options?: {
80
+ topic?: string;
81
+ skip?: (path: string) => boolean;
82
+ }): MiddlewareHandler;
83
+
84
+ export { HttpError, type HttpErrorStatus, type ParseSchema, badRequest, changedEventsMiddleware, conflict, forbidden, jsonNotFound, notFound, pluginErrorHandler, readBody, readQuery };
package/dist/hono.js ADDED
@@ -0,0 +1 @@
1
+ var m=Object.defineProperty;var n=(s,e)=>m(s,"name",{value:e,configurable:!0});var d=class extends Error{static{n(this,"HttpError")}constructor(s,e,t,r){super(t),this.status=s,this.code=e,this.detail=r,this.name="HttpError"}status;code;detail},c=n((s,e,t)=>new d(400,s,e,t),"badRequest"),f=n((s,e)=>new d(403,s,e),"forbidden"),g=n(s=>new d(404,"not_found",`${s} was not found or is not visible to you.`),"notFound"),l=n((s,e,t)=>new d(409,s,e,t),"conflict");function h(s,e){const t=s.issues[0],r=t?.path.map(String).join(".")??"",o=r.length>0?r:e;return{path:o,message:`${o}: ${t?.message??"invalid"}`}}n(h,"firstIssue");async function p(s,e){let t;try{t=await s.req.json()}catch{throw c("invalid_json","The request body is not valid JSON.")}const r=e.safeParse(t);if(r.success)return r.data;const{path:o,message:a}=h(r.error,"body");throw c("invalid_body",a,{path:o})}n(p,"readBody");function v(s,e){const t=e.safeParse(s.req.query());if(t.success)return t.data;const{path:r,message:o}=h(t.error,"query");throw c("invalid_query",o,{path:r})}n(v,"readQuery");var y=n(s=>s.json({error:{code:"no_route",message:`No such route: ${s.req.method} ${new URL(s.req.url).pathname}`}},404),"jsonNotFound");function q(s){if(s instanceof Error&&!(s.name!=="AccessDeniedError"&&s.name!=="AccessAdminError"))return s}n(q,"accessError");function w(s={}){return(e,t)=>{const r=q(e);if(r)return r.name==="AccessAdminError"?t.json({error:{code:r.code??"forbidden",message:r.message}},r.status):r.status===404?t.json({error:{code:"not_found",message:"Not found"}},404):t.json({error:{code:"permission_required",message:r.message,permission:r.decision?.permission,reason:r.decision?.reason}},403);if(e instanceof d)return t.json({error:{code:e.code,message:e.message,...e.detail?{detail:e.detail}:{}}},e.status);if(e.status===429&&e.message.startsWith("Tego host API ")){const i=e.retryAfter;return typeof i=="string"&&t.header("Retry-After",i),t.json({error:{code:"rate_limited",message:"Too many requests; try again shortly."}},429)}const a=t.env;return(s.log?.(t)??a?.host?.log)?.error("request failed",{method:t.req.method,path:new URL(t.req.url).pathname,error:e instanceof Error?e.message:String(e)}),t.json({error:{code:"internal",message:s.message??"The request could not be completed."}},500)}}n(w,"pluginErrorHandler");function E(s={}){return async(e,t)=>{await t();const r=e.req.method;if(r==="GET"||r==="HEAD"||r==="OPTIONS"||e.res.status>=400)return;const o=new URL(e.req.url).pathname;if(s.skip?.(o))return;const a=e.env,u=a?.host;if(u)try{await u.events.publish(s.topic??"changed",{method:r,path:o,byAgent:!!a?.ctx?.avatarId})}catch(i){u.log.warn("change notice not published",{error:i instanceof Error?i.message:String(i)})}}}n(E,"changedEventsMiddleware");export{d as HttpError,c as badRequest,E as changedEventsMiddleware,l as conflict,f as forbidden,y as jsonNotFound,g as notFound,w as pluginErrorHandler,p as readBody,v as readQuery};
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";var a=Object.defineProperty;var e=(t,n)=>a(t,"name",{value:n,configurable:!0});var chunkKMN5RHLX_cjs=require("./chunk-KMN5RHLX.cjs"),chunkROFUQLOZ_cjs=require("./chunk-ROFUQLOZ.cjs"),chunkC2OCOPZQ_cjs=require("./chunk-C2OCOPZQ.cjs");function readClawTools(t){if(!t)return[];const n=t.contracts;if(typeof n!="object"||n===null)return[];const u=n.tools;return Array.isArray(u)?u.filter(i=>typeof i=="string"):[]}e(readClawTools,"readClawTools");function auditDeclarationGaps(t,n){const u=Object.keys(t.audit?.tools??{}),i=new Set(n);return{undeclared:n.filter(o=>!t.audit?.tools?.[o]),unknown:u.filter(o=>!i.has(o))}}e(auditDeclarationGaps,"auditDeclarationGaps");function buildInstallDisclosure(t,n=null,u=null){const i=(t.parts.frontend?.nav??[]).map(r=>({key:r.key,labelKey:r.labelKey,permission:chunkROFUQLOZ_cjs.pluginMenuPermission(t.id,r.key),path:chunkROFUQLOZ_cjs.pluginNavPath(t.id,r.path)})),o=chunkROFUQLOZ_cjs.resolvePluginParts(t).backend,s=readClawTools(n);return{requiredPermissions:[...new Set(t.permissions?.required??[])].sort(),egress:o?.egress??[],publicRoutes:(o?.publicRoutes??[]).map(chunkROFUQLOZ_cjs.formatPublicRoute).sort(),...o?{backendRuntime:o.runtime}:{},scope:chunkROFUQLOZ_cjs.resolvePluginScope(t),parts:chunkROFUQLOZ_cjs.declaredPartNames(t),declares:chunkROFUQLOZ_cjs.resolvePluginDeclares(t),nav:i,clawTools:s,auditUndeclaredTools:auditDeclarationGaps(t,s).undeclared,config:{frontend:chunkKMN5RHLX_cjs.summarizeConfigDeclaration(t.parts.frontend?.config),backend:chunkKMN5RHLX_cjs.summarizeConfigDeclaration(t.parts.backend?.config),claw:chunkKMN5RHLX_cjs.summarizeClawConfig(n)},...u?{access:{permissions:Object.values(u.permissions).filter(r=>!r.reserved).length,roles:Object.keys(u.roles).length}}:{}}}e(buildInstallDisclosure,"buildInstallDisclosure");function requiresBackendConfig(t){return t.config.backend.required.length>0}e(requiresBackendConfig,"requiresBackendConfig");function diffInstallDisclosure(t,n){const u=[];for(const r of n.requiredPermissions??[])t?.requiredPermissions?.includes(r)||u.push({kind:"permission",value:r});for(const r of n.egress??[])!t?.egress?.includes(r)&&!t?.egress?.includes("*")&&u.push({kind:"egress",value:r});for(const r of n.publicRoutes??[])t?.publicRoutes?.includes(r)||u.push({kind:"publicRoute",value:r});const i=new Set(t?.parts??[]);for(const r of n.parts)i.has(r)||u.push({kind:"part",value:r});for(const r of["database","storage","events"])n.declares[r]&&(t?.declares[r]||u.push({kind:"declares",value:r}));const o=new Set((t?.nav??[]).map(r=>r.key));for(const r of n.nav)o.has(r.key)||u.push({kind:"nav",value:r.key});const s=new Set(t?.clawTools??[]);for(const r of n.clawTools)s.has(r)||u.push({kind:"clawTool",value:r});for(const r of["frontend","backend","claw"]){const l=new Set(t?.config[r]?.required??[]);for(const c of n.config[r].required)l.has(c)||u.push({kind:"configRequired",value:`${r}.${c}`})}return u}e(diffInstallDisclosure,"diffInstallDisclosure"),Object.defineProperty(exports,"EMPTY_CONFIG_SECTION_SUMMARY",{enumerable:!0,get:e(function(){return chunkKMN5RHLX_cjs.EMPTY_CONFIG_SECTION_SUMMARY},"get")}),Object.defineProperty(exports,"PLUGIN_CONFIG_SECRET_SENTINEL",{enumerable:!0,get:e(function(){return chunkKMN5RHLX_cjs.PLUGIN_CONFIG_SECRET_SENTINEL},"get")}),Object.defineProperty(exports,"PLUGIN_CONFIG_SECTIONS",{enumerable:!0,get:e(function(){return chunkKMN5RHLX_cjs.PLUGIN_CONFIG_SECTIONS},"get")}),Object.defineProperty(exports,"applyConfigDefaults",{enumerable:!0,get:e(function(){return chunkKMN5RHLX_cjs.applyConfigDefaults},"get")}),Object.defineProperty(exports,"configFieldLabel",{enumerable:!0,get:e(function(){return chunkKMN5RHLX_cjs.configFieldLabel},"get")}),Object.defineProperty(exports,"redactSecrets",{enumerable:!0,get:e(function(){return chunkKMN5RHLX_cjs.redactSecrets},"get")}),Object.defineProperty(exports,"restoreSecrets",{enumerable:!0,get:e(function(){return chunkKMN5RHLX_cjs.restoreSecrets},"get")}),Object.defineProperty(exports,"summarizeClawConfig",{enumerable:!0,get:e(function(){return chunkKMN5RHLX_cjs.summarizeClawConfig},"get")}),Object.defineProperty(exports,"summarizeConfigDeclaration",{enumerable:!0,get:e(function(){return chunkKMN5RHLX_cjs.summarizeConfigDeclaration},"get")}),Object.defineProperty(exports,"CLAW_MANIFEST_FILENAME",{enumerable:!0,get:e(function(){return chunkROFUQLOZ_cjs.CLAW_MANIFEST_FILENAME},"get")}),Object.defineProperty(exports,"MAX_PLUGIN_MENU_PERMISSION_LENGTH",{enumerable:!0,get:e(function(){return chunkROFUQLOZ_cjs.MAX_PLUGIN_MENU_PERMISSION_LENGTH},"get")}),Object.defineProperty(exports,"PLUGIN_AVATAR_PUBLIC_API_PATH",{enumerable:!0,get:e(function(){return chunkROFUQLOZ_cjs.PLUGIN_AVATAR_PUBLIC_API_PATH},"get")}),Object.defineProperty(exports,"PLUGIN_AVATAR_PUBLIC_PAGES_PATH",{enumerable:!0,get:e(function(){return chunkROFUQLOZ_cjs.PLUGIN_AVATAR_PUBLIC_PAGES_PATH},"get")}),Object.defineProperty(exports,"PLUGIN_BACKEND_PLATFORMS",{enumerable:!0,get:e(function(){return chunkROFUQLOZ_cjs.PLUGIN_BACKEND_PLATFORMS},"get")}),Object.defineProperty(exports,"PLUGIN_BACKEND_RUNTIMES",{enumerable:!0,get:e(function(){return chunkROFUQLOZ_cjs.PLUGIN_BACKEND_RUNTIMES},"get")}),Object.defineProperty(exports,"PLUGIN_MANIFEST_FILENAME",{enumerable:!0,get:e(function(){return chunkROFUQLOZ_cjs.PLUGIN_MANIFEST_FILENAME},"get")}),Object.defineProperty(exports,"PLUGIN_NAV_ICONS",{enumerable:!0,get:e(function(){return chunkROFUQLOZ_cjs.PLUGIN_NAV_ICONS},"get")}),Object.defineProperty(exports,"PLUGIN_NAV_ICON_FALLBACK",{enumerable:!0,get:e(function(){return chunkROFUQLOZ_cjs.PLUGIN_NAV_ICON_FALLBACK},"get")}),Object.defineProperty(exports,"PLUGIN_PART_DEFAULTS",{enumerable:!0,get:e(function(){return chunkROFUQLOZ_cjs.PLUGIN_PART_DEFAULTS},"get")}),Object.defineProperty(exports,"PLUGIN_PART_NAMES",{enumerable:!0,get:e(function(){return chunkROFUQLOZ_cjs.PLUGIN_PART_NAMES},"get")}),Object.defineProperty(exports,"PLUGIN_PUBLIC_API_PATH",{enumerable:!0,get:e(function(){return chunkROFUQLOZ_cjs.PLUGIN_PUBLIC_API_PATH},"get")}),Object.defineProperty(exports,"PLUGIN_PUBLIC_PAGES_PATH",{enumerable:!0,get:e(function(){return chunkROFUQLOZ_cjs.PLUGIN_PUBLIC_PAGES_PATH},"get")}),Object.defineProperty(exports,"PLUGIN_PUBLIC_ROUTE_METHODS",{enumerable:!0,get:e(function(){return chunkROFUQLOZ_cjs.PLUGIN_PUBLIC_ROUTE_METHODS},"get")}),Object.defineProperty(exports,"PLUGIN_SUPPORTED_LOCALES",{enumerable:!0,get:e(function(){return chunkROFUQLOZ_cjs.PLUGIN_SUPPORTED_LOCALES},"get")}),Object.defineProperty(exports,"declaredPartNames",{enumerable:!0,get:e(function(){return chunkROFUQLOZ_cjs.declaredPartNames},"get")}),Object.defineProperty(exports,"formatPublicRoute",{enumerable:!0,get:e(function(){return chunkROFUQLOZ_cjs.formatPublicRoute},"get")}),Object.defineProperty(exports,"localizedTextToString",{enumerable:!0,get:e(function(){return chunkROFUQLOZ_cjs.localizedTextToString},"get")}),Object.defineProperty(exports,"matchPublicRoute",{enumerable:!0,get:e(function(){return chunkROFUQLOZ_cjs.matchPublicRoute},"get")}),Object.defineProperty(exports,"pluginApiPath",{enumerable:!0,get:e(function(){return chunkROFUQLOZ_cjs.pluginApiPath},"get")}),Object.defineProperty(exports,"pluginBasePath",{enumerable:!0,get:e(function(){return chunkROFUQLOZ_cjs.pluginBasePath},"get")}),Object.defineProperty(exports,"pluginI18nKey",{enumerable:!0,get:e(function(){return chunkROFUQLOZ_cjs.pluginI18nKey},"get")}),Object.defineProperty(exports,"pluginI18nNamespace",{enumerable:!0,get:e(function(){return chunkROFUQLOZ_cjs.pluginI18nNamespace},"get")}),Object.defineProperty(exports,"pluginMenuKey",{enumerable:!0,get:e(function(){return chunkROFUQLOZ_cjs.pluginMenuKey},"get")}),Object.defineProperty(exports,"pluginMenuPermission",{enumerable:!0,get:e(function(){return chunkROFUQLOZ_cjs.pluginMenuPermission},"get")}),Object.defineProperty(exports,"pluginNavPath",{enumerable:!0,get:e(function(){return chunkROFUQLOZ_cjs.pluginNavPath},"get")}),Object.defineProperty(exports,"publicApiBaseFromPagePath",{enumerable:!0,get:e(function(){return chunkROFUQLOZ_cjs.publicApiBaseFromPagePath},"get")}),Object.defineProperty(exports,"resolvePluginDeclares",{enumerable:!0,get:e(function(){return chunkROFUQLOZ_cjs.resolvePluginDeclares},"get")}),Object.defineProperty(exports,"resolvePluginParts",{enumerable:!0,get:e(function(){return chunkROFUQLOZ_cjs.resolvePluginParts},"get")}),Object.defineProperty(exports,"resolvePluginScope",{enumerable:!0,get:e(function(){return chunkROFUQLOZ_cjs.resolvePluginScope},"get")}),Object.defineProperty(exports,"HOST_API_VERSION",{enumerable:!0,get:e(function(){return chunkC2OCOPZQ_cjs.HOST_API_VERSION},"get")}),Object.defineProperty(exports,"SHARED_MODULE_IDS",{enumerable:!0,get:e(function(){return chunkC2OCOPZQ_cjs.SHARED_MODULE_IDS},"get")}),Object.defineProperty(exports,"hostApiRangeMinimum",{enumerable:!0,get:e(function(){return chunkC2OCOPZQ_cjs.hostApiRangeMinimum},"get")}),Object.defineProperty(exports,"isValidHostApiRange",{enumerable:!0,get:e(function(){return chunkC2OCOPZQ_cjs.isValidHostApiRange},"get")}),Object.defineProperty(exports,"satisfiesHostApiRange",{enumerable:!0,get:e(function(){return chunkC2OCOPZQ_cjs.satisfiesHostApiRange},"get")}),exports.auditDeclarationGaps=auditDeclarationGaps,exports.buildInstallDisclosure=buildInstallDisclosure,exports.diffInstallDisclosure=diffInstallDisclosure,exports.requiresBackendConfig=requiresBackendConfig;
1
+ "use strict";var a=Object.defineProperty;var e=(t,n)=>a(t,"name",{value:n,configurable:!0});var chunkMWMGJJ74_cjs=require("./chunk-MWMGJJ74.cjs"),chunk2P6UEAPV_cjs=require("./chunk-2P6UEAPV.cjs"),chunkN4SXZZPY_cjs=require("./chunk-N4SXZZPY.cjs"),chunk6HNG5OZI_cjs=require("./chunk-6HNG5OZI.cjs");function readClawTools(t){if(!t)return[];const n=t.contracts;if(typeof n!="object"||n===null)return[];const u=n.tools;return Array.isArray(u)?u.filter(i=>typeof i=="string"):[]}e(readClawTools,"readClawTools");function auditDeclarationGaps(t,n){const u=Object.keys(t.audit?.tools??{}),i=new Set(n);return{undeclared:n.filter(o=>!t.audit?.tools?.[o]),unknown:u.filter(o=>!i.has(o))}}e(auditDeclarationGaps,"auditDeclarationGaps");function buildInstallDisclosure(t,n=null,u=null){const i=(t.parts.frontend?.nav??[]).map(r=>({key:r.key,labelKey:r.labelKey,permission:chunkN4SXZZPY_cjs.pluginMenuPermission(t.id,r.key),path:chunkN4SXZZPY_cjs.pluginNavPath(t.id,r.path)})),o=chunk2P6UEAPV_cjs.resolvePluginParts(t).backend,s=readClawTools(n);return{requiredPermissions:[...new Set(t.permissions?.required??[])].sort(),egress:o?.egress??[],publicRoutes:(o?.publicRoutes??[]).map(chunk2P6UEAPV_cjs.formatPublicRoute).sort(),...o?{backendRuntime:o.runtime}:{},scope:chunk2P6UEAPV_cjs.resolvePluginScope(t),parts:chunk2P6UEAPV_cjs.declaredPartNames(t),declares:chunk2P6UEAPV_cjs.resolvePluginDeclares(t),nav:i,clawTools:s,auditUndeclaredTools:auditDeclarationGaps(t,s).undeclared,config:{frontend:chunkMWMGJJ74_cjs.summarizeConfigDeclaration(t.parts.frontend?.config),backend:chunkMWMGJJ74_cjs.summarizeConfigDeclaration(t.parts.backend?.config),claw:chunkMWMGJJ74_cjs.summarizeClawConfig(n)},...u?{access:{permissions:Object.values(u.permissions).filter(r=>!r.reserved).length,roles:Object.keys(u.roles).length}}:{}}}e(buildInstallDisclosure,"buildInstallDisclosure");function requiresBackendConfig(t){return t.config.backend.required.length>0}e(requiresBackendConfig,"requiresBackendConfig");function diffInstallDisclosure(t,n){const u=[];for(const r of n.requiredPermissions??[])t?.requiredPermissions?.includes(r)||u.push({kind:"permission",value:r});for(const r of n.egress??[])!t?.egress?.includes(r)&&!t?.egress?.includes("*")&&u.push({kind:"egress",value:r});for(const r of n.publicRoutes??[])t?.publicRoutes?.includes(r)||u.push({kind:"publicRoute",value:r});const i=new Set(t?.parts??[]);for(const r of n.parts)i.has(r)||u.push({kind:"part",value:r});for(const r of["database","storage","events"])n.declares[r]&&(t?.declares[r]||u.push({kind:"declares",value:r}));const o=new Set((t?.nav??[]).map(r=>r.key));for(const r of n.nav)o.has(r.key)||u.push({kind:"nav",value:r.key});const s=new Set(t?.clawTools??[]);for(const r of n.clawTools)s.has(r)||u.push({kind:"clawTool",value:r});for(const r of["frontend","backend","claw"]){const l=new Set(t?.config[r]?.required??[]);for(const c of n.config[r].required)l.has(c)||u.push({kind:"configRequired",value:`${r}.${c}`})}return u}e(diffInstallDisclosure,"diffInstallDisclosure"),Object.defineProperty(exports,"EMPTY_CONFIG_SECTION_SUMMARY",{enumerable:!0,get:e(function(){return chunkMWMGJJ74_cjs.EMPTY_CONFIG_SECTION_SUMMARY},"get")}),Object.defineProperty(exports,"PLUGIN_CONFIG_SECRET_SENTINEL",{enumerable:!0,get:e(function(){return chunkMWMGJJ74_cjs.PLUGIN_CONFIG_SECRET_SENTINEL},"get")}),Object.defineProperty(exports,"PLUGIN_CONFIG_SECTIONS",{enumerable:!0,get:e(function(){return chunkMWMGJJ74_cjs.PLUGIN_CONFIG_SECTIONS},"get")}),Object.defineProperty(exports,"applyConfigDefaults",{enumerable:!0,get:e(function(){return chunkMWMGJJ74_cjs.applyConfigDefaults},"get")}),Object.defineProperty(exports,"configFieldLabel",{enumerable:!0,get:e(function(){return chunkMWMGJJ74_cjs.configFieldLabel},"get")}),Object.defineProperty(exports,"redactSecrets",{enumerable:!0,get:e(function(){return chunkMWMGJJ74_cjs.redactSecrets},"get")}),Object.defineProperty(exports,"restoreSecrets",{enumerable:!0,get:e(function(){return chunkMWMGJJ74_cjs.restoreSecrets},"get")}),Object.defineProperty(exports,"summarizeClawConfig",{enumerable:!0,get:e(function(){return chunkMWMGJJ74_cjs.summarizeClawConfig},"get")}),Object.defineProperty(exports,"summarizeConfigDeclaration",{enumerable:!0,get:e(function(){return chunkMWMGJJ74_cjs.summarizeConfigDeclaration},"get")}),Object.defineProperty(exports,"CLAW_MANIFEST_FILENAME",{enumerable:!0,get:e(function(){return chunk2P6UEAPV_cjs.CLAW_MANIFEST_FILENAME},"get")}),Object.defineProperty(exports,"PLUGIN_AVATAR_PUBLIC_API_PATH",{enumerable:!0,get:e(function(){return chunk2P6UEAPV_cjs.PLUGIN_AVATAR_PUBLIC_API_PATH},"get")}),Object.defineProperty(exports,"PLUGIN_AVATAR_PUBLIC_PAGES_PATH",{enumerable:!0,get:e(function(){return chunk2P6UEAPV_cjs.PLUGIN_AVATAR_PUBLIC_PAGES_PATH},"get")}),Object.defineProperty(exports,"PLUGIN_BACKEND_PLATFORMS",{enumerable:!0,get:e(function(){return chunk2P6UEAPV_cjs.PLUGIN_BACKEND_PLATFORMS},"get")}),Object.defineProperty(exports,"PLUGIN_BACKEND_RUNTIMES",{enumerable:!0,get:e(function(){return chunk2P6UEAPV_cjs.PLUGIN_BACKEND_RUNTIMES},"get")}),Object.defineProperty(exports,"PLUGIN_MANIFEST_FILENAME",{enumerable:!0,get:e(function(){return chunk2P6UEAPV_cjs.PLUGIN_MANIFEST_FILENAME},"get")}),Object.defineProperty(exports,"PLUGIN_NAV_ICONS",{enumerable:!0,get:e(function(){return chunk2P6UEAPV_cjs.PLUGIN_NAV_ICONS},"get")}),Object.defineProperty(exports,"PLUGIN_NAV_ICON_FALLBACK",{enumerable:!0,get:e(function(){return chunk2P6UEAPV_cjs.PLUGIN_NAV_ICON_FALLBACK},"get")}),Object.defineProperty(exports,"PLUGIN_PART_DEFAULTS",{enumerable:!0,get:e(function(){return chunk2P6UEAPV_cjs.PLUGIN_PART_DEFAULTS},"get")}),Object.defineProperty(exports,"PLUGIN_PART_NAMES",{enumerable:!0,get:e(function(){return chunk2P6UEAPV_cjs.PLUGIN_PART_NAMES},"get")}),Object.defineProperty(exports,"PLUGIN_PUBLIC_API_PATH",{enumerable:!0,get:e(function(){return chunk2P6UEAPV_cjs.PLUGIN_PUBLIC_API_PATH},"get")}),Object.defineProperty(exports,"PLUGIN_PUBLIC_PAGES_PATH",{enumerable:!0,get:e(function(){return chunk2P6UEAPV_cjs.PLUGIN_PUBLIC_PAGES_PATH},"get")}),Object.defineProperty(exports,"PLUGIN_PUBLIC_ROUTE_METHODS",{enumerable:!0,get:e(function(){return chunk2P6UEAPV_cjs.PLUGIN_PUBLIC_ROUTE_METHODS},"get")}),Object.defineProperty(exports,"declaredPartNames",{enumerable:!0,get:e(function(){return chunk2P6UEAPV_cjs.declaredPartNames},"get")}),Object.defineProperty(exports,"formatPublicRoute",{enumerable:!0,get:e(function(){return chunk2P6UEAPV_cjs.formatPublicRoute},"get")}),Object.defineProperty(exports,"matchPublicRoute",{enumerable:!0,get:e(function(){return chunk2P6UEAPV_cjs.matchPublicRoute},"get")}),Object.defineProperty(exports,"publicApiBaseFromPagePath",{enumerable:!0,get:e(function(){return chunk2P6UEAPV_cjs.publicApiBaseFromPagePath},"get")}),Object.defineProperty(exports,"resolvePluginDeclares",{enumerable:!0,get:e(function(){return chunk2P6UEAPV_cjs.resolvePluginDeclares},"get")}),Object.defineProperty(exports,"resolvePluginParts",{enumerable:!0,get:e(function(){return chunk2P6UEAPV_cjs.resolvePluginParts},"get")}),Object.defineProperty(exports,"resolvePluginScope",{enumerable:!0,get:e(function(){return chunk2P6UEAPV_cjs.resolvePluginScope},"get")}),Object.defineProperty(exports,"MAX_PLUGIN_MENU_PERMISSION_LENGTH",{enumerable:!0,get:e(function(){return chunkN4SXZZPY_cjs.MAX_PLUGIN_MENU_PERMISSION_LENGTH},"get")}),Object.defineProperty(exports,"PLUGIN_SUPPORTED_LOCALES",{enumerable:!0,get:e(function(){return chunkN4SXZZPY_cjs.PLUGIN_SUPPORTED_LOCALES},"get")}),Object.defineProperty(exports,"localizedTextToString",{enumerable:!0,get:e(function(){return chunkN4SXZZPY_cjs.localizedTextToString},"get")}),Object.defineProperty(exports,"pluginApiPath",{enumerable:!0,get:e(function(){return chunkN4SXZZPY_cjs.pluginApiPath},"get")}),Object.defineProperty(exports,"pluginBasePath",{enumerable:!0,get:e(function(){return chunkN4SXZZPY_cjs.pluginBasePath},"get")}),Object.defineProperty(exports,"pluginI18nKey",{enumerable:!0,get:e(function(){return chunkN4SXZZPY_cjs.pluginI18nKey},"get")}),Object.defineProperty(exports,"pluginI18nNamespace",{enumerable:!0,get:e(function(){return chunkN4SXZZPY_cjs.pluginI18nNamespace},"get")}),Object.defineProperty(exports,"pluginMenuKey",{enumerable:!0,get:e(function(){return chunkN4SXZZPY_cjs.pluginMenuKey},"get")}),Object.defineProperty(exports,"pluginMenuPermission",{enumerable:!0,get:e(function(){return chunkN4SXZZPY_cjs.pluginMenuPermission},"get")}),Object.defineProperty(exports,"pluginNavPath",{enumerable:!0,get:e(function(){return chunkN4SXZZPY_cjs.pluginNavPath},"get")}),Object.defineProperty(exports,"HOST_API_VERSION",{enumerable:!0,get:e(function(){return chunk6HNG5OZI_cjs.HOST_API_VERSION},"get")}),Object.defineProperty(exports,"SHARED_MODULE_IDS",{enumerable:!0,get:e(function(){return chunk6HNG5OZI_cjs.SHARED_MODULE_IDS},"get")}),Object.defineProperty(exports,"hostApiRangeMinimum",{enumerable:!0,get:e(function(){return chunk6HNG5OZI_cjs.hostApiRangeMinimum},"get")}),Object.defineProperty(exports,"isValidHostApiRange",{enumerable:!0,get:e(function(){return chunk6HNG5OZI_cjs.isValidHostApiRange},"get")}),Object.defineProperty(exports,"satisfiesHostApiRange",{enumerable:!0,get:e(function(){return chunk6HNG5OZI_cjs.satisfiesHostApiRange},"get")}),exports.auditDeclarationGaps=auditDeclarationGaps,exports.buildInstallDisclosure=buildInstallDisclosure,exports.diffInstallDisclosure=diffInstallDisclosure,exports.requiresBackendConfig=requiresBackendConfig;
package/dist/index.d.cts CHANGED
@@ -272,12 +272,17 @@ declare function diffInstallDisclosure(accepted: PluginInstallDisclosure | null,
272
272
  * identity revision fencing and durable document moves by externalId. Managed
273
273
  * collection reads use existing ACLs; policyScope is deprecated.
274
274
  *
275
+ * 1.37.0 adds the shell-neutral capabilities a page needs to behave the same
276
+ * in the web app and the desktop app: `PluginHostContext.platform`,
277
+ * `openExternal`, `download` and `copyText`. `window.open`, `<a download>`
278
+ * and `navigator.clipboard` are unreliable inside the desktop webview.
279
+ *
275
280
  * This numbers the *host API surface*, not the `@tego/plugin-sdk` npm package
276
281
  * — the two move on their own clocks. A manifest declares compatibility
277
282
  * against this constant through `hostApiRange`; the SDK's own package version
278
283
  * is a build-time dependency and never reaches a manifest.
279
284
  */
280
- declare const HOST_API_VERSION = "1.36.0";
285
+ declare const HOST_API_VERSION = "1.37.0";
281
286
  /**
282
287
  * Module specifiers the shell provides to plugin frontends.
283
288
  *
package/dist/index.d.ts CHANGED
@@ -272,12 +272,17 @@ declare function diffInstallDisclosure(accepted: PluginInstallDisclosure | null,
272
272
  * identity revision fencing and durable document moves by externalId. Managed
273
273
  * collection reads use existing ACLs; policyScope is deprecated.
274
274
  *
275
+ * 1.37.0 adds the shell-neutral capabilities a page needs to behave the same
276
+ * in the web app and the desktop app: `PluginHostContext.platform`,
277
+ * `openExternal`, `download` and `copyText`. `window.open`, `<a download>`
278
+ * and `navigator.clipboard` are unreliable inside the desktop webview.
279
+ *
275
280
  * This numbers the *host API surface*, not the `@tego/plugin-sdk` npm package
276
281
  * — the two move on their own clocks. A manifest declares compatibility
277
282
  * against this constant through `hostApiRange`; the SDK's own package version
278
283
  * is a build-time dependency and never reaches a manifest.
279
284
  */
280
- declare const HOST_API_VERSION = "1.36.0";
285
+ declare const HOST_API_VERSION = "1.37.0";
281
286
  /**
282
287
  * Module specifiers the shell provides to plugin frontends.
283
288
  *
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- var d=Object.defineProperty;var l=(o,s)=>d(o,"name",{value:s,configurable:!0});import{summarizeClawConfig as f,summarizeConfigDeclaration as a}from"./chunk-2ORO36BY.js";export{EMPTY_CONFIG_SECTION_SUMMARY,PLUGIN_CONFIG_SECRET_SENTINEL,PLUGIN_CONFIG_SECTIONS,applyConfigDefaults,configFieldLabel,redactSecrets,restoreSecrets,summarizeClawConfig,summarizeConfigDeclaration}from"./chunk-2ORO36BY.js";import{pluginNavPath as p,pluginMenuPermission as _,resolvePluginParts as I,resolvePluginDeclares as N,declaredPartNames as A,resolvePluginScope as g,formatPublicRoute as S}from"./chunk-BTHXQWHT.js";export{CLAW_MANIFEST_FILENAME,MAX_PLUGIN_MENU_PERMISSION_LENGTH,PLUGIN_AVATAR_PUBLIC_API_PATH,PLUGIN_AVATAR_PUBLIC_PAGES_PATH,PLUGIN_BACKEND_PLATFORMS,PLUGIN_BACKEND_RUNTIMES,PLUGIN_MANIFEST_FILENAME,PLUGIN_NAV_ICONS,PLUGIN_NAV_ICON_FALLBACK,PLUGIN_PART_DEFAULTS,PLUGIN_PART_NAMES,PLUGIN_PUBLIC_API_PATH,PLUGIN_PUBLIC_PAGES_PATH,PLUGIN_PUBLIC_ROUTE_METHODS,PLUGIN_SUPPORTED_LOCALES,declaredPartNames,formatPublicRoute,localizedTextToString,matchPublicRoute,pluginApiPath,pluginBasePath,pluginI18nKey,pluginI18nNamespace,pluginMenuKey,pluginMenuPermission,pluginNavPath,publicApiBaseFromPagePath,resolvePluginDeclares,resolvePluginParts,resolvePluginScope}from"./chunk-BTHXQWHT.js";export{HOST_API_VERSION,SHARED_MODULE_IDS,hostApiRangeMinimum,isValidHostApiRange,satisfiesHostApiRange}from"./chunk-7XHF3YSK.js";function L(o){if(!o)return[];const s=o.contracts;if(typeof s!="object"||s===null)return[];const n=s.tools;return Array.isArray(n)?n.filter(i=>typeof i=="string"):[]}l(L,"readClawTools");function c(o,s){const n=Object.keys(o.audit?.tools??{}),i=new Set(s);return{undeclared:s.filter(r=>!o.audit?.tools?.[r]),unknown:n.filter(r=>!i.has(r))}}l(c,"auditDeclarationGaps");function T(o,s=null,n=null){const i=(o.parts.frontend?.nav??[]).map(e=>({key:e.key,labelKey:e.labelKey,permission:_(o.id,e.key),path:p(o.id,e.path)})),r=I(o).backend,t=L(s);return{requiredPermissions:[...new Set(o.permissions?.required??[])].sort(),egress:r?.egress??[],publicRoutes:(r?.publicRoutes??[]).map(S).sort(),...r?{backendRuntime:r.runtime}:{},scope:g(o),parts:A(o),declares:N(o),nav:i,clawTools:t,auditUndeclaredTools:c(o,t).undeclared,config:{frontend:a(o.parts.frontend?.config),backend:a(o.parts.backend?.config),claw:f(s)},...n?{access:{permissions:Object.values(n.permissions).filter(e=>!e.reserved).length,roles:Object.keys(n.roles).length}}:{}}}l(T,"buildInstallDisclosure");function E(o){return o.config.backend.required.length>0}l(E,"requiresBackendConfig");function m(o,s){const n=[];for(const e of s.requiredPermissions??[])o?.requiredPermissions?.includes(e)||n.push({kind:"permission",value:e});for(const e of s.egress??[])!o?.egress?.includes(e)&&!o?.egress?.includes("*")&&n.push({kind:"egress",value:e});for(const e of s.publicRoutes??[])o?.publicRoutes?.includes(e)||n.push({kind:"publicRoute",value:e});const i=new Set(o?.parts??[]);for(const e of s.parts)i.has(e)||n.push({kind:"part",value:e});for(const e of["database","storage","events"])s.declares[e]&&(o?.declares[e]||n.push({kind:"declares",value:e}));const r=new Set((o?.nav??[]).map(e=>e.key));for(const e of s.nav)r.has(e.key)||n.push({kind:"nav",value:e.key});const t=new Set(o?.clawTools??[]);for(const e of s.clawTools)t.has(e)||n.push({kind:"clawTool",value:e});for(const e of["frontend","backend","claw"]){const P=new Set(o?.config[e]?.required??[]);for(const u of s.config[e].required)P.has(u)||n.push({kind:"configRequired",value:`${e}.${u}`})}return n}l(m,"diffInstallDisclosure");export{c as auditDeclarationGaps,T as buildInstallDisclosure,m as diffInstallDisclosure,E as requiresBackendConfig};
1
+ var d=Object.defineProperty;var l=(o,s)=>d(o,"name",{value:s,configurable:!0});import{summarizeClawConfig as f,summarizeConfigDeclaration as a}from"./chunk-DEGQDJF6.js";export{EMPTY_CONFIG_SECTION_SUMMARY,PLUGIN_CONFIG_SECRET_SENTINEL,PLUGIN_CONFIG_SECTIONS,applyConfigDefaults,configFieldLabel,redactSecrets,restoreSecrets,summarizeClawConfig,summarizeConfigDeclaration}from"./chunk-DEGQDJF6.js";import{resolvePluginParts as p,resolvePluginDeclares as _,declaredPartNames as I,resolvePluginScope as N,formatPublicRoute as A}from"./chunk-IVJECPGG.js";export{CLAW_MANIFEST_FILENAME,PLUGIN_AVATAR_PUBLIC_API_PATH,PLUGIN_AVATAR_PUBLIC_PAGES_PATH,PLUGIN_BACKEND_PLATFORMS,PLUGIN_BACKEND_RUNTIMES,PLUGIN_MANIFEST_FILENAME,PLUGIN_NAV_ICONS,PLUGIN_NAV_ICON_FALLBACK,PLUGIN_PART_DEFAULTS,PLUGIN_PART_NAMES,PLUGIN_PUBLIC_API_PATH,PLUGIN_PUBLIC_PAGES_PATH,PLUGIN_PUBLIC_ROUTE_METHODS,declaredPartNames,formatPublicRoute,matchPublicRoute,publicApiBaseFromPagePath,resolvePluginDeclares,resolvePluginParts,resolvePluginScope}from"./chunk-IVJECPGG.js";import{pluginNavPath as g,pluginMenuPermission as S}from"./chunk-INCA3GFW.js";export{MAX_PLUGIN_MENU_PERMISSION_LENGTH,PLUGIN_SUPPORTED_LOCALES,localizedTextToString,pluginApiPath,pluginBasePath,pluginI18nKey,pluginI18nNamespace,pluginMenuKey,pluginMenuPermission,pluginNavPath}from"./chunk-INCA3GFW.js";export{HOST_API_VERSION,SHARED_MODULE_IDS,hostApiRangeMinimum,isValidHostApiRange,satisfiesHostApiRange}from"./chunk-56QOZQTY.js";function L(o){if(!o)return[];const s=o.contracts;if(typeof s!="object"||s===null)return[];const n=s.tools;return Array.isArray(n)?n.filter(i=>typeof i=="string"):[]}l(L,"readClawTools");function c(o,s){const n=Object.keys(o.audit?.tools??{}),i=new Set(s);return{undeclared:s.filter(r=>!o.audit?.tools?.[r]),unknown:n.filter(r=>!i.has(r))}}l(c,"auditDeclarationGaps");function m(o,s=null,n=null){const i=(o.parts.frontend?.nav??[]).map(e=>({key:e.key,labelKey:e.labelKey,permission:S(o.id,e.key),path:g(o.id,e.path)})),r=p(o).backend,t=L(s);return{requiredPermissions:[...new Set(o.permissions?.required??[])].sort(),egress:r?.egress??[],publicRoutes:(r?.publicRoutes??[]).map(A).sort(),...r?{backendRuntime:r.runtime}:{},scope:N(o),parts:I(o),declares:_(o),nav:i,clawTools:t,auditUndeclaredTools:c(o,t).undeclared,config:{frontend:a(o.parts.frontend?.config),backend:a(o.parts.backend?.config),claw:f(s)},...n?{access:{permissions:Object.values(n.permissions).filter(e=>!e.reserved).length,roles:Object.keys(n.roles).length}}:{}}}l(m,"buildInstallDisclosure");function T(o){return o.config.backend.required.length>0}l(T,"requiresBackendConfig");function E(o,s){const n=[];for(const e of s.requiredPermissions??[])o?.requiredPermissions?.includes(e)||n.push({kind:"permission",value:e});for(const e of s.egress??[])!o?.egress?.includes(e)&&!o?.egress?.includes("*")&&n.push({kind:"egress",value:e});for(const e of s.publicRoutes??[])o?.publicRoutes?.includes(e)||n.push({kind:"publicRoute",value:e});const i=new Set(o?.parts??[]);for(const e of s.parts)i.has(e)||n.push({kind:"part",value:e});for(const e of["database","storage","events"])s.declares[e]&&(o?.declares[e]||n.push({kind:"declares",value:e}));const r=new Set((o?.nav??[]).map(e=>e.key));for(const e of s.nav)r.has(e.key)||n.push({kind:"nav",value:e.key});const t=new Set(o?.clawTools??[]);for(const e of s.clawTools)t.has(e)||n.push({kind:"clawTool",value:e});for(const e of["frontend","backend","claw"]){const P=new Set(o?.config[e]?.required??[]);for(const u of s.config[e].required)P.has(u)||n.push({kind:"configRequired",value:`${e}.${u}`})}return n}l(E,"diffInstallDisclosure");export{c as auditDeclarationGaps,m as buildInstallDisclosure,E as diffInstallDisclosure,T as requiresBackendConfig};
package/dist/manifest.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";var n=Object.defineProperty;var e=(r,t)=>n(r,"name",{value:t,configurable:!0});var chunk3WA3M347_cjs=require("./chunk-3WA3M347.cjs");require("./chunk-ROFUQLOZ.cjs"),require("./chunk-C2OCOPZQ.cjs"),require("./chunk-AJY7ZHXK.cjs"),require("./chunk-OP64Y4IN.cjs"),Object.defineProperty(exports,"PYTHON_LOCK_FILENAME",{enumerable:!0,get:e(function(){return chunk3WA3M347_cjs.PYTHON_LOCK_FILENAME},"get")}),Object.defineProperty(exports,"PYTHON_RUNNER_VERSION",{enumerable:!0,get:e(function(){return chunk3WA3M347_cjs.PYTHON_RUNNER_VERSION},"get")}),Object.defineProperty(exports,"PYTHON_WHEELS_DIR",{enumerable:!0,get:e(function(){return chunk3WA3M347_cjs.PYTHON_WHEELS_DIR},"get")}),Object.defineProperty(exports,"checkPluginAccessCatalog",{enumerable:!0,get:e(function(){return chunk3WA3M347_cjs.checkPluginAccessCatalog},"get")}),Object.defineProperty(exports,"checkPythonVendoring",{enumerable:!0,get:e(function(){return chunk3WA3M347_cjs.checkPythonVendoring},"get")}),Object.defineProperty(exports,"normalizePackageName",{enumerable:!0,get:e(function(){return chunk3WA3M347_cjs.normalizePackageName},"get")}),Object.defineProperty(exports,"parseRequirementsLock",{enumerable:!0,get:e(function(){return chunk3WA3M347_cjs.parseRequirementsLock},"get")}),Object.defineProperty(exports,"parseTegoPluginManifest",{enumerable:!0,get:e(function(){return chunk3WA3M347_cjs.parseTegoPluginManifest},"get")}),Object.defineProperty(exports,"parseWheelFilename",{enumerable:!0,get:e(function(){return chunk3WA3M347_cjs.parseWheelFilename},"get")}),Object.defineProperty(exports,"tegoPluginManifestSchema",{enumerable:!0,get:e(function(){return chunk3WA3M347_cjs.tegoPluginManifestSchema},"get")});
1
+ "use strict";var n=Object.defineProperty;var e=(r,t)=>n(r,"name",{value:t,configurable:!0});var chunkSKNPNPJP_cjs=require("./chunk-SKNPNPJP.cjs"),chunk45Y4CR4R_cjs=require("./chunk-45Y4CR4R.cjs");require("./chunk-2P6UEAPV.cjs"),require("./chunk-N4SXZZPY.cjs"),require("./chunk-6HNG5OZI.cjs"),require("./chunk-AJY7ZHXK.cjs"),require("./chunk-OP64Y4IN.cjs"),Object.defineProperty(exports,"PYTHON_LOCK_FILENAME",{enumerable:!0,get:e(function(){return chunkSKNPNPJP_cjs.PYTHON_LOCK_FILENAME},"get")}),Object.defineProperty(exports,"PYTHON_RUNNER_VERSION",{enumerable:!0,get:e(function(){return chunkSKNPNPJP_cjs.PYTHON_RUNNER_VERSION},"get")}),Object.defineProperty(exports,"PYTHON_WHEELS_DIR",{enumerable:!0,get:e(function(){return chunkSKNPNPJP_cjs.PYTHON_WHEELS_DIR},"get")}),Object.defineProperty(exports,"checkPluginAccessCatalog",{enumerable:!0,get:e(function(){return chunkSKNPNPJP_cjs.checkPluginAccessCatalog},"get")}),Object.defineProperty(exports,"checkPythonVendoring",{enumerable:!0,get:e(function(){return chunkSKNPNPJP_cjs.checkPythonVendoring},"get")}),Object.defineProperty(exports,"normalizePackageName",{enumerable:!0,get:e(function(){return chunkSKNPNPJP_cjs.normalizePackageName},"get")}),Object.defineProperty(exports,"parseRequirementsLock",{enumerable:!0,get:e(function(){return chunkSKNPNPJP_cjs.parseRequirementsLock},"get")}),Object.defineProperty(exports,"parseTegoPluginManifest",{enumerable:!0,get:e(function(){return chunkSKNPNPJP_cjs.parseTegoPluginManifest},"get")}),Object.defineProperty(exports,"parseWheelFilename",{enumerable:!0,get:e(function(){return chunkSKNPNPJP_cjs.parseWheelFilename},"get")}),Object.defineProperty(exports,"tegoPluginManifestSchema",{enumerable:!0,get:e(function(){return chunkSKNPNPJP_cjs.tegoPluginManifestSchema},"get")}),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")});
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { T as TegoPluginManifest, y as PluginBackendPlatform } from './manifest-types-CUTNfJTa.cjs';
2
+ import { T as TegoPluginManifest, P as PluginNavEntry, y as PluginBackendPlatform } from './manifest-types-CUTNfJTa.cjs';
3
3
  import { A as AccessCatalog } from './catalog-CSSbrfEf.cjs';
4
4
 
5
5
  /**
@@ -22,6 +22,59 @@ type PluginAccessCatalogResult = {
22
22
  */
23
23
  declare function checkPluginAccessCatalog(manifest: Pick<TegoPluginManifest, 'access' | 'parts'>, raw: unknown): PluginAccessCatalogResult;
24
24
 
25
+ /** `zh-CN.json`, `en.json` — the locale file shape the host's loader expects. */
26
+ declare const LOCALE_FILENAME_PATTERN: RegExp;
27
+ /**
28
+ * `0001_initial.sql`. The numeric prefix is what makes the apply order
29
+ * deterministic; without it two migrations could be applied in either order,
30
+ * which is only discovered when a column is missing in production.
31
+ */
32
+ declare const MIGRATION_FILENAME_PATTERN: RegExp;
33
+ /** Read access to a plugin package or source tree, paths relative to its root with `/`. */
34
+ interface PluginTreeReader {
35
+ /** Names of the files directly in `dir` (not nested deeper); empty when it does not exist. */
36
+ list(dir: string): Promise<string[]>;
37
+ /** The file as UTF-8, or `null` when absent. */
38
+ readText(path: string): Promise<string | null>;
39
+ }
40
+ /** Every leaf path of a nested translation object, in `a.b.c` form. */
41
+ declare function flattenLocaleKeys(value: unknown, prefix?: string, into?: Set<string>): Set<string>;
42
+ interface LocaleVerification {
43
+ /** Locale tags found (`zh-CN`, `en`, …), in directory order. */
44
+ locales: string[];
45
+ /** Parsed bundles by tag, for checks that need the values. */
46
+ bundles: Map<string, unknown>;
47
+ errors: string[];
48
+ }
49
+ /**
50
+ * Every locale bundle exists, parses, and carries the same keys.
51
+ *
52
+ * Three separate failures, all of which used to reach a user as a sidebar row
53
+ * showing `crm.nav.deals`:
54
+ *
55
+ * - a missing language, where the platform is translated and this plugin is
56
+ * not. Falling back would hide it permanently, so it is refused where the
57
+ * author can act
58
+ * - a key present in one bundle and absent in another, which is what a
59
+ * half-finished translation pass leaves behind
60
+ * - a nav entry pointing at a key nobody wrote
61
+ *
62
+ * Keys are compared against `zh-CN`, the platform's own default, rather than
63
+ * against a union of all files — a union would let a typo in one file demand
64
+ * the same typo in the other nine.
65
+ */
66
+ declare function verifyFrontendLocales(reader: PluginTreeReader, frontend: {
67
+ i18nDirPath: string;
68
+ nav: PluginNavEntry[];
69
+ }): Promise<LocaleVerification>;
70
+ interface MigrationVerification {
71
+ /** Valid migration file names, sorted (= apply order). */
72
+ migrations: string[];
73
+ errors: string[];
74
+ }
75
+ /** Migration files are `<ordinal>_<description>.sql` with unique ordinals. */
76
+ declare function verifyMigrationNames(reader: PluginTreeReader, migrationsDirPath: string): Promise<MigrationVerification>;
77
+
25
78
  /**
26
79
  * Whether a python backend's vendored wheels will install in the runner.
27
80
  *
@@ -2821,4 +2874,4 @@ interface PluginManifestParseResult {
2821
2874
  */
2822
2875
  declare function parseTegoPluginManifest(value: unknown): PluginManifestParseResult;
2823
2876
 
2824
- export { type LockedRequirement, PYTHON_LOCK_FILENAME, PYTHON_RUNNER_VERSION, PYTHON_WHEELS_DIR, type ParsedTegoPluginManifest, type ParsedWheelFilename, type PluginAccessCatalogResult, type PluginManifestParseResult, type VendoredWheel, checkPluginAccessCatalog, checkPythonVendoring, normalizePackageName, parseRequirementsLock, parseTegoPluginManifest, parseWheelFilename, tegoPluginManifestSchema };
2877
+ export { LOCALE_FILENAME_PATTERN, type LocaleVerification, type LockedRequirement, MIGRATION_FILENAME_PATTERN, type MigrationVerification, PYTHON_LOCK_FILENAME, PYTHON_RUNNER_VERSION, PYTHON_WHEELS_DIR, type ParsedTegoPluginManifest, type ParsedWheelFilename, type PluginAccessCatalogResult, type PluginManifestParseResult, type PluginTreeReader, type VendoredWheel, checkPluginAccessCatalog, checkPythonVendoring, flattenLocaleKeys, normalizePackageName, parseRequirementsLock, parseTegoPluginManifest, parseWheelFilename, tegoPluginManifestSchema, verifyFrontendLocales, verifyMigrationNames };
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { T as TegoPluginManifest, y as PluginBackendPlatform } from './manifest-types-CUTNfJTa.js';
2
+ import { T as TegoPluginManifest, P as PluginNavEntry, y as PluginBackendPlatform } from './manifest-types-CUTNfJTa.js';
3
3
  import { A as AccessCatalog } from './catalog-CSSbrfEf.js';
4
4
 
5
5
  /**
@@ -22,6 +22,59 @@ type PluginAccessCatalogResult = {
22
22
  */
23
23
  declare function checkPluginAccessCatalog(manifest: Pick<TegoPluginManifest, 'access' | 'parts'>, raw: unknown): PluginAccessCatalogResult;
24
24
 
25
+ /** `zh-CN.json`, `en.json` — the locale file shape the host's loader expects. */
26
+ declare const LOCALE_FILENAME_PATTERN: RegExp;
27
+ /**
28
+ * `0001_initial.sql`. The numeric prefix is what makes the apply order
29
+ * deterministic; without it two migrations could be applied in either order,
30
+ * which is only discovered when a column is missing in production.
31
+ */
32
+ declare const MIGRATION_FILENAME_PATTERN: RegExp;
33
+ /** Read access to a plugin package or source tree, paths relative to its root with `/`. */
34
+ interface PluginTreeReader {
35
+ /** Names of the files directly in `dir` (not nested deeper); empty when it does not exist. */
36
+ list(dir: string): Promise<string[]>;
37
+ /** The file as UTF-8, or `null` when absent. */
38
+ readText(path: string): Promise<string | null>;
39
+ }
40
+ /** Every leaf path of a nested translation object, in `a.b.c` form. */
41
+ declare function flattenLocaleKeys(value: unknown, prefix?: string, into?: Set<string>): Set<string>;
42
+ interface LocaleVerification {
43
+ /** Locale tags found (`zh-CN`, `en`, …), in directory order. */
44
+ locales: string[];
45
+ /** Parsed bundles by tag, for checks that need the values. */
46
+ bundles: Map<string, unknown>;
47
+ errors: string[];
48
+ }
49
+ /**
50
+ * Every locale bundle exists, parses, and carries the same keys.
51
+ *
52
+ * Three separate failures, all of which used to reach a user as a sidebar row
53
+ * showing `crm.nav.deals`:
54
+ *
55
+ * - a missing language, where the platform is translated and this plugin is
56
+ * not. Falling back would hide it permanently, so it is refused where the
57
+ * author can act
58
+ * - a key present in one bundle and absent in another, which is what a
59
+ * half-finished translation pass leaves behind
60
+ * - a nav entry pointing at a key nobody wrote
61
+ *
62
+ * Keys are compared against `zh-CN`, the platform's own default, rather than
63
+ * against a union of all files — a union would let a typo in one file demand
64
+ * the same typo in the other nine.
65
+ */
66
+ declare function verifyFrontendLocales(reader: PluginTreeReader, frontend: {
67
+ i18nDirPath: string;
68
+ nav: PluginNavEntry[];
69
+ }): Promise<LocaleVerification>;
70
+ interface MigrationVerification {
71
+ /** Valid migration file names, sorted (= apply order). */
72
+ migrations: string[];
73
+ errors: string[];
74
+ }
75
+ /** Migration files are `<ordinal>_<description>.sql` with unique ordinals. */
76
+ declare function verifyMigrationNames(reader: PluginTreeReader, migrationsDirPath: string): Promise<MigrationVerification>;
77
+
25
78
  /**
26
79
  * Whether a python backend's vendored wheels will install in the runner.
27
80
  *
@@ -2821,4 +2874,4 @@ interface PluginManifestParseResult {
2821
2874
  */
2822
2875
  declare function parseTegoPluginManifest(value: unknown): PluginManifestParseResult;
2823
2876
 
2824
- export { type LockedRequirement, PYTHON_LOCK_FILENAME, PYTHON_RUNNER_VERSION, PYTHON_WHEELS_DIR, type ParsedTegoPluginManifest, type ParsedWheelFilename, type PluginAccessCatalogResult, type PluginManifestParseResult, type VendoredWheel, checkPluginAccessCatalog, checkPythonVendoring, normalizePackageName, parseRequirementsLock, parseTegoPluginManifest, parseWheelFilename, tegoPluginManifestSchema };
2877
+ export { LOCALE_FILENAME_PATTERN, type LocaleVerification, type LockedRequirement, MIGRATION_FILENAME_PATTERN, type MigrationVerification, PYTHON_LOCK_FILENAME, PYTHON_RUNNER_VERSION, PYTHON_WHEELS_DIR, type ParsedTegoPluginManifest, type ParsedWheelFilename, type PluginAccessCatalogResult, type PluginManifestParseResult, type PluginTreeReader, type VendoredWheel, checkPluginAccessCatalog, checkPythonVendoring, flattenLocaleKeys, normalizePackageName, parseRequirementsLock, parseTegoPluginManifest, parseWheelFilename, tegoPluginManifestSchema, verifyFrontendLocales, verifyMigrationNames };
package/dist/manifest.js CHANGED
@@ -1 +1 @@
1
- export{PYTHON_LOCK_FILENAME,PYTHON_RUNNER_VERSION,PYTHON_WHEELS_DIR,checkPluginAccessCatalog,checkPythonVendoring,normalizePackageName,parseRequirementsLock,parseTegoPluginManifest,parseWheelFilename,tegoPluginManifestSchema}from"./chunk-5CU33JVY.js";import"./chunk-BTHXQWHT.js";import"./chunk-7XHF3YSK.js";import"./chunk-VMCGDDGP.js";import"./chunk-SNUCO2MK.js";
1
+ export{PYTHON_LOCK_FILENAME,PYTHON_RUNNER_VERSION,PYTHON_WHEELS_DIR,checkPluginAccessCatalog,checkPythonVendoring,normalizePackageName,parseRequirementsLock,parseTegoPluginManifest,parseWheelFilename,tegoPluginManifestSchema}from"./chunk-EIVA5BR6.js";export{LOCALE_FILENAME_PATTERN,MIGRATION_FILENAME_PATTERN,flattenLocaleKeys,verifyFrontendLocales,verifyMigrationNames}from"./chunk-2NTAWYCG.js";import"./chunk-IVJECPGG.js";import"./chunk-INCA3GFW.js";import"./chunk-56QOZQTY.js";import"./chunk-VMCGDDGP.js";import"./chunk-SNUCO2MK.js";