@thebackstoryis/engineering-with-ai 0.2.9

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 (334) hide show
  1. package/Docs/README.md +50 -0
  2. package/Docs/adoption/consultancy-and-multi-project-rollout.md +135 -0
  3. package/Docs/adoption/non-technical-team-guide.md +126 -0
  4. package/Docs/archaeology-technology-and-hosting-discovery.md +212 -0
  5. package/Docs/blast-radius-and-impact-routing-guide.md +325 -0
  6. package/Docs/blueprints/internal-blueprint-catalogue.md +146 -0
  7. package/Docs/blueprints/maintaining-organisation-blueprints.md +154 -0
  8. package/Docs/blueprints/validation-and-troubleshooting.md +168 -0
  9. package/Docs/cli-reference.md +113 -0
  10. package/Docs/completed-phase-evidence-amendments.md +74 -0
  11. package/Docs/consultancy-network-rollout-control-plane-guide.md +202 -0
  12. package/Docs/context-aware-delivery-companion-guide.md +198 -0
  13. package/Docs/context-aware-delivery-companion-user-guide.md +184 -0
  14. package/Docs/context-management-and-token-efficiency.md +113 -0
  15. package/Docs/design-systems/design-system-implementation-guide.md +85 -0
  16. package/Docs/design-systems/design-system-pack-authoring-guide.md +95 -0
  17. package/Docs/design-systems/design-system-review-guide.md +51 -0
  18. package/Docs/design-systems/design-system-user-guide.md +96 -0
  19. package/Docs/design-systems/product-owner-guide.md +49 -0
  20. package/Docs/designing-organisation-blueprint-packs.md +384 -0
  21. package/Docs/developer-delivery-guide.md +224 -0
  22. package/Docs/error-reporting-guide.md +110 -0
  23. package/Docs/error-reporting-provider-guide.md +49 -0
  24. package/Docs/examples/error-report-adapter.md +70 -0
  25. package/Docs/examples/meeting-review.md +76 -0
  26. package/Docs/examples/minimal-design-system.md +67 -0
  27. package/Docs/examples/prototype-review-inputs.md +175 -0
  28. package/Docs/examples/reproducible-archaeology-depth-example.md +144 -0
  29. package/Docs/examples/test-scenario-input.md +68 -0
  30. package/Docs/examples/worked-examples.md +147 -0
  31. package/Docs/existing-project-onboarding-guide.md +214 -0
  32. package/Docs/explanation/core-concepts.md +26 -0
  33. package/Docs/explanation/delivery-workflow.md +48 -0
  34. package/Docs/governance/governance-team-guide.md +139 -0
  35. package/Docs/governed-starter-project-materialisation-guide.md +284 -0
  36. package/Docs/guide-catalogue.md +117 -0
  37. package/Docs/guided-discovery-facilitator-guide.md +172 -0
  38. package/Docs/guided-intent-workspace-guide.md +109 -0
  39. package/Docs/guided-phase-evidence-drafting-guide.md +119 -0
  40. package/Docs/human-approval-and-assurance-guide.md +146 -0
  41. package/Docs/knowledge-proposals-implementer-guide.md +106 -0
  42. package/Docs/knowledge-proposals-user-guide.md +247 -0
  43. package/Docs/maintainers/context-benchmarks.md +29 -0
  44. package/Docs/maintainers/contributing.md +58 -0
  45. package/Docs/maintainers/evidence-depth-acceptance.md +72 -0
  46. package/Docs/maintainers/verification-walkthroughs.md +104 -0
  47. package/Docs/meeting-evidence-implementer-guide.md +132 -0
  48. package/Docs/meeting-evidence-user-guide.md +200 -0
  49. package/Docs/operations/dashboard-and-delivery-state.md +153 -0
  50. package/Docs/operations/dashboard-configuration.md +87 -0
  51. package/Docs/operations/installation-updating-and-entitlements.md +135 -0
  52. package/Docs/operations/premium-personas-setup.md +76 -0
  53. package/Docs/operations/troubleshooting-and-recovery.md +205 -0
  54. package/Docs/organisation-rollout-guide.md +142 -0
  55. package/Docs/persona-entitlement-provider-guide.md +199 -0
  56. package/Docs/persona-guided-prototype-iteration.md +129 -0
  57. package/Docs/personas/organisation-specific-personas.md +103 -0
  58. package/Docs/personas/persona-authoring-cookbook.md +176 -0
  59. package/Docs/personas/persona-engagement-ui.md +133 -0
  60. package/Docs/personas/persona-governance.md +118 -0
  61. package/Docs/platform-export-analysis-guide.md +336 -0
  62. package/Docs/policies/governance-owner-guide.md +36 -0
  63. package/Docs/policies/implementation-guide.md +42 -0
  64. package/Docs/policies/organisation-policy-design-gates.md +58 -0
  65. package/Docs/policies/policy-pack-authoring-guide.md +108 -0
  66. package/Docs/policies/product-owner-guide.md +43 -0
  67. package/Docs/policies/technical-owner-guide.md +37 -0
  68. package/Docs/product-owner-guide.md +327 -0
  69. package/Docs/project-portfolio-orchestration-guide.md +199 -0
  70. package/Docs/quality/manual-qa-and-acceptance.md +162 -0
  71. package/Docs/quality/persona-driven-test-scenarios.md +172 -0
  72. package/Docs/quality/reproducible-archaeology-depth-review-checklist.md +89 -0
  73. package/Docs/reference/capabilities-and-project-layout.md +678 -0
  74. package/Docs/reference/cli-and-configuration.md +398 -0
  75. package/Docs/reference/contributions-api.md +23 -0
  76. package/Docs/reference/security-adapter-authoring.md +81 -0
  77. package/Docs/reference/starter-adapter-authoring.md +74 -0
  78. package/Docs/repository-source-map-guide.md +381 -0
  79. package/Docs/reproducible-archaeology-and-discovery-depth.md +292 -0
  80. package/Docs/screen-prototype-creation-guide.md +324 -0
  81. package/Docs/security-validation-guide.md +353 -0
  82. package/Docs/solution-readiness-review-guide.md +123 -0
  83. package/Docs/standards/project-standards-authoring.md +157 -0
  84. package/Docs/team-hub-guide.md +162 -0
  85. package/Docs/team-hub-resource-registry-guide.md +167 -0
  86. package/Docs/tutorials/first-delivery.md +83 -0
  87. package/Docs/tutorials/first-session.md +62 -0
  88. package/Docs/using-lifecycle-hooks.md +381 -0
  89. package/Docs/working-with-personas.md +274 -0
  90. package/LICENSE +165 -0
  91. package/README.md +96 -0
  92. package/agents-src/claude/ewai-security-reviewer.md +15 -0
  93. package/bin/ewai +5 -0
  94. package/config/archaeology-record-families.yaml +59 -0
  95. package/config/delivery-artifacts.yaml +121 -0
  96. package/config/delivery-stages.yaml +77 -0
  97. package/config/design-system.schema.json +46 -0
  98. package/config/error-reporting.schema.json +79 -0
  99. package/config/evidence-depth.schema.json +53 -0
  100. package/config/intent.schema.json +90 -0
  101. package/config/knowledge-proposals-proposal.schema.json +34 -0
  102. package/config/lifecycle-event.schema.json +68 -0
  103. package/config/lifecycle-handler.schema.json +45 -0
  104. package/config/lifecycle-hook-ack.schema.json +19 -0
  105. package/config/meeting-evidence-candidate.schema.json +102 -0
  106. package/config/organisation-policy.schema.json +137 -0
  107. package/config/pack.schema.json +250 -0
  108. package/config/persona-pack.schema.json +21 -0
  109. package/config/persona.schema.json +17 -0
  110. package/config/policy-evaluation.schema.json +77 -0
  111. package/config/policy-facts.schema.json +140 -0
  112. package/config/portfolio.schema.json +68 -0
  113. package/config/project.schema.json +313 -0
  114. package/config/prototype-iteration.schema.json +128 -0
  115. package/config/rollout.schema.json +87 -0
  116. package/config/security-adapter.schema.json +31 -0
  117. package/config/security-scan-request.schema.json +64 -0
  118. package/config/security-scan-response.schema.json +52 -0
  119. package/config/security-validation-policy.schema.json +74 -0
  120. package/config/starter-source-acknowledgement.schema.json +13 -0
  121. package/config/starter-source-adapter.schema.json +38 -0
  122. package/config/starter-source-request.schema.json +61 -0
  123. package/package.json +77 -0
  124. package/packs/core/pack.yaml +7 -0
  125. package/packs/design-systems/default/experience-promise.md +9 -0
  126. package/packs/design-systems/default/intentional-review.md +10 -0
  127. package/packs/design-systems/default/interaction-and-entry.md +9 -0
  128. package/packs/design-systems/default/meaningful-content-and-states.md +9 -0
  129. package/packs/design-systems/default/pack.yaml +44 -0
  130. package/packs/design-systems/default/principles.md +10 -0
  131. package/packs/personas/core/pack.yaml +7 -0
  132. package/packs/personas/core/personas/archaeologist.md +37 -0
  133. package/packs/personas/core/personas/end-user.md +17 -0
  134. package/packs/personas/core/personas/maintainer.md +17 -0
  135. package/packs/personas/core/personas/operator.md +17 -0
  136. package/packs/personas/core/personas/specs-knowledge-curator.md +35 -0
  137. package/packs/technologies/laravel/pack.yaml +30 -0
  138. package/packs/technologies/laravel-nuxt/pack.yaml +30 -0
  139. package/packs/technologies/nuxt/pack.yaml +30 -0
  140. package/packs/technologies/power-platform/pack.yaml +31 -0
  141. package/packs/technologies/salesforce/pack.yaml +25 -0
  142. package/public/app.js +4896 -0
  143. package/public/apple-touch-icon.png +0 -0
  144. package/public/assets/backstory-icon.png +0 -0
  145. package/public/dashboard-navigation.js +98 -0
  146. package/public/favicon-16.png +0 -0
  147. package/public/favicon-32.png +0 -0
  148. package/public/favicon.ico +0 -0
  149. package/public/index.html +789 -0
  150. package/public/styles.css +2693 -0
  151. package/public/team-hub/app.js +202 -0
  152. package/public/team-hub/index.html +79 -0
  153. package/public/team-hub/styles.css +90 -0
  154. package/scripts/publication-check.mjs +140 -0
  155. package/scripts/setup.mjs +21 -0
  156. package/skills-src/ewai-archaeology/SKILL.md +334 -0
  157. package/skills-src/ewai-archaeology/agents/openai.yaml +4 -0
  158. package/skills-src/ewai-archaeology/references/archaeology-contract.md +201 -0
  159. package/skills-src/ewai-archaeology/references/lifecycle-reconstruction.md +177 -0
  160. package/skills-src/ewai-archaeology/references/maximum-detail-reconstruction.md +97 -0
  161. package/skills-src/ewai-archaeology/references/model-routing.md +26 -0
  162. package/skills-src/ewai-architecture/SKILL.md +108 -0
  163. package/skills-src/ewai-architecture/agents/openai.yaml +4 -0
  164. package/skills-src/ewai-architecture/references/architecture-contract.md +176 -0
  165. package/skills-src/ewai-context/SKILL.md +68 -0
  166. package/skills-src/ewai-context/agents/openai.yaml +4 -0
  167. package/skills-src/ewai-context-import/SKILL.md +118 -0
  168. package/skills-src/ewai-context-import/agents/openai.yaml +4 -0
  169. package/skills-src/ewai-context-import/references/context-import-contract.md +106 -0
  170. package/skills-src/ewai-dashboard-configuration/SKILL.md +20 -0
  171. package/skills-src/ewai-deliver/SKILL.md +122 -0
  172. package/skills-src/ewai-deliver/references/delivery-evidence.md +92 -0
  173. package/skills-src/ewai-deliver/references/phase-routing.md +31 -0
  174. package/skills-src/ewai-design-system-apply/SKILL.md +27 -0
  175. package/skills-src/ewai-design-system-apply/agents/openai.yaml +4 -0
  176. package/skills-src/ewai-design-system-apply/references/application-contract.md +36 -0
  177. package/skills-src/ewai-design-system-author/SKILL.md +28 -0
  178. package/skills-src/ewai-design-system-author/agents/openai.yaml +4 -0
  179. package/skills-src/ewai-design-system-author/references/authoring-contract.md +38 -0
  180. package/skills-src/ewai-design-system-review/SKILL.md +26 -0
  181. package/skills-src/ewai-design-system-review/agents/openai.yaml +4 -0
  182. package/skills-src/ewai-design-system-review/references/review-contract.md +40 -0
  183. package/skills-src/ewai-error-reporting/SKILL.md +46 -0
  184. package/skills-src/ewai-error-reporting/agents/openai.yaml +4 -0
  185. package/skills-src/ewai-error-reporting/references/provider-contract.md +74 -0
  186. package/skills-src/ewai-evidence-depth/SKILL.md +72 -0
  187. package/skills-src/ewai-evidence-depth/agents/openai.yaml +4 -0
  188. package/skills-src/ewai-evidence-depth/references/evidence-depth-contract.md +127 -0
  189. package/skills-src/ewai-intent/SKILL.md +68 -0
  190. package/skills-src/ewai-intent/agents/openai.yaml +4 -0
  191. package/skills-src/ewai-intent/references/intent-contract.md +42 -0
  192. package/skills-src/ewai-knowledge-proposals/SKILL.md +105 -0
  193. package/skills-src/ewai-knowledge-proposals/agents/openai.yaml +4 -0
  194. package/skills-src/ewai-knowledge-proposals/references/proposal-contract.md +59 -0
  195. package/skills-src/ewai-meeting-evidence/SKILL.md +106 -0
  196. package/skills-src/ewai-meeting-evidence/agents/openai.yaml +4 -0
  197. package/skills-src/ewai-meeting-evidence/references/candidate-contract.md +64 -0
  198. package/skills-src/ewai-organisation-policy/SKILL.md +62 -0
  199. package/skills-src/ewai-organisation-policy/agents/openai.yaml +4 -0
  200. package/skills-src/ewai-organisation-policy/references/policy-contract.md +94 -0
  201. package/skills-src/ewai-palace-housekeeping/SKILL.md +55 -0
  202. package/skills-src/ewai-palace-housekeeping/agents/openai.yaml +4 -0
  203. package/skills-src/ewai-persona-entitlement/SKILL.md +60 -0
  204. package/skills-src/ewai-persona-entitlement/agents/openai.yaml +4 -0
  205. package/skills-src/ewai-phase-evidence/SKILL.md +79 -0
  206. package/skills-src/ewai-phase-evidence/agents/openai.yaml +4 -0
  207. package/skills-src/ewai-pipeline/SKILL.md +130 -0
  208. package/skills-src/ewai-pipeline/agents/openai.yaml +4 -0
  209. package/skills-src/ewai-pipeline/references/cli.md +86 -0
  210. package/skills-src/ewai-pipeline/references/specs-contract.md +16 -0
  211. package/skills-src/ewai-portfolio/SKILL.md +70 -0
  212. package/skills-src/ewai-portfolio/agents/openai.yaml +4 -0
  213. package/skills-src/ewai-portfolio/references/portfolio-contract.md +67 -0
  214. package/skills-src/ewai-project-discovery/SKILL.md +95 -0
  215. package/skills-src/ewai-project-discovery/agents/openai.yaml +4 -0
  216. package/skills-src/ewai-project-discovery/references/discovery-contract.md +34 -0
  217. package/skills-src/ewai-prototype-iteration/SKILL.md +30 -0
  218. package/skills-src/ewai-prototype-iteration/agents/openai.yaml +4 -0
  219. package/skills-src/ewai-prototype-iteration/references/review-contract.md +49 -0
  220. package/skills-src/ewai-retro/SKILL.md +48 -0
  221. package/skills-src/ewai-retro/agents/openai.yaml +4 -0
  222. package/skills-src/ewai-retro/references/asset-routing.md +14 -0
  223. package/skills-src/ewai-rollout/SKILL.md +74 -0
  224. package/skills-src/ewai-rollout/agents/openai.yaml +4 -0
  225. package/skills-src/ewai-rollout/references/rollout-contract.md +74 -0
  226. package/skills-src/ewai-shape-intents/SKILL.md +84 -0
  227. package/skills-src/ewai-shape-intents/agents/openai.yaml +4 -0
  228. package/skills-src/ewai-shape-intents/references/intent-mapping-contract.md +109 -0
  229. package/skills-src/ewai-solution-readiness/SKILL.md +55 -0
  230. package/skills-src/ewai-solution-readiness/agents/openai.yaml +4 -0
  231. package/skills-src/ewai-standards-check/SKILL.md +93 -0
  232. package/skills-src/ewai-standards-check/agents/openai.yaml +4 -0
  233. package/skills-src/ewai-standards-check/references/report-contract.md +116 -0
  234. package/skills-src/ewai-test-scenarios/SKILL.md +94 -0
  235. package/skills-src/ewai-test-scenarios/agents/openai.yaml +4 -0
  236. package/skills-src/ewai-test-scenarios/references/scenario-contract.md +88 -0
  237. package/src/afk-worker.mjs +16 -0
  238. package/src/archaeology.mjs +1333 -0
  239. package/src/checkin.mjs +261 -0
  240. package/src/cli.mjs +2427 -0
  241. package/src/companion-guidance.mjs +257 -0
  242. package/src/companion-opening.mjs +62 -0
  243. package/src/companion.mjs +256 -0
  244. package/src/context.mjs +210 -0
  245. package/src/dashboard-preferences.mjs +80 -0
  246. package/src/delivery-artifacts.mjs +204 -0
  247. package/src/delivery-documents.mjs +248 -0
  248. package/src/delivery-gates.mjs +317 -0
  249. package/src/delivery.mjs +1433 -0
  250. package/src/design-system-application.mjs +291 -0
  251. package/src/design-system-authoring.mjs +101 -0
  252. package/src/design-systems.mjs +466 -0
  253. package/src/discovery.mjs +1314 -0
  254. package/src/error-reporting.mjs +323 -0
  255. package/src/evidence-depth.mjs +543 -0
  256. package/src/execution-state.mjs +243 -0
  257. package/src/install.mjs +166 -0
  258. package/src/intent-dependencies.mjs +117 -0
  259. package/src/intent-maps.mjs +402 -0
  260. package/src/intents.mjs +747 -0
  261. package/src/knowledge-proposals.mjs +717 -0
  262. package/src/launcher.mjs +51 -0
  263. package/src/meeting-evidence.mjs +703 -0
  264. package/src/network-rollout.mjs +386 -0
  265. package/src/organisation-blueprints.mjs +438 -0
  266. package/src/organisation-policies.mjs +448 -0
  267. package/src/packs.mjs +44 -0
  268. package/src/paths.mjs +62 -0
  269. package/src/persona-entitlements.mjs +438 -0
  270. package/src/persona-licence-config.mjs +98 -0
  271. package/src/persona-website-provider.mjs +134 -0
  272. package/src/persona-zip.mjs +87 -0
  273. package/src/personas.mjs +159 -0
  274. package/src/platform-metadata-analysis.mjs +314 -0
  275. package/src/policy-design-gates.mjs +623 -0
  276. package/src/policy-gate-integration.mjs +318 -0
  277. package/src/portfolio.mjs +509 -0
  278. package/src/power-platform-source-map.mjs +190 -0
  279. package/src/project.mjs +449 -0
  280. package/src/prototype-iterations.mjs +730 -0
  281. package/src/repository-source-map.mjs +603 -0
  282. package/src/runtime/afk-conductor.mjs +973 -0
  283. package/src/runtime/context-assembly.mjs +457 -0
  284. package/src/runtime/context-benchmarks.mjs +115 -0
  285. package/src/runtime/dashboard-actions.mjs +109 -0
  286. package/src/runtime/dashboard-handoffs.mjs +149 -0
  287. package/src/runtime/dashboard-server.mjs +1272 -0
  288. package/src/runtime/dashboard.mjs +197 -0
  289. package/src/runtime/database.mjs +789 -0
  290. package/src/runtime/error-reporting.mjs +581 -0
  291. package/src/runtime/evidence-depth-workspace.mjs +412 -0
  292. package/src/runtime/execution-leases.mjs +299 -0
  293. package/src/runtime/guided-discovery.mjs +350 -0
  294. package/src/runtime/guided-intents.mjs +517 -0
  295. package/src/runtime/impact-analysis.mjs +535 -0
  296. package/src/runtime/intents.mjs +222 -0
  297. package/src/runtime/knowledge.mjs +86 -0
  298. package/src/runtime/lifecycle-hooks.mjs +1239 -0
  299. package/src/runtime/mcp-config.mjs +110 -0
  300. package/src/runtime/mcp-server.mjs +1885 -0
  301. package/src/runtime/palace.mjs +362 -0
  302. package/src/runtime/paths.mjs +58 -0
  303. package/src/runtime/persona-engagement.mjs +255 -0
  304. package/src/runtime/phase-contributions.mjs +594 -0
  305. package/src/runtime/policy-workspace.mjs +170 -0
  306. package/src/runtime/prototype-iterations.mjs +235 -0
  307. package/src/runtime/provider-adapters.mjs +163 -0
  308. package/src/runtime/repository-index.mjs +838 -0
  309. package/src/runtime/runs.mjs +185 -0
  310. package/src/runtime/security-validation.mjs +1230 -0
  311. package/src/runtime/starter-materialisation.mjs +1155 -0
  312. package/src/runtime/team-hub-client.mjs +479 -0
  313. package/src/runtime/team-hub-database.mjs +288 -0
  314. package/src/runtime/team-hub-server.mjs +191 -0
  315. package/src/runtime/team-hub.mjs +110 -0
  316. package/src/runtime/tree-sitter-index.mjs +390 -0
  317. package/src/runtime/version.mjs +1 -0
  318. package/src/runtime/work.mjs +633 -0
  319. package/src/salesforce-source-map.mjs +212 -0
  320. package/src/security-validation-config.mjs +224 -0
  321. package/src/solution-readiness.mjs +620 -0
  322. package/src/starter-materialisation-contract.mjs +407 -0
  323. package/src/task-graph.mjs +544 -0
  324. package/src/team-hub-resources.mjs +239 -0
  325. package/src/team-hub.mjs +242 -0
  326. package/src/test-scenarios.mjs +622 -0
  327. package/src/validation-config.mjs +289 -0
  328. package/templates/SPECS/1.Scope/personas/registry.yaml +12 -0
  329. package/templates/SPECS/5.Strategy/patterns/context-packet.md +119 -0
  330. package/templates/SPECS/6.Build/_tracker-template.md +16 -0
  331. package/templates/SPECS/pipeline.yaml +62 -0
  332. package/templates/discovery-answers.yaml +86 -0
  333. package/templates/intent-body.md +21 -0
  334. package/tests/fixtures/context-benchmarks.json +9 -0
@@ -0,0 +1,288 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { existsSync, lstatSync, mkdirSync } from 'node:fs';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { DatabaseSync } from 'node:sqlite';
5
+ import { TEAM_HUB_AUTHORITY_NOTICE, validateTeamHubEnvelope } from '../team-hub.mjs';
6
+ import { projectTeamHubResourceRelease, validateTeamHubResourcePackage } from '../team-hub-resources.mjs';
7
+
8
+ export function validateTeamHubDataRoot(dataRoot) {
9
+ const root = resolve(String(dataRoot ?? ''));
10
+ if (!String(dataRoot ?? '').trim()) throw new Error('Team Hub requires an explicit data root.');
11
+ if (/(?:^|[/\\])\.ewai-pipeline(?:[/\\]|$)/i.test(root)) throw new Error('Team Hub data must be separate from every project-local .ewai-pipeline runtime.');
12
+ if (/[/\\](?:CloudStorage|OneDrive|Dropbox|Google Drive)(?:[/\\]|$)/i.test(root) || root.startsWith('/Volumes/')) {
13
+ throw new Error('Team Hub SQLite data must not use a network, removable or cloud-synchronised folder.');
14
+ }
15
+ if (existsSync(root) && lstatSync(root).isSymbolicLink()) throw new Error('Team Hub data root must not be a symbolic link.');
16
+ return root;
17
+ }
18
+
19
+ export function teamHubDatabasePath(dataRoot) {
20
+ return resolve(validateTeamHubDataRoot(dataRoot), 'data/team-hub.sqlite');
21
+ }
22
+
23
+ export function openTeamHubDatabase(dataRoot) {
24
+ const path = teamHubDatabasePath(dataRoot);
25
+ mkdirSync(dirname(path), { recursive: true });
26
+ const database = new DatabaseSync(path);
27
+ database.exec('PRAGMA foreign_keys = ON;');
28
+ database.exec('PRAGMA busy_timeout = 5000;');
29
+ database.exec('PRAGMA journal_mode = WAL;');
30
+ database.exec(`
31
+ CREATE TABLE IF NOT EXISTS team_hub_projects (
32
+ project_id TEXT PRIMARY KEY,
33
+ name TEXT NOT NULL,
34
+ snapshot_json TEXT NOT NULL,
35
+ snapshot_digest TEXT NOT NULL,
36
+ generated_at TEXT NOT NULL,
37
+ accepted_at TEXT NOT NULL,
38
+ receipt_id TEXT NOT NULL,
39
+ updated_at TEXT NOT NULL
40
+ );
41
+ CREATE TABLE IF NOT EXISTS team_hub_receipts (
42
+ receipt_id TEXT PRIMARY KEY,
43
+ project_id TEXT NOT NULL,
44
+ snapshot_digest TEXT NOT NULL,
45
+ idempotency_key TEXT NOT NULL UNIQUE,
46
+ accepted_at TEXT NOT NULL,
47
+ receipt_json TEXT NOT NULL
48
+ );
49
+ CREATE INDEX IF NOT EXISTS idx_team_hub_projects_accepted ON team_hub_projects(accepted_at DESC);
50
+ CREATE INDEX IF NOT EXISTS idx_team_hub_receipts_project ON team_hub_receipts(project_id, accepted_at DESC);
51
+ CREATE TABLE IF NOT EXISTS team_hub_resource_releases (
52
+ resource_id TEXT NOT NULL,
53
+ version TEXT NOT NULL,
54
+ kind TEXT NOT NULL,
55
+ name TEXT NOT NULL,
56
+ description TEXT NOT NULL,
57
+ publisher_id TEXT NOT NULL,
58
+ publisher_name TEXT NOT NULL,
59
+ compatibility_ewai TEXT NOT NULL,
60
+ package_digest TEXT NOT NULL,
61
+ package_json TEXT NOT NULL,
62
+ published_at TEXT NOT NULL,
63
+ receipt_id TEXT NOT NULL,
64
+ PRIMARY KEY(resource_id, version)
65
+ );
66
+ CREATE TABLE IF NOT EXISTS team_hub_resource_publication_receipts (
67
+ receipt_id TEXT PRIMARY KEY,
68
+ resource_id TEXT NOT NULL,
69
+ version TEXT NOT NULL,
70
+ package_digest TEXT NOT NULL,
71
+ idempotency_key TEXT NOT NULL UNIQUE,
72
+ published_at TEXT NOT NULL,
73
+ receipt_json TEXT NOT NULL
74
+ );
75
+ CREATE INDEX IF NOT EXISTS idx_team_hub_resource_releases_published
76
+ ON team_hub_resource_releases(resource_id, published_at DESC, version DESC);
77
+ `);
78
+ return database;
79
+ }
80
+
81
+ function publicationReceiptFromRow(row, replayed) {
82
+ return { ...JSON.parse(row.receipt_json), replayed };
83
+ }
84
+
85
+ export function publishTeamHubResource(database, input, options = {}) {
86
+ const resourcePackage = validateTeamHubResourcePackage(input);
87
+ const resource = resourcePackage.resource;
88
+ const idempotencyKey = String(options.idempotencyKey ?? `${resource.id}@${resource.version}:${resourcePackage.digest}`);
89
+ if (!idempotencyKey || idempotencyKey.length > 500 || /[\u0000-\u001f\u007f]/.test(idempotencyKey)) {
90
+ throw Object.assign(new Error('Resource publication idempotency key is invalid.'), { status: 400, code: 'invalid-idempotency-key' });
91
+ }
92
+ const publishedAt = options.at ?? new Date().toISOString();
93
+ const receipt = {
94
+ schema: 'ewai.team-hub-resource-publication-receipt/v1',
95
+ receiptId: options.receiptId ?? `resource-publication-${randomUUID()}`,
96
+ resourceId: resource.id,
97
+ version: resource.version,
98
+ packageDigest: resourcePackage.digest,
99
+ idempotencyKey,
100
+ publishedAt,
101
+ replayed: false,
102
+ authority: 'Publication records immutable transport availability; it does not select or apply this resource.',
103
+ };
104
+ const release = projectTeamHubResourceRelease(resourcePackage);
105
+ database.exec('BEGIN IMMEDIATE;');
106
+ try {
107
+ const replay = database.prepare('SELECT receipt_json FROM team_hub_resource_publication_receipts WHERE idempotency_key = ?').get(idempotencyKey);
108
+ if (replay) {
109
+ database.exec('COMMIT;');
110
+ return publicationReceiptFromRow(replay, true);
111
+ }
112
+ const existing = database.prepare('SELECT package_digest FROM team_hub_resource_releases WHERE resource_id = ? AND version = ?').get(resource.id, resource.version);
113
+ if (existing) {
114
+ if (existing.package_digest !== resourcePackage.digest) {
115
+ throw Object.assign(new Error('A different immutable release already uses this resource ID and version.'), { status: 409, code: 'resource-release-conflict' });
116
+ }
117
+ const prior = database.prepare('SELECT receipt_json FROM team_hub_resource_publication_receipts WHERE resource_id = ? AND version = ? AND package_digest = ? ORDER BY published_at LIMIT 1').get(resource.id, resource.version, resourcePackage.digest);
118
+ if (prior) {
119
+ database.exec('COMMIT;');
120
+ return publicationReceiptFromRow(prior, true);
121
+ }
122
+ }
123
+ database.prepare(`
124
+ INSERT INTO team_hub_resource_releases
125
+ (resource_id, version, kind, name, description, publisher_id, publisher_name,
126
+ compatibility_ewai, package_digest, package_json, published_at, receipt_id)
127
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
128
+ `).run(
129
+ resource.id, resource.version, resource.kind, resource.name, resource.description,
130
+ resource.publisher.id, resource.publisher.name, resource.compatibility.ewai,
131
+ resourcePackage.digest, JSON.stringify(resourcePackage), publishedAt, receipt.receiptId,
132
+ );
133
+ database.prepare(`
134
+ INSERT INTO team_hub_resource_publication_receipts
135
+ (receipt_id, resource_id, version, package_digest, idempotency_key, published_at, receipt_json)
136
+ VALUES (?, ?, ?, ?, ?, ?, ?)
137
+ `).run(receipt.receiptId, resource.id, resource.version, resourcePackage.digest, idempotencyKey, publishedAt, JSON.stringify(receipt));
138
+ database.exec('COMMIT;');
139
+ } catch (error) {
140
+ database.exec('ROLLBACK;');
141
+ throw error;
142
+ }
143
+ return { ...receipt, release: { ...release, publishedAt, receiptId: receipt.receiptId } };
144
+ }
145
+
146
+ function releaseFromRow(row) {
147
+ const resourcePackage = JSON.parse(row.package_json);
148
+ return {
149
+ schema: 'ewai.team-hub-resource-release/v1',
150
+ kind: row.kind,
151
+ id: row.resource_id,
152
+ name: row.name,
153
+ description: row.description,
154
+ version: row.version,
155
+ publisher: { id: row.publisher_id, name: row.publisher_name },
156
+ compatibility: { ewai: row.compatibility_ewai },
157
+ digest: row.package_digest,
158
+ fileCount: resourcePackage.files.length,
159
+ totalBytes: resourcePackage.files.reduce((sum, file) => sum + file.size, 0),
160
+ publishedAt: row.published_at,
161
+ receiptId: row.receipt_id,
162
+ };
163
+ }
164
+
165
+ export function readTeamHubResourceCatalogue(database) {
166
+ const releases = database.prepare(`
167
+ SELECT resource_id, version, kind, name, description, publisher_id, publisher_name,
168
+ compatibility_ewai, package_digest, package_json, published_at, receipt_id
169
+ FROM team_hub_resource_releases
170
+ ORDER BY resource_id COLLATE NOCASE, published_at DESC, version DESC
171
+ `).all().map(releaseFromRow);
172
+ return {
173
+ schema: 'ewai.team-hub-resource-catalogue/v1',
174
+ releases,
175
+ authority: 'Discovery does not install, select or apply a resource.',
176
+ };
177
+ }
178
+
179
+ export function readTeamHubResourcePackage(database, resourceId, version) {
180
+ const row = database.prepare('SELECT package_json FROM team_hub_resource_releases WHERE resource_id = ? AND version = ?').get(String(resourceId), String(version));
181
+ if (!row) throw Object.assign(new Error('Team Hub resource release was not found.'), { status: 404, code: 'resource-release-not-found' });
182
+ return validateTeamHubResourcePackage(JSON.parse(row.package_json));
183
+ }
184
+
185
+ function receiptFromRow(row, replayed) {
186
+ const receipt = JSON.parse(row.receipt_json);
187
+ return { ...receipt, replayed };
188
+ }
189
+
190
+ export function ingestTeamHubEnvelope(database, input, options = {}) {
191
+ const envelope = validateTeamHubEnvelope(input);
192
+ const acceptedAt = options.at ?? new Date().toISOString();
193
+ const receipt = {
194
+ schema: 'ewai.team-hub-receipt/v1',
195
+ receiptId: options.receiptId ?? `receipt-${randomUUID()}`,
196
+ projectId: envelope.snapshot.project.id,
197
+ snapshotDigest: envelope.snapshotDigest,
198
+ idempotencyKey: envelope.idempotencyKey,
199
+ acceptedAt,
200
+ replayed: false,
201
+ };
202
+ const receiptJson = JSON.stringify(receipt);
203
+ const snapshotJson = JSON.stringify(envelope.snapshot);
204
+
205
+ database.exec('BEGIN IMMEDIATE;');
206
+ try {
207
+ const existing = database.prepare('SELECT receipt_json FROM team_hub_receipts WHERE idempotency_key = ?').get(envelope.idempotencyKey);
208
+ if (existing) {
209
+ database.exec('COMMIT;');
210
+ return receiptFromRow(existing, true);
211
+ }
212
+ database.prepare(`
213
+ INSERT INTO team_hub_receipts (receipt_id, project_id, snapshot_digest, idempotency_key, accepted_at, receipt_json)
214
+ VALUES (?, ?, ?, ?, ?, ?)
215
+ `).run(receipt.receiptId, receipt.projectId, receipt.snapshotDigest, receipt.idempotencyKey, receipt.acceptedAt, receiptJson);
216
+ database.prepare(`
217
+ INSERT INTO team_hub_projects (project_id, name, snapshot_json, snapshot_digest, generated_at, accepted_at, receipt_id, updated_at)
218
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
219
+ ON CONFLICT(project_id) DO UPDATE SET
220
+ name = excluded.name,
221
+ snapshot_json = excluded.snapshot_json,
222
+ snapshot_digest = excluded.snapshot_digest,
223
+ generated_at = excluded.generated_at,
224
+ accepted_at = excluded.accepted_at,
225
+ receipt_id = excluded.receipt_id,
226
+ updated_at = excluded.updated_at
227
+ WHERE excluded.generated_at >= team_hub_projects.generated_at
228
+ `).run(
229
+ receipt.projectId, envelope.snapshot.project.name, snapshotJson, envelope.snapshotDigest,
230
+ envelope.snapshot.generatedAt, receipt.acceptedAt, receipt.receiptId, acceptedAt,
231
+ );
232
+ database.exec('COMMIT;');
233
+ } catch (error) {
234
+ database.exec('ROLLBACK;');
235
+ throw error;
236
+ }
237
+ return receipt;
238
+ }
239
+
240
+ function freshness(acceptedAt, nowValue, staleAfterHours) {
241
+ const accepted = Date.parse(acceptedAt);
242
+ const now = Date.parse(nowValue);
243
+ if (!Number.isFinite(accepted) || !Number.isFinite(now)) return { state: 'unknown', acceptedAt, ageHours: null };
244
+ const ageHours = Math.max(0, Math.floor((now - accepted) / 3_600_000));
245
+ return { state: ageHours > staleAfterHours ? 'stale' : 'current', acceptedAt, ageHours };
246
+ }
247
+
248
+ export function readTeamHubPortfolio(database, options = {}) {
249
+ const now = options.now ?? new Date().toISOString();
250
+ const staleAfterHours = Math.max(1, Math.min(8_760, Number(options.staleAfterHours ?? 72)));
251
+ const rows = database.prepare('SELECT * FROM team_hub_projects ORDER BY name COLLATE NOCASE, project_id').all();
252
+ const projects = rows.map((row) => {
253
+ const snapshot = JSON.parse(row.snapshot_json);
254
+ const observed = freshness(row.accepted_at, now, staleAfterHours);
255
+ const attention = [...(snapshot.delivery?.attention ?? [])];
256
+ if (observed.state === 'stale') attention.unshift({ code: 'snapshot-stale', severity: 'warning', summary: 'The latest accepted snapshot is stale.' });
257
+ return {
258
+ id: row.project_id,
259
+ name: row.name,
260
+ freshness: observed,
261
+ evidence: {
262
+ submittedAt: snapshot.generatedAt,
263
+ acceptedAt: row.accepted_at,
264
+ snapshotDigest: row.snapshot_digest,
265
+ receiptId: row.receipt_id,
266
+ },
267
+ ewaiVersion: snapshot.ewaiVersion,
268
+ revisionDigest: snapshot.revision.digest,
269
+ intents: snapshot.intents,
270
+ delivery: snapshot.delivery,
271
+ resources: snapshot.resources,
272
+ attention: attention.slice(0, 20),
273
+ };
274
+ });
275
+ return {
276
+ schema: 'ewai.team-hub-portfolio/v1',
277
+ generatedAt: now,
278
+ staleAfterHours,
279
+ projects,
280
+ summary: {
281
+ total: projects.length,
282
+ current: projects.filter(({ freshness: item }) => item.state === 'current').length,
283
+ stale: projects.filter(({ freshness: item }) => item.state === 'stale').length,
284
+ attention: projects.filter(({ attention }) => attention.length > 0).length,
285
+ },
286
+ authority: { canApprove: false, notice: TEAM_HUB_AUTHORITY_NOTICE },
287
+ };
288
+ }
@@ -0,0 +1,191 @@
1
+ import { createHash, timingSafeEqual } from 'node:crypto';
2
+ import { createServer } from 'node:http';
3
+ import { existsSync, readFileSync, statSync } from 'node:fs';
4
+ import { dirname, extname, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import {
7
+ ingestTeamHubEnvelope,
8
+ openTeamHubDatabase,
9
+ publishTeamHubResource,
10
+ readTeamHubPortfolio,
11
+ readTeamHubResourceCatalogue,
12
+ readTeamHubResourcePackage,
13
+ validateTeamHubDataRoot,
14
+ } from './team-hub-database.mjs';
15
+ import { TEAM_HUB_RESOURCE_LIMITS } from '../team-hub-resources.mjs';
16
+
17
+ const moduleDirectory = dirname(fileURLToPath(import.meta.url));
18
+ const defaultStaticRoot = resolve(moduleDirectory, '../../public/team-hub');
19
+ const DEFAULT_MAXIMUM_BODY_BYTES = 256 * 1024;
20
+
21
+ function loopback(host) {
22
+ return ['127.0.0.1', 'localhost', '::1'].includes(String(host ?? '').toLowerCase());
23
+ }
24
+
25
+ function json(response, status, value) {
26
+ const body = `${JSON.stringify(value)}\n`;
27
+ response.writeHead(status, {
28
+ 'content-type': 'application/json; charset=utf-8',
29
+ 'content-length': Buffer.byteLength(body),
30
+ 'cache-control': 'no-store',
31
+ 'x-content-type-options': 'nosniff',
32
+ });
33
+ response.end(body);
34
+ }
35
+
36
+ function safeError(response, status, code, message) {
37
+ json(response, status, { error: { code, message } });
38
+ }
39
+
40
+ function authorised(request, token) {
41
+ if (!token) return false;
42
+ const supplied = String(request.headers.authorization ?? '');
43
+ const expected = `Bearer ${token}`;
44
+ const left = createHash('sha256').update(supplied).digest();
45
+ const right = createHash('sha256').update(expected).digest();
46
+ return timingSafeEqual(left, right);
47
+ }
48
+
49
+ async function readBody(request, maximumBytes) {
50
+ const declared = Number(request.headers['content-length'] ?? 0);
51
+ if (Number.isFinite(declared) && declared > maximumBytes) throw Object.assign(new Error('Request body exceeds the Team Hub limit.'), { status: 413, code: 'body-too-large' });
52
+ const chunks = [];
53
+ let size = 0;
54
+ for await (const chunk of request) {
55
+ size += chunk.length;
56
+ if (size > maximumBytes) throw Object.assign(new Error('Request body exceeds the Team Hub limit.'), { status: 413, code: 'body-too-large' });
57
+ chunks.push(chunk);
58
+ }
59
+ try { return JSON.parse(Buffer.concat(chunks).toString('utf8')); }
60
+ catch { throw Object.assign(new Error('Request body must be valid JSON.'), { status: 400, code: 'invalid-json' }); }
61
+ }
62
+
63
+ function staticAsset(response, staticRoot, pathname) {
64
+ const routes = new Map([
65
+ ['/', 'index.html'], ['/index.html', 'index.html'], ['/app.js', 'app.js'], ['/styles.css', 'styles.css'],
66
+ ]);
67
+ const name = routes.get(pathname);
68
+ if (!name) return false;
69
+ const path = resolve(staticRoot, name);
70
+ if (!existsSync(path) || !statSync(path).isFile()) return false;
71
+ const body = readFileSync(path);
72
+ const type = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8' }[extname(path)] ?? 'application/octet-stream';
73
+ response.writeHead(200, {
74
+ 'content-type': type, 'content-length': body.length, 'cache-control': 'no-store',
75
+ 'x-content-type-options': 'nosniff', 'content-security-policy': "default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self'; base-uri 'none'; frame-ancestors 'none'",
76
+ });
77
+ response.end(body);
78
+ return true;
79
+ }
80
+
81
+ export async function createTeamHubHttpServer(options = {}) {
82
+ const dataRoot = validateTeamHubDataRoot(options.dataRoot);
83
+ const host = String(options.host ?? '127.0.0.1');
84
+ if (!loopback(host) && options.allowNetwork !== true) throw new Error('A non-loopback Team Hub bind requires --allow-network.');
85
+ const token = String(options.token ?? '');
86
+ if (token.length < 16 || token.length > 1_024) throw new Error('Team Hub token must contain 16 to 1024 characters.');
87
+ const publisherToken = String(options.publisherToken ?? '');
88
+ if (publisherToken && (publisherToken.length < 16 || publisherToken.length > 1_024)) throw new Error('Team Hub publisher token must contain 16 to 1024 characters.');
89
+ if (publisherToken && publisherToken === token) throw new Error('Team Hub reader and publisher tokens must be different.');
90
+ const database = openTeamHubDatabase(dataRoot);
91
+ const startedAt = options.startedAt ?? new Date().toISOString();
92
+ const staticRoot = resolve(options.staticRoot ?? defaultStaticRoot);
93
+ const maximumBodyBytes = Number(options.maximumBodyBytes ?? DEFAULT_MAXIMUM_BODY_BYTES);
94
+ const maximumResourceBodyBytes = Number(options.maximumResourceBodyBytes ?? TEAM_HUB_RESOURCE_LIMITS.maximumPackageBytes);
95
+
96
+ const server = createServer(async (request, response) => {
97
+ try {
98
+ const url = new URL(request.url ?? '/', `http://${request.headers.host ?? '127.0.0.1'}`);
99
+ if (url.search) return safeError(response, 400, 'unexpected-query', 'This Team Hub route does not accept query parameters.');
100
+ if (request.method === 'GET' && url.pathname === '/health') {
101
+ return json(response, 200, { schema: 'ewai.team-hub-health/v1', status: 'running', pid: process.pid, startedAt, bind: loopback(host) ? 'loopback' : 'network', tls: false, publisherConfigured: Boolean(publisherToken) });
102
+ }
103
+ if (request.method === 'GET' && ['/','/index.html','/app.js','/styles.css'].includes(url.pathname)) {
104
+ if (staticAsset(response, staticRoot, url.pathname)) return;
105
+ }
106
+ if (request.method === 'POST' && url.pathname === '/api/v1/resources') {
107
+ if (!publisherToken) return safeError(response, 503, 'publisher-not-configured', 'Resource publication is not configured on this Team Hub.');
108
+ if (!authorised(request, publisherToken)) return safeError(response, 401, 'unauthorised', 'Publisher authentication is required.');
109
+ if (!String(request.headers['content-type'] ?? '').toLowerCase().startsWith('application/json')) {
110
+ return safeError(response, 415, 'unsupported-media-type', 'Resource publication requires application/json.');
111
+ }
112
+ const input = await readBody(request, maximumResourceBodyBytes);
113
+ const receipt = publishTeamHubResource(database, input, { idempotencyKey: request.headers['idempotency-key'] });
114
+ return json(response, receipt.replayed ? 200 : 202, receipt);
115
+ }
116
+ if (request.method === 'GET' && url.pathname === '/api/v1/resources') {
117
+ if (!authorised(request, token) && !authorised(request, publisherToken)) return safeError(response, 401, 'unauthorised', 'Authentication is required.');
118
+ return json(response, 200, readTeamHubResourceCatalogue(database));
119
+ }
120
+ const resourceRelease = url.pathname.match(/^\/api\/v1\/resources\/([^/]+)\/releases\/([^/]+)$/);
121
+ if (request.method === 'GET' && resourceRelease) {
122
+ if (!authorised(request, token) && !authorised(request, publisherToken)) return safeError(response, 401, 'unauthorised', 'Authentication is required.');
123
+ let resourceId;
124
+ let version;
125
+ try { resourceId = decodeURIComponent(resourceRelease[1]); version = decodeURIComponent(resourceRelease[2]); }
126
+ catch { return safeError(response, 400, 'invalid-resource-identity', 'Resource identity is invalid.'); }
127
+ if (!/^org\.[a-z0-9]+(?:-[a-z0-9]+)*\.[a-z0-9]+(?:[.-][a-z0-9]+)*$/.test(resourceId) || !/^\d+\.\d+\.\d+$/.test(version)) {
128
+ return safeError(response, 400, 'invalid-resource-identity', 'Resource identity is invalid.');
129
+ }
130
+ return json(response, 200, readTeamHubResourcePackage(database, resourceId, version));
131
+ }
132
+ if (url.pathname.startsWith('/api/v1/') && !authorised(request, token)) {
133
+ return safeError(response, 401, 'unauthorised', 'Authentication is required.');
134
+ }
135
+ if (request.method === 'POST' && url.pathname === '/api/v1/snapshots') {
136
+ if (!String(request.headers['content-type'] ?? '').toLowerCase().startsWith('application/json')) {
137
+ return safeError(response, 415, 'unsupported-media-type', 'Snapshots require application/json.');
138
+ }
139
+ const input = await readBody(request, maximumBodyBytes);
140
+ const receipt = ingestTeamHubEnvelope(database, input);
141
+ return json(response, receipt.replayed ? 200 : 202, receipt);
142
+ }
143
+ if (request.method === 'GET' && url.pathname === '/api/v1/portfolio') {
144
+ return json(response, 200, readTeamHubPortfolio(database));
145
+ }
146
+ return safeError(response, 404, 'not-found', 'Team Hub route not found.');
147
+ } catch (error) {
148
+ const status = Number(error.status ?? 400);
149
+ return safeError(response, status >= 400 && status < 600 ? status : 400, error.code ?? 'invalid-request', status >= 500 ? 'Team Hub could not complete the request.' : String(error.message ?? 'Invalid request.').slice(0, 240));
150
+ }
151
+ });
152
+
153
+ await new Promise((accept, reject) => {
154
+ server.once('error', reject);
155
+ server.listen(Number(options.port ?? 0), host, accept);
156
+ });
157
+ const address = server.address();
158
+ const displayHost = host === '::1' ? '[::1]' : host;
159
+ const url = `http://${displayHost}:${address.port}`;
160
+ return {
161
+ schema: 'ewai.team-hub-running-service/v1', url, host, port: address.port, startedAt,
162
+ close: async () => {
163
+ await new Promise((accept, reject) => server.close((error) => error ? reject(error) : accept()));
164
+ database.close();
165
+ },
166
+ };
167
+ }
168
+
169
+ function option(args, name, fallback = '') {
170
+ const index = args.indexOf(name);
171
+ return index === -1 ? fallback : args[index + 1];
172
+ }
173
+
174
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
175
+ const args = process.argv.slice(2);
176
+ const tokenEnv = option(args, '--token-env');
177
+ const token = process.env[tokenEnv] ?? '';
178
+ const publisherTokenEnv = option(args, '--publisher-token-env');
179
+ const publisherToken = publisherTokenEnv ? process.env[publisherTokenEnv] ?? '' : '';
180
+ createTeamHubHttpServer({
181
+ dataRoot: option(args, '--data'), host: option(args, '--host', '127.0.0.1'),
182
+ port: Number(option(args, '--port', '0')), token, publisherToken, allowNetwork: args.includes('--allow-network'),
183
+ }).then((running) => {
184
+ const shutdown = async () => { await running.close(); process.exit(0); };
185
+ process.once('SIGTERM', shutdown);
186
+ process.once('SIGINT', shutdown);
187
+ }).catch((error) => {
188
+ process.stderr.write(`Team Hub failed to start: ${error.message}\n`);
189
+ process.exit(1);
190
+ });
191
+ }
@@ -0,0 +1,110 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { dirname, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { validateTeamHubDataRoot } from './team-hub-database.mjs';
7
+
8
+ const moduleDirectory = dirname(fileURLToPath(import.meta.url));
9
+ const serverEntry = resolve(moduleDirectory, 'team-hub-server.mjs');
10
+
11
+ function servicePaths(dataRoot) {
12
+ const root = validateTeamHubDataRoot(dataRoot);
13
+ return { root, state: resolve(root, 'runtime/team-hub.json'), log: resolve(root, 'logs/team-hub.log') };
14
+ }
15
+
16
+ function readState(path) {
17
+ if (!existsSync(path)) return null;
18
+ try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; }
19
+ }
20
+
21
+ function writeState(path, value) {
22
+ mkdirSync(dirname(path), { recursive: true });
23
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
24
+ }
25
+
26
+ async function health(url, timeout = 800) {
27
+ try {
28
+ const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(timeout) });
29
+ if (!response.ok) return null;
30
+ const value = await response.json();
31
+ return value.schema === 'ewai.team-hub-health/v1' ? value : null;
32
+ } catch { return null; }
33
+ }
34
+
35
+ function basePort(dataRoot) {
36
+ const digest = createHash('sha256').update(resolve(dataRoot)).digest();
37
+ return 48000 + digest.readUInt16BE(0) % 1000;
38
+ }
39
+
40
+ function ports(dataRoot, configured) {
41
+ if (Number(configured) > 0) return [Number(configured)];
42
+ const first = basePort(dataRoot);
43
+ return Array.from({ length: 20 }, (_, index) => 48000 + ((first - 48000 + index) % 1000));
44
+ }
45
+
46
+ export async function teamHubStatus(options = {}) {
47
+ const paths = servicePaths(options.dataRoot);
48
+ const state = readState(paths.state);
49
+ if (!state?.url) return { schema: 'ewai.team-hub-service-status/v1', status: 'stopped', state: null };
50
+ const observed = await health(state.url);
51
+ if (!observed || Number(observed.pid) !== Number(state.pid)) return { schema: 'ewai.team-hub-service-status/v1', status: 'stale', state };
52
+ return { schema: 'ewai.team-hub-service-status/v1', status: 'running', state: { ...state, bind: observed.bind, tls: observed.tls, publisherConfigured: observed.publisherConfigured === true } };
53
+ }
54
+
55
+ export async function ensureTeamHub(options = {}) {
56
+ const paths = servicePaths(options.dataRoot);
57
+ const tokenEnv = String(options.tokenEnv ?? '');
58
+ if (!/^[A-Z][A-Z0-9_]{2,100}$/.test(tokenEnv)) throw new Error('Team Hub requires a safe --token-env name.');
59
+ if (!String((options.env ?? process.env)[tokenEnv] ?? '')) throw new Error(`Team Hub token environment variable is unavailable: ${tokenEnv}`);
60
+ const publisherTokenEnv = String(options.publisherTokenEnv ?? '');
61
+ if (publisherTokenEnv && !/^[A-Z][A-Z0-9_]{2,100}$/.test(publisherTokenEnv)) throw new Error('Team Hub requires a safe --publisher-token-env name.');
62
+ if (publisherTokenEnv && !String((options.env ?? process.env)[publisherTokenEnv] ?? '')) throw new Error(`Team Hub publisher token environment variable is unavailable: ${publisherTokenEnv}`);
63
+ if (publisherTokenEnv && publisherTokenEnv === tokenEnv) throw new Error('Team Hub reader and publisher token environment variables must be different.');
64
+ const host = String(options.host ?? '127.0.0.1');
65
+ if (!['127.0.0.1', 'localhost', '::1'].includes(host) && options.allowNetwork !== true) throw new Error('A non-loopback Team Hub bind requires --allow-network.');
66
+ const current = await teamHubStatus({ dataRoot: paths.root });
67
+ if (current.status === 'running') return { status: 'running', started: false, ...current.state };
68
+ rmSync(paths.state, { force: true });
69
+ mkdirSync(dirname(paths.log), { recursive: true });
70
+ const log = openSync(paths.log, 'a');
71
+ try {
72
+ for (const port of ports(paths.root, options.port)) {
73
+ const displayHost = host === '::1' ? '[::1]' : host;
74
+ const url = `http://${displayHost}:${port}`;
75
+ if (await health(url, 250)) continue;
76
+ const args = [serverEntry, '--data', paths.root, '--host', host, '--port', String(port), '--token-env', tokenEnv];
77
+ if (publisherTokenEnv) args.push('--publisher-token-env', publisherTokenEnv);
78
+ if (options.allowNetwork) args.push('--allow-network');
79
+ const child = spawn(process.execPath, args, { detached: true, stdio: ['ignore', log, log], env: options.env ?? process.env });
80
+ child.unref();
81
+ const deadline = Date.now() + Number(options.timeoutMs ?? 5_000);
82
+ while (Date.now() < deadline) {
83
+ await new Promise((accept) => setTimeout(accept, 100));
84
+ const observed = await health(url, 400);
85
+ if (observed && Number(observed.pid) === Number(child.pid)) {
86
+ const state = { schema: 'ewai.team-hub-service-state/v1', pid: child.pid, host, port, url, tokenEnv, publisherTokenEnv: publisherTokenEnv || null, startedAt: observed.startedAt };
87
+ writeState(paths.state, state);
88
+ return { status: 'running', started: true, ...state };
89
+ }
90
+ if (child.exitCode !== null) break;
91
+ }
92
+ }
93
+ } finally { closeSync(log); }
94
+ throw new Error(`Unable to start Team Hub. Review the service log under the configured data root.`);
95
+ }
96
+
97
+ export async function stopTeamHub(options = {}) {
98
+ const paths = servicePaths(options.dataRoot);
99
+ const current = await teamHubStatus({ dataRoot: paths.root });
100
+ if (current.status !== 'running') {
101
+ rmSync(paths.state, { force: true });
102
+ return { schema: 'ewai.team-hub-service-status/v1', status: 'stopped', stopped: false };
103
+ }
104
+ try { process.kill(Number(current.state.pid), 'SIGTERM'); }
105
+ catch (error) { if (error.code !== 'ESRCH') throw error; }
106
+ const deadline = Date.now() + 3_000;
107
+ while (Date.now() < deadline && await health(current.state.url, 250)) await new Promise((accept) => setTimeout(accept, 100));
108
+ rmSync(paths.state, { force: true });
109
+ return { schema: 'ewai.team-hub-service-status/v1', status: 'stopped', stopped: true };
110
+ }