@warpgogol/werkstatt-shared 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (403) hide show
  1. package/package.json +352 -0
  2. package/src/checks/index.ts +24 -0
  3. package/src/checks/lib/astro-site-url.ts +30 -0
  4. package/src/checks/lib/i18n.ts +30 -0
  5. package/src/checks/result-helpers.ts +102 -0
  6. package/src/checks/suppressions-config.ts +195 -0
  7. package/src/index.ts +14 -0
  8. package/src/integration/README.md +38 -0
  9. package/src/integration/adapters.ts +143 -0
  10. package/src/integration/crm-buffer.ts +491 -0
  11. package/src/integration/dispatch.ts +106 -0
  12. package/src/integration/funnel.ts +379 -0
  13. package/src/integration/index.ts +56 -0
  14. package/src/integration/lifecycle.ts +155 -0
  15. package/src/integration/orchestration.ts +439 -0
  16. package/src/integration/port-barrel.ts +124 -0
  17. package/src/integration/port.ts +143 -0
  18. package/src/integration/qstash.ts +134 -0
  19. package/src/integration/sharding.ts +133 -0
  20. package/src/integration/tests/deliver-event.test.ts +52 -0
  21. package/src/integration/tests/dispatch.test.ts +88 -0
  22. package/src/integration/tests/funnel-bridge.test.ts +46 -0
  23. package/src/integration/tests/hub-runtime.test.ts +128 -0
  24. package/src/integration/tests/lifecycle.test.ts +69 -0
  25. package/src/integration/tests/qstash.test.ts +83 -0
  26. package/src/integration/tests/sharding.test.ts +64 -0
  27. package/src/integration/vitest.config.ts +8 -0
  28. package/src/integration-adapter-supabase-crm/adapter.ts +437 -0
  29. package/src/integration-adapter-supabase-crm/client.ts +605 -0
  30. package/src/integration-adapter-supabase-crm/index.ts +28 -0
  31. package/src/integration-adapter-supabase-crm/pipedrive-sync-target.ts +482 -0
  32. package/src/integration-adapter-supabase-crm/supabase-rest.ts +89 -0
  33. package/src/integration-adapter-supabase-crm/tenant-registry.ts +334 -0
  34. package/src/integration-adapter-supabase-crm/tests/change-balance.pbt.test.ts +83 -0
  35. package/src/integration-adapter-supabase-crm/tests/funnel-persistence.test.ts +409 -0
  36. package/src/integration-adapter-supabase-crm/tests/lifecycle-sync.test.ts +288 -0
  37. package/src/integration-adapter-supabase-crm/tests/stage-map.test.ts +80 -0
  38. package/src/integration-adapter-supabase-crm/vitest.config.ts +8 -0
  39. package/src/integration-adapter-supabase-crm/worker.ts +223 -0
  40. package/src/observability/conventions.ts +93 -0
  41. package/src/observability/env-resolver.ts +57 -0
  42. package/src/observability/index.ts +57 -0
  43. package/src/observability/metric-registry.ts +184 -0
  44. package/src/observability/otlp-converter.ts +125 -0
  45. package/src/observability/otlp-json.ts +135 -0
  46. package/src/observability/otlp-transport.ts +68 -0
  47. package/src/observability/pusher.ts +142 -0
  48. package/src/observability/redact.ts +27 -0
  49. package/src/observability/tests/conventions.test.ts +100 -0
  50. package/src/observability/tests/metric-registry.test.ts +117 -0
  51. package/src/observability/tests/otlp-converter.pbt.test.ts +77 -0
  52. package/src/observability/tests/otlp-converter.test.ts +157 -0
  53. package/src/observability/tests/otlp-json.test.ts +120 -0
  54. package/src/observability/tests/pusher.test.ts +75 -0
  55. package/src/observability/tests/redact.pbt.test.ts +71 -0
  56. package/src/observability/tests/redact.test.ts +37 -0
  57. package/src/observability/typed-refs.ts +137 -0
  58. package/src/observability/vitest.config.ts +8 -0
  59. package/src/ontology/analytics/matomo-binding.yaml +172 -0
  60. package/src/ontology/analytics/matomo-fleet.registry.yaml +26 -0
  61. package/src/ontology/analytics/messkanon.yaml +151 -0
  62. package/src/ontology/archetype-registry.ts +61 -0
  63. package/src/ontology/archetypes/components/brand-label.yaml +19 -0
  64. package/src/ontology/archetypes/components/copyright.yaml +19 -0
  65. package/src/ontology/archetypes/components/currency-aware-price-display.yaml +19 -0
  66. package/src/ontology/archetypes/components/currency-selector.yaml +19 -0
  67. package/src/ontology/archetypes/components/footer-promo.yaml +19 -0
  68. package/src/ontology/archetypes/components/lang-switcher.yaml +19 -0
  69. package/src/ontology/archetypes/components/layout.yaml +19 -0
  70. package/src/ontology/archetypes/components/live-photo.yaml +20 -0
  71. package/src/ontology/archetypes/components/material-credit.yaml +20 -0
  72. package/src/ontology/archetypes/components/media.yaml +21 -0
  73. package/src/ontology/archetypes/components/nachweis-card.yaml +34 -0
  74. package/src/ontology/archetypes/components/nachweis-detail.yaml +43 -0
  75. package/src/ontology/archetypes/components/nachweis-list.yaml +37 -0
  76. package/src/ontology/archetypes/components/nachweis-verify.yaml +31 -0
  77. package/src/ontology/archetypes/components/not-found.yaml +22 -0
  78. package/src/ontology/archetypes/components/offer-capacity.yaml +21 -0
  79. package/src/ontology/archetypes/components/passport-header.yaml +19 -0
  80. package/src/ontology/archetypes/components/passport-provenance.yaml +20 -0
  81. package/src/ontology/archetypes/components/passport-score-grid.yaml +20 -0
  82. package/src/ontology/archetypes/components/passport-star-map.yaml +20 -0
  83. package/src/ontology/archetypes/components/person-profile.yaml +20 -0
  84. package/src/ontology/archetypes/components/pulsar.yaml +20 -0
  85. package/src/ontology/archetypes/components/responsive-image.yaml +19 -0
  86. package/src/ontology/archetypes/components/scroll-to-top.yaml +22 -0
  87. package/src/ontology/archetypes/components/social-meta.yaml +20 -0
  88. package/src/ontology/archetypes/components/structured-data.yaml +19 -0
  89. package/src/ontology/archetypes/index.json +1003 -0
  90. package/src/ontology/archetypes/index.yaml +821 -0
  91. package/src/ontology/archetypes/sections/approach.yaml +23 -0
  92. package/src/ontology/archetypes/sections/article-list.yaml +30 -0
  93. package/src/ontology/archetypes/sections/audience-cards.yaml +30 -0
  94. package/src/ontology/archetypes/sections/breadcrumbs.yaml +21 -0
  95. package/src/ontology/archetypes/sections/chat-widget.yaml +37 -0
  96. package/src/ontology/archetypes/sections/comparison-cards.yaml +31 -0
  97. package/src/ontology/archetypes/sections/controlled-responsibility-block.yaml +34 -0
  98. package/src/ontology/archetypes/sections/credits.yaml +29 -0
  99. package/src/ontology/archetypes/sections/donation-card.yaml +53 -0
  100. package/src/ontology/archetypes/sections/donation-use.yaml +23 -0
  101. package/src/ontology/archetypes/sections/dynamic-status-block.yaml +35 -0
  102. package/src/ontology/archetypes/sections/faq-list.yaml +26 -0
  103. package/src/ontology/archetypes/sections/final-cta.yaml +53 -0
  104. package/src/ontology/archetypes/sections/hero-decision-card.yaml +65 -0
  105. package/src/ontology/archetypes/sections/hero.yaml +50 -0
  106. package/src/ontology/archetypes/sections/impact.yaml +23 -0
  107. package/src/ontology/archetypes/sections/markdown.yaml +43 -0
  108. package/src/ontology/archetypes/sections/mountain-journey.yaml +43 -0
  109. package/src/ontology/archetypes/sections/navigation.yaml +20 -0
  110. package/src/ontology/archetypes/sections/notausgang-block.yaml +41 -0
  111. package/src/ontology/archetypes/sections/open-source-registry.yaml +26 -0
  112. package/src/ontology/archetypes/sections/ownership-block.yaml +57 -0
  113. package/src/ontology/archetypes/sections/people.yaml +48 -0
  114. package/src/ontology/archetypes/sections/price-card.yaml +50 -0
  115. package/src/ontology/archetypes/sections/problem.yaml +23 -0
  116. package/src/ontology/archetypes/sections/send-message.yaml +63 -0
  117. package/src/ontology/archetypes/sections/service-metadata-block.yaml +40 -0
  118. package/src/ontology/archetypes/sections/social-proof.yaml +27 -0
  119. package/src/ontology/archetypes/sections/transparency.yaml +32 -0
  120. package/src/ontology/archetypes/sections/trust-strip.yaml +42 -0
  121. package/src/ontology/archetypes/sections/video-section.yaml +27 -0
  122. package/src/ontology/archetypes/sections/women.yaml +29 -0
  123. package/src/ontology/biome-token-projection.ts +229 -0
  124. package/src/ontology/biomes/check-concrete-blueprint.yaml +118 -0
  125. package/src/ontology/biomes/handwerk-material-warm.yaml +135 -0
  126. package/src/ontology/biomes/nonprofit-trust.yaml +123 -0
  127. package/src/ontology/blueprints/offer.yaml +39 -0
  128. package/src/ontology/blueprints/prompts/local-market-signal.md +13 -0
  129. package/src/ontology/blueprints/prompts/website-local-narrative.md +24 -0
  130. package/src/ontology/blueprints/ratgeber.yaml +62 -0
  131. package/src/ontology/blueprints/website-local.yaml +309 -0
  132. package/src/ontology/blueprints/website-service.yaml +84 -0
  133. package/src/ontology/capabilities/lead.submit.yaml +44 -0
  134. package/src/ontology/cosmic/index.ts +48 -0
  135. package/src/ontology/cosmic/moon-catalog.ts +173 -0
  136. package/src/ontology/cosmic/planet-catalog.ts +82 -0
  137. package/src/ontology/cosmic/star-catalog.ts +337 -0
  138. package/src/ontology/enums.ts +159 -0
  139. package/src/ontology/external-surfaces/index.ts +158 -0
  140. package/src/ontology/external-surfaces/jsonld-types.yaml +135 -0
  141. package/src/ontology/external-surfaces/sitemap-shape.yaml +6 -0
  142. package/src/ontology/external-surfaces/url-schema.yaml +95 -0
  143. package/src/ontology/growth/events/contact-email-click.yaml +21 -0
  144. package/src/ontology/growth/events/contact-form-submit.yaml +25 -0
  145. package/src/ontology/growth/events/contact-phone-click.yaml +21 -0
  146. package/src/ontology/growth/events/contact-route-click.yaml +21 -0
  147. package/src/ontology/growth/events/contact-submit.yaml +28 -0
  148. package/src/ontology/growth/events/contact-whatsapp-click.yaml +21 -0
  149. package/src/ontology/growth/events/cta-click.yaml +30 -0
  150. package/src/ontology/growth/events/donation-confirmed.yaml +32 -0
  151. package/src/ontology/growth/events/donation-form-start.yaml +21 -0
  152. package/src/ontology/growth/events/donation-form-submit.yaml +29 -0
  153. package/src/ontology/growth/events/donation-intent.yaml +29 -0
  154. package/src/ontology/growth/events/form-error.yaml +27 -0
  155. package/src/ontology/growth/events/form-start.yaml +21 -0
  156. package/src/ontology/growth/events/form-submit.yaml +21 -0
  157. package/src/ontology/growth/events/outbound-click.yaml +25 -0
  158. package/src/ontology/growth/events/page-view.yaml +24 -0
  159. package/src/ontology/growth/events/passport-view.yaml +28 -0
  160. package/src/ontology/growth/events/star-map-navigate.yaml +30 -0
  161. package/src/ontology/index.ts +165 -0
  162. package/src/ontology/manifest.ts +344 -0
  163. package/src/ontology/operations/index.ts +1 -0
  164. package/src/ontology/schemas/biome.ts +303 -0
  165. package/src/ontology/schemas/capability.ts +104 -0
  166. package/src/ontology/schemas/constellation.ts +114 -0
  167. package/src/ontology/schemas/dns-records.ts +53 -0
  168. package/src/ontology/schemas/index.ts +95 -0
  169. package/src/ontology/schemas/manifest-resolver.ts +123 -0
  170. package/src/ontology/schemas/page-entry.ts +213 -0
  171. package/src/ontology/schemas/section-archetype.ts +109 -0
  172. package/src/ontology/schemas/site-family.ts +98 -0
  173. package/src/ontology/schemas/system/growth.ts +80 -0
  174. package/src/ontology/schemas/system/integrations.ts +143 -0
  175. package/src/ontology/schemas/system/manifest.ts +492 -0
  176. package/src/ontology/schemas/system/page-output.ts +99 -0
  177. package/src/ontology/schemas/system/release.ts +58 -0
  178. package/src/ontology/schemas/system/text.ts +58 -0
  179. package/src/ontology/schemas/system.ts +53 -0
  180. package/src/ontology/shared-section-props/body-fragments.ts +182 -0
  181. package/src/ontology/shared-section-props/catalog.ts +195 -0
  182. package/src/ontology/shared-section-props/common.ts +178 -0
  183. package/src/ontology/shared-section-props/index.ts +34 -0
  184. package/src/ontology/shared-section-props/visual-header.ts +163 -0
  185. package/src/ontology/site-families/charity-donation-trust/cultural-rules.yaml +6 -0
  186. package/src/ontology/site-families/charity-donation-trust/family.yaml +44 -0
  187. package/src/ontology/site-families/charity-donation-trust/linguistic-rules.yaml +6 -0
  188. package/src/ontology/site-families/check-quality-operator/cultural-rules.yaml +6 -0
  189. package/src/ontology/site-families/check-quality-operator/family.yaml +40 -0
  190. package/src/ontology/site-families/check-quality-operator/linguistic-rules.yaml +6 -0
  191. package/src/ontology/site-families/handwerk-trust-engineering/cultural-rules.yaml +6 -0
  192. package/src/ontology/site-families/handwerk-trust-engineering/family.yaml +53 -0
  193. package/src/ontology/site-families/handwerk-trust-engineering/linguistic-rules.yaml +6 -0
  194. package/src/ontology/tests/catalogs.pbt.test.ts +89 -0
  195. package/src/ontology/tests/enums.test.ts +203 -0
  196. package/src/ontology/tests/sternsystem-owner.test.ts +63 -0
  197. package/src/ontology/vitest.config.ts +8 -0
  198. package/src/passport/dht-sign.test.ts +99 -0
  199. package/src/passport/dht-sign.ts +73 -0
  200. package/src/passport/identity-sign.test.ts +134 -0
  201. package/src/passport/identity-sign.ts +85 -0
  202. package/src/passport/index.ts +53 -0
  203. package/src/passport/key-rotate.ts +133 -0
  204. package/src/passport/schema.ts +198 -0
  205. package/src/passport/sign.ts +225 -0
  206. package/src/passport/tests/schema.pbt.test.ts +93 -0
  207. package/src/passport/tests/schema.test.ts +222 -0
  208. package/src/passport/vitest.config.ts +8 -0
  209. package/src/share/agent/api-catalog.ts +77 -0
  210. package/src/share/agent/capability.ts +68 -0
  211. package/src/share/agent/dns-aid.test.ts +74 -0
  212. package/src/share/agent/dns-aid.ts +53 -0
  213. package/src/share/agent/fleet-catalog.ts +149 -0
  214. package/src/share/agent/index.ts +29 -0
  215. package/src/share/agent/knowledge.ts +96 -0
  216. package/src/share/agent/manifest.ts +126 -0
  217. package/src/share/agent/mcp-card.ts +63 -0
  218. package/src/share/agent/omit-empty.ts +77 -0
  219. package/src/share/agent/openapi.ts +157 -0
  220. package/src/share/agent/proof.ts +61 -0
  221. package/src/share/attribution-display.ts +86 -0
  222. package/src/share/check-hints.ts +38 -0
  223. package/src/share/content/entity-id.ts +65 -0
  224. package/src/share/content/index.ts +16 -0
  225. package/src/share/content/merge.ts +86 -0
  226. package/src/share/content/resolve-field-path.ts +39 -0
  227. package/src/share/content/substitute-deep.ts +47 -0
  228. package/src/share/content-discipline/index.ts +14 -0
  229. package/src/share/content-discipline/parsers.ts +66 -0
  230. package/src/share/content-discipline/types.ts +118 -0
  231. package/src/share/content-reference.ts +333 -0
  232. package/src/share/counter-utils.ts +75 -0
  233. package/src/share/css-value-normalize.ts +48 -0
  234. package/src/share/dev-props-validator.ts +131 -0
  235. package/src/share/entitlement.ts +172 -0
  236. package/src/share/env.d.ts +73 -0
  237. package/src/share/feature-policy.ts +367 -0
  238. package/src/share/formula-eval.ts +364 -0
  239. package/src/share/fs/index.ts +96 -0
  240. package/src/share/i18n/localization.ts +57 -0
  241. package/src/share/image-provider.ts +314 -0
  242. package/src/share/index.ts +86 -0
  243. package/src/share/knowledge/claim.ts +129 -0
  244. package/src/share/knowledge/derivation.ts +55 -0
  245. package/src/share/knowledge/freshness.ts +127 -0
  246. package/src/share/knowledge/index.ts +23 -0
  247. package/src/share/knowledge/ledger.ts +222 -0
  248. package/src/share/knowledge/plan.ts +122 -0
  249. package/src/share/knowledge/source.ts +170 -0
  250. package/src/share/legal/translation-policy.ts +133 -0
  251. package/src/share/material-credits.ts +181 -0
  252. package/src/share/middleware/language-redirect.ts +49 -0
  253. package/src/share/offer-capacity.ts +149 -0
  254. package/src/share/onboarding-yaml/index.ts +162 -0
  255. package/src/share/page.ts +351 -0
  256. package/src/share/redirects.test.ts +62 -0
  257. package/src/share/redirects.ts +54 -0
  258. package/src/share/rfc0042-utils.ts +95 -0
  259. package/src/share/runtime-context.ts +70 -0
  260. package/src/share/schemas/claim-records.ts +69 -0
  261. package/src/share/schemas/claims.ts +150 -0
  262. package/src/share/schemas/effects.ts +212 -0
  263. package/src/share/schemas/features.ts +147 -0
  264. package/src/share/schemas/horizontal-align.ts +22 -0
  265. package/src/share/schemas/icon-color.ts +27 -0
  266. package/src/share/schemas/index.ts +43 -0
  267. package/src/share/schemas/live-photo.ts +53 -0
  268. package/src/share/schemas/material-credit.ts +292 -0
  269. package/src/share/schemas/media.ts +220 -0
  270. package/src/share/schemas/navigation.ts +74 -0
  271. package/src/share/schemas/page-base.ts +33 -0
  272. package/src/share/schemas/participant.ts +247 -0
  273. package/src/share/schemas/person.ts +24 -0
  274. package/src/share/schemas/print.ts +93 -0
  275. package/src/share/schemas/section-background.ts +106 -0
  276. package/src/share/schemas/section-body.ts +138 -0
  277. package/src/share/schemas/section-cards.ts +37 -0
  278. package/src/share/schemas/section-cta.ts +74 -0
  279. package/src/share/schemas/section-header.ts +77 -0
  280. package/src/share/schemas/section-image.ts +77 -0
  281. package/src/share/schemas/section-motion.ts +52 -0
  282. package/src/share/schemas/section-shell.ts +63 -0
  283. package/src/share/schemas/section-stats.ts +37 -0
  284. package/src/share/schemas/site-background.ts +78 -0
  285. package/src/share/schemas/standard-list-item.ts +38 -0
  286. package/src/share/scripts/external-links.ts +45 -0
  287. package/src/share/scripts/gsap-counter.ts +168 -0
  288. package/src/share/scripts/gsap-mountain-journey.ts +236 -0
  289. package/src/share/scripts/gsap-parallax.ts +50 -0
  290. package/src/share/scripts/gsap-reveal.ts +65 -0
  291. package/src/share/scripts/gsap-stagger.ts +54 -0
  292. package/src/share/scripts/index.ts +25 -0
  293. package/src/share/scripts/inline-number-animation.ts +120 -0
  294. package/src/share/scripts/lenis.ts +109 -0
  295. package/src/share/scripts/live-photos.ts +250 -0
  296. package/src/share/scripts/lordicon.ts +94 -0
  297. package/src/share/scripts/orchestrator.ts +138 -0
  298. package/src/share/scripts/scheduler.ts +53 -0
  299. package/src/share/scripts/video-player.ts +116 -0
  300. package/src/share/semantic/ai.ts +140 -0
  301. package/src/share/semantic/block-extraction.ts +52 -0
  302. package/src/share/semantic/block-extractors/index.ts +502 -0
  303. package/src/share/semantic/breadcrumbs.ts +120 -0
  304. package/src/share/semantic/build-page.ts +309 -0
  305. package/src/share/semantic/business-projection.ts +278 -0
  306. package/src/share/semantic/derived-prices-loader.ts +36 -0
  307. package/src/share/semantic/extract.ts +118 -0
  308. package/src/share/semantic/feed.ts +124 -0
  309. package/src/share/semantic/ids.ts +76 -0
  310. package/src/share/semantic/image-sitemap.ts +153 -0
  311. package/src/share/semantic/index.ts +36 -0
  312. package/src/share/semantic/jsonld/article.ts +47 -0
  313. package/src/share/semantic/jsonld/breadcrumb.ts +47 -0
  314. package/src/share/semantic/jsonld/collection-list.ts +30 -0
  315. package/src/share/semantic/jsonld/context.ts +46 -0
  316. package/src/share/semantic/jsonld/faq.ts +49 -0
  317. package/src/share/semantic/jsonld/initiative.ts +53 -0
  318. package/src/share/semantic/jsonld/organization.ts +101 -0
  319. package/src/share/semantic/jsonld/person.ts +71 -0
  320. package/src/share/semantic/jsonld/service.ts +80 -0
  321. package/src/share/semantic/jsonld/shared.ts +31 -0
  322. package/src/share/semantic/jsonld/types.ts +18 -0
  323. package/src/share/semantic/jsonld/webpage.ts +98 -0
  324. package/src/share/semantic/jsonld/website.ts +27 -0
  325. package/src/share/semantic/jsonld.ts +75 -0
  326. package/src/share/semantic/llms-policy.ts +74 -0
  327. package/src/share/semantic/llms.ts +394 -0
  328. package/src/share/semantic/markdown-hygiene.ts +68 -0
  329. package/src/share/semantic/markdown-twin-provenance.ts +242 -0
  330. package/src/share/semantic/models.ts +402 -0
  331. package/src/share/semantic/organization-profile.ts +160 -0
  332. package/src/share/semantic/output-projection.ts +131 -0
  333. package/src/share/semantic/page-builders/markdown-page.ts +84 -0
  334. package/src/share/semantic/page-markdown.ts +234 -0
  335. package/src/share/semantic/page-utils.ts +125 -0
  336. package/src/share/semantic/price-marker-resolver.ts +92 -0
  337. package/src/share/semantic/robots.ts +127 -0
  338. package/src/share/semantic/update-stamp.ts +136 -0
  339. package/src/share/shared-context.ts +177 -0
  340. package/src/share/string-utils.ts +19 -0
  341. package/src/share/tests/api-catalog.test.ts +97 -0
  342. package/src/share/tests/breadcrumbs.test.ts +126 -0
  343. package/src/share/tests/build-page-price-markers.test.ts +160 -0
  344. package/src/share/tests/capability.test.ts +107 -0
  345. package/src/share/tests/claim.test.ts +90 -0
  346. package/src/share/tests/content-ref-index.test.ts +328 -0
  347. package/src/share/tests/create-dev-props-validator.test.ts +38 -0
  348. package/src/share/tests/derivation.test.ts +39 -0
  349. package/src/share/tests/dev-props-validator.test.ts +88 -0
  350. package/src/share/tests/fleet-catalog.test.ts +134 -0
  351. package/src/share/tests/formula-eval.test.ts +619 -0
  352. package/src/share/tests/freshness.test.ts +60 -0
  353. package/src/share/tests/hero-section-numbering-parity.test.ts +51 -0
  354. package/src/share/tests/image-sitemap.test.ts +101 -0
  355. package/src/share/tests/jsonld-service.test.ts +282 -0
  356. package/src/share/tests/knowledge.test.ts +86 -0
  357. package/src/share/tests/ledger.test.ts +125 -0
  358. package/src/share/tests/llms-0789.test.ts +63 -0
  359. package/src/share/tests/manifest.test.ts +100 -0
  360. package/src/share/tests/markdown-twin-provenance.test.ts +108 -0
  361. package/src/share/tests/mcp-card.test.ts +57 -0
  362. package/src/share/tests/onboarding-yaml.test.ts +111 -0
  363. package/src/share/tests/ontology-role-by-cosmic-name.test.ts +28 -0
  364. package/src/share/tests/openapi.test.ts +116 -0
  365. package/src/share/tests/organization-jsonld.test.ts +74 -0
  366. package/src/share/tests/output-projection.test.ts +61 -0
  367. package/src/share/tests/plan.test.ts +97 -0
  368. package/src/share/tests/price-marker-resolver.test.ts +141 -0
  369. package/src/share/tests/proof.test.ts +72 -0
  370. package/src/share/tests/resolve-field-path.test.ts +52 -0
  371. package/src/share/tests/source.test.ts +81 -0
  372. package/src/share/tests/substitute-deep.test.ts +65 -0
  373. package/src/share/tests/text-normalize.pbt.test.ts +143 -0
  374. package/src/share/tests/text-normalize.test.ts +285 -0
  375. package/src/share/tests/twin-path.test.ts +43 -0
  376. package/src/share/text-normalize.ts +548 -0
  377. package/src/share/text-position.ts +19 -0
  378. package/src/share/types/hls-js-light.d.ts +16 -0
  379. package/src/share/visibility.ts +167 -0
  380. package/src/share/vitest.config.ts +8 -0
  381. package/src/share/wrap-inline-numbers.ts +139 -0
  382. package/src/surface/blueprint-schema.ts +314 -0
  383. package/src/surface/blueprint-types.ts +256 -0
  384. package/src/surface/blueprint.ts +300 -0
  385. package/src/surface/breaker.ts +57 -0
  386. package/src/surface/decision-composer.ts +156 -0
  387. package/src/surface/eligibility.ts +372 -0
  388. package/src/surface/evidence-records.ts +97 -0
  389. package/src/surface/fleet.ts +60 -0
  390. package/src/surface/geo.ts +170 -0
  391. package/src/surface/governance.ts +146 -0
  392. package/src/surface/index.ts +216 -0
  393. package/src/surface/io/index.ts +18 -0
  394. package/src/surface/io/surface-module-context-io.ts +60 -0
  395. package/src/surface/io/visibility-outcomes-io.ts +46 -0
  396. package/src/surface/module-context.ts +120 -0
  397. package/src/surface/substance.ts +161 -0
  398. package/src/surface/tests/eligibility.pbt.test.ts +137 -0
  399. package/src/surface/tests/eligibility.test.ts +302 -0
  400. package/src/surface/types.ts +289 -0
  401. package/src/surface/visibility.ts +57 -0
  402. package/src/surface/vitest.config.ts +8 -0
  403. package/tsconfig.json +17 -0
@@ -0,0 +1,482 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>RFC-0186: CrmSyncTarget port + PipedriveSyncTarget adapter. The port
4
+ abstracts the CRM destination (Pipedrive today, other vendors tomorrow) so the
5
+ sync worker is a thin orchestrator: read outbox → route to sync target by vendor.
6
+ All Pipedrive-specific logic (API calls, stage mapping, credential handling) lives
7
+ here. The adapter reads buffer rows via CrmBufferReader and writes back destination
8
+ IDs through the same port — no raw fetch leaks into the worker.</purpose>
9
+ <non-goals>
10
+ <item>Do not import astro:env — destination credentials are injected.</item>
11
+ <item>Do not call the buffer directly — use the injected CrmBufferReader.</item>
12
+ </non-goals>
13
+ </MODULE_CONTRACT>
14
+ <CHANGE_SUMMARY>
15
+ <item>Extracted from worker.ts: Pipedrive sync logic + stage map behind CrmSyncTarget port.</item>
16
+ <item>RFC-0386: Added syncSubscription / syncInvoice for P3/P4 lifecycle sync.</item>
17
+ </CHANGE_SUMMARY>
18
+ */
19
+
20
+ import type {
21
+ CrmBufferReader,
22
+ SyncOutboxRow,
23
+ BufferDealStage,
24
+ BufferSubscription,
25
+ } from "@warpgogol/werkstatt-shared/integration/crm-buffer";
26
+ import { BUFFER_DEAL_STAGES } from "@warpgogol/werkstatt-shared/integration/crm-buffer";
27
+ import type { SubscriptionStatus } from "@warpgogol/werkstatt-shared/integration";
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // CrmSyncTarget — vendor-agnostic sync port
31
+ // ---------------------------------------------------------------------------
32
+
33
+ /** Destination credentials for a CRM sync target. */
34
+ export interface SyncTargetCredentials {
35
+ destination_token: string;
36
+ destination_domain: string;
37
+ /** RFC-0386: P3 pipeline stage ids for lifecycle deal moves. */
38
+ p3StageMap?: P3StageMap;
39
+ /** RFC-0386: P4 pipeline stage ids for change-deal moves. */
40
+ p4StageMap?: P4StageMap;
41
+ }
42
+
43
+ /**
44
+ * Vendor-agnostic port for syncing buffer entities to a CRM destination.
45
+ * The sync worker routes each outbox op to the appropriate method.
46
+ * The seam is justified by PipedriveSyncTarget (production) and future
47
+ * test fakes that can be injected without touching the worker.
48
+ */
49
+ export interface CrmSyncTarget {
50
+ syncContact(task: SyncOutboxRow, buffer: CrmBufferReader): Promise<void>;
51
+ syncOrganization(task: SyncOutboxRow, buffer: CrmBufferReader): Promise<void>;
52
+ syncDeal(task: SyncOutboxRow, buffer: CrmBufferReader): Promise<void>;
53
+ syncDealStage(task: SyncOutboxRow, buffer: CrmBufferReader): Promise<void>;
54
+ // RFC-0386: lifecycle sync (delta 5)
55
+ syncSubscription(task: SyncOutboxRow, buffer: CrmBufferReader): Promise<void>;
56
+ syncInvoice(task: SyncOutboxRow, buffer: CrmBufferReader): Promise<void>;
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Pipedrive stage mapping
61
+ // ---------------------------------------------------------------------------
62
+
63
+ /**
64
+ * Maps every BufferDealStage to a Pipedrive pipeline stage_id, or null when
65
+ * Pipedrive expects a `status` field instead of a stage_id (won / lost).
66
+ * Typed as Record<BufferDealStage, …> so a BUFFER_DEAL_STAGES catalog change
67
+ * causes a compile error here rather than a silent fallback.
68
+ */
69
+ export const STAGE_MAP: Record<BufferDealStage, number | null> = {
70
+ new: 1,
71
+ contacted: 2,
72
+ qualified: 3,
73
+ proposal: 4,
74
+ negotiation: 5,
75
+ won: null,
76
+ lost: null,
77
+ };
78
+
79
+ // Exhaustiveness guard: fails to compile if BUFFER_DEAL_STAGES grows without updating STAGE_MAP.
80
+ const _stageMapExhaustive: readonly BufferDealStage[] = BUFFER_DEAL_STAGES;
81
+ void _stageMapExhaustive;
82
+
83
+ /**
84
+ * Returns the Pipedrive deal update payload fragment for the given stage:
85
+ * - Active stages → `{ stage_id: <number> }`
86
+ * - won / lost → `{ status: "won" | "lost" }` (Pipedrive closes the deal)
87
+ */
88
+ export function resolvePipedriveStageUpdate(
89
+ stage: string,
90
+ ): { stage_id: number } | { status: "won" | "lost" } {
91
+ if (stage === "won") return { status: "won" };
92
+ if (stage === "lost") return { status: "lost" };
93
+ const id = STAGE_MAP[stage as BufferDealStage];
94
+ if (id === undefined) {
95
+ console.warn(`[pipedrive] unknown stage "${stage}", falling back to stage_id 1 (new)`);
96
+ return { stage_id: 1 };
97
+ }
98
+ if (id === null) {
99
+ // won/lost are handled above; null for any other stage is a STAGE_MAP misconfiguration
100
+ console.warn(`[pipedrive] stage "${stage}" maps to null in STAGE_MAP`);
101
+ return { stage_id: 1 };
102
+ }
103
+ return { stage_id: id };
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // PipedriveSyncTarget — production adapter
108
+ // ---------------------------------------------------------------------------
109
+
110
+ type FetchImpl = typeof fetch;
111
+
112
+ /**
113
+ * RFC-0386: P3 (Subscription & Lifecycle) stage map — maps subscription status to
114
+ * a Pipedrive P3 pipeline stage_id. Tenant-specific; injected via credentials.
115
+ */
116
+ export type P3StageMap = Partial<Record<SubscriptionStatus, number>>;
117
+
118
+ /**
119
+ * RFC-0386: P4 (Change & Support) stage map — maps change-deal states to a
120
+ * Pipedrive P4 pipeline stage_id. Tenant-specific; injected via credentials.
121
+ */
122
+ export interface P4StageMap {
123
+ change_requested?: number;
124
+ payment_pending?: number;
125
+ done?: number;
126
+ }
127
+
128
+ /** Pipedrive API credentials. */
129
+ export interface PipedriveCredentials {
130
+ token: string;
131
+ domain: string;
132
+ /** RFC-0386: P3 pipeline stage ids for lifecycle deal moves. */
133
+ p3StageMap?: P3StageMap;
134
+ /** RFC-0386: P4 pipeline stage ids for change-deal moves. */
135
+ p4StageMap?: P4StageMap;
136
+ }
137
+
138
+ /**
139
+ * Pipedrive CRM sync target. Reads buffer rows via CrmBufferReader, syncs
140
+ * to Pipedrive via REST API, and writes back Pipedrive IDs through the buffer.
141
+ */
142
+ export class PipedriveSyncTarget implements CrmSyncTarget {
143
+ private readonly creds: PipedriveCredentials;
144
+ private readonly fetchImpl: FetchImpl;
145
+ private readonly p3StageMap: P3StageMap;
146
+ private readonly p4StageMap: P4StageMap;
147
+
148
+ constructor(creds: PipedriveCredentials, fetchImpl: FetchImpl = fetch) {
149
+ if (!creds.token || !creds.domain) {
150
+ throw new Error("missing Pipedrive credentials");
151
+ }
152
+ this.creds = creds;
153
+ this.fetchImpl = fetchImpl;
154
+ this.p3StageMap = creds.p3StageMap ?? {};
155
+ this.p4StageMap = creds.p4StageMap ?? {};
156
+ }
157
+
158
+ async syncContact(task: SyncOutboxRow, buffer: CrmBufferReader): Promise<void> {
159
+ const contactId = task.payload.contact_id;
160
+ if (typeof contactId !== "string") throw new Error("upsert_contact: missing contact_id");
161
+
162
+ const contact = await buffer.getContact(task.tenant_id, contactId);
163
+ if (!contact) throw new Error(`upsert_contact: contact ${contactId} not found`);
164
+
165
+ const body = {
166
+ name: contact.name ?? contact.email ?? "Unknown",
167
+ ...(contact.email ? { email: [{ value: contact.email, primary: true }] } : {}),
168
+ ...(contact.phone ? { phone: [{ value: contact.phone, primary: true }] } : {}),
169
+ };
170
+
171
+ if (contact.pipedrive_person_id) {
172
+ await this.request(
173
+ `https://${this.creds.domain}.pipedrive.com/api/v1/persons/${contact.pipedrive_person_id}`,
174
+ "PUT",
175
+ body,
176
+ );
177
+ } else {
178
+ const res = await this.request(
179
+ `https://${this.creds.domain}.pipedrive.com/api/v1/persons`,
180
+ "POST",
181
+ body,
182
+ );
183
+ const personId = (res as { data?: { id?: number } }).data?.id;
184
+ if (personId) {
185
+ await buffer.patchContactPipedriveId(task.tenant_id, contactId, personId);
186
+ }
187
+ }
188
+ }
189
+
190
+ async syncOrganization(task: SyncOutboxRow, buffer: CrmBufferReader): Promise<void> {
191
+ const orgId = task.payload.organization_id;
192
+ if (typeof orgId !== "string") throw new Error("upsert_organization: missing organization_id");
193
+
194
+ const org = await buffer.getOrganization(task.tenant_id, orgId);
195
+ if (!org) throw new Error(`upsert_organization: organization ${orgId} not found`);
196
+
197
+ if (org.pipedrive_org_id) {
198
+ await this.request(
199
+ `https://${this.creds.domain}.pipedrive.com/api/v1/organizations/${org.pipedrive_org_id}`,
200
+ "PUT",
201
+ { name: org.name },
202
+ );
203
+ } else {
204
+ const res = await this.request(
205
+ `https://${this.creds.domain}.pipedrive.com/api/v1/organizations`,
206
+ "POST",
207
+ { name: org.name },
208
+ );
209
+ const pdOrgId = (res as { data?: { id?: number } }).data?.id;
210
+ if (pdOrgId) {
211
+ await buffer.patchOrganizationPipedriveId(task.tenant_id, orgId, pdOrgId);
212
+ }
213
+ }
214
+ }
215
+
216
+ async syncDeal(task: SyncOutboxRow, buffer: CrmBufferReader): Promise<void> {
217
+ const dealId = task.payload.deal_id;
218
+ if (typeof dealId !== "string") throw new Error("upsert_deal: missing deal_id");
219
+
220
+ const deal = await buffer.getDeal(task.tenant_id, dealId);
221
+ if (!deal) throw new Error(`upsert_deal: deal ${dealId} not found`);
222
+
223
+ const contact = deal.contact_id
224
+ ? await buffer.getContact(task.tenant_id, deal.contact_id)
225
+ : null;
226
+ const personId = contact?.pipedrive_person_id;
227
+
228
+ // RFC-0190: resolve the linked Organization's Pipedrive id so the deal attaches to it.
229
+ let orgId: number | undefined;
230
+ if (deal.organization_id) {
231
+ const org = await buffer.getOrganization(task.tenant_id, deal.organization_id);
232
+ orgId = org?.pipedrive_org_id;
233
+ }
234
+
235
+ if (deal.pipedrive_deal_id) {
236
+ await this.request(
237
+ `https://${this.creds.domain}.pipedrive.com/api/v1/deals/${deal.pipedrive_deal_id}`,
238
+ "PUT",
239
+ {
240
+ title: deal.title,
241
+ ...(personId ? { person_id: personId } : {}),
242
+ ...(orgId ? { org_id: orgId } : {}),
243
+ ...(deal.value !== undefined ? { value: deal.value } : {}),
244
+ ...resolvePipedriveStageUpdate(deal.stage),
245
+ },
246
+ );
247
+ } else if (personId) {
248
+ const res = await this.request(
249
+ `https://${this.creds.domain}.pipedrive.com/api/v1/deals`,
250
+ "POST",
251
+ {
252
+ title: deal.title,
253
+ person_id: personId,
254
+ ...(orgId ? { org_id: orgId } : {}),
255
+ ...(deal.value !== undefined ? { value: deal.value } : {}),
256
+ },
257
+ );
258
+ const pdDealId = (res as { data?: { id?: number } }).data?.id;
259
+ if (pdDealId) {
260
+ await buffer.patchDealPipedriveIds(task.tenant_id, dealId, {
261
+ pipedrive_deal_id: pdDealId,
262
+ });
263
+ }
264
+ } else {
265
+ const res = await this.request(
266
+ `https://${this.creds.domain}.pipedrive.com/api/v1/leads`,
267
+ "POST",
268
+ { title: deal.title },
269
+ );
270
+ const pdLeadId = (res as { data?: { id?: string } }).data?.id;
271
+ if (pdLeadId) {
272
+ await buffer.patchDealPipedriveIds(task.tenant_id, dealId, {
273
+ pipedrive_lead_id: pdLeadId,
274
+ });
275
+ }
276
+ }
277
+ }
278
+
279
+ async syncDealStage(task: SyncOutboxRow, buffer: CrmBufferReader): Promise<void> {
280
+ const dealId = task.payload.deal_id;
281
+ const toStage = task.payload.to_stage;
282
+ if (typeof dealId !== "string") throw new Error("update_deal_stage: missing deal_id");
283
+ if (typeof toStage !== "string") throw new Error("update_deal_stage: missing to_stage");
284
+
285
+ const deal = await buffer.getDeal(task.tenant_id, dealId);
286
+ if (!deal?.pipedrive_deal_id) return;
287
+
288
+ await this.request(
289
+ `https://${this.creds.domain}.pipedrive.com/api/v1/deals/${deal.pipedrive_deal_id}`,
290
+ "PUT",
291
+ resolvePipedriveStageUpdate(toStage) as Record<string, unknown>,
292
+ );
293
+ }
294
+
295
+ // --- RFC-0386: Lifecycle sync (delta 5) ---
296
+
297
+ /**
298
+ * Map a subscription status to a P3 pipeline stage_id using the tenant's P3 stage map.
299
+ * Returns undefined when no mapping is configured for the status (the deal stays put).
300
+ */
301
+ private resolveP3StageId(status: SubscriptionStatus): number | undefined {
302
+ return this.p3StageMap[status];
303
+ }
304
+
305
+ /**
306
+ * Resolve or create the P3 (Subscription & Lifecycle) deal linked to the
307
+ * Organization. The buffer subscription row carries `pipedrive_deal_id` once
308
+ * the P3 deal is created; subsequent syncs move the deal's stage.
309
+ */
310
+ async syncSubscription(task: SyncOutboxRow, buffer: CrmBufferReader): Promise<void> {
311
+ const subscriptionId = task.payload.subscription_id;
312
+ if (typeof subscriptionId !== "string") {
313
+ throw new Error("upsert_subscription: missing subscription_id");
314
+ }
315
+
316
+ const sub = await buffer.getSubscription(task.tenant_id, subscriptionId);
317
+ if (!sub) throw new Error(`upsert_subscription: subscription ${subscriptionId} not found`);
318
+
319
+ const org = sub.organization_id
320
+ ? await buffer.getOrganization(task.tenant_id, sub.organization_id)
321
+ : null;
322
+ const orgId = org?.pipedrive_org_id;
323
+
324
+ const title = `P3: ${sub.plan} (${sub.stripe_subscription_id.slice(0, 12)})`;
325
+ const stageId = this.resolveP3StageId(sub.status);
326
+
327
+ if (sub.pipedrive_deal_id) {
328
+ // Move the existing P3 deal's stage.
329
+ const body: Record<string, unknown> = { title };
330
+ if (orgId) body.org_id = orgId;
331
+ if (stageId !== undefined) body.stage_id = stageId;
332
+ await this.request(
333
+ `https://${this.creds.domain}.pipedrive.com/api/v1/deals/${sub.pipedrive_deal_id}`,
334
+ "PUT",
335
+ body,
336
+ );
337
+ } else {
338
+ // Create the P3 deal.
339
+ const body: Record<string, unknown> = { title };
340
+ if (orgId) body.org_id = orgId;
341
+ if (stageId !== undefined) body.stage_id = stageId;
342
+ const res = await this.request(
343
+ `https://${this.creds.domain}.pipedrive.com/api/v1/deals`,
344
+ "POST",
345
+ body,
346
+ );
347
+ const pdDealId = (res as { data?: { id?: number } }).data?.id;
348
+ if (pdDealId) {
349
+ await buffer.patchSubscriptionPipedriveDealId(task.tenant_id, subscriptionId, pdDealId);
350
+ }
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Record an invoice against the P3/P4 deal. On a paid cycle invoice, reset
356
+ * the subscription's `included_changes_balance` to `included_changes_per_cycle`.
357
+ * A change invoice opens or moves a P4 change deal.
358
+ */
359
+ async syncInvoice(task: SyncOutboxRow, buffer: CrmBufferReader): Promise<void> {
360
+ const invoiceId = task.payload.invoice_id;
361
+ if (typeof invoiceId !== "string") {
362
+ throw new Error("upsert_invoice: missing invoice_id");
363
+ }
364
+
365
+ const inv = await buffer.getInvoice(task.tenant_id, invoiceId);
366
+ if (!inv) throw new Error(`upsert_invoice: invoice ${invoiceId} not found`);
367
+
368
+ // Resolve the linked subscription and its P3 deal for balance reset.
369
+ let sub: BufferSubscription | null = null;
370
+ if (inv.subscription_id) {
371
+ sub = await buffer.getSubscription(task.tenant_id, inv.subscription_id);
372
+ }
373
+
374
+ // On a paid cycle invoice, reset the included-changes balance to the per-cycle allowance.
375
+ if (inv.kind === "cycle" && inv.status === "paid" && sub) {
376
+ const perCycle = sub.included_changes_per_cycle;
377
+ const current = sub.included_changes_balance;
378
+ const delta = perCycle - current;
379
+ if (delta !== 0) {
380
+ await buffer.adjustChangeBalance(task.tenant_id, sub.id, delta);
381
+ }
382
+ }
383
+
384
+ // Record the invoice as a note on the P3 deal (if linked).
385
+ if (sub?.pipedrive_deal_id) {
386
+ const noteBody = {
387
+ content: `Invoice ${inv.stripe_invoice_id}: ${inv.kind} ${inv.amount_cents / 100} ${inv.currency} (${inv.status})`,
388
+ deal_id: sub.pipedrive_deal_id,
389
+ };
390
+ await this.request(
391
+ `https://${this.creds.domain}.pipedrive.com/api/v1/notes`,
392
+ "POST",
393
+ noteBody,
394
+ );
395
+ }
396
+
397
+ // A change invoice opens or moves a P4 change deal.
398
+ if (inv.kind === "change" && sub?.pipedrive_deal_id) {
399
+ const p4StageId = this.p4StageMap.change_requested;
400
+ const p4Body: Record<string, unknown> = {
401
+ title: `P4: Change (${inv.stripe_invoice_id.slice(0, 12)})`,
402
+ org_id: (await buffer.getOrganization(task.tenant_id, sub.organization_id))
403
+ ?.pipedrive_org_id,
404
+ };
405
+ if (p4StageId !== undefined) p4Body.stage_id = p4StageId;
406
+ await this.request(`https://${this.creds.domain}.pipedrive.com/api/v1/deals`, "POST", p4Body);
407
+ }
408
+ }
409
+
410
+ // --- Internal ---
411
+
412
+ private async request(
413
+ url: string,
414
+ method: "GET" | "POST" | "PUT",
415
+ body?: Record<string, unknown>,
416
+ ): Promise<unknown> {
417
+ const MAX_RETRIES = 3;
418
+ const BASE_DELAY_MS = 500;
419
+
420
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
421
+ const res = await this.fetchImpl(url, {
422
+ method,
423
+ headers: {
424
+ "Content-Type": "application/json",
425
+ Authorization: `Bearer ${this.creds.token}`,
426
+ },
427
+ ...(body ? { body: JSON.stringify(body) } : {}),
428
+ });
429
+
430
+ if (res.ok) return res.json();
431
+
432
+ // Retry on 429 (rate limit) and 5xx (server errors) with exponential backoff
433
+ const retryable = res.status === 429 || (res.status >= 500 && res.status < 600);
434
+ if (retryable && attempt < MAX_RETRIES) {
435
+ const delay = BASE_DELAY_MS * Math.pow(2, attempt);
436
+ console.warn(
437
+ `[pipedrive] ${res.status} on ${method} ${url}, retrying in ${delay}ms (attempt ${attempt + 1}/${MAX_RETRIES})`,
438
+ );
439
+ await new Promise((r) => setTimeout(r, delay));
440
+ continue;
441
+ }
442
+
443
+ const text = await res.text();
444
+ throw new Error(`Pipedrive ${method} ${url}: ${res.status} ${text}`);
445
+ }
446
+
447
+ // Unreachable — loop always returns or throws
448
+ throw new Error(`Pipedrive ${method} ${url}: exhausted retries`);
449
+ }
450
+ }
451
+
452
+ // ---------------------------------------------------------------------------
453
+ // Factory: create a CrmSyncTarget by vendor name
454
+ // ---------------------------------------------------------------------------
455
+
456
+ /**
457
+ * Create a CrmSyncTarget for the given vendor. Throws for unsupported vendors.
458
+ * The sync worker calls this once per tenant to route outbox tasks.
459
+ *
460
+ * When a second vendor is added, extend this function with an additional
461
+ * branch. A registry pattern is premature while there is only one vendor —
462
+ * a simple function is easier to read, test, and type-check. The switch
463
+ * will naturally emerge when there are 2+ vendors.
464
+ */
465
+ export function createSyncTarget(
466
+ vendor: string,
467
+ creds: SyncTargetCredentials,
468
+ fetchImpl: FetchImpl = fetch,
469
+ ): CrmSyncTarget {
470
+ if (vendor === "pipedrive") {
471
+ return new PipedriveSyncTarget(
472
+ {
473
+ token: creds.destination_token,
474
+ domain: creds.destination_domain,
475
+ p3StageMap: creds.p3StageMap,
476
+ p4StageMap: creds.p4StageMap,
477
+ },
478
+ fetchImpl,
479
+ );
480
+ }
481
+ throw new Error(`unsupported vendor: ${vendor}`);
482
+ }
@@ -0,0 +1,89 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Low-level Supabase REST transport for the CRM buffer client. Keeps
4
+ PostgREST and RPC request construction in one place while preserving caller-injected
5
+ secrets and tenant-scoped RLS headers.</purpose>
6
+ <non-goals>
7
+ <item>Do not import the Supabase SDK; transport stays raw fetch.</item>
8
+ <item>Do not encode CRM buffer table semantics; callers own payload shape and casts.</item>
9
+ </non-goals>
10
+ </MODULE_CONTRACT>
11
+ <CHANGE_SUMMARY>
12
+ <item>Extracted from client.ts as the shared transport layer for Supabase CRM buffer operations.</item>
13
+ </CHANGE_SUMMARY>
14
+ */
15
+
16
+ /** Minimal config for the Supabase REST client (secrets injected by the caller). */
17
+ export interface SupabaseClientConfig {
18
+ /** Supabase project URL, e.g. https://xyz.supabase.co */
19
+ url: string;
20
+ /** Service role key (server-only; never sent to the client). */
21
+ serviceKey: string;
22
+ /** The tenant UUID to scope all queries with RLS. */
23
+ tenantId: string;
24
+ }
25
+
26
+ export type FetchImpl = typeof fetch;
27
+
28
+ /** Execute a raw SQL statement via the Supabase RPC /rest/v1/rpc endpoint. */
29
+ export async function rpc(
30
+ config: SupabaseClientConfig,
31
+ fn: string,
32
+ params: Record<string, unknown>,
33
+ fetchImpl: FetchImpl,
34
+ ): Promise<unknown> {
35
+ const res = await fetchImpl(`${config.url}/rest/v1/rpc/${fn}`, {
36
+ method: "POST",
37
+ headers: {
38
+ apikey: config.serviceKey,
39
+ authorization: `Bearer ${config.serviceKey}`,
40
+ "content-type": "application/json",
41
+ },
42
+ body: JSON.stringify(params),
43
+ });
44
+ if (!res.ok) {
45
+ const err = await res.text().catch(() => res.statusText);
46
+ throw new Error(`supabase rpc ${fn}: ${res.status} ${err}`);
47
+ }
48
+ return res.json();
49
+ }
50
+
51
+ /** Generic PostgREST table request (GET / POST / PATCH). */
52
+ export async function rest(
53
+ config: SupabaseClientConfig,
54
+ method: "GET" | "POST" | "PATCH",
55
+ table: string,
56
+ opts: {
57
+ body?: unknown;
58
+ params?: Record<string, string>;
59
+ prefer?: string;
60
+ },
61
+ fetchImpl: FetchImpl,
62
+ ): Promise<unknown> {
63
+ const url = new URL(`${config.url}/rest/v1/${table}`);
64
+ if (opts.params) {
65
+ for (const [k, v] of Object.entries(opts.params)) {
66
+ url.searchParams.set(k, v);
67
+ }
68
+ }
69
+ // Inject tenant RLS via x-set-config header (Supabase supports per-request set_config).
70
+ const headers: Record<string, string> = {
71
+ apikey: config.serviceKey,
72
+ authorization: `Bearer ${config.serviceKey}`,
73
+ "x-set-config": `app.current_tenant=${config.tenantId}`,
74
+ };
75
+ if (opts.prefer) headers["prefer"] = opts.prefer;
76
+ if (opts.body !== undefined) headers["content-type"] = "application/json";
77
+
78
+ const res = await fetchImpl(url.toString(), {
79
+ method,
80
+ headers,
81
+ body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
82
+ });
83
+ if (!res.ok) {
84
+ const err = await res.text().catch(() => res.statusText);
85
+ throw new Error(`supabase ${method} /${table}: ${res.status} ${err}`);
86
+ }
87
+ const text = await res.text();
88
+ return text ? JSON.parse(text) : null;
89
+ }