@aglyn/plugins-crm 1.0.0-beta.218 → 1.0.0-beta.219

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 (249) hide show
  1. package/package.json +14 -13
  2. package/src/index.d.ts +1 -0
  3. package/src/index.js.map +1 -1
  4. package/src/lib/components/companies-bulk-bar.js +12 -4
  5. package/src/lib/components/companies-bulk-bar.js.map +1 -1
  6. package/src/lib/components/company-detail-page.js +11 -0
  7. package/src/lib/components/company-detail-page.js.map +1 -1
  8. package/src/lib/components/company-edit-drawer.js +12 -2
  9. package/src/lib/components/company-edit-drawer.js.map +1 -1
  10. package/src/lib/components/company-import-drawer.d.ts +1 -1
  11. package/src/lib/components/company-picker.js +10 -3
  12. package/src/lib/components/company-picker.js.map +1 -1
  13. package/src/lib/components/contact-detail-page.d.ts +4 -4
  14. package/src/lib/components/contact-detail-page.js +50 -53
  15. package/src/lib/components/contact-detail-page.js.map +1 -1
  16. package/src/lib/components/contact-list-columns.d.ts +3 -0
  17. package/src/lib/components/contact-list-columns.js +21 -5
  18. package/src/lib/components/contact-list-columns.js.map +1 -1
  19. package/src/lib/components/contact-merge-dialog.js +2 -1
  20. package/src/lib/components/contact-merge-dialog.js.map +1 -1
  21. package/src/lib/components/contact-timeline-card.js +4 -3
  22. package/src/lib/components/contact-timeline-card.js.map +1 -1
  23. package/src/lib/components/contacts-bulk-bar.d.ts +3 -3
  24. package/src/lib/components/contacts-bulk-bar.js +24 -51
  25. package/src/lib/components/contacts-bulk-bar.js.map +1 -1
  26. package/src/lib/components/contacts-section.js +9 -3
  27. package/src/lib/components/contacts-section.js.map +1 -1
  28. package/src/lib/components/crm-export-all-button.d.ts +1 -1
  29. package/src/lib/components/crm-export-all-button.js.map +1 -1
  30. package/src/lib/components/crm-send-email-dialog.js +9 -6
  31. package/src/lib/components/crm-send-email-dialog.js.map +1 -1
  32. package/src/lib/components/csv-import-drawer.js +2 -2
  33. package/src/lib/components/csv-import-drawer.js.map +1 -1
  34. package/src/lib/components/deal-detail-page.js +18 -2
  35. package/src/lib/components/deal-detail-page.js.map +1 -1
  36. package/src/lib/components/deal-edit-drawer.js +10 -0
  37. package/src/lib/components/deal-edit-drawer.js.map +1 -1
  38. package/src/lib/components/deals-bulk-bar.js +13 -5
  39. package/src/lib/components/deals-bulk-bar.js.map +1 -1
  40. package/src/lib/components/lead-campaigns-card.js +6 -0
  41. package/src/lib/components/lead-campaigns-card.js.map +1 -1
  42. package/src/lib/components/lead-detail-page.js +25 -5
  43. package/src/lib/components/lead-detail-page.js.map +1 -1
  44. package/src/lib/components/lead-properties-card.js +6 -0
  45. package/src/lib/components/lead-properties-card.js.map +1 -1
  46. package/src/lib/components/lead-surfaces-note.js +4 -5
  47. package/src/lib/components/lead-surfaces-note.js.map +1 -1
  48. package/src/lib/components/lead-unqualify-dialog.js +6 -0
  49. package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
  50. package/src/lib/components/leads-bulk-bar.js +28 -3
  51. package/src/lib/components/leads-bulk-bar.js.map +1 -1
  52. package/src/lib/components/leads-section.js +36 -7
  53. package/src/lib/components/leads-section.js.map +1 -1
  54. package/src/lib/components/org-lead-surfaces-note.js +6 -2
  55. package/src/lib/components/org-lead-surfaces-note.js.map +1 -1
  56. package/src/lib/components/recipes-card.js +9 -4
  57. package/src/lib/components/recipes-card.js.map +1 -1
  58. package/src/lib/components/record-sharing-card.d.ts +57 -0
  59. package/src/lib/components/record-sharing-card.js +244 -0
  60. package/src/lib/components/record-sharing-card.js.map +1 -0
  61. package/src/lib/components/settings-section.js +7 -0
  62. package/src/lib/components/settings-section.js.map +1 -1
  63. package/src/lib/components/share-records-dialog.d.ts +28 -0
  64. package/src/lib/components/share-records-dialog.js +184 -0
  65. package/src/lib/components/share-records-dialog.js.map +1 -0
  66. package/src/lib/components/sharing-rule-drawer.d.ts +37 -0
  67. package/src/lib/components/sharing-rule-drawer.js +374 -0
  68. package/src/lib/components/sharing-rule-drawer.js.map +1 -0
  69. package/src/lib/components/sharing-rules-card.d.ts +24 -0
  70. package/src/lib/components/sharing-rules-card.js +287 -0
  71. package/src/lib/components/sharing-rules-card.js.map +1 -0
  72. package/src/lib/components/use-crm-api.d.ts +1 -1
  73. package/src/lib/components/use-crm-api.js.map +1 -1
  74. package/src/lib/components/use-site-route-context.d.ts +25 -0
  75. package/src/lib/components/use-site-route-context.js +58 -0
  76. package/src/lib/components/use-site-route-context.js.map +1 -0
  77. package/src/lib/constants/api-routes.d.ts +31 -0
  78. package/src/lib/constants/api-routes.js +27 -1
  79. package/src/lib/constants/api-routes.js.map +1 -1
  80. package/src/lib/declarations.console-server.d.ts +37 -0
  81. package/src/lib/declarations.console-server.js +103 -0
  82. package/src/lib/declarations.console-server.js.map +1 -0
  83. package/src/lib/declarations.server.d.ts +6 -0
  84. package/src/lib/declarations.server.js +33 -0
  85. package/src/lib/declarations.server.js.map +1 -1
  86. package/src/lib/hooks/use-contact-remove.d.ts +35 -0
  87. package/src/lib/hooks/use-contact-remove.js +77 -0
  88. package/src/lib/hooks/use-contact-remove.js.map +1 -0
  89. package/src/lib/hooks/use-crm-sharing.d.ts +30 -0
  90. package/src/lib/hooks/use-crm-sharing.js +109 -0
  91. package/src/lib/hooks/use-crm-sharing.js.map +1 -0
  92. package/src/lib/model/companies-csv.d.ts +1 -1
  93. package/src/lib/model/companies-csv.js +2 -2
  94. package/src/lib/model/companies-csv.js.map +1 -1
  95. package/src/lib/model/contact-holder.d.ts +68 -0
  96. package/src/lib/model/contact-holder.js +129 -0
  97. package/src/lib/model/contact-holder.js.map +1 -0
  98. package/src/lib/model/contact-interaction-links.d.ts +46 -0
  99. package/src/lib/model/contact-interaction-links.js +65 -0
  100. package/src/lib/model/contact-interaction-links.js.map +1 -0
  101. package/src/lib/model/contact-merge.d.ts +136 -0
  102. package/src/lib/model/contact-merge.js +412 -0
  103. package/src/lib/model/contact-merge.js.map +1 -0
  104. package/src/lib/model/contact-record.d.ts +7 -1
  105. package/src/lib/model/contact-record.js +6 -5
  106. package/src/lib/model/contact-record.js.map +1 -1
  107. package/src/lib/model/contact-remove.d.ts +48 -0
  108. package/src/lib/model/contact-remove.js +22 -0
  109. package/src/lib/model/contact-remove.js.map +1 -0
  110. package/src/lib/model/contacts-bulk-writes.d.ts +2 -71
  111. package/src/lib/model/contacts-bulk-writes.js +2 -82
  112. package/src/lib/model/contacts-bulk-writes.js.map +1 -1
  113. package/src/lib/model/contacts-csv.d.ts +2 -2
  114. package/src/lib/model/contacts-csv.js +2 -2
  115. package/src/lib/model/contacts-csv.js.map +1 -1
  116. package/src/lib/model/crm-csv.d.ts +239 -0
  117. package/src/lib/model/crm-csv.js +467 -0
  118. package/src/lib/model/crm-csv.js.map +1 -0
  119. package/src/lib/model/crm-digest.d.ts +185 -0
  120. package/src/lib/model/crm-digest.js +272 -0
  121. package/src/lib/model/crm-digest.js.map +1 -0
  122. package/src/lib/model/crm-import.d.ts +7 -1
  123. package/src/lib/model/crm-import.js +2 -1
  124. package/src/lib/model/crm-import.js.map +1 -1
  125. package/src/lib/model/crm-record-routes.d.ts +5 -0
  126. package/src/lib/model/crm-record-routes.js +7 -0
  127. package/src/lib/model/crm-record-routes.js.map +1 -1
  128. package/src/lib/model/crm-routes.d.ts +6 -5
  129. package/src/lib/model/crm-routes.js +7 -7
  130. package/src/lib/model/crm-routes.js.map +1 -1
  131. package/src/lib/model/crm-search-sources.d.ts +15 -0
  132. package/src/lib/model/crm-search-sources.js +162 -0
  133. package/src/lib/model/crm-search-sources.js.map +1 -0
  134. package/src/lib/model/crm-sharing-api.d.ts +32 -0
  135. package/src/lib/model/crm-sharing-api.js +59 -0
  136. package/src/lib/model/crm-sharing-api.js.map +1 -0
  137. package/src/lib/model/crm-sharing.d.ts +333 -0
  138. package/src/lib/model/crm-sharing.js +603 -0
  139. package/src/lib/model/crm-sharing.js.map +1 -0
  140. package/src/lib/model/deals-csv.d.ts +1 -1
  141. package/src/lib/model/deals-csv.js +4 -5
  142. package/src/lib/model/deals-csv.js.map +1 -1
  143. package/src/lib/model/lead-filters.d.ts +1 -1
  144. package/src/lib/model/leads-csv.d.ts +1 -1
  145. package/src/lib/model/leads-csv.js +1 -1
  146. package/src/lib/model/leads-csv.js.map +1 -1
  147. package/src/lib/model/tasks-csv.d.ts +1 -1
  148. package/src/lib/model/tasks-csv.js +1 -1
  149. package/src/lib/model/tasks-csv.js.map +1 -1
  150. package/src/lib/plan-entitlements.d.ts +75 -0
  151. package/src/lib/plan-entitlements.js +56 -0
  152. package/src/lib/plan-entitlements.js.map +1 -0
  153. package/src/lib/plugin.js +3 -0
  154. package/src/lib/plugin.js.map +1 -1
  155. package/src/lib/server/api-v1/contacts-merge.d.ts +20 -0
  156. package/src/lib/server/api-v1/contacts-merge.js +120 -0
  157. package/src/lib/server/api-v1/contacts-merge.js.map +1 -0
  158. package/src/lib/server/api-v1/contacts.d.ts +18 -0
  159. package/src/lib/server/api-v1/contacts.js +1076 -0
  160. package/src/lib/server/api-v1/contacts.js.map +1 -0
  161. package/src/lib/server/api-v1/crm-activities.d.ts +18 -0
  162. package/src/lib/server/api-v1/crm-activities.js +269 -0
  163. package/src/lib/server/api-v1/crm-activities.js.map +1 -0
  164. package/src/lib/server/api-v1/crm-companies.d.ts +18 -0
  165. package/src/lib/server/api-v1/crm-companies.js +391 -0
  166. package/src/lib/server/api-v1/crm-companies.js.map +1 -0
  167. package/src/lib/server/api-v1/crm-deals.d.ts +18 -0
  168. package/src/lib/server/api-v1/crm-deals.js +631 -0
  169. package/src/lib/server/api-v1/crm-deals.js.map +1 -0
  170. package/src/lib/server/api-v1/crm-email-templates.d.ts +18 -0
  171. package/src/lib/server/api-v1/crm-email-templates.js +336 -0
  172. package/src/lib/server/api-v1/crm-email-templates.js.map +1 -0
  173. package/src/lib/server/api-v1/crm-leads.d.ts +18 -0
  174. package/src/lib/server/api-v1/crm-leads.js +832 -0
  175. package/src/lib/server/api-v1/crm-leads.js.map +1 -0
  176. package/src/lib/server/api-v1/crm-pipelines.d.ts +76 -0
  177. package/src/lib/server/api-v1/crm-pipelines.js +146 -0
  178. package/src/lib/server/api-v1/crm-pipelines.js.map +1 -0
  179. package/src/lib/server/api-v1/crm-shared.d.ts +271 -0
  180. package/src/lib/server/api-v1/crm-shared.js +436 -0
  181. package/src/lib/server/api-v1/crm-shared.js.map +1 -0
  182. package/src/lib/server/api-v1/crm-tasks.d.ts +18 -0
  183. package/src/lib/server/api-v1/crm-tasks.js +398 -0
  184. package/src/lib/server/api-v1/crm-tasks.js.map +1 -0
  185. package/src/lib/server/api-v1/openapi.d.ts +29 -0
  186. package/src/lib/server/api-v1/openapi.js +985 -0
  187. package/src/lib/server/api-v1/openapi.js.map +1 -0
  188. package/src/lib/server/api-v1/usage.d.ts +26 -0
  189. package/src/lib/server/api-v1/usage.js +73 -0
  190. package/src/lib/server/api-v1/usage.js.map +1 -0
  191. package/src/lib/server/consent-group-participant.js +1 -1
  192. package/src/lib/server/consent-group-participant.js.map +1 -1
  193. package/src/lib/server/contact-lifecycle-floor.d.ts +89 -0
  194. package/src/lib/server/contact-lifecycle-floor.js +91 -0
  195. package/src/lib/server/contact-lifecycle-floor.js.map +1 -0
  196. package/src/lib/server/contact-remove.d.ts +75 -0
  197. package/src/lib/server/contact-remove.js +244 -0
  198. package/src/lib/server/contact-remove.js.map +1 -0
  199. package/src/lib/server/contact-update.js +2 -1
  200. package/src/lib/server/contact-update.js.map +1 -1
  201. package/src/lib/server/contacts-import.d.ts +47 -11
  202. package/src/lib/server/contacts-import.js +171 -71
  203. package/src/lib/server/contacts-import.js.map +1 -1
  204. package/src/lib/server/contacts-merge.d.ts +1 -1
  205. package/src/lib/server/contacts-merge.js +3 -2
  206. package/src/lib/server/contacts-merge.js.map +1 -1
  207. package/src/lib/server/convert-host-lead.d.ts +189 -0
  208. package/src/lib/server/convert-host-lead.js +518 -0
  209. package/src/lib/server/convert-host-lead.js.map +1 -0
  210. package/src/lib/server/crm-sharing.d.ts +108 -0
  211. package/src/lib/server/crm-sharing.js +790 -0
  212. package/src/lib/server/crm-sharing.js.map +1 -0
  213. package/src/lib/server/daily-digest-route.d.ts +107 -0
  214. package/src/lib/server/daily-digest-route.js +588 -0
  215. package/src/lib/server/daily-digest-route.js.map +1 -0
  216. package/src/lib/server/export-route.d.ts +60 -0
  217. package/src/lib/server/export-route.js +447 -0
  218. package/src/lib/server/export-route.js.map +1 -0
  219. package/src/lib/server/inbound-route.d.ts +74 -0
  220. package/src/lib/server/inbound-route.js +257 -0
  221. package/src/lib/server/inbound-route.js.map +1 -0
  222. package/src/lib/server/lead-convert.d.ts +1 -1
  223. package/src/lib/server/lead-convert.js +2 -2
  224. package/src/lib/server/lead-convert.js.map +1 -1
  225. package/src/lib/server/merge-contacts.d.ts +55 -0
  226. package/src/lib/server/merge-contacts.js +274 -0
  227. package/src/lib/server/merge-contacts.js.map +1 -0
  228. package/src/lib/server/pipeline-record-index.d.ts +36 -0
  229. package/src/lib/server/pipeline-record-index.js +87 -0
  230. package/src/lib/server/pipeline-record-index.js.map +1 -0
  231. package/src/lib/server/record-facts.js +2 -1
  232. package/src/lib/server/record-facts.js.map +1 -1
  233. package/src/lib/server/task-reminders-route.d.ts +82 -0
  234. package/src/lib/server/task-reminders-route.js +497 -0
  235. package/src/lib/server/task-reminders-route.js.map +1 -0
  236. package/src/lib/server-deal-stage.js +2 -1
  237. package/src/lib/server-deal-stage.js.map +1 -1
  238. package/src/lib/server.d.ts +5 -2
  239. package/src/lib/server.js +47 -2
  240. package/src/lib/server.js.map +1 -1
  241. package/src/lib/testing/stand-in-site-record-routes.d.ts +26 -0
  242. package/src/lib/testing/stand-in-site-record-routes.js +55 -0
  243. package/src/lib/testing/stand-in-site-record-routes.js.map +1 -0
  244. package/src/lib/usage-axes.d.ts +35 -0
  245. package/src/lib/usage-axes.js +68 -0
  246. package/src/lib/usage-axes.js.map +1 -0
  247. package/src/lib/components/use-emails-hub-path.d.ts +0 -24
  248. package/src/lib/components/use-emails-hub-path.js +0 -59
  249. package/src/lib/components/use-emails-hub-path.js.map +0 -1
@@ -0,0 +1,271 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * What the CRM resources of `/v1` have in common (AGL-2606).
19
+ *
20
+ * Companies, pipelines, deals, tasks and activities are five collections
21
+ * with one shape (`CrmScoped`), one scope model (`crmScopeTokens`) and one
22
+ * set of questions every write has to answer — which site is this for, is
23
+ * that uid a member here, does the contact it names exist. Each handler
24
+ * module answers those through this file, so the answer cannot differ by
25
+ * resource.
26
+ *
27
+ * ## Reads are org-wide, writes name a site
28
+ *
29
+ * An API key is an ORGANIZATION credential (`api-keys.ts`), so a list here
30
+ * reads the whole collection: every company the org's sites know, whichever
31
+ * site created it. That is the same answer `GET /v1/contacts` gives, and it
32
+ * is right for the caller the key represents — an integration acting for the
33
+ * account, not a member sitting on one site.
34
+ *
35
+ * A write is different, because a CRM record carries `visibleTo` and the
36
+ * console enforces it: a record stamped for the wrong scope is a record a
37
+ * site's own team cannot open. So every create names the site it belongs to
38
+ * — `consentSiteId`, the parameter the contacts write already uses for the
39
+ * same question — and the record is stamped exactly as that site's console
40
+ * would stamp it ({@link crmCreateStamp}). There is no default site for the
41
+ * reason the contacts write gives: picking the org's only site works until
42
+ * the org has two.
43
+ */
44
+ import { type ContactFieldDefinition, type CrmCollection, type CrmCustomValue, type CrmFieldObject, type CrmPicklist } from '@aglyn/aglyn/server';
45
+ import { Timestamp } from 'firebase-admin/firestore';
46
+ import { type ApiV1Context } from '@aglyn/tenant-data-admin/server/api-v1-kit';
47
+ /** A title, a name — one line. */
48
+ export declare const CRM_TITLE_MAX = 200;
49
+ /** Free text — notes, a body, a lost reason. */
50
+ export declare const CRM_TEXT_MAX = 5000;
51
+ /** A short label — an industry, an outcome, a job title. */
52
+ export declare const CRM_LABEL_MAX = 120;
53
+ /** A stored id — Firestore's own ceiling is 1,500 bytes; nothing here is close. */
54
+ export declare const CRM_ID_MAX = 200;
55
+ /**
56
+ * `orgs/{orgId}/{name}` — every CRM collection sits directly under the org.
57
+ *
58
+ * Nothing in this module dereferences `CRM_COLLECTIONS` at load time, on
59
+ * purpose: the `/v1` suites mock `@aglyn/aglyn/server` as a closed world,
60
+ * and a module-level `CRM_COLLECTIONS.x` would throw at import in every
61
+ * suite that never asked for the CRM, reading as a failure of theirs.
62
+ */
63
+ export declare function crmCollection(ctx: ApiV1Context, name: CrmCollection): FirebaseFirestore.CollectionReference;
64
+ /**
65
+ * The records band, asked before a company or a deal is created (AGL-2611).
66
+ *
67
+ * `null` when there is room; the refusal otherwise, in the shape
68
+ * `POST /v1/contacts` has always answered at the same band — `plan_required`
69
+ * with a code, because on the one plan that refuses (Free) an upgrade is the
70
+ * whole remedy, and every paid plan meters the excess instead. The measure
71
+ * is the same three aggregates the monthly rollup bills from, so the API
72
+ * and the invoice cannot disagree about whether the band was full.
73
+ */
74
+ export declare function crmRecordsBandRefusal(ctx: ApiV1Context): Promise<Response | null>;
75
+ /**
76
+ * The `consentSiteId` a create carries, validated.
77
+ *
78
+ * Required, and required to be one of the organization's own sites, for the
79
+ * reason the module header gives. The message names the field so an
80
+ * integrator sees the same key they would on a contact write.
81
+ */
82
+ export declare function readCrmSite(ctx: ApiV1Context, noun: string, body: Record<string, unknown>): {
83
+ siteId: string;
84
+ } | {
85
+ response: Response;
86
+ };
87
+ /**
88
+ * The fields every CRM create stamps, for a record created from `siteId`.
89
+ *
90
+ * `visibleTo` is `crmScopeTokens` — the contact create path's own expression,
91
+ * which is what puts an API-created deal in exactly the scope a deal created
92
+ * from that site's console would land in. `hostId` is provenance and is never
93
+ * rewritten. `createdByUid` is the literal `'api'` because a key has no uid;
94
+ * it is the attribution the contact create makes through `sources.api`, in
95
+ * the field the CRM types already carry.
96
+ *
97
+ * Both timestamps are set here, once, so a fresh record's `updated` equals
98
+ * its `created` and an `updatedAfter` sweep that started before the create
99
+ * picks it up.
100
+ */
101
+ export declare function crmCreateStamp(ctx: ApiV1Context, siteId: string): {
102
+ visibleTo: import("@aglyn/aglyn/src").ScopeToken[];
103
+ hostId: string;
104
+ createdByUid: string;
105
+ createdAt: Timestamp;
106
+ updatedAt: Timestamp;
107
+ };
108
+ /**
109
+ * Whether `uid` is a member of the authenticated organization.
110
+ *
111
+ * One document read at `orgs/{orgId}/members/{uid}` — the roster
112
+ * `listOrgMembers` reads whole answers the same question for the price of
113
+ * the whole roster, and a create should not pay for members it never asked
114
+ * about.
115
+ */
116
+ export declare function isOrgMember(ctx: ApiV1Context, uid: string): Promise<boolean>;
117
+ /** A clearable scalar: a value to write, `null` to delete, absent to leave. */
118
+ export type Clearable<T> = T | null | undefined;
119
+ /**
120
+ * The membership check for an owner or assignee, run after the synchronous
121
+ * grammar so a body that is already refused never spends the read.
122
+ */
123
+ export declare function memberError(ctx: ApiV1Context, field: string, uid: Clearable<string>): Promise<Record<string, string>>;
124
+ /**
125
+ * `values` as an `update()` payload: `null` becomes a field delete, an
126
+ * `undefined` is left out, so an omitted key leaves the field alone and an
127
+ * explicit `null` clears it — the PATCH contract every resource here shares.
128
+ */
129
+ export declare function updatePayload(values: Record<string, unknown>): Record<string, unknown>;
130
+ /**
131
+ * `values` as a `create()` payload: `null` and `undefined` are both left out,
132
+ * because a fresh document has nothing to clear.
133
+ */
134
+ export declare function createPayload(values: Record<string, unknown>): Record<string, unknown>;
135
+ /** The three records a CRM row can point at, by the field that points. */
136
+ export interface CrmRefs {
137
+ contactId?: string;
138
+ companyId?: string;
139
+ dealId?: string;
140
+ }
141
+ /**
142
+ * Each reference in `refs` that points at nothing, as `field → reason`.
143
+ *
144
+ * A dangling id is refused rather than stored because the console renders a
145
+ * task by the contact it names, and a task pointing at a contact that does
146
+ * not exist is a task nobody can find from anywhere. One read per reference
147
+ * given, none for a reference omitted.
148
+ */
149
+ export declare function crmRefErrors(ctx: ApiV1Context, refs: CrmRefs): Promise<Record<string, string>>;
150
+ /**
151
+ * An ISO 8601 instant as milliseconds, or `null` when the value is not one.
152
+ *
153
+ * `Date.parse` is permissive — it takes `"2026"` and `"March 3"` — and a
154
+ * client sending a bare date almost always means midnight in THEIR zone,
155
+ * which we cannot know. So only the full form is accepted: a date, a `T`, a
156
+ * time and an offset (`Z` or `±hh:mm`). Anything less answers `null` and the
157
+ * caller names the field.
158
+ */
159
+ export declare function parseIsoInstant(value: unknown): number | null;
160
+ /** Milliseconds as ISO 8601, or `null` for anything that is not a finite number. */
161
+ export declare function isoFromMs(value: unknown): string | null;
162
+ /** The `created`/`updated` pair every CRM view publishes. */
163
+ export declare function crmTimes(data: FirebaseFirestore.DocumentData): {
164
+ created: string;
165
+ updated: string;
166
+ };
167
+ /**
168
+ * A trimmed, bounded string from a body field, or `null` for an explicit
169
+ * `null`, or `undefined` when the field was not sent.
170
+ *
171
+ * `null` is how a PATCH clears an optional field — the caller turns it into
172
+ * a delete — so it has to survive as a value distinct from "absent". A
173
+ * number or a boolean is not silently stringified: the field is named.
174
+ */
175
+ export declare function readOptionalText(body: Record<string, unknown>, key: string, max: number, errors: Record<string, string>): string | null | undefined;
176
+ /** One of a fixed list, or the field is named with the list. */
177
+ export declare function readChoice<T extends string>(body: Record<string, unknown>, key: string, choices: readonly T[], errors: Record<string, string>): T | undefined;
178
+ /**
179
+ * A reference id from a body field — trimmed and bounded, `null` to clear.
180
+ *
181
+ * Existence is the caller's question ({@link crmRefErrors}); this only rules
182
+ * out a value that could never be an id.
183
+ */
184
+ export declare function readRefId(body: Record<string, unknown>, key: string, errors: Record<string, string>): string | null | undefined;
185
+ /** Every key of `body` that is not in `allowed`, named as not writable. */
186
+ export declare function refuseUnknownKeys(body: Record<string, unknown>, allowed: ReadonlySet<string>, noun: string, errors: Record<string, string>): void;
187
+ /** The `400 validation_failed` every CRM write and filter answers with. */
188
+ export declare function crmValidationFailed(ctx: ApiV1Context, noun: string, fields: Record<string, string>): Response;
189
+ /** One `?field=value` a list accepts, in the order it prefers to index them. */
190
+ export interface CrmEqualityFilter {
191
+ field: string;
192
+ value: string;
193
+ }
194
+ /**
195
+ * The CRM list grammar: at most one Firestore clause, the rest on the page.
196
+ *
197
+ * ## Why one clause
198
+ *
199
+ * Every equality here has a single-field index, and every list in this API
200
+ * ends in `orderBy(__name__)`, so ONE `where` is served without anything
201
+ * shipped. Two equalities, or an equality beside a range, is a composite
202
+ * index per combination — five resources with four filters each is more
203
+ * indexes than the whole product carries today, every one a query that
204
+ * answers `500` in production until it is deployed by hand. So the FIRST
205
+ * filter in `filters` goes to Firestore, and the caller orders them by
206
+ * selectivity: an id before a status, because `?dealId=` selects a handful
207
+ * of rows and `?status=open` selects most of the collection. The rest are
208
+ * applied to the page, which can come back short — the
209
+ * [short page](apps/docs/api/conventions.md) every filtered list already
210
+ * documents.
211
+ *
212
+ * ## `updatedAfter` changes the order
213
+ *
214
+ * The one range filter, and the one filter that cannot be a post-filter
215
+ * without becoming useless: a sync asking "what changed since T" over an
216
+ * id-ordered list still walks the whole collection to find out. So when it is
217
+ * given the list is ordered by `updated` ascending (then id), the range goes
218
+ * to Firestore, and EVERY equality moves to the page — a range on one field
219
+ * beside an equality on another is the composite index above. The cursor
220
+ * carries the timestamp and the id together, at Firestore's own precision,
221
+ * because a millisecond cursor would replay a row updated in the same
222
+ * millisecond on the next page.
223
+ */
224
+ export declare function listCrm(ctx: ApiV1Context, collection: FirebaseFirestore.CollectionReference, url: URL, filters: CrmEqualityFilter[], view: (doc: FirebaseFirestore.QueryDocumentSnapshot) => unknown): Promise<Response>;
225
+ /**
226
+ * The equality filters a list URL carries, in the caller's order, with an
227
+ * empty value meaning "absent" as every filter in this API does.
228
+ */
229
+ export declare function readEqualityFilters(url: URL, fields: readonly string[]): CrmEqualityFilter[];
230
+ /**
231
+ * The org's custom field definitions, retired ones included and every
232
+ * object's, for a resource's `custom` validation (AGL-2661).
233
+ *
234
+ * One bounded read of a small collection, paid only when a body carries
235
+ * `custom` — the contacts resource's own reader, shared so the three
236
+ * resources judge a map the same way. Retired definitions come back too
237
+ * because the reader refuses a write under one BY NAME, which is a better
238
+ * answer than "no such field" for a key the integration wrote last month.
239
+ */
240
+ export declare function readOrgFieldDefinitions(ctx: ApiV1Context): Promise<ContactFieldDefinition[]>;
241
+ /**
242
+ * The org's lead source list (AGL-3298) — the values a lead's
243
+ * `leadSource` is restricted to on every write through the API.
244
+ */
245
+ export declare function readOrgLeadSourcePicklist(ctx: ApiV1Context): Promise<CrmPicklist>;
246
+ /**
247
+ * A body's `custom` map, judged against the org's definitions for ONE
248
+ * object, or the refusals to answer with (AGL-2661).
249
+ *
250
+ * `undefined` when the body carried no map — nothing to validate and
251
+ * nothing to write. The read happens only then, so a body without
252
+ * `custom` pays for no definitions.
253
+ */
254
+ export declare function readCrmCustomBody(ctx: ApiV1Context, body: Record<string, unknown>, object: CrmFieldObject): Promise<{
255
+ values: Record<string, CrmCustomValue>;
256
+ } | {
257
+ errors: Record<string, string>;
258
+ } | undefined>;
259
+ /**
260
+ * A stored `custom` map as published: the map itself, or `{}` on a record
261
+ * that has none, so a client can index it without a guard (AGL-2661).
262
+ */
263
+ export declare function crmCustomView(data: FirebaseFirestore.DocumentData): Record<string, CrmCustomValue>;
264
+ /**
265
+ * The `custom` values as a PATCH writes them: one dotted path per key, so
266
+ * the stored map is merged rather than replaced — `custom` on a PATCH
267
+ * means "these keys", and an integration correcting one value must not
268
+ * have to resend the other nine to keep them. A `null` stays a `null`:
269
+ * the key remains, explicitly cleared, the shape a `where` can find.
270
+ */
271
+ export declare function crmCustomUpdate(values: Record<string, CrmCustomValue>): Record<string, unknown>;