everyapp 0.2.2 → 0.3.0-rc.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 (120) hide show
  1. package/README.md +6 -77
  2. package/bin/everyapp.js +235 -0
  3. package/dist/access-F3GKBKAW.js +1 -0
  4. package/dist/activity-4MFMH5JP.js +12 -0
  5. package/dist/apps-63K2DGVT.js +9 -0
  6. package/dist/chunk-2UFJRKEE.js +4 -0
  7. package/dist/chunk-3AAHHUJC.js +11 -0
  8. package/dist/chunk-3Z53CH4F.js +6 -0
  9. package/dist/chunk-5ZTFC6DK.js +1 -0
  10. package/dist/chunk-6O3GJS5H.js +15 -0
  11. package/dist/chunk-74EST4VW.js +2 -0
  12. package/dist/chunk-7LG3F4FF.js +10 -0
  13. package/dist/chunk-AUDGOIHE.js +1 -0
  14. package/dist/chunk-BHMRY53V.js +2 -0
  15. package/dist/chunk-DQWOXSL3.js +32 -0
  16. package/dist/chunk-E3EPK27P.js +10 -0
  17. package/dist/chunk-E7EUXUVD.js +1 -0
  18. package/dist/chunk-ECLJ5IAL.js +126 -0
  19. package/dist/chunk-EQX4GKJ2.js +4 -0
  20. package/dist/chunk-EUNGJMJ6.js +20 -0
  21. package/dist/chunk-F6IH47GT.js +4 -0
  22. package/dist/chunk-G64S77GC.js +33 -0
  23. package/dist/chunk-GD2JZMD3.js +1 -0
  24. package/dist/chunk-GRFPP4UW.js +7 -0
  25. package/dist/chunk-IA534DGW.js +10 -0
  26. package/dist/chunk-IG26HYRR.js +45 -0
  27. package/dist/chunk-IZEOAOKL.js +6 -0
  28. package/dist/chunk-JYKRUS5C.js +1 -0
  29. package/dist/chunk-KMAOZRV4.js +18 -0
  30. package/dist/chunk-MC56CUQI.js +7 -0
  31. package/dist/chunk-MDORVBPD.js +1 -0
  32. package/dist/chunk-N4E5YE4X.js +5 -0
  33. package/dist/chunk-NJFWEYX7.js +1 -0
  34. package/dist/chunk-OEOVNJAN.js +1 -0
  35. package/dist/chunk-OVKVV6MK.js +2 -0
  36. package/dist/chunk-PL6EORDJ.js +15 -0
  37. package/dist/chunk-PMPIMHKG.js +6 -0
  38. package/dist/chunk-QZWSSWMP.js +22 -0
  39. package/dist/chunk-R4CGF622.js +2 -0
  40. package/dist/chunk-RFVMR32Y.js +1 -0
  41. package/dist/chunk-RQFHVU3Y.js +25 -0
  42. package/dist/chunk-SAT3PGZU.js +57 -0
  43. package/dist/chunk-SPPR6WCI.js +1 -0
  44. package/dist/chunk-TPWSGRNT.js +30 -0
  45. package/dist/chunk-VVQRET35.js +2 -0
  46. package/dist/chunk-VXJQGJ7Y.js +2 -0
  47. package/dist/chunk-WEM3WLOC.js +1 -0
  48. package/dist/chunk-Y3VIFPN5.js +1 -0
  49. package/dist/chunk-YJBYZWSP.js +22 -0
  50. package/dist/chunk-ZYL2ZHCF.js +98 -0
  51. package/dist/configure-2KMHM22T.js +8 -0
  52. package/dist/create-ELEJMZBS.js +22 -0
  53. package/dist/db-MZIAKGBN.js +9 -0
  54. package/dist/delete-H47KPJSB.js +3 -0
  55. package/dist/deploy-HGFJLLUO.js +1 -0
  56. package/dist/dev-62VA7UEM.js +86 -0
  57. package/dist/email-NNFD3VVB.js +10 -0
  58. package/dist/events-JDY76EG6.js +15 -0
  59. package/dist/groups-NQFXDZZP.js +8 -0
  60. package/dist/index.js +92 -1
  61. package/dist/install-JTZWRZIL.js +1 -0
  62. package/dist/login-P6LYQQWP.js +1 -0
  63. package/dist/mcp-NCFWEBMD.js +1 -0
  64. package/dist/provider-WVBI2VED.js +13 -0
  65. package/dist/release-6FYSPXXA.js +1 -0
  66. package/dist/restore-XBU7DOJV.js +3 -0
  67. package/dist/secret-ML5EXS3O.js +1 -0
  68. package/dist/setup-IEXCISLP.js +1 -0
  69. package/dist/status-OPLPKJ4U.js +4 -0
  70. package/dist/teardown-3JMMLK6O.js +22 -0
  71. package/dist/update-37ZDATLR.js +1 -0
  72. package/dist/workspace-DAFZVYQ2.js +21 -0
  73. package/package.json +30 -27
  74. package/skills/build-every-app/SKILL.md +199 -0
  75. package/skills/build-every-app/patterns.md +134 -0
  76. package/skills/everyapp/SKILL.md +292 -0
  77. package/skills/everyapp/mcp.md +336 -0
  78. package/skills/everyapp/security-review.md +61 -0
  79. package/skills/everyapp/setup/README.md +263 -0
  80. package/skills/everyapp/setup/access.md +178 -0
  81. package/skills/everyapp/setup/context.md +117 -0
  82. package/skills/everyapp/setup/dataforseo.md +287 -0
  83. package/skills/everyapp/setup/email.md +366 -0
  84. package/skills/everyapp/setup/gateway.md +243 -0
  85. package/skills/everyapp/setup/google.md +360 -0
  86. package/skills/everyapp/setup/microsoft-365.md +281 -0
  87. package/skills/everyapp/setup/openai.md +318 -0
  88. package/skills/everyapp/setup/openrouter.md +309 -0
  89. package/skills/everyapp/setup/providers.md +353 -0
  90. package/skills/everyapp/setup/remote-mcp.md +199 -0
  91. package/skills/everyapp/setup/slack.md +344 -0
  92. package/skills/everyapp/setup/workers-ai.md +138 -0
  93. package/bin/__everyapp_bash_complete.js +0 -12
  94. package/dist/bash-complete.js +0 -3
  95. package/dist/chunk-7BOQJFI5.js +0 -8
  96. package/dist/chunk-7YVJFS4A.js +0 -1
  97. package/dist/chunk-BIXKPCDH.js +0 -2
  98. package/dist/chunk-DLGPLHBH.js +0 -47
  99. package/dist/chunk-FD6K2YA7.js +0 -9
  100. package/dist/chunk-GAJEQYJH.js +0 -39
  101. package/dist/chunk-O2KDOP5R.js +0 -13
  102. package/dist/chunk-OI2MBGBG.js +0 -4
  103. package/dist/chunk-POSDLD3G.js +0 -8
  104. package/dist/chunk-RB5D2R45.js +0 -1
  105. package/dist/chunk-U4LIQ5VR.js +0 -1
  106. package/dist/chunk-VEYSNPDH.js +0 -3
  107. package/dist/chunk-VP6SKW44.js +0 -7
  108. package/dist/chunk-WO3NE7ZU.js +0 -7
  109. package/dist/chunk-XTO2VTPY.js +0 -6
  110. package/dist/chunk-YJ2DKIZS.js +0 -1
  111. package/dist/create-DYQ3NBWJ.js +0 -22
  112. package/dist/deploy-PYTCMDGV.js +0 -7
  113. package/dist/deploy-RMAHOA4Z.js +0 -55
  114. package/dist/dev-SAE237FS.js +0 -0
  115. package/dist/generateConfig-46Z3ERF3.js +0 -1
  116. package/dist/login-B6EF3SMX.js +0 -1
  117. package/dist/logout-XMKZBLNV.js +0 -1
  118. package/dist/remoteD1Shell-A67JMO3H.js +0 -6
  119. package/dist/setupLocal-TZJUW5ES.js +0 -6
  120. package/gateway/every-app-gateway-build.tar.gz +0 -0
@@ -0,0 +1,22 @@
1
+ import{a as it,d as ct}from"./chunk-5ZTFC6DK.js";import{a as Ze,b as He,c as je}from"./chunk-QZWSSWMP.js";import"./chunk-E3EPK27P.js";import"./chunk-IG26HYRR.js";import"./chunk-IA534DGW.js";import"./chunk-MDORVBPD.js";import{a as ee,c as st,d as de,e as at}from"./chunk-VVQRET35.js";import{a as tt,e as ot,i as nt,j as rt}from"./chunk-NJFWEYX7.js";import{a as Ve,c as Ge,d as Ye,h as Je,i as Xe,j as qe,k as Q,l as Qe}from"./chunk-AUDGOIHE.js";import{a as q,b as Be,i as et}from"./chunk-OVKVV6MK.js";import"./chunk-RFVMR32Y.js";import{a as ve,h as $e,j as Ce,l as Re,p as Te,v as Pe}from"./chunk-6O3GJS5H.js";import{a as Me,d as Ue}from"./chunk-GRFPP4UW.js";import"./chunk-Y3VIFPN5.js";import{a as W,d as Fe}from"./chunk-PL6EORDJ.js";import"./chunk-EUNGJMJ6.js";import{a as Oe,k as Se,oa as We,q as le,r as Le,t as xe}from"./chunk-ZYL2ZHCF.js";import"./chunk-SPPR6WCI.js";import{a as _e}from"./chunk-7LG3F4FF.js";import"./chunk-GD2JZMD3.js";import{a as De,f as Ie}from"./chunk-F6IH47GT.js";import{d as be,j as Ne}from"./chunk-JYKRUS5C.js";import{Ab as Ae,Ub as Ee,Wb as ce,d as ye,f as E,gc as ze,ib as ie,k as D,oc as Ke,tb as $,vb as ke,xb as x,zb as U}from"./chunk-SAT3PGZU.js";import F from"chalk";import $t from"node:fs";import Ct from"node:os";import lt from"node:path";var mt=["23bc28f0b638fbfc1c7f838573c17ee3"];var Rt=/^[0-9a-f]{32}$/,pt=e=>Rt.test(e),me="EVERYAPP_TEST_ACCOUNTS";function Tt(e=process.env){let o=e.XDG_CONFIG_HOME||lt.join(Ct.homedir(),".config");return lt.join(o,"everyapp","test-accounts")}function dt(e){let o=new Set,a=[];for(let n of e.split(/\r?\n/)){let c=n.replace(/#.*$/,"");for(let i of c.split(/[\s,]+/)){if(!i)continue;let m=i.toLowerCase();pt(m)?o.add(m):a.push(i)}}return{ids:[...o],ignored:a}}function bt(e=process.env){let o=Tt(e),a="";try{a=$t.readFileSync(o,"utf8")}catch(i){if(i.code!=="ENOENT")throw i}let n=dt(e[me]??""),c=dt(a);return{ids:[...new Set([...n.ids,...c.ids])],ignored:[...n.ignored,...c.ignored],file:o}}function Nt(e,o){return["only for a dedicated test account that holds nothing else (docs/runbooks/onboarding-loop-test-accounts.md):",`set ${me}=${e} for this one command, or add a line ${e} to ${o}`].join(" ")}function ft({accountId:e,out:o,env:a=process.env}){let n=e.trim().toLowerCase();if(!pt(n))throw new $("TEARDOWN_INVALID_ACCOUNT_ID",`"${e.trim()}" isn't a Cloudflare account id
2
+ An account id is 32 hex characters (Compute (Workers) \u2192 Overview, or any dashboard URL dash.cloudflare.com/<account id>/...).`,{fix:"pass the test account's id from the dashboard as --account-id",data:{accountId:e.trim()}});if(mt.includes(n))throw new $("TEARDOWN_ACCOUNT_FORBIDDEN",`Cloudflare account ${n} can never be torn down
3
+ It is a live account, refused by id before any allowlist is read.`,{data:{accountId:n}});let c=bt(a);if(c.ignored.length>0&&o.emit({type:"warning",code:"TEARDOWN_ALLOWLIST_IGNORED",message:`Ignored ${E(c.ignored.length,"entry")} of the teardown allowlist that ${c.ignored.length===1?"isn't":"aren't"} account ids: ${D(c.ignored)}
4
+ An account id is 32 hex characters (${me}, ${c.file}).`,data:{ignored:c.ignored,file:c.file}}),!c.ids.includes(n))throw new $("TEARDOWN_NOT_ALLOWLISTED",`Cloudflare account ${n} isn't on the teardown allowlist
5
+ teardown deletes everything Every App made in an account, so it runs only against accounts listed as dedicated test accounts.`,{fix:Nt(n,c.file),data:{accountId:n,file:c.file}});if(!a.CLOUDFLARE_API_TOKEN)throw new $("TEARDOWN_NEEDS_TOKEN",`teardown needs CLOUDFLARE_API_TOKEN
6
+ A Cloudflare login is a person, who sees every account they belong to; teardown runs only with an API token scoped to the test account (runbook step 5).`,{fix:"set CLOUDFLARE_API_TOKEN to the test account's token for this one command, and run it again",data:{accountId:n}});return{accountId:n,allowlist:c.ids}}function ut({accountId:e,allowlist:o,whoami:a}){if(!a.authenticated||a.tokenValid===!1)throw new $("CLOUDFLARE_TOKEN_REJECTED",`Cloudflare didn't accept CLOUDFLARE_API_TOKEN
7
+ It may be mistyped, expired or revoked.`,{fix:"set the test account's token (runbook step 5)"});if(a.authSource&&!/CLOUDFLARE_API_TOKEN/.test(a.authSource))throw new $("TEARDOWN_NEEDS_TOKEN",`teardown needs CLOUDFLARE_API_TOKEN, but cf authenticated with ${a.authSource}
8
+ A Cloudflare login is a person, who sees every account they belong to; teardown runs only with an API token scoped to the test account (runbook step 5).`,{fix:"set CLOUDFLARE_API_TOKEN to the test account's token for this one command, and run it again",data:{accountId:e,authSource:a.authSource}});let n=(a.accounts??[]).map(i=>({id:i.id.toLowerCase(),name:i.name??null})),c=n.filter(i=>mt.includes(i.id)||!o.includes(i.id));if(c.length>0)throw new $("TEARDOWN_TOKEN_TOO_BROAD",[`CLOUDFLARE_API_TOKEN sees ${E(c.length,"account")} that ${c.length===1?"isn't a":"aren't"} test account${c.length===1?"":"s"}: ${D(c.map(i=>i.name?`${i.name} (${i.id})`:i.id))}`,"A teardown token must be scoped to the test account alone (Account Resources \u2192 Include \u2192 that account), so there is no path from here to a live account."].join(`
9
+ `),{fix:"mint a token for the test account only (runbook step 5)",data:{accountId:e,accounts:c}});if(!n.some(i=>i.id===e))throw new $("TEARDOWN_ACCOUNT_NOT_VISIBLE",`CLOUDFLARE_API_TOKEN doesn't list Cloudflare account ${e}
10
+ ${n.length===0?"It lists no accounts.":`It lists ${D(n.map(i=>i.id))}.`}`,{fix:"use that account's token, with Account Settings: Read on it (runbook step 5)",data:{accountId:e,accounts:n}})}import gt from"chalk";var te="every-",I=e=>e.startsWith(te),oe=e=>e.type==="MX"&&/\.mx\.cloudflare\.net\.?$/i.test(e.content??"")||e.type==="TXT"&&/_spf\.mx\.cloudflare\.net/i.test(e.content??""),Pt=new Set(["A","AAAA","CNAME"]),Z=e=>Pt.has(e.type.toUpperCase()),z=[["access","Access application or policy","Access applications and policy"],["email","Email Routing change","Email Routing changes"],["workflows","Workflow","Workflows"],["workers","Worker","Workers"],["certificates","certificate pack","certificate packs"],["d1","D1 database","D1 databases"],["kv","KV namespace","KV namespaces"],["r2","R2 bucket","R2 buckets"],["hyperdrive","Hyperdrive config","Hyperdrive configs"],["dns","DNS record","DNS records"]],H={access:"Zero Trust \u2192 Access \u2192 Applications and Policies",email:"the zone \u2192 Email \u2192 Email Routing",workflows:"Compute (Workers) \u2192 Workflows",workers:"Compute (Workers) \u2192 Workers & Pages",certificates:"the zone \u2192 SSL/TLS \u2192 Edge Certificates",d1:"Storage & Databases \u2192 D1",kv:"Storage & Databases \u2192 KV",r2:"Storage & Databases \u2192 R2 Object Storage",hyperdrive:"Storage & Databases \u2192 Hyperdrive",dns:"the zone \u2192 DNS \u2192 Records"},j=e=>e.reduce((o,a)=>o+a.rules.length+(a.catchAllWorker?1:0)+(a.disable?1:0),0),pe=e=>e.reduce((o,a)=>o+a.records.length,0);function K(e){return{access:e.access.applications.length+(e.access.policy?1:0),email:j(e.email),workflows:e.workflows.length,workers:e.workers.length,certificates:e.certificates.length,d1:e.d1.length,kv:e.kv.length,r2:e.r2.length,hyperdrive:e.hyperdrive.length,dns:pe(e.dns)}}function _(e){return Object.values(K(e)).reduce((o,a)=>o+a,0)}var k={worker:e=>`${W.worker} "${e}"`,workflow:e=>`Workflow "${e}"`,d1:e=>`${W.d1} "${e}"`,kv:e=>`${W.kv} "${e}"`,r2:e=>`${W.r2} "${e}"`,hyperdrive:e=>`${W.hyperdrive} "${e}"`,application:e=>`Access application "${e}"`,policy:e=>`Access policy "${e}"`,rule:(e,o)=>`Email Routing rule for ${e??"(no address)"} on ${o}`,catchAll:e=>`catch-all of ${e}`,routing:e=>`Email Routing on ${e}`,pack:(e,o)=>`certificate pack ${e} for ${o}`,record:(e,o)=>`DNS ${e} record at ${o}`};function Dt(e){let o=K(e),a={access:[...e.access.applications.map(n=>`application "${n.name}"`),...e.access.policy?[`policy "${e.access.policy.name}"`]:[]],email:e.email.flatMap(n=>[...n.rules.map(c=>`rule for ${c.address??"(no address)"} on ${n.zoneName}`),...n.catchAllWorker?[`${n.zoneName}'s catch-all back to dropping mail`]:[],...n.disable?[`Email Routing off for ${n.zoneName}`]:[]]),workflows:e.workflows,workers:e.workers.map(n=>n.domains.length>0?`${n.name} (${n.domains.join(", ")})`:n.name),certificates:e.certificates.map(n=>`${n.packId} (${n.hostname})`),d1:e.d1.map(n=>n.name),kv:e.kv.map(n=>n.title),r2:e.r2,hyperdrive:e.hyperdrive.map(n=>n.name),dns:e.dns.flatMap(n=>n.records.map(c=>`${c.type} at ${n.name}`))};return z.flatMap(([n,,c])=>o[n]===0?[]:[{label:`${c} (${o[n]})`,items:a[n]}])}function ht(e){let o=Dt(e).map(({label:a,items:n})=>({label:a,value:n.join(", ")}));return[...o.length>0?ze(o):[gt.dim("Nothing of Every App's to delete.")],...e.kept.length>0?["",gt.dim(`Kept: ${e.kept.join("; ")}.`)]:[]]}function ne(e){let o=K(e),a=z.flatMap(([n,c,i])=>o[n]===0?[]:[o[n]===1?`1 ${c}`:`${o[n]} ${i}`]);return`${E(_(e),"resource")}${a.length>0?`: ${a.join(", ")}`:""}`}function B(e){return{accountId:e.accountId,gateway:e.gateway,counts:K(e),total:_(e),workers:e.workers,workflows:e.workflows,d1:e.d1,kv:e.kv,r2:e.r2,hyperdrive:e.hyperdrive,access:e.access,email:e.email,certificates:e.certificates,dns:e.dns,kept:e.kept}}var It=e=>typeof e=="object"&&e!==null&&"removedAs"in e,_t={name:"",enabled:!1,matchers:[{type:"all"}],actions:[{type:"drop"}]},re="teardown.delete",Ot=5e3;async function yt({cf:e,plan:o,out:a,sleep:n=c=>new Promise(i=>setTimeout(i,c))}){let c=[],i=[],m=async(r,d,A)=>{try{let w=await A();if(w===!1)return;c.push(It(w)?w.removedAs:d)}catch(w){if(U(w,"not_found"))return;let f=x(w).split(`
11
+ `)[0]??"";i.push({kind:r,described:d,place:H[r],reason:f}),a.emit({type:"warning",code:"TEARDOWN_DELETE_FAILED",step:re,message:`Couldn't delete the ${d}
12
+ ${f}`,data:{kind:r,described:d,reason:f,fix:`delete it in the Cloudflare dashboard (${H[r]})`}})}},l=async(r,d,A)=>{if(d===0)return;let[,w,f]=z.find(([C])=>C===r),v=d===1?w:f,O=c.length,S=i.length;a.emit({type:"step",step:re,status:"start",message:`Deleting ${d} ${v}...`,data:{kind:r,count:d}}),await A();let N=c.length-O,L=i.length-S;a.emit({type:"step",step:re,status:"ok",message:L===0?`Deleted ${N} ${N===1?w:f}`:`Deleted ${N} ${N===1?w:f}; kept ${L}`,data:{kind:r,removed:c.slice(O),kept:i.slice(S).map(C=>C.described)}})};await l("access",o.access.applications.length+(o.access.policy?1:0),async()=>{for(let r of o.access.applications)await m("access",k.application(r.name),()=>Te(e,r.id));if(o.access.policy){let{id:r,name:d}=o.access.policy;await m("access",k.policy(d),()=>Ce(e,r))}}),await l("email",j(o.email),async()=>{for(let r of o.email){for(let d of r.rules)await m("email",k.rule(d.address,r.zoneName),()=>Je(e,r.zoneId,d.id));r.catchAllWorker&&await m("email",k.catchAll(r.zoneName),()=>qe(e,r.zoneId,_t)),r.disable&&await m("email",k.routing(r.zoneName),()=>Ge(e,r.zoneId))}}),await l("workflows",o.workflows.length,async()=>{for(let r of o.workflows)await m("workflows",k.workflow(r),()=>rt(e,r))}),await l("workers",o.workers.length,async()=>{for(let r of o.workers)await m("workers",k.worker(r.name),()=>Ue(e,r.name,{withReferences:!0}))}),await l("certificates",o.certificates.length,async()=>{for(let r of o.certificates){let d=k.pack(r.packId,r.hostname);await m("certificates",d,async()=>{try{await at(e,r.zoneId,r.packId);return}catch(A){if(U(A,"not_found"))throw A;let w=x(A).split(`
13
+ `)[0]??"",f=()=>ee(e,r.zoneId,{allStatuses:!0}),v=await f().catch(()=>null);if(v&&!de(v,r.packId)&&(await n(Ot),v=await f().catch(()=>v)),v&&de(v,r.packId))return{removedAs:`${d} (gone with its Custom Domain)`};throw new Error(`${w}. Cloudflare removes a hostname's pack with its Custom Domain, so it is likely on its way out; if the zone still lists it, finish it in the dashboard`)}})}});let h=o.d1.find(r=>r.id===o.gateway?.databaseId)??null,T=o.d1.filter(r=>r!==h);await l("d1",T.length,async()=>{for(let r of T)await m("d1",k.d1(r.name),()=>le(e,r.id))}),await l("kv",o.kv.length,async()=>{for(let r of o.kv)await m("kv",k.kv(r.title),()=>Le(e,r.id))}),await l("r2",o.r2.length,async()=>{for(let r of o.r2)await m("r2",k.r2(r),()=>ct(e,r))}),await l("hyperdrive",o.hyperdrive.length,async()=>{for(let r of o.hyperdrive)await m("hyperdrive",k.hyperdrive(r.name),()=>ot(e,r.id))});let b=[];for(let r of o.dns)try{for(let d of await q(e,r.zoneId,r.name))Z(d)&&(oe({type:d.type,content:d.content??null})||b.push({zoneId:r.zoneId,name:r.name,id:d.id,type:d.type}))}catch(d){a.emit({type:"warning",code:"TEARDOWN_DELETE_FAILED",step:re,message:`Couldn't read the DNS records at ${r.name}
14
+ ${x(d).split(`
15
+ `)[0]}`,data:{kind:"dns",name:r.name,fix:`check them in the Cloudflare dashboard (${H.dns})`}})}return await l("dns",b.length,async()=>{for(let r of b)await m("dns",k.record(r.type,r.name),()=>Be(e,r.zoneId,r.id))}),await l("d1",h?1:0,async()=>{if(!h)return;let r=i.filter(d=>d.kind==="dns");if(r.length>0){i.push({kind:"d1",described:k.d1(h.name),place:H.d1,reason:`kept on purpose: the next run reads the hostnames of the ${E(r.length,"kept DNS record")} above from it`});return}await m("d1",k.d1(h.name),()=>le(e,h.id))}),{removed:c,kept:i}}var fe="teardown.inventory";function St(e,o){return e===ie?1:o===ie?-1:e.localeCompare(o)}var Lt=(e,o)=>e===o.name||e.endsWith(`.${o.name}`);async function kt({cf:e,accountId:o,out:a}){a.emit({type:"step",step:fe,status:"start",message:`Looking for what Every App created in ${o}...`,data:{accountId:o}});let n=(t,s,p)=>a.emit({type:"step",step:"teardown.found",status:"ok",message:`${s} ${z.find(([u])=>u===t)[s===1?1:2]}`,data:{kind:t,count:s,items:p}}),c=[],i=async(t,s,p)=>{try{return await p()}catch(u){let g=x(u).split(`
16
+ `)[0]??"";if(Ae(u)){let R=U(u,"r2_not_enabled")?"R2":"Cloudflare Access";return a.emit({type:"step",step:"teardown.found",status:"skip",message:`${R} isn't enabled in this account: nothing of its to delete`,data:{what:t,service:R,reason:g}}),s}return c.push(`${t} (couldn't be listed: ${g})`),a.emit({type:"warning",code:"TEARDOWN_READ_FAILED",step:fe,message:`Couldn't list ${t}; whatever is there is kept
17
+ ${g}`,data:{what:t,reason:g,fix:"check the token's permissions (runbook step 5), then run again"}}),s}},m=await i("the Gateway's database",null,()=>xe(e)),l=m?Oe(e,m):null,h=l?await i("the Gateway's install row",null,()=>Se(l)):null,T=l?await i("the Gateway's apps",[],()=>We(l)):[],b=l?await i("the apps' recorded resources",[],()=>Fe(l)):[],r=t=>new Set(b.filter(s=>s.kind===t).map(s=>s.name)),d=(t,s)=>I(s)||r(t).has(s),A=new Set(T.flatMap(t=>t.workerName?[t.workerName]:[])),w=await i("the Workers",[],()=>Me(e)),f=w.filter(t=>d("worker",t.name)||A.has(t.name)).sort((t,s)=>St(t.name,s.name)).map(t=>({name:t.name,domains:(t.references?.domains??[]).map(s=>s.hostname.toLowerCase()).sort()}));n("workers",f.length,f.map(t=>t.name));let v=(await i("the Workflows",[],()=>nt(e,te))).filter(t=>I(t.script_name??"")||I(t.name)).map(t=>t.name).sort();n("workflows",v.length,v);let O=(await i("the D1 databases",[],()=>ce(e,["d1","list"],be,{perPage:100,maxPages:10}))).filter(t=>d("d1",t.name)).map(t=>({name:t.name,id:t.uuid})).sort((t,s)=>t.id===m?1:s.id===m?-1:t.name.localeCompare(s.name));n("d1",O.length,O.map(t=>t.name));let S=(await i("the KV namespaces",[],()=>ce(e,["kv","namespaces","list"],Ne,{perPage:100,maxPages:50}))).filter(t=>d("kv",t.title)).map(t=>({title:t.title,id:t.id})).sort((t,s)=>t.title.localeCompare(s.title));n("kv",S.length,S.map(t=>t.title));let N=(await i("the R2 buckets",[],()=>it(e,te))).filter(I).sort();n("r2",N.length,N);let L=(await i("the Hyperdrive configs",[],()=>tt(e))).filter(t=>d("hyperdrive",t.name)).map(t=>({name:t.name,id:t.id})).sort((t,s)=>t.name.localeCompare(s.name));n("hyperdrive",L.length,L.map(t=>t.name));let C={applications:[],policy:null},ue=await i("the Zero Trust team",null,()=>ve(e));if(ue){let t=h?{gatewayHost:h.gatewayHost.toLowerCase(),appBaseDomain:h.appBaseDomain.toLowerCase()}:null,s=await i("the Access applications",[],()=>Re(e)),p=t?Pe(s,t):{gateway:null,apps:null},u=s.filter(y=>y.name===He||y.name===je),g=new Map;for(let y of[p.gateway,p.apps,...u])y&&g.set(y.id,y);C.applications=[...g.values()].map(y=>({id:y.id,name:y.name??y.id}));let R=(await i("the Access policies",[],()=>$e(e))).find(y=>y.name===Ze);C.policy=R?{id:R.id,name:R.name}:null,c.push(`the Zero Trust team ${ue.auth_domain} and its identity providers`)}n("access",C.applications.length+(C.policy?1:0),{applications:C.applications.map(t=>t.name),policy:C.policy?.name??null});let M=new Set;if(h){M.add(h.gatewayHost.toLowerCase());for(let t of h.liveDomains)M.add(t.toLowerCase())}for(let t of T)t.hostname&&M.add(t.hostname.toLowerCase());for(let t of f)for(let s of t.domains)M.add(s);let ge=new Map((await i("the account's zones",[],()=>et(e,o))).map(t=>[t.id,{id:t.id,name:t.name}])),V=new Map,he=(t,s)=>{let p=ge.get(t);p?V.set(p.id,p):c.push(`the zone ${s.toLowerCase()} (${t}), which isn't in account ${o}`)};h&&he(h.zoneId,h.zoneName);for(let t of w)if(f.some(s=>s.name===t.name))for(let s of t.references?.domains??[])s.zone_id&&s.zone_name&&he(s.zone_id,s.zone_name);let Et=t=>{let s=[...ge.values()].filter(p=>Lt(t,p)).sort((p,u)=>u.name.length-p.name.length)[0];return s&&V.set(s.id,s),s??null},se=new Map;for(let t of[...M].sort()){let s=Et(t);s&&se.set(t,s)}for(let t of V.values())c.push(`the zone ${t.name}`);c.push("the Workers Paid subscription","the workers.dev subdomain");let G=[];for(let t of[...V.values()].sort((s,p)=>s.name.localeCompare(p.name))){let s=await i(`Email Routing on ${t.name}`,null,async()=>({settings:await Ve(e,t.id),rules:await Ye(e,t.id),catchAll:await Xe(e,t.id)}));if(!s)continue;let{settings:p,rules:u,catchAll:g}=s,R=u.filter(P=>P.id!==null&&I(Q(P)??"")),y=Q(g),ae=y!==null&&I(y);if(R.length===0&&!ae)continue;let vt=!g.enabled||g.actions.every(P=>P.type==="drop");G.push({zoneId:t.id,zoneName:t.name,rules:R.map(P=>({id:P.id,address:Qe(P),worker:Q(P)})),catchAllWorker:ae?y:null,disable:p.enabled&&R.length===u.length&&(ae||vt)})}n("email",j(G),G);let Y=[],we=new Map;for(let[t,s]of se){let p=we.get(s.id);p||(p=await i(`the certificate packs of ${s.name}`,[],()=>ee(e,s.id)),we.set(s.id,p));for(let u of st(p,t))p.splice(p.indexOf(u),1),Y.push({zoneId:s.id,zoneName:s.name,hostname:t,packId:u.id})}n("certificates",Y.length,Y.map(t=>`${t.packId} (${t.hostname})`));let J=[];for(let[t,s]of[...se].sort(([p],[u])=>p.localeCompare(u))){let p=await i(`the DNS records at ${t}`,[],()=>q(e,s.id,t)),u=[...new Set(p.filter(g=>!Z(g)&&!oe({type:g.type,content:g.content??null})).map(g=>g.type.toUpperCase()))].sort();u.length>0&&c.push(`the ${D(u)} ${u.length===1?"record":"records"} at ${t}, which Every App never created`),J.push({zoneId:s.id,zoneName:s.name,name:t,records:p.filter(Z).map(g=>({id:g.id,type:g.type,content:g.content??null}))})}n("dns",pe(J),J.filter(t=>t.records.length>0));let X={accountId:o,gateway:m?{databaseId:m,gatewayHost:h?.gatewayHost??null}:null,workers:f,workflows:v,d1:O,kv:S,r2:N,hyperdrive:L,access:C,email:G,certificates:Y,dns:J,kept:c};return a.emit({type:"step",step:fe,status:"ok",message:`Found ${E(_(X),"resource")} of Every App's in ${o}.`,data:{accountId:o,counts:K(X),total:_(X)}}),X}var At="teardown.check";async function Yo(e){await Ke(e,o=>xt({flags:e,out:o}))}async function xt(e){let{flags:o,out:a}=e,n=e.env??process.env,c=e.createCf??(f=>Ee({accountId:f,env:n})),i=e.interactive??De(o.json??!1),m=o["dry-run"]??!1;a.emit({type:"step",step:At,status:"start",message:"Checking the account may be torn down..."});let{accountId:l,allowlist:h}=ft({accountId:o["account-id"],out:a,env:n}),T=o.confirm?.trim().toLowerCase();if(T!==void 0&&T!==l)throw new $("TEARDOWN_CONFIRM_MISMATCH",`--confirm ${o.confirm} isn't the account id
18
+ It must repeat --account-id ${l} exactly. Nothing was read or changed.`,{fix:`pass --confirm ${l}`,data:{accountId:l,confirm:o.confirm}});ut({accountId:l,allowlist:h,whoami:await _e(c())}),a.emit({type:"step",step:At,status:"ok",message:`${l} is a test account, and the token sees it alone`,data:{accountId:l}});let b=c(l),r=await kt({cf:b,accountId:l,out:a}),d=_(r),A=ht(r);if(m){let f=d===0?`Nothing of Every App is left in ${l}.`:`Dry run: ${ne(r)} would be removed from ${l}. Nothing was changed.`;a.emit({type:"done",message:f,display:[F.bold(d===0?f:`Would remove ${ne(r)} from ${l}`),...d===0?A.slice(1):A,...d===0?[]:["",F.dim("Nothing was changed (--dry-run).")]].join(`
19
+ `),data:{accountId:l,dryRun:!0,removed:[],kept:[],plan:B(r)}});return}if(d===0){let f=`Nothing of Every App is left in ${l}.`;a.emit({type:"done",message:f,display:[F.green(f),...A.slice(1)].join(`
20
+ `),data:{accountId:l,dryRun:!1,removed:[],kept:[],plan:B(r)}});return}if(T===void 0){if(!i)throw new $("TEARDOWN_CONFIRMATION_REQUIRED",`Tearing down ${l} needs its id typed at a terminal, or --confirm ${l}
21
+ ${ne(r)} would be deleted for good. Nothing was changed.`,{rerun:ye(["--confirm",l]),rerunNote:"only for a dedicated test account",authorize:!0,data:{accountId:l,plan:B(r)}});let f=(await(e.askConfirmation??Wt)(l,A)).trim().toLowerCase();if(f!==l)throw new ke(f?`"${f}" doesn't match ${l}. Nothing deleted.`:"Cancelled. Nothing deleted.")}else i&&a.emit({type:"step",step:"teardown.plan",status:"ok",message:`Deleting everything Every App created in ${l}:`,data:{accountId:l,lines:A}});let w=await yt({cf:b,plan:r,out:a,...e.sleep?{sleep:e.sleep}:{}});a.emit({type:"done",...zt(l,r,w)})}function Wt(e,o){return Ie(`Type ${e} to delete all of this`,{context:[`This deletes everything Every App created in Cloudflare account ${e}, for good:`,"",...o,""]})}function zt(e,o,{removed:a,kept:n}){let c=n.length===0?`Removed ${E(a.length,"resource")} from ${e}.`:`Removed ${E(a.length,"resource")} from ${e}; kept ${n.length} (${D(n.map(m=>`${m.described}: ${m.reason}`))}).`,i=[F.green(`Removed ${E(a.length,"resource")} from ${e}`),...n.length>0?["",`Kept ${E(n.length,"resource")}:`,...n.map(m=>` ${m.described} ${F.dim(`\xB7 ${m.reason} \xB7 ${m.place}`)}`)]:[],...o.kept.length>0?["",F.dim(`Left as they were: ${o.kept.join("; ")}.`)]:[]].join(`
22
+ `);return{message:c,display:i,data:{accountId:e,dryRun:!1,removed:a,kept:n,plan:B(o)}}}export{Yo as teardown,xt as teardownAccount};
@@ -0,0 +1 @@
1
+ import{b as e}from"./chunk-VXJQGJ7Y.js";import{c as o}from"./chunk-YJBYZWSP.js";import"./chunk-N4E5YE4X.js";import"./chunk-QZWSSWMP.js";import"./chunk-E3EPK27P.js";import"./chunk-IG26HYRR.js";import"./chunk-IA534DGW.js";import"./chunk-MDORVBPD.js";import"./chunk-IZEOAOKL.js";import"./chunk-KMAOZRV4.js";import"./chunk-PMPIMHKG.js";import"./chunk-OVKVV6MK.js";import"./chunk-RFVMR32Y.js";import"./chunk-6O3GJS5H.js";import"./chunk-GRFPP4UW.js";import"./chunk-Y3VIFPN5.js";import"./chunk-R4CGF622.js";import"./chunk-PL6EORDJ.js";import"./chunk-DQWOXSL3.js";import"./chunk-3AAHHUJC.js";import"./chunk-EUNGJMJ6.js";import"./chunk-ZYL2ZHCF.js";import"./chunk-SPPR6WCI.js";import"./chunk-7LG3F4FF.js";import"./chunk-GD2JZMD3.js";import"./chunk-F6IH47GT.js";import"./chunk-JYKRUS5C.js";import"./chunk-SAT3PGZU.js";async function m(a){await e(a,(t,y)=>o(a,t,y))}export{m as update};
@@ -0,0 +1,21 @@
1
+ import{a as w,b as X,e as v,f as Z,g as Q,h as $,i as b}from"./chunk-OEOVNJAN.js";import{a as S,b as G,f as L,i as j,j as D,k as E,l as F,m as K,n as Y,o as q,p as B,q as J,r as V,s as H}from"./chunk-ECLJ5IAL.js";import{a as T}from"./chunk-Y3VIFPN5.js";import{$ as W,qa as _,sa as P}from"./chunk-ZYL2ZHCF.js";import"./chunk-SPPR6WCI.js";import"./chunk-7LG3F4FF.js";import{a as R,i as N}from"./chunk-F6IH47GT.js";import"./chunk-JYKRUS5C.js";import{Aa as x,c as h,f as y,gc as C,hc as U,ib as O,jc as M,k as z,oc as l,tb as f}from"./chunk-SAT3PGZU.js";import{stripVTControlCharacters as A}from"node:util";import d from"chalk";var u="workspace.update",ee="workspace.rename",te="workspace.role";async function Ue(e,n){await l(e,t=>ce({flags:e,out:t},n))}async function Me(e,n,t){await l(e,o=>re({flags:e,out:o},n,t))}function ie(e){let{flags:n,out:t}=e;return P({gateway:_(n.gateway),accountId:n["account-id"],interactive:R(n.json??!1),login:{json:n.json,out:t,step:"login"},createCf:e.createCf})}var ae=e=>`${/^[aeiou]/.test(e)?"an":"a"} ${e}`;async function re(e,n,t){let{out:o}=e,i=n.trim().toLowerCase(),c=await ie(e),s=await W(c.db);if(!s)throw new f("MEMBER_NOT_FOUND",`This Gateway has no workspace yet
2
+ Nobody has claimed its owner link, so it has no members.`,{rerun:`${h} setup`,rerunNote:"to claim it"});let{organization:r}=s,m={id:r.id,name:r.name};o.emit({type:"step",step:te,status:"start",message:`Setting ${i}'s role...`});let a=await S(c.db,r.id,i,t);if(a.kind==="not_found")throw new f("MEMBER_NOT_FOUND",`No member ${i} in ${r.name}
3
+ Someone still invited isn't a member until they accept.`,{fix:"check the address against Manage workspace \u2192 Users, or invite them first",data:{workspace:m,email:i}});let p={id:a.member.id,email:a.member.email,name:a.member.name};if(a.kind==="last_owner")throw new f("LAST_OWNER",`${i} is the last owner of ${r.name}
4
+ The workspace always keeps one owner, so their role stays until someone else is an owner.`,{rerun:`${h} workspace role <email> owner`,rerunNote:"for someone else, then this command again",data:{workspace:m,member:p,role:t}});let g=a.kind==="changed",k=g?`${i} is now ${ae(t)} of ${r.name}`:`${i} is already ${ae(t)} of ${r.name}`;o.emit({type:"step",step:te,status:"ok",message:k,data:{email:i,role:t,changed:g}});let I=g?"The Gateway picks up the change within 30 seconds.":"Nothing changed.";o.emit({type:"done",message:A(`${k}. ${I}`),display:[g?M(k):d.dim(`${k}.`),d.dim(I)].join(`
5
+ `),data:{workspace:m,member:p,role:t,changed:g,...g?{previousRole:a.previousRole}:{}}})}async function ce(e,n){let{out:t}=e,o=x(n);if(!o)throw new f("WORKSPACE_NAME_INVALID",`Use 1 to ${64} characters with no line breaks
6
+ A name is trimmed, and runs of spaces become one.`,{fix:"give the name as one argument, quoted when it has spaces"});let i=await ie(e);t.emit({type:"step",step:ee,status:"start",message:"Renaming the workspace..."});let{organization:c,previousName:s,changed:r}=await G(i.db,o);t.emit({type:"step",step:ee,status:"ok",message:r?`Renamed ${s} to ${o}`:`${o} already`,data:{id:c.id,name:o,previousName:s,changed:r}});let m=r?`Renamed the workspace to ${o}.`:`The workspace is named ${o} already.`,a=r?"Members see the name on their next page.":"Nothing changed.";t.emit({type:"done",message:A(`${m} ${a}`),display:[m,d.dim(a)].join(`
7
+ `),data:{organization:{id:c.id,name:c.name,slug:c.slug},previousName:s,changed:r,gateway:i.gatewayUrl}})}async function Se(e){await l(e,n=>de({flags:e,out:n}))}async function Ge(e){await l(e,n=>pe({flags:e,out:n}))}async function Le(e,n){await l(e,t=>le({flags:e,out:t},n,e.role))}async function je(e,n){await l(e,t=>ge({flags:e,out:t},n))}async function De(e,n){await l(e,t=>we({flags:e,out:t},n))}async function Fe(e,n){await l(e,t=>ue({flags:e,out:t},n))}async function Ke(e,n){await l(e,t=>ye({flags:e,out:t},n))}async function Ye(e,n){await l(e,t=>ve({flags:e,out:t},n))}async function qe(e,n){await l(e,t=>ke({flags:e,out:t},n))}var ne=["CLOUDFLARE_ACCESS_AUTO_JOIN_DOMAINS","CLOUDFLARE_ACCESS_OUTER_RING_AUTO_JOIN"];function me(e){return e.includes("EMAIL_FROM")&&(e.includes("RESEND_API_KEY")||e.includes("EMAIL_REST_API_TOKEN"))}async function de(e){let{out:n}=e,{gateway:t,ws:o,gatewayUrl:i}=await w(e),[c,s]=await Promise.all([j(o),T(t.cf,O)]),{organization:r,owners:m,counts:a}=c,p=o.operator,g=me(s),k=p.email===null?`${p.actor} (an API token; no account)`:p.userId?`${p.email}`:`${p.email} (no account on this Gateway)`,I=[{label:"Organization",value:`${r.name} ${d.dim(`(${r.slug}, ${r.id})`)}`},{label:"Gateway",value:d.cyan(i)},{label:"Owners",value:m.length>0?m.join(", "):d.yellow("none yet (claim the Gateway: everyapp setup)")},{label:"You",value:k},{label:"Email",value:g?"configured: the Gateway can mail invitations from Admin \u2192 Users":d.dim("not configured: this CLI prints links to hand over")},{label:"Members",value:`${a.members}${a.deactivated>0?d.dim(` (${a.deactivated} deactivated)`):""}`},{label:"Invitations",value:`${a.invitations} pending`},{label:"Groups",value:String(a.groups)},{label:"Apps",value:String(a.apps)}],oe=`${r.name}: ${y(a.members,"member")}, ${y(a.invitations,"pending invitation")}, ${y(a.groups,"group")}, ${y(a.apps,"app")}`;n.emit({type:"done",message:oe,display:C(I).join(`
8
+ `),data:{organization:r,gateway:i,owners:m,operator:{actor:p.actor,email:p.email,userId:p.userId},email:{configured:g},counts:a}})}async function pe(e){let{out:n}=e,{ws:t}=await w(e),{members:o,invitations:i}=await D(t),c=[...o.map(a=>[a.email,a.name&&a.name!==a.email?a.name:d.dim("\u2013"),a.primaryRole??a.role,Q(a),v(a.createdAt)]),...i.map(a=>[a.email,d.dim("\u2013"),a.role,a.live?d.cyan(`invited, expires ${v(a.expiresAt)}`):d.dim("invitation expired"),v(a.createdAt)])],s=U(["EMAIL","NAME","ROLE","STATUS","SINCE"],c),r=i.filter(a=>!a.live).length,m=`${y(o.length,"member")} and ${y(i.length-r,"pending invitation")}${r>0?` (${r} expired)`:""} in ${t.organization.name}`;n.emit({type:"done",message:m,display:c.length>0?s.join(`
9
+ `):d.dim(`Nobody in ${t.organization.name} yet.`),data:{organization:t.organization,members:o.map($),invitations:i.map(b)}})}async function le(e,n,t){let{out:o}=e,{ws:i,gatewayUrl:c}=await w(e);o.emit({type:"step",step:u,status:"start",message:`Inviting ${n}...`});let{invitation:s,token:r}=await K(i,n,t),m=E(c,r);o.emit({type:"step",step:u,status:"ok",message:`Invited ${s.email} as ${t}`,data:{invitationId:s.id}});let a=[`Invited ${s.email} to ${i.organization.name} as ${t}.`,`Send them this link (it is the invitation's only secret; it expires ${v(s.expiresAt)} UTC):`,` ${m}`,"The Gateway mailed nothing; Admin \u2192 Users mails invitations when email is configured."];o.emit({type:"done",message:A(a.join(`
10
+ `)),display:[a[0],d.dim(a[1]),d.cyan(a[2]),d.dim(a[3])].join(`
11
+ `),data:{organization:i.organization,invitation:b(s),url:m,emailed:!1,next:[{command:`${h} workspace invite-link ${s.email}`,note:"a new link, if this one is lost"}]}})}async function ge(e,n){let{out:t}=e,{ws:o,gatewayUrl:i}=await w(e),{invitation:c,token:s}=await Y(o,n),r=E(i,s),m=[`A new link for ${c.email}'s invitation to ${o.organization.name} (expires ${v(c.expiresAt)} UTC):`,` ${r}`,"Earlier links keep working until the invitation is accepted or canceled."];t.emit({type:"done",message:A(m.join(`
12
+ `)),display:[m[0],d.cyan(m[1]),d.dim(m[2])].join(`
13
+ `),data:{organization:o.organization,invitation:b(c),url:r}})}async function we(e,n){let{out:t}=e,{ws:o}=await w(e);t.emit({type:"step",step:u,status:"start",message:`Canceling the invitation for ${n}...`});let i=await q(o,n);t.emit({type:"step",step:u,status:"ok",message:`Canceled ${i.email}'s invitation`}),t.emit({type:"done",message:`Canceled ${i.email}'s invitation to ${o.organization.name}. Its links no longer work.`,data:{organization:o.organization,invitation:b(i)}})}async function ue(e,n){let{out:t}=e,{ws:o}=await w(e);t.emit({type:"step",step:u,status:"start",message:`Deactivating ${n}...`});let{member:i,already:c}=await B(o,n);t.emit({type:"step",step:u,status:"ok",message:`Deactivated ${i.email}`,data:{userId:i.id,already:c}});let s=c?[`${i.email} was deactivated already.`,"Their sessions, tokens and AI connections are revoked again, in case any came back."]:[`Deactivated ${i.email}.`,"They can't sign in anywhere on this Gateway; their sessions are ended and their personal access tokens and AI connections revoked.",`${h} workspace reactivate ${i.email} lets them back in (revoked tokens and connections stay revoked).`];t.emit({type:"done",message:s.join(`
14
+ `),display:[s[0],...s.slice(1).map(r=>d.dim(r))].join(`
15
+ `),data:{organization:o.organization,member:$(i),already:c}})}async function ye(e,n){let{out:t}=e,{ws:o}=await w(e);t.emit({type:"step",step:u,status:"start",message:`Reactivating ${n}...`});let{member:i,already:c}=await J(o,n);t.emit({type:"step",step:u,status:"ok",message:`Reactivated ${i.email}`,data:{userId:i.id,already:c}});let s=c?[`${i.email} was active already. Nothing changed.`]:[`Reactivated ${i.email}.`,"They can sign in again. Tokens and AI connections revoked at deactivation stay revoked: they make new ones."];t.emit({type:"done",message:s.join(`
16
+ `),display:[s[0],...s.slice(1).map(r=>d.dim(r))].join(`
17
+ `),data:{organization:o.organization,member:$(i),already:c}})}async function ve(e,n){let{out:t,flags:o}=e,{gateway:i,ws:c}=await w(e),s=await L(c,n);await N({yes:o.yes,interactive:X(e),confirm:e.confirm,question:`Remove ${s.email} from ${c.organization.name}?`,unattended:"Removing a member needs confirmation at a terminal, or --yes",context:["Their membership, app grants, group places, tokens and AI connections for the organization go;","their account too, when it belongs to no other organization."],cancelled:"Cancelled. Nothing removed.",data:{userId:s.id,email:s.email}}),t.emit({type:"step",step:u,status:"start",message:`Removing ${s.email}...`});let r=i.install.deploymentMode==="hosted",m=await V(c,s.id,{deleteAccounts:!r,accessMayLetBackIn:async()=>{let p=await T(i.cf,O);return ne.some(g=>p.includes(g))}});t.emit({type:"step",step:u,status:"ok",message:m.deactivatedInstead?`Deactivated ${s.email} instead`:`Removed ${s.email}`,data:{userId:s.id,deactivatedInstead:m.deactivatedInstead,accountDeleted:m.accountDeleted}});let a=m.deactivatedInstead?[`Deactivated ${s.email} instead of removing them.`,`The Gateway has ${z(ne)} or one of them set, so Cloudflare Access may let them straight back in with a new account; a deactivation keeps them out. They stay listed.`,`To delete the account anyway, unset the setting first (${h} secret unset <NAME>) and run this again.`]:[`Removed ${s.email} from ${c.organization.name}.`,"Their grants and group places went with the membership; their tokens for it and AI connections to it are revoked.",m.accountDeleted?"Their account is deleted: it belonged to no other organization.":m.otherOrganizations>0?`Their account stays: they belong to ${y(m.otherOrganizations,"other organization")}.`:"Their account stays: this Gateway keeps accounts."];t.emit({type:"done",message:a.join(`
18
+ `),display:[a[0],...a.slice(1).map(p=>d.dim(p))].join(`
19
+ `),data:{organization:c.organization,member:$(m.member),deactivatedInstead:m.deactivatedInstead,accountDeleted:m.accountDeleted,otherOrganizations:m.otherOrganizations}})}async function ke(e,n){let{out:t}=e,{ws:o,gatewayUrl:i}=await w(e),{member:c,token:s,expiresAt:r}=await H(o,n),m=F(i,s),a=[`A password-reset link for ${c.email} (single use; expires ${v(r)} UTC):`,` ${m}`,"Whoever opens it sets the account's password: hand it to them yourself."];t.emit({type:"done",message:A(a.join(`
20
+ `)),display:[a[0],d.cyan(a[1]),d.dim(a[2])].join(`
21
+ `),data:{organization:o.organization,member:$(c),url:m,expiresAt:Z(r)}})}export{we as cancelInvitation,Fe as deactivate,ue as deactivateMember,Le as invite,je as inviteLink,le as inviteMember,pe as listUsers,ge as newInviteLink,ke as passwordResetLink,Ke as reactivate,ye as reactivateMember,Ye as remove,ve as removeMember,Ue as rename,ce as renameWorkspace,qe as resetLink,Me as role,re as setRole,Se as show,de as showWorkspace,De as uninvite,Ge as users};
package/package.json CHANGED
@@ -4,23 +4,32 @@
4
4
  "description": "Every App CLI - Deploy and manage apps.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
- "version": "0.2.2",
7
+ "version": "0.3.0-rc.1",
8
8
  "files": [
9
9
  "dist",
10
10
  "bin",
11
- "gateway"
11
+ "skills"
12
12
  ],
13
13
  "bin": {
14
- "everyapp": "./bin/everyapp.js",
15
- "__everyapp_bash_complete": "./bin/__everyapp_bash_complete.js"
14
+ "everyapp": "./bin/everyapp.js"
16
15
  },
17
16
  "engines": {
18
- "node": ">=22"
17
+ "node": ">=22.18.0"
18
+ },
19
+ "scripts": {
20
+ "types:check": "tsc --noEmit",
21
+ "prebuild": "tsc",
22
+ "build": "tsup --silent",
23
+ "dev": "tsup --watch",
24
+ "install:global": "pnpm run build && npm link --ignore-scripts",
25
+ "knip": "knip",
26
+ "test": "vitest run --silent",
27
+ "test:built": "EVERYAPP_BUILT_CLI=1 vitest run src/bin/builtCli.test.ts",
28
+ "docs:update": "UPDATE_DOCS=1 vitest run src/docs --silent"
19
29
  },
20
30
  "tsup": {
21
31
  "entry": {
22
- "index": "src/bin/cli.ts",
23
- "bash-complete": "src/bin/bash-complete.ts"
32
+ "index": "src/bin/cli.ts"
24
33
  },
25
34
  "format": [
26
35
  "esm"
@@ -31,36 +40,30 @@
31
40
  "minify": true,
32
41
  "noExternal": [
33
42
  "@every-app/perimeter",
34
- "@every-app/sdk"
43
+ "@every-app/sdk",
44
+ "@every-app/toolbus"
35
45
  ]
36
46
  },
37
47
  "dependencies": {
38
- "@stricli/auto-complete": "^1.2.4",
39
- "@stricli/core": "^1.2.4",
48
+ "@cloudflare/config": "0.24.1",
49
+ "@inquirer/prompts": "^8.7.3",
50
+ "@stricli/core": "^1.3.0",
51
+ "cf": "1.0.0-beta.14",
40
52
  "chalk": "^5.6.2",
41
- "enquirer": "^2.4.1",
53
+ "cli-cursor": "^5.0.0",
42
54
  "execa": "^9.6.0",
43
55
  "jiti": "^2.6.1",
44
- "jsonc-parser": "^3.3.1",
45
- "smol-toml": "^1.4.2",
46
- "zod": "^4.1.13"
56
+ "undici": "^7.30.1",
57
+ "zod": "^4.2.0"
47
58
  },
48
59
  "devDependencies": {
60
+ "@every-app/perimeter": "workspace:*",
61
+ "@every-app/sdk": "workspace:*",
62
+ "@every-app/toolbus": "workspace:*",
49
63
  "@types/node": "^22.18.13",
50
64
  "knip": "^5.66.4",
51
65
  "tsup": "^6.7.0",
52
66
  "typescript": "5.9.3",
53
- "vitest": "^3.2.4",
54
- "@every-app/perimeter": "0.1.0"
55
- },
56
- "scripts": {
57
- "types:check": "tsc --noEmit",
58
- "prebuild": "tsc",
59
- "build": "tsup --silent",
60
- "dev": "tsup --watch",
61
- "install:global": "pnpm run build && npm link --ignore-scripts",
62
- "build:prepack": "pnpm install --ignore-scripts && npm run types:check && npm run build && node scripts/bundle-gateway.js",
63
- "knip": "knip",
64
- "test": "vitest run --silent"
67
+ "vitest": "^3.2.4"
65
68
  }
66
- }
69
+ }
@@ -0,0 +1,199 @@
1
+ ---
2
+ name: build-every-app
3
+ description: Build or change an app on Every App: create one, add a feature, a page, a table, an AI tool, a capability, a public route or a secret, the way the apps already on Every App do it. Use before writing any code for an Every App app, for "build me an app", "add X to the app" or "make the app do Y".
4
+ ---
5
+
6
+ # Build an app on Every App
7
+
8
+ An Every App app is a private web app on the person's own Cloudflare account,
9
+ behind their Gateway. The Gateway signs people in, holds every key, serves the
10
+ app's tools to AI clients, and gives the app nothing it doesn't declare in
11
+ `everyapp.config.ts`. You build the app; the plumbing is done.
12
+
13
+ ## Read first
14
+
15
+ 1. `patterns.md`, beside this file (in an app,
16
+ `.agents/skills/build-every-app/patterns.md`; in the checkout,
17
+ `packages/cli/skills/build-every-app/patterns.md`; on the web,
18
+ https://everyapp.com/docs/agent/patterns): how the apps on Every App do
19
+ each thing, and the file to copy from, in the starter or in one of four
20
+ open apps (OpenCRM, OpenAnalytics, OpenSEO, Agentic Inbox), at
21
+ `https://github.com/every-app/<name>`. Read the sections for the job
22
+ before the first edit, every time: identity and tests for a table, tools
23
+ for a tool, the manifest for a capability. Copy a working file over
24
+ inventing one; when a cited file is out of reach, the row's "What it is"
25
+ and "Copy this" columns are the pattern.
26
+ 2. The everyapp skill beside this one drives the CLI: every command,
27
+ `--json`, setup, secrets, what only a person can do. Read it before running
28
+ `pnpm everyapp`; never guess a flag.
29
+ 3. https://everyapp.com/llms.txt lists every agent page with a one-line
30
+ summary; a page is Markdown at its URL plus `.md`. In an Every App checkout
31
+ they are `landing-page/content/agent/*.md`. Read the page for the job:
32
+ manifest (`everyapp.config.ts`: resources, public, providers,
33
+ capabilities, secrets, migrations), identity (what the Worker receives,
34
+ the SDK's exports), secrets (where a key lives, `.dev.vars`), mcp (tools,
35
+ the connector, MCP apps),
36
+ dev (dev vs production), deploy (release and install), email.
37
+
38
+ ## The shape of an app
39
+
40
+ `pnpm everyapp create ../my-app`, from the every-app folder, copies the starter
41
+ (`starter/` in the checkout; in the app it made, a `starter/` path is that path
42
+ in the app) to `my-app` beside the checkout (the path's last part is the app's
43
+ id), a git repository of its own linked to the checkout: a TanStack Start app
44
+ on a Worker with a D1 database, a todo list whose first screen shows who you
45
+ are and what the app was allowed. One repository per app is the default. (With
46
+ many apps, keep them inside the checkout instead: `pnpm everyapp create my-app`
47
+ there makes `apps/my-app`, untracked by git.)
48
+ Read the app's `README.md`, `AGENTS.md` and every file under `src/` once; they
49
+ are short. An app may carry skills of its own at its top level
50
+ (`.agents/skills/<name>`, `.claude/skills/<name>`), as OpenSEO's repository
51
+ does; read them before changing the app.
52
+
53
+ Keep, the plumbing: `everyapp.config.ts` (the manifest), `cloudflare.config.ts`
54
+ (builds the Worker from it), `src/entry.worker.ts` (`everyApp()` verifies the
55
+ identity and serves the tools), `src/middleware/ensureUser.ts` (who the
56
+ Gateway signed in), `src/db/index.ts`, `src/env.d.ts`, `vite.config.ts`,
57
+ `drizzle.config.ts`, `vitest.config.ts`, `src/routes/__root.tsx`,
58
+ `src/router.tsx`.
59
+
60
+ The example, which the app outgrows: `src/routes/index.tsx` (the page),
61
+ `src/serverFunctions/` (what the page calls, each behind
62
+ `ensureUserMiddleware`), `src/server/todos.ts` and its test (the queries),
63
+ `src/db/schema.ts` and `drizzle/` (the table and its migrations),
64
+ `src/tools.ts` (the same queries as tools), `src/routes/health.ts` (a JSON
65
+ endpoint without a page; private, like every route not under `public`). Keep
66
+ the layering: page, then server function, then a query in `src/server/`, so the
67
+ page, the tools and the test share one set of queries.
68
+
69
+ Add means add. A feature the person asks for goes beside what the app has:
70
+ the todo list, its first screen and its migration stay until the person asks
71
+ for a different app or says to take them out. Replace the example when the app
72
+ becomes something else, not on the first feature.
73
+
74
+ ## The rules
75
+
76
+ 1. Identity only through the SDK: `context.user` behind `ensureUserMiddleware`
77
+ in a server function, the `user` a tool handler receives,
78
+ `getEveryAppUser()` on a route without a page (`null` only on a public
79
+ route, for anyone not signed in). Never from a header, cookie, query or
80
+ body a client sent.
81
+ 2. Scope every query by `user.id` unless sharing is the app's intent. Then the
82
+ unit is the organization: the four apps key every row by the organization
83
+ in the signed identity (`user.orgId`), every member sees the organization's
84
+ rows, and owners and admins (`user.orgRole`) manage. An app for the whole
85
+ organization also says so in the manifest: `access: "everyone"` (the
86
+ default, `admins`, opens it to the deployer, owners and admins alone).
87
+ Never an allowlist of users of your own: the Gateway decides who may open
88
+ the app.
89
+ 3. Routes are private unless listed under `public` in `everyapp.config.ts`. A
90
+ public route exists only when the app needs one (a webhook, a collector, an
91
+ OAuth callback); the starter has none, and its manifest's comment shows a
92
+ webhook. It is open to the internet and checks what it must itself (a
93
+ signature, a token the app issued, a rate limit), and returns nothing that
94
+ is anyone's without that check.
95
+ 4. Never a login page, session, password, token check or MCP server of the
96
+ app's own. The Gateway signs people in, and its one `/mcp` is the connector
97
+ for every app; an app's host answers `/mcp` only with the manifest opt-in
98
+ `mcp: { by, why }`, which none of the four apps needs. Never set a cookie:
99
+ the Gateway strips every cookie before a request reaches the app.
100
+ 5. A key only this app uses goes under `secrets` (required by default;
101
+ `optional: true` when one feature can wait; `anyOf` for one of a group),
102
+ typed in `src/env.d.ts`, read from `env`. After the first deploy,
103
+ `pnpm everyapp configure <app>` is the one command that sets up everything
104
+ the app declares, each value at a hidden prompt (before that deploy,
105
+ `pnpm everyapp configure` in the app's directory sets the Gateway's
106
+ settings); `pnpm everyapp secret set NAME` sets one name, and is the form
107
+ for a script. Locally, `.dev.vars`. Never ask for, print or repeat a value
108
+ in the chat; never put one in a file you commit or on a command line.
109
+ 6. AI through the Gateway: the provider under `providers` with a `why`, called
110
+ with `createGatewayFetch({ env, for: request })`; `ai.models` lists the
111
+ models the app may ask for. The Gateway holds the key; a provider the
112
+ manifest doesn't list is refused.
113
+ 7. A capability (`members`, `email`, `dataforseo`, `context`, an OAuth client)
114
+ under `capabilities` with `access` and a `why`; an OAuth client
115
+ (`google-workspace`, `microsoft-365`, `slack`) also with `scopes: [...]`,
116
+ the least that does the job. Called with `listMembers()`, `sendEmail()`,
117
+ `integrationFetch()`; a refusal is a state the page shows, never a crash.
118
+ 8. Tools in `defineTools()` over the same `src/server/` queries, each with an
119
+ honest `risk` (`read`, `write`, `destructive`), a description the model can
120
+ act on, and `instructions` for the set. Throw `ToolError` for a message
121
+ the model may read. A destructive tool stays hidden until the Gateway has
122
+ approvals; declare it anyway.
123
+ 9. Schema changes through drizzle: edit `src/db/schema.ts`, run
124
+ `pnpm run db:generate`, commit the migration it writes. Never edit a
125
+ migration that has been deployed.
126
+ 10. A test over the queries (`src/server/*.test.ts`: an in-memory SQLite with
127
+ the app's migrations applied), including one that two people's rows stay
128
+ apart.
129
+ 11. Before a pull request: `pnpm types:check`, `pnpm test`, and a security
130
+ review when identity, public routes, tools, secrets or queries changed:
131
+ `/security-review` in Claude Code against the app boundaries in
132
+ `.agents/skills/everyapp/security-review.md` (in the checkout,
133
+ `packages/cli/skills/everyapp/security-review.md`); any other agent
134
+ reads `git diff` against them. No CI job, no API key.
135
+ 12. `description` in `everyapp.config.ts` is the one line the app library
136
+ shows; change it, and `name`, as soon as the app stops being a todo list.
137
+ Add an `icon` (an SVG the app serves) when it has a look.
138
+ 13. The way back: a link to the Gateway. The starter's header has one ("All
139
+ apps", `launcher` in `src/serverFunctions/home.ts`: the identity issuer,
140
+ `EVERYAPP_IDENTITY_ISSUER`, or the dev host under `everyapp dev`; the four
141
+ apps read `GATEWAY_URL` first, then the issuer); keep it. When the app
142
+ gets a sidebar, the convention the identity page states is the first row:
143
+ muted, "‹ Every App", aria-label "Back to Every App", above the app's own
144
+ name. OpenCRM and OpenSEO keep theirs in the sidebar footer today,
145
+ labelled "Back to Every App".
146
+ 14. After editing `everyapp.config.ts`, restart `pnpm dev`: the local Gateway
147
+ reads it when it starts (the tools catalog is the exception). Every
148
+ manifest change reaches the real Gateway on the next deploy.
149
+
150
+ ## Working with the person
151
+
152
+ For a new app, or when the ask leaves these open, ask five short questions:
153
+ what the app is for; who uses it (just them, or everyone in the organization:
154
+ then `access: "everyone"`); what it stores (the tables); what it does on its
155
+ own (sends mail, calls a model, runs on a schedule, serves a script to a
156
+ website); what stays private to each person. Repeat the answers back in one
157
+ paragraph, then build. For a clear request, build.
158
+
159
+ The loop: `pnpm everyapp create ../my-app` from the every-app folder (or work
160
+ in the app you are in); the schema and the queries, the server functions, the
161
+ page, the tools, the test; `pnpm dev` in the background, open the printed URL
162
+ as the test user; iterate; `pnpm run deploy` when they say so (it prints the
163
+ app's address and who can open it: tell them). Manage the workspace (people,
164
+ groups, who can open an app, caps, usage, what agents did):
165
+ `pnpm everyapp workspace --help`, or the `everyapp_workspace_*` tools on
166
+ `/mcp` as an owner or admin. A required secret, provider or capability keeps
167
+ the app at "Needs setup" until it is set: say which, and the one command,
168
+ `pnpm everyapp configure <app>`. For anything the CLI does, read the everyapp
169
+ skill. Before building a tool over a data provider the Gateway offers as an
170
+ MCP app, say so: DataForSEO's tools reach the person's AI client with no page
171
+ and nothing to deploy, `pnpm everyapp mcp on dataforseo`
172
+ (https://everyapp.com/docs/agent/mcp#mcp-apps).
173
+
174
+ For a non-technical person, say in plain words: what they will see (the page,
175
+ its first screen); where the app is (its own address, behind their Gateway, on
176
+ their Cloudflare account); who can open it (every member of the organization,
177
+ or owners and admins; a public route, if there is one, is open to anyone);
178
+ what it costs (Cloudflare usage, and any paid service a `why` line names); and
179
+ that they can use it from Claude, ChatGPT or Cursor through the Gateway's MCP
180
+ connector. No stack traces, no file paths.
181
+
182
+ ## Patterns, the ten most used
183
+
184
+ | Pattern | Copy from |
185
+ | -------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
186
+ | Identity in a server function | `starter/src/middleware/ensureUser.ts`, `starter/src/serverFunctions/todos.ts` |
187
+ | Identity in a tool handler | `starter/src/tools.ts`; `open-crm/src/server/everyapp/crm-tool.ts` |
188
+ | Identity on a public route | `open-analytics/src/lib/edge/collector/collect.ts`; `open-crm/src/server/features/tracking/trackingRoutes.ts` |
189
+ | Organization-wide rows | `open-crm/src/middleware/ensure-user/everyApp.ts`; `agentic-inbox/workers/everyapp/identity.ts` |
190
+ | A public route with its own token | `open-analytics/src/lib/edge/auth/collect-token.ts`, `open-analytics/src/lib/edge/collector/collect.ts` |
191
+ | AI through the Gateway | `open-crm/src/server/lib/openrouter.ts`; `agentic-inbox/workers/everyapp/gateway.ts` |
192
+ | Sending and receiving email | `agentic-inbox/workers/everyapp/email.ts`; `agentic-inbox/workers/app.ts` |
193
+ | A Durable Object agent | `open-crm/src/server/features/agent/CrmAgent.ts`; `agentic-inbox/workers/agent/index.ts` |
194
+ | Tools per feature, with output schemas | `open-crm/src/server/everyapp/tools.ts`, `open-crm/src/server/everyapp/tools/companies.ts` |
195
+ | Needs setup inside the app | `open-crm/src/client/features/agent/AgentPaused.tsx`; `agentic-inbox/app/components/SendingOffBanner.tsx` |
196
+
197
+ All of them, with every manifest field, Workflows and a second Worker, cookies,
198
+ the lean Worker bundle, tests, dev vs production, release and install:
199
+ `patterns.md` beside this file.
@@ -0,0 +1,134 @@
1
+ # Example patterns
2
+
3
+ How the apps on Every App do each thing, and the file to copy from. `starter/` is what `pnpm everyapp create` copies (in the Every App checkout); in an app `everyapp create` made, a `starter/` path is that path in the app. The four apps are open, at `https://github.com/every-app/<name>`, cited by their repository names: `open-crm/` (OpenCRM), `open-analytics/` (OpenAnalytics), `open-seo/` (OpenSEO) and `agentic-inbox/` (Agentic Inbox); a path is relative to that repository's root. Read a file before copying from it: each is short where it matters, and its comments say why. When a cited file is out of reach, the "What it is" and "Copy this" columns are the pattern; start from the starter's file when the row names one.
4
+
5
+ ## Identity
6
+
7
+ | Pattern | What it is | Where | Copy this |
8
+ | --- | --- | --- | --- |
9
+ | Identity in a server function | `requireEveryAppUser(getRequest(), env)` in a TanStack Start middleware; every server function lists it and reads `context.user` | `starter/src/middleware/ensureUser.ts`, `starter/src/serverFunctions/todos.ts` | `.middleware([ensureUserMiddleware])` on every server function, and `context.user.id` as the first argument of every query |
10
+ | Identity resolved once, with the organization | `getEveryAppUser()` over the request's headers; the deploy's `EVERY_APP_ORG_ID` pin; the role from the token, never from a member row; one context `{ userId, userEmail, organizationId, role }` every repository filters by | `open-crm/src/middleware/ensure-user/everyApp.ts`, `open-crm/src/middleware/ensureUser.ts`; the same shape in `open-seo/src/middleware/ensure-user/everyApp.ts` | `resolveEveryAppUserContext(user)`: the pin check, `ROLES`, and that a server function accepts only a credential holding every scope (`hasScope(user, "*")`) |
11
+ | Identity at the Worker entry | `everyApp()`'s handler gets `user` as its fourth argument; the app refuses another organization, keeps the user in a `WeakMap` by request, and its routes read it from there | `agentic-inbox/workers/app.ts`, `agentic-inbox/workers/everyapp/identity.ts` | `requireOrganization(env, user)` before routing; `roleOf()`; the `requireAdmin` middleware on the routes only owners and admins may use |
12
+ | Identity mirrored into the app's tables | A `Viewer` from `getEveryAppUser()`; the person, their organization and their membership are mirrored into rows so other rows can join on them, and access still comes from the signed identity alone | `open-analytics/src/lib/edge/auth/viewer.ts` | `mirror()` writes at most once an hour per person and is never consulted for access |
13
+ | Identity in a tool handler | The handler's second argument carries the verified caller (`user.id`, `user.orgId`, `user.orgRole`, `user.scopes`); the starter passes `user.id` to the same queries the page uses | `starter/src/tools.ts`; `open-crm/src/server/everyapp/crm-tool.ts` | `crmTool()`: one wrapper that resolves the context, scopes the handler to the organization, and turns an `AppError` into a `ToolError` |
14
+ | Identity on a public route | A public route is open to anyone, so it never assumes a member: `getEveryAppUser(request, env)` is the member when the Gateway signed one in (a member's session still travels on a public route) and `null` otherwise. The four apps' public routes read no identity at all: OpenAnalytics' collector checks the token its tracker was issued, OpenCRM's tracker takes the site from the id in the URL | `open-analytics/src/lib/edge/collector/collect.ts`, `open-analytics/src/lib/hono/routes/collect.ts`; `open-crm/src/server/features/tracking/trackingRoutes.ts` | A public route exists only when the app needs one (a webhook, a collector, an OAuth callback); it checks what it must itself and answers nothing that is anyone's without that check |
15
+ | Organization-wide rows | Sharing by design: every table keys on the organization in the signed identity, every member sees the organization's rows, owners and admins manage. OpenCRM hangs every table off `organization_id`; OpenAnalytics' `teamId` is the organization; Agentic Inbox is one organization per deploy, every mailbox shared | `open-crm/docs/maintainers/every-app.md`, `open-crm/src/middleware/ensure-user/everyApp.ts`; `open-analytics/src/lib/edge/auth/viewer.ts`; `agentic-inbox/workers/everyapp/identity.ts` | The rule in words: authorization reads the organization and the role from the token on every request, never from a row the app wrote |
16
+ | Roles from the Gateway | `owner`, `admin` and `member` from `user.orgRole`; anything else is a member | `open-crm/src/server/auth/org-gate.ts`; `agentic-inbox/workers/everyapp/identity.ts` | `requireOrgPermission(context, permissions)` throwing `FORBIDDEN`; `isAdmin(user)` |
17
+ | The way back | A link to the Gateway. The starter's `launcher` is the identity issuer (`EVERYAPP_IDENTITY_ISSUER`, the Gateway's URL), or the dev host one label up under `everyapp dev`; the apps read `GATEWAY_URL` (set by the deploy, absent under `everyapp dev`) first, then the issuer. Where: the starter's header ("All apps"), OpenCRM's and OpenSEO's sidebar footer, Agentic Inbox's signed-in line ("Back to Every App"). The convention the identity page states for a new app: the first sidebar row, muted "‹ Every App", aria-label "Back to Every App" | `starter/src/serverFunctions/home.ts`, `starter/src/routes/index.tsx`; `open-crm/src/client/components/Sidebar.tsx`, `open-crm/src/serverFunctions/everyApp.ts`; `open-seo/src/client/components/Sidebar.tsx`; `agentic-inbox/app/routes/home.tsx`, `agentic-inbox/workers/everyapp/identity.ts` | `launcher` (the starter, dev-aware); `getEveryAppGatewayUrl()`: `GATEWAY_URL`, then `EVERYAPP_IDENTITY_ISSUER` |
18
+
19
+ ## Public routes with their own checks
20
+
21
+ | Pattern | What it is | Where | Copy this |
22
+ | --- | --- | --- | --- |
23
+ | A public route with its own token | OpenAnalytics' tracker: `/script.js` (GET) issues a signed collect token (an HMAC over the site id and the client's address, 12 hours, derived from `MAIN_SECRET`); `/collect` (POST) verifies it, then runs the site's blocking rules and bot checks before anything is ingested | `open-analytics/everyapp.config.ts` (`public`), `open-analytics/src/lib/edge/collector/script-endpoint.ts`, `open-analytics/src/lib/edge/auth/collect-token.ts`, `open-analytics/src/lib/edge/collector/collect.ts`, `open-analytics/src/lib/hono/routes/collect.ts` | `verifyCollectToken()` before anything is stored; the tracker posts `text/plain`, so the browser sends no preflight and `OPTIONS` stays private |
24
+ | A public tracker for another website | OpenCRM's `/t/*`: the loader, a per-site tracker with its config baked in, and a collector that always answers 204; the organization comes from the site id, the client's address only counts requests | `open-crm/src/server/features/tracking/trackingRoutes.ts`, `open-crm/everyapp.config.ts` | The route sets its own CORS and resource-policy headers; `withinAddressLimit()` before any work |
25
+ | A public route armed by a secret | An operator endpoint that answers 404 until `ERASURE_SECRET` is set, and then only to a request signed with it in the last five minutes | `open-crm/src/server/features/erasure/erasureRoute.ts` | Compare signatures in constant time; keep the secret `optional` and unset it after use |
26
+ | Share links and assets | Public pages, their data and the built assets they load, each listed with its method | `open-analytics/everyapp.config.ts` | `/share/**`, `/api/public/share/**`, `/assets/**`: list the assets a public page loads, or it renders bare |
27
+ | No public routes | `public: []` with a comment saying why nothing is open; the Gateway's own tool calls carry their own tokens | `open-seo/everyapp.config.ts`, `agentic-inbox/everyapp.config.ts` | Say why in the manifest comment |
28
+
29
+ ## The manifest, field by field
30
+
31
+ | Pattern | What it is | Where | Copy this |
32
+ | --- | --- | --- | --- |
33
+ | `id`, `name`, `description`, `icon` | The id names the hostname and the Worker; the description is the app library's one line; the icon an SVG the app serves | `open-crm/everyapp.config.ts` | `icon: "/everyapp-icon.svg"` |
34
+ | `resources.d1` | The database, bound as `env.DB`; the deploy creates it | `starter/everyapp.config.ts` | `resources: { d1: ["DB"] }` |
35
+ | `resources.kv` | Per-site settings read on the hot path | `open-analytics/everyapp.config.ts` (`SITE_SETTINGS_KV`), `open-seo/everyapp.config.ts` (`KV`) | One namespace per purpose, named for it |
36
+ | `resources.durableObjects` | `{ name, className }`, the class exported from the Worker entry; `worker` when a helper Worker defines it | `agentic-inbox/everyapp.config.ts` (`MAILBOX`, `EMAIL_AGENT`), `open-crm/everyapp.config.ts` (`CRM_AGENT`), `open-seo/everyapp.config.ts` (`AUDIT_SCRATCHPAD` on the `audit` Worker) | The comment above each: what one instance is (a mailbox, a research session) |
37
+ | `resources.workflows` and `workers` | Workflows by binding and class, one of them on a helper Worker; the helper declared with its service binding | `open-seo/everyapp.config.ts` | `workers: [{ name: "audit", binding: "AUDIT_ENGINE" }]` with `SITE_AUDIT_WORKFLOW` on it |
38
+ | An R2 bucket | Added in `cloudflare.config.ts` beside the manifest's bindings, named after the Worker (`bindings.r2({ name: worker.name })`); `resources.r2` in the manifest does the same. The deploy creates the bucket; an account that never enabled R2 stops with `R2_NOT_ENABLED` until the person enables it once in the Cloudflare dashboard | `open-crm/cloudflare.config.ts`, `open-seo/cloudflare.config.ts`, `agentic-inbox/cloudflare.config.ts` | Spread `worker.env` and add to it; import nothing the Worker imports there |
39
+ | `migrations` with drizzle | `{ engine: "drizzle", dir, config, binding }`, and a drizzle config that opens the local D1 `pnpm dev` serves | `open-crm/everyapp.config.ts`, `open-crm/drizzle/sqlite.config.ts`, `open-crm/drizzle/sqlite/0000_init.sql`; the starter's default is `starter/drizzle.config.ts` with no `migrations` field | `localD1Path()` from `@every-app/sdk/cloudflare` for drizzle-kit studio |
40
+ | `migrations` with hand-written SQL | `{ engine: "d1-sql", dir: "migrations", binding: "DB" }` and plain SQL files, one `;`-terminated statement after another | `open-analytics/everyapp.config.ts`, `open-analytics/migrations/0001_baseline.sql` | For an app whose schema drizzle doesn't own |
41
+ | `public` | `{ path, methods }` per route, `GET` by default; a route is listed only when something other than a signed-in member must reach it: a webhook, a collector, an OAuth callback. The starter lists none (its manifest's comment shows a webhook); OpenAnalytics opens its tracker, collector and share pages, OpenCRM its tracker; OpenSEO and Agentic Inbox nothing | `starter/everyapp.config.ts` (the comment), `open-analytics/everyapp.config.ts`, `open-crm/everyapp.config.ts` | Comment each route with why it is open; `/mcp` can't be one |
42
+ | `providers` with `why` | The AI provider the Gateway calls for the app; required unless `optional: true` | `open-crm/everyapp.config.ts` (`openrouter`), `agentic-inbox/everyapp.config.ts` (`openrouter` required, `workers-ai` optional) | One sentence for whoever sets the key |
43
+ | `capabilities`: an API key | `dataforseo` and `context` with `access: "read"` and a `why`; `optional` where a feature can say what is missing | `open-seo/everyapp.config.ts` | `context: { access: "read", optional: true, why }` |
44
+ | `capabilities`: an OAuth client with scopes | `google-workspace`, `microsoft-365` and `slack` with `access: "write"`, `scopes: [...]` (the least that does the job) and `optional: true`; the Gateway holds the client, the app keeps each member's tokens | `open-crm/everyapp.config.ts` | `scopes: ["gmail.read", "calendar.read"]` |
45
+ | `capabilities.email` and `capabilities.members` | Email as `access: "write"` (required for an inbox, optional for reports); members as `access: "read"` to know who can open the app | `agentic-inbox/everyapp.config.ts`, `open-analytics/everyapp.config.ts`, `starter/everyapp.config.ts` | The starter's `members` and `listMembers()` in `starter/src/serverFunctions/home.ts` |
46
+ | `secrets`, required | The app's own root secret, with a `why` that says how to make one | `open-analytics/everyapp.config.ts` (`MAIN_SECRET`) | "32 or more random characters, e.g. openssl rand -base64 32" |
47
+ | `secrets`, optional | Keys one feature needs, each with a `why`, and `docs` where a page exists (`GITHUB_TOKEN`); the feature says so where it is used | `open-crm/everyapp.config.ts` (`TOKEN_ENCRYPTION_KEY`, `GITHUB_TOKEN`, `OPENROUTER_MODEL`, `ERASURE_SECRET`) | `{ name, optional: true, why }`, plus `docs` when there is a page |
48
+ | `access: "everyone"` | Every member of the organization can open the app; owners and admins manage inside it. OpenSEO leaves the default | `open-crm/everyapp.config.ts`, `open-analytics/everyapp.config.ts`, `agentic-inbox/everyapp.config.ts` | The default is `admins`: say `everyone` when the app is for the team |
49
+ | `ai.models` | The model ids the Gateway accepts from the app; any other is `model_not_allowed` | `agentic-inbox/everyapp.config.ts` | List every id the code can send, per provider |
50
+ | `email.inbound` | The Worker exports an `email()` handler; the addresses are set after the deploy with `everyapp email set --inbound` | `agentic-inbox/everyapp.config.ts` | `email: { inbound: true }` |
51
+ | `build` and `dev` | Commands when the app's build is more than `cf build` (bundles to build first, an environment variable to set) | `open-analytics/everyapp.config.ts`, `open-seo/everyapp.config.ts` | `dev` must listen on `PORT` |
52
+ | `compatibilityDate` | The runtime date every build runs on | `open-analytics/everyapp.config.ts`, `open-seo/everyapp.config.ts` | Pin it when porting an app that ran under Wrangler |
53
+ | `cloudflare.config.ts` beyond the manifest | Crons, compatibility flags and plain text bindings read when the Worker is built | `open-crm/cloudflare.config.ts` (`crons: ["* * * * *"]`, `global_fetch_strictly_public`), `agentic-inbox/cloudflare.config.ts` (`AI_PROVIDER`, `AGENT_MODEL`), `open-seo/cloudflare.config.ts` | `everyAppWorker({ manifest, entrypoint, ctx, crons })`, then spread and extend |
54
+
55
+ ## Sending and receiving email
56
+
57
+ | Pattern | What it is | Where | Copy this |
58
+ | --- | --- | --- | --- |
59
+ | Sending email | `sendEmail({ env, for, message })` through the Gateway's `email` capability; the Gateway checks the sender against what `everyapp email set --sender` allowed, and the budgets | `agentic-inbox/workers/everyapp/email.ts`; `open-analytics/src/lib/notifications/edge/delivery.ts` | `gatewayEmail()`: the SDK call shaped as the `send_email` binding upstream's code expects, every refusal an `EmailSendError` with the Gateway's code; OpenAnalytics skips the mail, and says so, on a Gateway without email |
60
+ | Receiving email | An `email()` export beside `everyApp()`'s `fetch`; the deploy makes the Email Routing rules from `email.inbound` and the addresses `everyapp email set --inbound` names | `agentic-inbox/workers/app.ts`, `agentic-inbox/workers/everyapp/inbound.ts`, `agentic-inbox/test/receive-email.test.ts` | `export default { ...everyApp(handler, { tools }), async email(event, env, ctx) { … } }`; rethrow so Email Routing retries or bounces |
61
+
62
+ ## AI through the Gateway
63
+
64
+ | Pattern | What it is | Where | Copy this |
65
+ | --- | --- | --- | --- |
66
+ | AI through the Gateway | `createGatewayFetch({ env, for })` as the `fetch` of an OpenAI-compatible client, with a base URL on the Gateway's `/api/ai/<provider>/…` path; the Gateway holds the key, so the client's own key is a placeholder | `open-crm/src/server/lib/openrouter.ts`; `agentic-inbox/workers/everyapp/gateway.ts` | OpenCRM: `createOpenRouter` over the Gateway fetch, the model from the optional secret `OPENROUTER_MODEL`; Agentic Inbox: `gatewayModel()` with `@ai-sdk/openai-compatible`, and `gatewayAi()` as a `run()` shim for code written against Workers AI |
67
+ | Every AI failure as a sentence | `ConfigurationError` (no Gateway and no dev key), `ai_provider_not_configured` (the key isn't set on the Gateway) and every other refusal read with `gatewayRefusal()`, each turned into one sentence that names the setting and who sets it | `agentic-inbox/workers/everyapp/agent-ai.ts`, `agentic-inbox/workers/everyapp/gateway.ts` | `aiSetupProblem()` before the first call; `aiProviderNotSetUpMessage()` names the command, never a value |
68
+ | Whose call it is | Attribution: while serving a request, `for: request`; for work no request started (a cron, a Workflow, an agent's turn), `for: { background: true, organizationId }` | `open-crm/src/server/lib/gateway-attribution.ts`, `open-seo/src/server/lib/gateway-attribution.ts`; `agentic-inbox/workers/everyapp/agent-ai.ts` (`forMailbox()`) | `serveEveryAppRequest(request, user, fn)` at the entry and in tools; `forOrganization(id, fn)` around background work |
69
+
70
+ ## Agents, Workflows and a second Worker
71
+
72
+ | Pattern | What it is | Where | Copy this |
73
+ | --- | --- | --- | --- |
74
+ | A Durable Object agent | One Durable Object per conversation or task, named for its organization; authorized in the Worker before the request reaches the object; the model through the Gateway | `open-crm/src/server/features/agent/CrmAgent.ts` (on `@cloudflare/think`), `open-crm/src/server/features/agent/agentRoutes.ts`; `agentic-inbox/workers/agent/index.ts` (on `@cloudflare/ai-chat`), `agentic-inbox/workers/app.ts`; `open-seo/src/server/features/sam/SamChatAgent.ts`, `open-seo/src/server.ts` | `routeAgentRequest(request, env, { onBeforeConnect, onBeforeRequest })` with a guard that checks the session is the caller's organization's |
75
+ | Workflows | A `WorkflowEntrypoint` a cron starts; the organization named once around `run()` and carried into every step | `open-seo/src/server/workflows/RankCheckWorkflow.ts`, `open-seo/src/server/workflows/pgStep.ts`, `open-seo/cloudflare.config.ts` (the cron) | `forOrganization()` around `run()`; `carryOrganization()` inside `pgStep()` |
76
+ | A second Worker | A helper Worker for work that should not share the app's memory (a crawler, Lighthouse); it exports its Workflow and Durable Object classes and an RPC `WorkerEntrypoint`, and only the app's Worker can reach it | `open-seo/src/audit-worker.ts`, `open-seo/vite-plugin-every-app.ts`, `open-seo/everyapp.config.ts` (`workers`) | `everyAppAuxiliaryWorker({ manifest, worker: "audit", entrypoint, ctx })` under the Cloudflare Vite plugin's `auxiliaryWorkers` |
77
+ | The lean Worker bundle | A Vite plugin that stubs dead-weight packages, swaps zod's locales for English alone, and fails the build when a denylisted module reaches the Worker's eager import graph | `open-seo/vite-plugin-lean-worker-bundle.ts` | `EAGER_DENYLIST` with an `expected` note per entry; the `generateBundle` assertion |
78
+
79
+ ## Tools
80
+
81
+ | Pattern | What it is | Where | Copy this |
82
+ | --- | --- | --- | --- |
83
+ | The starter's catalog | Four tools over the same queries as the server functions, the zod schemas shared; `instructions` for the set; `ToolError` for a message the model may read | `starter/src/tools.ts`, `starter/src/server/todos.ts` | `risk` per tool, `idempotent` where true, `handler: (input, { user }) => todos.listTodos(user.id)` |
84
+ | Tools per feature, with output schemas | One module per feature exporting a map, spread into one `defineTools()`; a `crmTool()` wrapper that adds `output` schemas, with the shapes several tools share (`personOutput`, `recordRefOutput`, `pageOf()`) | `open-crm/src/server/everyapp/tools.ts`, `open-crm/src/server/everyapp/tools/companies.ts`, `open-crm/src/server/everyapp/crm-tool.ts`, `open-crm/docs/maintainers/mcp.md` | People named by email, never by id, in what a model reads; "Never guess ids" in `INSTRUCTIONS` |
85
+ | Wrapping an existing tool registry | An app that had an MCP server keeps its registry and adapts each entry: `risk` from the annotations and the name, `objectSchema()` for input and output, `toolResult({ text, structured })` from the old `CallToolResult` | `open-seo/src/server/everyapp/tools.ts` | `riskOf()`; the old MCP server is off: the Gateway's `/mcp` serves the catalog (an app's host answers `/mcp` only with the manifest's `mcp: { by, why }`, which none of the four sets) |
86
+ | Tools over the private API, in-process | Each tool calls the same Hono API the dashboard uses, with the call's identity, so permissions and validation live in one place | `open-analytics/src/lib/mcp/tools.ts` | `resolveViewer()` from the call; `MAX_RESULT_BYTES` asking for a narrower question |
87
+ | Tools loaded on first use | The tools module behind a dynamic import on `/__everyapp/*`, so the app's eager bundle stays small | `open-crm/src/server.ts` | `if (pathname.startsWith("/__everyapp/")) { const { everyAppTools } = await import(...) }` |
88
+ | Schemas without zod 4.2 | `{ jsonSchema, validate }` from an older zod's `toJSONSchema` and Standard Schema | `agentic-inbox/workers/everyapp/tools.ts` | `plainSchema(schema)` |
89
+ | Testing the catalog | The catalog read the way the Gateway reads it (`POST /__everyapp/tools/list` with a system token), the risk of every tool checked, and calls as a read-only and a read-write caller | `open-crm/src/server/everyapp/catalog.test.ts`, `open-crm/src/server/everyapp/toolTestSupport.ts`; `agentic-inbox/test/tools.test.ts` | `reads` equals every `list_`, `get_` and `search_` tool |
90
+
91
+ ## Needs setup inside the app
92
+
93
+ Every app says, inside itself, what the Gateway hasn't got yet: the setting's name and who sets it, never a value, never the Gateway's own refusal sentence.
94
+
95
+ | Pattern | What it is | Where | Copy this |
96
+ | --- | --- | --- | --- |
97
+ | The starter's first screen | Who the Gateway signed in and what the app was allowed, read from the inlined manifest; a `GatewayRefusalError` from `listMembers()` is a state the page shows | `starter/src/routes/index.tsx`, `starter/src/serverFunctions/home.ts` | `whoCanOpen()`: catch the refusal, return its code |
98
+ | A paused agent | A checklist of the Gateway's keys for the agent (`OPENROUTER_API_KEY`, `CONTEXT_API_KEY`): done, checking or missing, each with what being unset costs and the command an owner or admin runs; members are told whom to ask | `open-crm/src/server/features/setup/services/SetupService.ts`, `open-crm/src/client/features/agent/AgentPaused.tsx`, `open-crm/src/serverFunctions/setup.ts` | `pausedResearchMessage(canManage)`: two sentences, by role |
99
+ | A required secret | Without the root secret every page redirects to one that names the secret and the command (`pnpm everyapp secret set MAIN_SECRET --app analytics`), or `.dev.vars` under `everyapp dev` | `open-analytics/src/middleware.ts`, `open-analytics/src/routes/$locale.runtime-config-error.tsx` | `loadRuntimeSetupMode()` asks the Worker whether it runs under `everyapp dev` |
100
+ | A capability | `integrationStatus({ env, provider }).ready` without calling the provider, driving the "set up DataForSEO" prompt | `open-seo/src/server/lib/provider-access.ts`, `open-seo/src/serverFunctions/config.ts` | `hasProviderAccess()` |
101
+ | Email | A banner before the person writes: no sender allowed yet (`everyapp email set --sender`) or no `EMAIL_FROM`; owners and admins get the command with the mailbox's domain filled in, members whom to ask; Send stays enabled, the server decides | `agentic-inbox/app/components/SendingOffBanner.tsx` | `config.ready === false` from `GET /api/v1/config` |
102
+
103
+ ## Cookies, headers and the bundle
104
+
105
+ | Pattern | What it is | Where | Copy this |
106
+ | --- | --- | --- | --- |
107
+ | App-host session cookies | The Gateway strips every `Cookie` header before a request reaches the app and refuses requests whose cookies repeat its own session cookie's name, so an app can never read back a cookie it set; `withoutCookies()` drops any `Set-Cookie` the app's framework adds (a WebSocket upgrade passes through) | `open-crm/src/server/everyapp/cookies.ts`, `open-seo/src/server/everyapp/cookies.ts`; `open-analytics/src/server.ts` | Wrap the response of `everyApp()`'s `fetch` in `withoutCookies()`; keep preferences in the browser |
108
+ | Security headers the app sets itself | `frame-ancestors 'self'`, an `img-src` of only what the app shows, a per-response nonce for inline scripts, on HTML documents without a policy of their own. OpenAnalytics sets one static policy on every page instead (`withPageHeaders` in `open-analytics/src/server.ts`: `frame-ancestors 'none'`, no nonce) | `open-crm/src/server.ts` | `APP_CSP` |
109
+
110
+ ## Tests
111
+
112
+ | Pattern | What it is | Where | Copy this |
113
+ | --- | --- | --- | --- |
114
+ | Tests over queries with an in-memory SQLite | libsql `:memory:` with the app's migrations applied by drizzle's migrator, `@/db` mocked to it; one test that two people's rows stay apart | `starter/src/server/todos.test.ts` | `vi.hoisted` and `vi.mock("@/db", …)`; `migrate(db, { migrationsFolder })` |
115
+ | A test database with every migration | `createSqliteTestDb()` applies every production migration so repositories run their real queries; `seedTwoOrganizations()` and tests that one organization never sees another's rows | `open-crm/src/server/testing/testDb.ts`, `open-crm/src/server/features/agent/repositories/isolation.test.ts` | Mock `cloudflare:workers`, `@/db` and `@/db/runBatch` from the hoisted database |
116
+ | A fake Gateway binding | A `fetch` that records every call's URL, body and identity header, standing in for `EVERY_APP_GATEWAY` | `agentic-inbox/test/with-gateway.test.ts`, `agentic-inbox/test/gateway-email.test.ts` | `fakeGateway(reply)` |
117
+ | Public routes under test | The collect token's issue and verify, and the collector's refusals | `open-analytics/src/lib/edge/__tests__/collect-token.test.ts`, `open-analytics/src/lib/edge/__tests__/collect.test.ts` | Test the refusal paths first |
118
+
119
+ ## Dev vs production
120
+
121
+ | Pattern | What it is | Where | Copy this |
122
+ | --- | --- | --- | --- |
123
+ | `.dev.vars` | Locally there is no Gateway: the app's own secrets and the Gateway settings its manifest names (a provider key, `EMAIL_FROM`, `EVERYAPP_EMAIL_SENDERS`) come from `.dev.vars`, gitignored; `pnpm dev` names the ones missing | `agentic-inbox/.dev.vars.example`, `open-analytics/.dev.vars.example`, `open-crm/.dev.vars.example`; `starter/src/env.d.ts` | Ship a `.dev.vars.example` that names each variable and where it comes from, with no values |
124
+ | The manifest inlined for the page | The Worker can't import `everyapp.config.ts`; Vite's `define` inlines it as `__EVERYAPP_MANIFEST__` for the first screen | `starter/vite.config.ts`, `starter/src/serverFunctions/home.ts`, `starter/src/env.d.ts` | `remoteBindings: false`: local dev never touches remote resources |
125
+ | Dev or deployed, in the app | `env.EVERYAPP_DEV` says which. The starter's `launcher` is `EVERYAPP_IDENTITY_ISSUER` (the Gateway's URL) when deployed and the dev host one label up under `everyapp dev`; the apps read `GATEWAY_URL` (set by the deploy) first, then the issuer | `starter/src/serverFunctions/home.ts`; `open-crm/src/serverFunctions/everyApp.ts`, `agentic-inbox/workers/everyapp/identity.ts`; `open-analytics/src/routes/$locale.runtime-config-error.tsx` | Name `.dev.vars` in dev and `pnpm everyapp configure <app>` when deployed |
126
+ | What a port changes | What the Gateway replaces in an app that ran elsewhere (sign-in, keys, MCP, email), in one table | `open-crm/docs/maintainers/every-app.md`, `open-seo/docs/maintainers/every-app.md`, `agentic-inbox/docs/EVERY_APP.md` | Keep such a document in the app |
127
+
128
+ ## Release and install
129
+
130
+ | Pattern | What it is | Where | Copy this |
131
+ | --- | --- | --- | --- |
132
+ | Release and install | `pnpm everyapp release` packs the production build, the migrations and a `release.json` into `everyapp-release.tar.gz`; from a GitHub release of the app's repository, anyone installs it with `pnpm everyapp install <repository url>`, no clone. `pnpm pack-for-app <app dir> --workflow` from an Every App checkout vendors the CLI and the SDK into the app's repository (`file:` tarballs under `vendor/`) and adds the Release workflow that publishes the tarball on every push. The vendored CLI runs `dev`, `db migrate` and `release`, not `deploy`: deploys run from an Every App checkout (`pnpm everyapp install <url>` there, or that checkout's CLI from the app's directory). OpenCRM, OpenSEO and Agentic Inbox bind an R2 bucket, so R2 is enabled once on the account before their first install (`R2_NOT_ENABLED` otherwise); OpenAnalytics needs nothing | `docs/runbooks/app-releases.md`, `scripts/pack-for-app.mjs`, `scripts/pack-for-app/release.yml` and `landing-page/content/agent/deploy.md` ("Release installs") in the checkout; `open-crm/docs/maintainers/releases.md` (tags, notes) | `pnpm pack-for-app <app dir> --workflow` once, then commit `vendor/`, `package.json`, `pnpm-lock.yaml` and the workflow; after a platform change the same command without `--workflow`; the version in `package.json` names the release |
133
+
134
+ A pattern missing here: read the four manifests first, then search the app for the SDK export you need (`createGatewayFetch`, `integrationFetch`, `sendEmail`, `listMembers`, `defineTools`).