@agent-native/core 0.123.0 → 0.123.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +16 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/templates-meta.ts +12 -0
  5. package/corpus/templates/crm/.agents/skills/agent-native-docs/SKILL.md +115 -0
  6. package/corpus/templates/crm/.agents/skills/agent-native-toolkit/SKILL.md +177 -0
  7. package/corpus/templates/crm/.agents/skills/crm/SKILL.md +205 -0
  8. package/corpus/templates/crm/.agents/skills/customizing-agent-native/SKILL.md +220 -0
  9. package/corpus/templates/crm/.agents/skills/feature-flags/SKILL.md +169 -0
  10. package/corpus/templates/crm/.agents/skills/sharing/SKILL.md +228 -0
  11. package/corpus/templates/crm/.agents/skills/storing-data/SKILL.md +178 -0
  12. package/corpus/templates/crm/.agents/skills/upgrade-agent-native/SKILL.md +122 -0
  13. package/corpus/templates/crm/.env.example +5 -0
  14. package/corpus/templates/crm/AGENTS.md +82 -0
  15. package/corpus/templates/crm/CHANGELOG.md +25 -0
  16. package/corpus/templates/crm/DEVELOPING.md +66 -0
  17. package/corpus/templates/crm/README.md +122 -0
  18. package/corpus/templates/crm/_gitignore +12 -0
  19. package/corpus/templates/crm/actions/_crm-action-utils.ts +98 -0
  20. package/corpus/templates/crm/actions/_crm-attribute-utils.ts +219 -0
  21. package/corpus/templates/crm/actions/_crm-dashboard.ts +69 -0
  22. package/corpus/templates/crm/actions/_crm-data-program-actions.ts +15 -0
  23. package/corpus/templates/crm/actions/_crm-list-utils.ts +775 -0
  24. package/corpus/templates/crm/actions/_crm-signal-utils.ts +79 -0
  25. package/corpus/templates/crm/actions/add-crm-record-to-list.ts +164 -0
  26. package/corpus/templates/crm/actions/apply-crm-proposals.ts +291 -0
  27. package/corpus/templates/crm/actions/archive-crm-attribute.ts +69 -0
  28. package/corpus/templates/crm/actions/attach-call-evidence.ts +170 -0
  29. package/corpus/templates/crm/actions/configure-crm-connection.ts +126 -0
  30. package/corpus/templates/crm/actions/configure-native-crm.ts +44 -0
  31. package/corpus/templates/crm/actions/create-crm-attribute.ts +350 -0
  32. package/corpus/templates/crm/actions/create-crm-list.ts +194 -0
  33. package/corpus/templates/crm/actions/create-crm-record.ts +148 -0
  34. package/corpus/templates/crm/actions/create-crm-signal-tracker.ts +58 -0
  35. package/corpus/templates/crm/actions/delete-crm-saved-view.ts +68 -0
  36. package/corpus/templates/crm/actions/delete-staged-dataset.ts +14 -0
  37. package/corpus/templates/crm/actions/estimate-crm-enrichment.ts +229 -0
  38. package/corpus/templates/crm/actions/find-crm-duplicates.ts +87 -0
  39. package/corpus/templates/crm/actions/get-crm-automation-recipe.ts +36 -0
  40. package/corpus/templates/crm/actions/get-crm-dashboard-panel.ts +35 -0
  41. package/corpus/templates/crm/actions/get-crm-dashboard.ts +14 -0
  42. package/corpus/templates/crm/actions/get-crm-overview.ts +13 -0
  43. package/corpus/templates/crm/actions/get-crm-pipeline-data.ts +61 -0
  44. package/corpus/templates/crm/actions/get-crm-record-page.ts +287 -0
  45. package/corpus/templates/crm/actions/get-crm-record.ts +75 -0
  46. package/corpus/templates/crm/actions/get-crm-workspace.ts +364 -0
  47. package/corpus/templates/crm/actions/install-crm-pipeline-dashboard.ts +81 -0
  48. package/corpus/templates/crm/actions/list-crm-attributes.ts +71 -0
  49. package/corpus/templates/crm/actions/list-crm-connections.ts +68 -0
  50. package/corpus/templates/crm/actions/list-crm-dashboard-revisions.ts +15 -0
  51. package/corpus/templates/crm/actions/list-crm-dashboards.ts +15 -0
  52. package/corpus/templates/crm/actions/list-crm-enrichment-slots.ts +28 -0
  53. package/corpus/templates/crm/actions/list-crm-list-entries.ts +192 -0
  54. package/corpus/templates/crm/actions/list-crm-lists.ts +80 -0
  55. package/corpus/templates/crm/actions/list-crm-proposals.ts +55 -0
  56. package/corpus/templates/crm/actions/list-crm-record-field-history.ts +201 -0
  57. package/corpus/templates/crm/actions/list-crm-record-values.ts +200 -0
  58. package/corpus/templates/crm/actions/list-crm-records.ts +103 -0
  59. package/corpus/templates/crm/actions/list-crm-saved-views.ts +66 -0
  60. package/corpus/templates/crm/actions/list-crm-signal-hits.ts +43 -0
  61. package/corpus/templates/crm/actions/list-crm-signal-trackers.ts +32 -0
  62. package/corpus/templates/crm/actions/list-crm-tasks.ts +22 -0
  63. package/corpus/templates/crm/actions/list-staged-datasets.ts +14 -0
  64. package/corpus/templates/crm/actions/list-workspace-connections.ts +47 -0
  65. package/corpus/templates/crm/actions/manage-crm-attribute-option.ts +272 -0
  66. package/corpus/templates/crm/actions/manage-crm-signal-tracker.ts +87 -0
  67. package/corpus/templates/crm/actions/manage-crm-task.ts +132 -0
  68. package/corpus/templates/crm/actions/merge-crm-records.ts +691 -0
  69. package/corpus/templates/crm/actions/navigate.ts +54 -0
  70. package/corpus/templates/crm/actions/provider-api-catalog.ts +22 -0
  71. package/corpus/templates/crm/actions/provider-api-docs.ts +54 -0
  72. package/corpus/templates/crm/actions/provider-api-request.ts +92 -0
  73. package/corpus/templates/crm/actions/query-staged-dataset.ts +44 -0
  74. package/corpus/templates/crm/actions/record-crm-call-insight.ts +160 -0
  75. package/corpus/templates/crm/actions/record-crm-smart-signal.ts +150 -0
  76. package/corpus/templates/crm/actions/remove-crm-list-entry.ts +79 -0
  77. package/corpus/templates/crm/actions/restore-crm-dashboard-revision.ts +23 -0
  78. package/corpus/templates/crm/actions/review-crm-signal.ts +31 -0
  79. package/corpus/templates/crm/actions/run-crm-attribute-fill.ts +443 -0
  80. package/corpus/templates/crm/actions/run-crm-enrichment.ts +647 -0
  81. package/corpus/templates/crm/actions/run-crm-saved-view-program.ts +103 -0
  82. package/corpus/templates/crm/actions/run-crm-signal-trackers.ts +265 -0
  83. package/corpus/templates/crm/actions/run.ts +7 -0
  84. package/corpus/templates/crm/actions/save-crm-dashboard.ts +51 -0
  85. package/corpus/templates/crm/actions/save-crm-saved-view.ts +296 -0
  86. package/corpus/templates/crm/actions/sync-crm.ts +146 -0
  87. package/corpus/templates/crm/actions/update-crm-attribute.ts +111 -0
  88. package/corpus/templates/crm/actions/update-crm-list-entry.ts +102 -0
  89. package/corpus/templates/crm/actions/update-crm-list.ts +97 -0
  90. package/corpus/templates/crm/actions/update-crm-record.ts +543 -0
  91. package/corpus/templates/crm/actions/view-screen.ts +324 -0
  92. package/corpus/templates/crm/agent-native.app-skill.json +69 -0
  93. package/corpus/templates/crm/app/components/crm/CreateCrmRecordDialog.tsx +158 -0
  94. package/corpus/templates/crm/app/components/crm/CrmDashboardPanel.tsx +78 -0
  95. package/corpus/templates/crm/app/components/crm/CrmSignalsPanel.tsx +220 -0
  96. package/corpus/templates/crm/app/components/crm/IntelligenceSettings.tsx +414 -0
  97. package/corpus/templates/crm/app/components/crm/RecordActions.tsx +648 -0
  98. package/corpus/templates/crm/app/components/crm/RecordGrid.tsx +488 -0
  99. package/corpus/templates/crm/app/components/crm/RecordWorkspace.tsx +232 -0
  100. package/corpus/templates/crm/app/components/crm/SavedViewDataProgram.tsx +93 -0
  101. package/corpus/templates/crm/app/components/crm/Surface.tsx +97 -0
  102. package/corpus/templates/crm/app/components/crm/WorkOverview.tsx +180 -0
  103. package/corpus/templates/crm/app/components/crm/board/CrmBoard.tsx +988 -0
  104. package/corpus/templates/crm/app/components/crm/board/board-model.ts +399 -0
  105. package/corpus/templates/crm/app/components/crm/board/entry-filter.ts +150 -0
  106. package/corpus/templates/crm/app/components/crm/board/view-draft.ts +227 -0
  107. package/corpus/templates/crm/app/components/crm/grid/CrmGrid.tsx +1033 -0
  108. package/corpus/templates/crm/app/components/crm/grid/GridCell.tsx +620 -0
  109. package/corpus/templates/crm/app/components/crm/grid/clipboard.ts +84 -0
  110. package/corpus/templates/crm/app/components/crm/grid/model.ts +304 -0
  111. package/corpus/templates/crm/app/components/crm/grid/navigation.ts +220 -0
  112. package/corpus/templates/crm/app/components/crm/grid/query.ts +214 -0
  113. package/corpus/templates/crm/app/components/crm/record/AttributePanel.tsx +192 -0
  114. package/corpus/templates/crm/app/components/crm/record/FieldHistory.tsx +179 -0
  115. package/corpus/templates/crm/app/components/crm/record/RecordHeader.tsx +103 -0
  116. package/corpus/templates/crm/app/components/crm/record/RecordLists.tsx +235 -0
  117. package/corpus/templates/crm/app/components/crm/record/RecordTabs.tsx +206 -0
  118. package/corpus/templates/crm/app/components/crm/record/field-editors.tsx +245 -0
  119. package/corpus/templates/crm/app/components/crm/record/record-data.ts +603 -0
  120. package/corpus/templates/crm/app/components/crm/settings/AdvancedSettings.tsx +49 -0
  121. package/corpus/templates/crm/app/components/crm/settings/AttributeOptionsEditor.tsx +441 -0
  122. package/corpus/templates/crm/app/components/crm/settings/ConnectionSettings.tsx +169 -0
  123. package/corpus/templates/crm/app/components/crm/settings/FieldsSettings.tsx +935 -0
  124. package/corpus/templates/crm/app/components/crm/settings/ListsSettings.tsx +339 -0
  125. package/corpus/templates/crm/app/components/crm/settings/settings-admin.ts +377 -0
  126. package/corpus/templates/crm/app/components/crm/shared/AttributeValueParts.tsx +69 -0
  127. package/corpus/templates/crm/app/components/crm/shared/attribute-value.ts +591 -0
  128. package/corpus/templates/crm/app/components/layout/CrmCommandMenu.tsx +556 -0
  129. package/corpus/templates/crm/app/components/layout/CrmLayout.tsx +130 -0
  130. package/corpus/templates/crm/app/components/layout/CrmSidebar.tsx +407 -0
  131. package/corpus/templates/crm/app/components/layout/command-palette-search.ts +126 -0
  132. package/corpus/templates/crm/app/components/layout/crm-ui-intents.ts +41 -0
  133. package/corpus/templates/crm/app/components/layout/sidebar-collapse.ts +40 -0
  134. package/corpus/templates/crm/app/components/layout/sidebar-lists.ts +109 -0
  135. package/corpus/templates/crm/app/components/ui/alert-dialog.tsx +1 -0
  136. package/corpus/templates/crm/app/components/ui/badge.tsx +1 -0
  137. package/corpus/templates/crm/app/components/ui/button.tsx +1 -0
  138. package/corpus/templates/crm/app/components/ui/card.tsx +1 -0
  139. package/corpus/templates/crm/app/components/ui/checkbox.tsx +1 -0
  140. package/corpus/templates/crm/app/components/ui/dialog.tsx +1 -0
  141. package/corpus/templates/crm/app/components/ui/dropdown-menu.tsx +1 -0
  142. package/corpus/templates/crm/app/components/ui/hover-card.tsx +1 -0
  143. package/corpus/templates/crm/app/components/ui/input.tsx +1 -0
  144. package/corpus/templates/crm/app/components/ui/label.tsx +1 -0
  145. package/corpus/templates/crm/app/components/ui/popover.tsx +1 -0
  146. package/corpus/templates/crm/app/components/ui/select.tsx +1 -0
  147. package/corpus/templates/crm/app/components/ui/sheet.tsx +1 -0
  148. package/corpus/templates/crm/app/components/ui/skeleton.tsx +1 -0
  149. package/corpus/templates/crm/app/components/ui/switch.tsx +1 -0
  150. package/corpus/templates/crm/app/components/ui/table.tsx +1 -0
  151. package/corpus/templates/crm/app/components/ui/tabs.tsx +1 -0
  152. package/corpus/templates/crm/app/components/ui/textarea.tsx +1 -0
  153. package/corpus/templates/crm/app/components/ui/toolkit-provider.tsx +17 -0
  154. package/corpus/templates/crm/app/components/ui/tooltip.tsx +1 -0
  155. package/corpus/templates/crm/app/design-system.ts +3 -0
  156. package/corpus/templates/crm/app/entry.client.tsx +19 -0
  157. package/corpus/templates/crm/app/entry.server.tsx +10 -0
  158. package/corpus/templates/crm/app/global.css +167 -0
  159. package/corpus/templates/crm/app/hooks/use-keyboard-shortcuts.ts +206 -0
  160. package/corpus/templates/crm/app/hooks/use-navigation-state.ts +48 -0
  161. package/corpus/templates/crm/app/i18n/en-US.ts +684 -0
  162. package/corpus/templates/crm/app/i18n/index.ts +34 -0
  163. package/corpus/templates/crm/app/lib/dashboard.ts +13 -0
  164. package/corpus/templates/crm/app/lib/navigation.ts +17 -0
  165. package/corpus/templates/crm/app/lib/tab-id.ts +1 -0
  166. package/corpus/templates/crm/app/lib/types.ts +172 -0
  167. package/corpus/templates/crm/app/lib/utils.ts +1 -0
  168. package/corpus/templates/crm/app/root.tsx +93 -0
  169. package/corpus/templates/crm/app/routes/_index.tsx +26 -0
  170. package/corpus/templates/crm/app/routes/accounts.tsx +41 -0
  171. package/corpus/templates/crm/app/routes/agent.tsx +5 -0
  172. package/corpus/templates/crm/app/routes/ask.tsx +53 -0
  173. package/corpus/templates/crm/app/routes/dashboard.tsx +118 -0
  174. package/corpus/templates/crm/app/routes/lists.tsx +228 -0
  175. package/corpus/templates/crm/app/routes/opportunities.tsx +41 -0
  176. package/corpus/templates/crm/app/routes/people.tsx +41 -0
  177. package/corpus/templates/crm/app/routes/proposals.tsx +388 -0
  178. package/corpus/templates/crm/app/routes/records.$recordId.tsx +53 -0
  179. package/corpus/templates/crm/app/routes/records.tsx +64 -0
  180. package/corpus/templates/crm/app/routes/settings.tsx +118 -0
  181. package/corpus/templates/crm/app/routes/setup.tsx +308 -0
  182. package/corpus/templates/crm/app/routes/tasks.tsx +252 -0
  183. package/corpus/templates/crm/app/routes/views.tsx +683 -0
  184. package/corpus/templates/crm/app/routes.ts +19 -0
  185. package/corpus/templates/crm/app/vite-env.d.ts +6 -0
  186. package/corpus/templates/crm/changelog/2026-07-26-any-list-or-view-can-be-shown-as-a-board-grouped-by-stage-so.md +6 -0
  187. package/corpus/templates/crm/changelog/2026-07-26-crm-now-finds-likely-duplicate-records-and-tells-you-exactly.md +6 -0
  188. package/corpus/templates/crm/changelog/2026-07-26-enrichment-now-shows-what-a-run-will-cost-before-it-starts-g.md +6 -0
  189. package/corpus/templates/crm/changelog/2026-07-26-lists-turn-any-set-of-records-into-a-pipeline-with-its-own-s.md +6 -0
  190. package/corpus/templates/crm/changelog/2026-07-26-moving-a-record-or-list-entry-into-a-stage-that-was-retired-.md +6 -0
  191. package/corpus/templates/crm/changelog/2026-07-26-opening-a-record-now-shows-one-page-with-its-fields-the-list.md +6 -0
  192. package/corpus/templates/crm/changelog/2026-07-26-press-cmd-k-to-jump-to-any-record-list-view-or-setting-and-u.md +6 -0
  193. package/corpus/templates/crm/changelog/2026-07-26-provider-record-edits-now-show-an-exact-before-after-diff-an.md +6 -0
  194. package/corpus/templates/crm/changelog/2026-07-26-record-page-attribute-labels-now-read-in-full-instead-of-bei.md +6 -0
  195. package/corpus/templates/crm/changelog/2026-07-26-records-now-use-typed-fields-currency-dates-ratings-status-s.md +6 -0
  196. package/corpus/templates/crm/changelog/2026-07-26-retyping-a-value-on-the-record-page-now-replaces-it-instead-.md +6 -0
  197. package/corpus/templates/crm/changelog/2026-07-26-settings-now-has-fields-and-lists-tabs-for-adding-fields-edi.md +6 -0
  198. package/corpus/templates/crm/changelog/2026-07-26-the-proposal-review-now-shows-the-field-that-actually-change.md +6 -0
  199. package/corpus/templates/crm/changelog/2026-07-26-the-record-grid-is-now-a-real-spreadsheet-filter-sort-and-pa.md +6 -0
  200. package/corpus/templates/crm/components.json +20 -0
  201. package/corpus/templates/crm/docs/architecture/crm-contract.md +214 -0
  202. package/corpus/templates/crm/docs/features/README.md +55 -0
  203. package/corpus/templates/crm/docs/features/f1-records-and-fields.mdx +52 -0
  204. package/corpus/templates/crm/docs/features/f2-lists-pipelines-and-views.mdx +48 -0
  205. package/corpus/templates/crm/docs/features/f3-work-and-record-quality.mdx +40 -0
  206. package/corpus/templates/crm/docs/features/f4-evidence-and-chat.mdx +47 -0
  207. package/corpus/templates/crm/learnings.defaults.md +5 -0
  208. package/corpus/templates/crm/netlify.toml +11 -0
  209. package/corpus/templates/crm/package.json +61 -0
  210. package/corpus/templates/crm/public/favicon.svg +1 -0
  211. package/corpus/templates/crm/public/icon-180.svg +1 -0
  212. package/corpus/templates/crm/public/manifest.json +11 -0
  213. package/corpus/templates/crm/react-router.config.ts +7 -0
  214. package/corpus/templates/crm/server/crm/adapter.ts +32 -0
  215. package/corpus/templates/crm/server/crm/crm-field-firewall.ts +64 -0
  216. package/corpus/templates/crm/server/crm/crm-mirror.ts +721 -0
  217. package/corpus/templates/crm/server/crm/field-policy-attributes.ts +48 -0
  218. package/corpus/templates/crm/server/crm/hubspot-adapter.ts +1146 -0
  219. package/corpus/templates/crm/server/crm/native-adapter.ts +1810 -0
  220. package/corpus/templates/crm/server/crm/provider-record-link.ts +264 -0
  221. package/corpus/templates/crm/server/crm/read-through.ts +207 -0
  222. package/corpus/templates/crm/server/crm/salesforce-adapter.ts +1255 -0
  223. package/corpus/templates/crm/server/db/crm-store.ts +1112 -0
  224. package/corpus/templates/crm/server/db/index.ts +191 -0
  225. package/corpus/templates/crm/server/db/schema.ts +647 -0
  226. package/corpus/templates/crm/server/lib/crm-query.ts +1558 -0
  227. package/corpus/templates/crm/server/lib/dedupe.ts +714 -0
  228. package/corpus/templates/crm/server/lib/enrichment-cost.ts +381 -0
  229. package/corpus/templates/crm/server/lib/enrichment-slots.ts +519 -0
  230. package/corpus/templates/crm/server/lib/intelligence/default-detectors.ts +65 -0
  231. package/corpus/templates/crm/server/lib/intelligence/evidence.ts +105 -0
  232. package/corpus/templates/crm/server/lib/intelligence/keyword-detector.ts +94 -0
  233. package/corpus/templates/crm/server/lib/intelligence/smart-detector.ts +39 -0
  234. package/corpus/templates/crm/server/lib/intelligence/summary.ts +19 -0
  235. package/corpus/templates/crm/server/lib/lifecycle.ts +634 -0
  236. package/corpus/templates/crm/server/lib/provider-api.ts +64 -0
  237. package/corpus/templates/crm/server/middleware/auth.ts +4 -0
  238. package/corpus/templates/crm/server/plugins/agent-chat.ts +65 -0
  239. package/corpus/templates/crm/server/plugins/auth.ts +14 -0
  240. package/corpus/templates/crm/server/plugins/core-routes.ts +17 -0
  241. package/corpus/templates/crm/server/plugins/db.ts +738 -0
  242. package/corpus/templates/crm/server/routes/[...page].get.ts +5 -0
  243. package/corpus/templates/crm/shared/crm-attributes.ts +469 -0
  244. package/corpus/templates/crm/shared/crm-automation-recipes.ts +132 -0
  245. package/corpus/templates/crm/shared/crm-contract.ts +417 -0
  246. package/corpus/templates/crm/shared/crm-navigation.ts +190 -0
  247. package/corpus/templates/crm/shared/crm-sales-config.ts +63 -0
  248. package/corpus/templates/crm/ssr-entry.ts +11 -0
  249. package/corpus/templates/crm/tsconfig.json +24 -0
  250. package/corpus/templates/crm/vite.config.ts +15 -0
  251. package/corpus/templates/crm/vitest.config.ts +5 -0
  252. package/corpus/templates/design/app/i18n-data.ts +12 -12
  253. package/corpus/templates/design/changelog/2026-07-26-apply-design-updates-from-a-centered-top-toolbar-without-cov.md +6 -0
  254. package/dist/cli/templates-meta.d.ts.map +1 -1
  255. package/dist/cli/templates-meta.js +12 -0
  256. package/dist/cli/templates-meta.js.map +1 -1
  257. package/dist/collab/routes.d.ts +1 -1
  258. package/dist/observability/routes.d.ts +3 -3
  259. package/dist/resources/handlers.d.ts +1 -1
  260. package/dist/server/realtime-token.d.ts +1 -1
  261. package/dist/server/transcribe-voice.d.ts +1 -1
  262. package/package.json +1 -1
  263. package/src/cli/templates-meta.ts +12 -0
@@ -0,0 +1,264 @@
1
+ import { accessFilter } from "@agent-native/core/sharing";
2
+ import { resolveWorkspaceConnectionForApp } from "@agent-native/core/workspace-connections";
3
+ import { and, inArray } from "drizzle-orm";
4
+
5
+ import { getDb, schema } from "../db/index.js";
6
+ import {
7
+ isConnectedCrmProvider,
8
+ type ConnectedCrmProvider,
9
+ } from "./adapter.js";
10
+
11
+ const CRM_APP_ID = "crm";
12
+
13
+ /**
14
+ * HubSpot addresses records by its fully-qualified object type id, not by the
15
+ * object name the CRM API takes. Custom objects already arrive shaped `2-1234`
16
+ * and pass straight through.
17
+ */
18
+ const HUBSPOT_OBJECT_TYPE_IDS: Record<string, string> = {
19
+ contact: "0-1",
20
+ contacts: "0-1",
21
+ company: "0-2",
22
+ companies: "0-2",
23
+ deal: "0-3",
24
+ deals: "0-3",
25
+ ticket: "0-5",
26
+ tickets: "0-5",
27
+ };
28
+
29
+ export type ProviderRecordLinkUnavailableReason =
30
+ | "missing-portal-id"
31
+ | "unsupported-object-type"
32
+ | "invalid-remote-id"
33
+ | "missing-instance-url"
34
+ | "workspace-connection-unavailable";
35
+
36
+ /**
37
+ * A link is either present or typed-absent with the reason. There is no
38
+ * empty-string or best-guess URL: a wrong deep link reads to the user as a
39
+ * working handoff and sends them to somebody else's record.
40
+ */
41
+ export type ProviderRecordLink =
42
+ | { available: true; url: string }
43
+ | { available: false; reason: ProviderRecordLinkUnavailableReason };
44
+
45
+ const HUBSPOT_PORTAL_ID = /^\d{1,20}$/;
46
+ const HUBSPOT_OBJECT_TYPE_ID = /^\d{1,4}-\d{1,10}$/;
47
+ const HUBSPOT_REMOTE_ID = /^[A-Za-z0-9_-]{1,64}$/;
48
+ const SALESFORCE_OBJECT_API_NAME = /^[A-Za-z][A-Za-z0-9_]{0,79}$/;
49
+ const SALESFORCE_RECORD_ID = /^[A-Za-z0-9]{15,18}$/;
50
+
51
+ function hubSpotObjectTypeId(objectType: string): string | null {
52
+ const normalized = objectType.trim().toLowerCase();
53
+ if (HUBSPOT_OBJECT_TYPE_ID.test(normalized)) return normalized;
54
+ return HUBSPOT_OBJECT_TYPE_IDS[normalized] ?? null;
55
+ }
56
+
57
+ function salesforceOrigin(configured: string): string | null {
58
+ let url: URL;
59
+ try {
60
+ url = new URL(configured);
61
+ } catch {
62
+ return null;
63
+ }
64
+ const host = url.hostname.toLowerCase();
65
+ const trustedHost =
66
+ host === "salesforce.com" ||
67
+ host.endsWith(".salesforce.com") ||
68
+ host.endsWith(".force.com");
69
+ if (
70
+ url.protocol !== "https:" ||
71
+ !trustedHost ||
72
+ url.port ||
73
+ url.username ||
74
+ url.password
75
+ ) {
76
+ return null;
77
+ }
78
+ return url.origin;
79
+ }
80
+
81
+ /**
82
+ * Builds the upstream record URL a person can open to complete a change by
83
+ * hand. Pure so both providers stay covered by one test.
84
+ */
85
+ export function buildProviderRecordUrl(input: {
86
+ provider: ConnectedCrmProvider;
87
+ objectType: string;
88
+ remoteId: string;
89
+ /** HubSpot portal id (`crm_connections.account_id`). */
90
+ portalId?: string | null;
91
+ /** Salesforce My Domain origin from the workspace connection config. */
92
+ instanceUrl?: string | null;
93
+ }): ProviderRecordLink {
94
+ const remoteId = input.remoteId.trim();
95
+ if (input.provider === "hubspot") {
96
+ const portalId = input.portalId?.trim();
97
+ if (!portalId || !HUBSPOT_PORTAL_ID.test(portalId)) {
98
+ return { available: false, reason: "missing-portal-id" };
99
+ }
100
+ const objectTypeId = hubSpotObjectTypeId(input.objectType);
101
+ if (!objectTypeId) {
102
+ return { available: false, reason: "unsupported-object-type" };
103
+ }
104
+ if (!HUBSPOT_REMOTE_ID.test(remoteId)) {
105
+ return { available: false, reason: "invalid-remote-id" };
106
+ }
107
+ return {
108
+ available: true,
109
+ url: `https://app.hubspot.com/contacts/${portalId}/record/${objectTypeId}/${remoteId}`,
110
+ };
111
+ }
112
+
113
+ const configured = input.instanceUrl?.trim();
114
+ if (!configured) return { available: false, reason: "missing-instance-url" };
115
+ const origin = salesforceOrigin(configured);
116
+ if (!origin) return { available: false, reason: "missing-instance-url" };
117
+ const objectApiName = input.objectType.trim();
118
+ if (!SALESFORCE_OBJECT_API_NAME.test(objectApiName)) {
119
+ return { available: false, reason: "unsupported-object-type" };
120
+ }
121
+ if (!SALESFORCE_RECORD_ID.test(remoteId)) {
122
+ return { available: false, reason: "invalid-remote-id" };
123
+ }
124
+ return {
125
+ available: true,
126
+ url: `${origin}/lightning/r/${objectApiName}/${remoteId}/view`,
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Everything a connection contributes to its records' links, resolved once.
132
+ * HubSpot's portal id is already mirrored on the CRM connection row; only
133
+ * Salesforce needs the workspace connection, which owns the My Domain origin.
134
+ */
135
+ type ConnectionLinkContext =
136
+ | { portalId: string | null; instanceUrl?: undefined }
137
+ | { instanceUrl: string | null; portalId?: undefined }
138
+ | { unavailable: ProviderRecordLinkUnavailableReason };
139
+
140
+ async function connectionLinkContext(input: {
141
+ provider: ConnectedCrmProvider;
142
+ accountId: string | null;
143
+ workspaceConnectionId: string | null;
144
+ }): Promise<ConnectionLinkContext> {
145
+ if (input.provider === "hubspot") return { portalId: input.accountId };
146
+ const resolved = await resolveWorkspaceConnectionForApp({
147
+ appId: CRM_APP_ID,
148
+ provider: "salesforce",
149
+ ...(input.workspaceConnectionId
150
+ ? { connectionId: input.workspaceConnectionId }
151
+ : {}),
152
+ });
153
+ if (!resolved.available || !resolved.connection) {
154
+ return { unavailable: "workspace-connection-unavailable" };
155
+ }
156
+ const config = resolved.connection.config as
157
+ | Record<string, unknown>
158
+ | undefined;
159
+ const instanceUrl = ["instanceUrl", "instance_url", "salesforceInstanceUrl"]
160
+ .map((key) => config?.[key])
161
+ .find(
162
+ (value): value is string => typeof value === "string" && !!value.trim(),
163
+ );
164
+ return { instanceUrl: instanceUrl ?? null };
165
+ }
166
+
167
+ function linkFromContext(
168
+ context: ConnectionLinkContext,
169
+ record: {
170
+ provider: ConnectedCrmProvider;
171
+ objectType: string;
172
+ remoteId: string;
173
+ },
174
+ ): ProviderRecordLink {
175
+ if ("unavailable" in context) {
176
+ return { available: false, reason: context.unavailable };
177
+ }
178
+ return buildProviderRecordUrl({
179
+ provider: record.provider,
180
+ objectType: record.objectType,
181
+ remoteId: record.remoteId,
182
+ portalId: context.portalId ?? null,
183
+ instanceUrl: context.instanceUrl ?? null,
184
+ });
185
+ }
186
+
187
+ export async function resolveProviderRecordLink(input: {
188
+ provider: ConnectedCrmProvider;
189
+ objectType: string;
190
+ remoteId: string;
191
+ accountId: string | null;
192
+ workspaceConnectionId: string | null;
193
+ }): Promise<ProviderRecordLink> {
194
+ return linkFromContext(await connectionLinkContext(input), input);
195
+ }
196
+
197
+ /**
198
+ * Bulk variant for list surfaces. Records with a native (or otherwise
199
+ * unconnected) provider are simply absent from the map — they have no upstream
200
+ * record to open.
201
+ */
202
+ export async function resolveProviderRecordLinks(
203
+ recordIds: string[],
204
+ ): Promise<Map<string, ProviderRecordLink>> {
205
+ const links = new Map<string, ProviderRecordLink>();
206
+ const ids = [...new Set(recordIds)];
207
+ if (!ids.length) return links;
208
+ const db = getDb();
209
+ const records = await db
210
+ .select({
211
+ id: schema.crmRecords.id,
212
+ connectionId: schema.crmRecords.connectionId,
213
+ provider: schema.crmRecords.provider,
214
+ objectType: schema.crmRecords.objectType,
215
+ remoteId: schema.crmRecords.remoteId,
216
+ })
217
+ .from(schema.crmRecords)
218
+ .where(
219
+ and(
220
+ inArray(schema.crmRecords.id, ids),
221
+ accessFilter(schema.crmRecords, schema.crmRecordShares),
222
+ ),
223
+ );
224
+ const connectedRecords = records.filter((record) =>
225
+ isConnectedCrmProvider(record.provider),
226
+ );
227
+ if (!connectedRecords.length) return links;
228
+ const connections = await db
229
+ .select({
230
+ id: schema.crmConnections.id,
231
+ accountId: schema.crmConnections.accountId,
232
+ workspaceConnectionId: schema.crmConnections.workspaceConnectionId,
233
+ })
234
+ .from(schema.crmConnections)
235
+ .where(
236
+ and(
237
+ inArray(schema.crmConnections.id, [
238
+ ...new Set(connectedRecords.map((record) => record.connectionId)),
239
+ ]),
240
+ accessFilter(schema.crmConnections, schema.crmConnectionShares),
241
+ ),
242
+ );
243
+ const connectionById = new Map(
244
+ connections.map((connection) => [connection.id, connection]),
245
+ );
246
+ // One workspace lookup per connection, not per record.
247
+ const contextByConnectionId = new Map<string, ConnectionLinkContext>();
248
+ for (const record of connectedRecords) {
249
+ const connection = connectionById.get(record.connectionId);
250
+ if (!connection) continue;
251
+ const provider = record.provider as ConnectedCrmProvider;
252
+ let context = contextByConnectionId.get(record.connectionId);
253
+ if (!context) {
254
+ context = await connectionLinkContext({
255
+ provider,
256
+ accountId: connection.accountId,
257
+ workspaceConnectionId: connection.workspaceConnectionId,
258
+ });
259
+ contextByConnectionId.set(record.connectionId, context);
260
+ }
261
+ links.set(record.id, linkFromContext(context, { ...record, provider }));
262
+ }
263
+ return links;
264
+ }
@@ -0,0 +1,207 @@
1
+ import type {
2
+ CrmAccessScope,
3
+ CrmAdapter,
4
+ CrmFieldStoragePolicy,
5
+ CrmObjectKind,
6
+ CrmRecord,
7
+ CrmRelationship,
8
+ } from "../../shared/crm-contract.js";
9
+
10
+ export const MAX_READ_THROUGH_RELATIONSHIPS = 100;
11
+
12
+ export type ReadThroughFieldPolicy = {
13
+ fieldName: string;
14
+ storagePolicy: CrmFieldStoragePolicy;
15
+ readable: boolean;
16
+ sensitive: boolean;
17
+ };
18
+
19
+ export type RelatedRecordSummary = {
20
+ id: string;
21
+ displayName: string;
22
+ kind: string;
23
+ relationshipType: string;
24
+ relationshipLabel?: string;
25
+ subtitle?: string;
26
+ };
27
+
28
+ export type ReadThroughRecordContext = {
29
+ id: string;
30
+ objectType: string;
31
+ kind: CrmObjectKind;
32
+ remoteId: string;
33
+ accessScopeJson: string;
34
+ fieldPolicies: ReadThroughFieldPolicy[];
35
+ };
36
+
37
+ export type VerifiedReadThroughRecord = {
38
+ remote: CrmRecord;
39
+ currentScope: CrmAccessScope;
40
+ relationships: CrmRelationship[];
41
+ currentScopes: Map<string, CrmAccessScope>;
42
+ };
43
+
44
+ type ReadThroughAdapter = CrmAdapter & {
45
+ getAccessScope(objectType: string): CrmAccessScope | Promise<CrmAccessScope>;
46
+ };
47
+
48
+ export function parseCrmAccessScope(value: string): CrmAccessScope | null {
49
+ try {
50
+ const scope = JSON.parse(value) as Partial<CrmAccessScope>;
51
+ if (
52
+ !scope ||
53
+ typeof scope.key !== "string" ||
54
+ !scope.key ||
55
+ (scope.mode !== "user" &&
56
+ scope.mode !== "service-account" &&
57
+ scope.mode !== "native") ||
58
+ typeof scope.objectReadable !== "boolean" ||
59
+ (scope.recordVisibility !== "actor" &&
60
+ scope.recordVisibility !== "cohort" &&
61
+ scope.recordVisibility !== "workspace" &&
62
+ scope.recordVisibility !== "unknown")
63
+ ) {
64
+ return null;
65
+ }
66
+ return scope as CrmAccessScope;
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+
72
+ export function scopesAreCompatible(
73
+ stored: CrmAccessScope | null,
74
+ current: CrmAccessScope,
75
+ ): boolean {
76
+ if (!stored || !stored.objectReadable || !current.objectReadable)
77
+ return false;
78
+ return (
79
+ stored.key === current.key &&
80
+ stored.mode === current.mode &&
81
+ stored.actorId === current.actorId &&
82
+ stored.grantId === current.grantId &&
83
+ stored.recordVisibility === current.recordVisibility &&
84
+ stored.fieldPermissionsHash === current.fieldPermissionsHash &&
85
+ stored.sharingFingerprint === current.sharingFingerprint
86
+ );
87
+ }
88
+
89
+ export function readThroughFieldNames(
90
+ policies: ReadThroughFieldPolicy[],
91
+ ): string[] {
92
+ return policies
93
+ .filter(
94
+ (policy) =>
95
+ policy.readable &&
96
+ !policy.sensitive &&
97
+ policy.storagePolicy === "mirrored",
98
+ )
99
+ .map((policy) => policy.fieldName)
100
+ .slice(0, 80);
101
+ }
102
+
103
+ export async function loadVerifiedReadThroughRecord(input: {
104
+ adapter: ReadThroughAdapter;
105
+ context: ReadThroughRecordContext;
106
+ }): Promise<VerifiedReadThroughRecord> {
107
+ const currentScope = await input.adapter.getAccessScope(
108
+ input.context.objectType,
109
+ );
110
+ const storedScope = parseCrmAccessScope(input.context.accessScopeJson);
111
+ if (!scopesAreCompatible(storedScope, currentScope)) {
112
+ throw new Error(
113
+ "CRM provider access changed; the local record is withheld until it is refreshed.",
114
+ );
115
+ }
116
+ const remote = await input.adapter.getRecord({
117
+ record: {
118
+ connectionId: input.adapter.connection.connectionId,
119
+ provider: input.adapter.connection.provider,
120
+ objectType: input.context.objectType,
121
+ kind: input.context.kind,
122
+ remoteId: input.context.remoteId,
123
+ localId: input.context.id,
124
+ },
125
+ fields: readThroughFieldNames(input.context.fieldPolicies),
126
+ });
127
+ if (!remote || !scopesAreCompatible(storedScope, remote.accessScope)) {
128
+ throw new Error(
129
+ "CRM provider access changed or the record is unavailable; the local record is withheld until it is refreshed.",
130
+ );
131
+ }
132
+ const relationshipPage = await input.adapter.listRelationships({
133
+ record: remote.ref,
134
+ limit: MAX_READ_THROUGH_RELATIONSHIPS,
135
+ });
136
+ const targetObjectTypes = Array.from(
137
+ new Set(
138
+ relationshipPage.relationships.map(
139
+ (relationship) => relationship.to.objectType,
140
+ ),
141
+ ),
142
+ ).slice(0, 20);
143
+ return {
144
+ remote,
145
+ currentScope,
146
+ relationships: relationshipPage.relationships,
147
+ currentScopes: new Map(
148
+ await Promise.all(
149
+ targetObjectTypes.map(
150
+ async (objectType) =>
151
+ [
152
+ objectType,
153
+ await input.adapter.getAccessScope(objectType),
154
+ ] as const,
155
+ ),
156
+ ),
157
+ ),
158
+ };
159
+ }
160
+
161
+ export function relatedSummaries<
162
+ T extends {
163
+ id: string;
164
+ remoteId: string;
165
+ objectType: string;
166
+ displayName: string;
167
+ kind: string;
168
+ primaryEmail: string | null;
169
+ domain: string | null;
170
+ },
171
+ >(
172
+ sourceRemoteId: string,
173
+ relationships: CrmRelationship[],
174
+ localRecords: T[],
175
+ ): Array<RelatedRecordSummary & { localId: string; remoteId: string }> {
176
+ const recordsByIdentity = new Map(
177
+ localRecords.map((record) => [
178
+ `${record.objectType}:${record.remoteId}`,
179
+ record,
180
+ ]),
181
+ );
182
+ return relationships
183
+ .filter((relationship) => relationship.from.remoteId === sourceRemoteId)
184
+ .slice(0, MAX_READ_THROUGH_RELATIONSHIPS)
185
+ .flatMap((relationship) => {
186
+ const record = recordsByIdentity.get(
187
+ `${relationship.to.objectType}:${relationship.to.remoteId}`,
188
+ );
189
+ if (!record) return [];
190
+ return [
191
+ {
192
+ id: record.id,
193
+ localId: record.id,
194
+ remoteId: record.remoteId,
195
+ displayName: record.displayName,
196
+ kind: record.kind,
197
+ relationshipType: relationship.relationshipType,
198
+ ...(relationship.label
199
+ ? { relationshipLabel: relationship.label }
200
+ : {}),
201
+ ...((record.domain ?? record.primaryEmail)
202
+ ? { subtitle: record.domain ?? record.primaryEmail ?? undefined }
203
+ : {}),
204
+ },
205
+ ];
206
+ });
207
+ }