@aglyn/tenant-data-admin 1.0.0-beta.217 → 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 (122) hide show
  1. package/package.json +9 -9
  2. package/src/lib/server/account-lock-mail.d.ts +74 -0
  3. package/src/lib/server/account-lock-mail.js +139 -0
  4. package/src/lib/server/account-lock-mail.js.map +1 -0
  5. package/src/lib/server/account-mail.d.ts +41 -0
  6. package/src/lib/server/account-mail.js +52 -0
  7. package/src/lib/server/account-mail.js.map +1 -0
  8. package/src/lib/server/api-v1-description.d.ts +112 -0
  9. package/src/lib/server/api-v1-description.js +126 -0
  10. package/src/lib/server/api-v1-description.js.map +1 -0
  11. package/src/lib/server/api-v1-kit.d.ts +156 -0
  12. package/src/lib/server/api-v1-kit.js +191 -0
  13. package/src/lib/server/api-v1-kit.js.map +1 -0
  14. package/src/lib/server/api-v1-resources.d.ts +144 -0
  15. package/src/lib/server/api-v1-resources.js +126 -0
  16. package/src/lib/server/api-v1-resources.js.map +1 -0
  17. package/src/lib/server/consent-group-change.d.ts +2 -1
  18. package/src/lib/server/consent-group-change.js +41 -16
  19. package/src/lib/server/consent-group-change.js.map +1 -1
  20. package/src/lib/server/contact-email-index.d.ts +22 -0
  21. package/src/lib/server/contact-email-index.js +108 -0
  22. package/src/lib/server/contact-email-index.js.map +1 -1
  23. package/src/lib/server/crm-email-activity.d.ts +1 -1
  24. package/src/lib/server/crm-email-activity.js +1 -1
  25. package/src/lib/server/crm-email-activity.js.map +1 -1
  26. package/src/lib/server/crm-inbound-email.d.ts +23 -0
  27. package/src/lib/server/crm-inbound-email.js +1 -16
  28. package/src/lib/server/crm-inbound-email.js.map +1 -1
  29. package/src/lib/server/crm-records.d.ts +9 -1
  30. package/src/lib/server/crm-records.js +19 -2
  31. package/src/lib/server/crm-records.js.map +1 -1
  32. package/src/lib/server/cron-auth.d.ts +44 -0
  33. package/src/lib/server/cron-auth.js +76 -0
  34. package/src/lib/server/cron-auth.js.map +1 -0
  35. package/src/lib/server/dataset-live-pages.js +6 -6
  36. package/src/lib/server/dataset-live-pages.js.map +1 -1
  37. package/src/lib/server/duplicate-resource.d.ts +26 -0
  38. package/src/lib/server/duplicate-resource.js +47 -25
  39. package/src/lib/server/duplicate-resource.js.map +1 -1
  40. package/src/lib/server/dynamic-list-materialize.js +4 -1
  41. package/src/lib/server/dynamic-list-materialize.js.map +1 -1
  42. package/src/lib/server/email-deliverability.js +21 -6
  43. package/src/lib/server/email-deliverability.js.map +1 -1
  44. package/src/lib/server/email-suppression.d.ts +83 -3
  45. package/src/lib/server/email-suppression.js +229 -3
  46. package/src/lib/server/email-suppression.js.map +1 -1
  47. package/src/lib/server/email-unsubscribe-link.d.ts +8 -5
  48. package/src/lib/server/email-unsubscribe-link.js +7 -7
  49. package/src/lib/server/email-unsubscribe-link.js.map +1 -1
  50. package/src/lib/server/erase.d.ts +19 -33
  51. package/src/lib/server/erase.js +37 -162
  52. package/src/lib/server/erase.js.map +1 -1
  53. package/src/lib/server/host-email-tokens.js +1 -1
  54. package/src/lib/server/host-email-tokens.js.map +1 -1
  55. package/src/lib/server/host-memberships.js +3 -3
  56. package/src/lib/server/host-memberships.js.map +1 -1
  57. package/src/lib/server/lockdown.js +2 -0
  58. package/src/lib/server/lockdown.js.map +1 -1
  59. package/src/lib/server/media-download-tokens.js +2 -1
  60. package/src/lib/server/media-download-tokens.js.map +1 -1
  61. package/src/lib/server/member-email-aliases.d.ts +1 -1
  62. package/src/lib/server/member-email-aliases.js +1 -1
  63. package/src/lib/server/member-email-aliases.js.map +1 -1
  64. package/src/lib/server/new-account-announcement.d.ts +62 -0
  65. package/src/lib/server/new-account-announcement.js +77 -0
  66. package/src/lib/server/new-account-announcement.js.map +1 -0
  67. package/src/lib/server/notifications.d.ts +8 -2
  68. package/src/lib/server/notifications.js +18 -4
  69. package/src/lib/server/notifications.js.map +1 -1
  70. package/src/lib/server/org-member-notice.js +5 -1
  71. package/src/lib/server/org-member-notice.js.map +1 -1
  72. package/src/lib/server/organizations.js +4 -0
  73. package/src/lib/server/organizations.js.map +1 -1
  74. package/src/lib/server/{connect-payout-failure.d.ts → payment-provider-stripe-connect-payouts.d.ts} +3 -0
  75. package/src/lib/server/{connect-payout-failure.js → payment-provider-stripe-connect-payouts.js} +1 -1
  76. package/src/lib/server/payment-provider-stripe-connect-payouts.js.map +1 -0
  77. package/src/lib/server/{connect-account-status.d.ts → payment-provider-stripe-connect-status.d.ts} +3 -0
  78. package/src/lib/server/{connect-account-status.js → payment-provider-stripe-connect-status.js} +1 -1
  79. package/src/lib/server/payment-provider-stripe-connect-status.js.map +1 -0
  80. package/src/lib/server/{stripe-account-mode.d.ts → payment-provider-stripe-connect.d.ts} +45 -70
  81. package/src/lib/server/{stripe-account-mode.js → payment-provider-stripe-connect.js} +78 -52
  82. package/src/lib/server/payment-provider-stripe-connect.js.map +1 -0
  83. package/src/lib/server/payment-provider.d.ts +173 -0
  84. package/src/lib/server/payment-provider.js +70 -0
  85. package/src/lib/server/payment-provider.js.map +1 -0
  86. package/src/lib/server/personal-data-export.js +1 -1
  87. package/src/lib/server/personal-data-export.js.map +1 -1
  88. package/src/lib/server/rate-limit-store.js +13 -3
  89. package/src/lib/server/rate-limit-store.js.map +1 -1
  90. package/src/lib/server/realm-plugins.js +8 -3
  91. package/src/lib/server/realm-plugins.js.map +1 -1
  92. package/src/lib/server/retained-refusals.d.ts +142 -0
  93. package/src/lib/server/retained-refusals.js +325 -0
  94. package/src/lib/server/retained-refusals.js.map +1 -0
  95. package/src/lib/server/risk-notice.js +21 -3
  96. package/src/lib/server/risk-notice.js.map +1 -1
  97. package/src/lib/server/serve-media-cdn.js +2 -2
  98. package/src/lib/server/serve-media-cdn.js.map +1 -1
  99. package/src/lib/server/serve-plugin-fetch.d.ts +17 -7
  100. package/src/lib/server/serve-plugin-fetch.js +19 -8
  101. package/src/lib/server/serve-plugin-fetch.js.map +1 -1
  102. package/src/lib/server/test-firestore-queries.d.ts +7 -0
  103. package/src/lib/server/test-firestore-queries.js +70 -27
  104. package/src/lib/server/test-firestore-queries.js.map +1 -1
  105. package/src/lib/server/upsert-contact.d.ts +46 -1
  106. package/src/lib/server/upsert-contact.js +85 -18
  107. package/src/lib/server/upsert-contact.js.map +1 -1
  108. package/src/tenancy.d.ts +4 -5
  109. package/src/tenancy.js +11 -6
  110. package/src/tenancy.js.map +1 -1
  111. package/src/lib/server/connect-account-status.js.map +0 -1
  112. package/src/lib/server/connect-payout-failure.js.map +0 -1
  113. package/src/lib/server/contact-lifecycle-floor.d.ts +0 -89
  114. package/src/lib/server/contact-lifecycle-floor.js +0 -91
  115. package/src/lib/server/contact-lifecycle-floor.js.map +0 -1
  116. package/src/lib/server/contact-merge.d.ts +0 -55
  117. package/src/lib/server/contact-merge.js +0 -288
  118. package/src/lib/server/contact-merge.js.map +0 -1
  119. package/src/lib/server/form-dataset-binding-token.d.ts +0 -35
  120. package/src/lib/server/form-dataset-binding-token.js +0 -107
  121. package/src/lib/server/form-dataset-binding-token.js.map +0 -1
  122. package/src/lib/server/stripe-account-mode.js.map +0 -1
@@ -1,288 +0,0 @@
1
- import { _ as _extends } from "@swc/helpers/_/_extends";
2
- /**
3
- * @license
4
- * Copyright 2026 Aglyn LLC
5
- *
6
- * Licensed under the Apache License, Version 2.0 (the "License");
7
- * you may not use this file except in compliance with the License.
8
- * You may obtain a copy of the License at
9
- *
10
- * http://www.apache.org/licenses/LICENSE-2.0
11
- *
12
- * Unless required by applicable law or agreed to in writing, software
13
- * distributed under the License is distributed on an "AS IS" BASIS,
14
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
- * See the License for the specific language governing permissions and
16
- * limitations under the License.
17
- */ /**
18
- * MERGING TWO CONTACTS — the write (AGL-2625).
19
- *
20
- * `planContactMerge` says what the survivor becomes; this performs it, and
21
- * moves everything that pointed at the merged record. One function behind
22
- * two doors — the console's `crm/contacts-merge` route and
23
- * `POST /v1/contacts/{id}/merge` — so the REST caller and the record page
24
- * cannot merge two different ways.
25
- *
26
- * ## The order is the crash-safety argument
27
- *
28
- * 1. Every deal, task and activity naming the merged contact is repointed
29
- * at the survivor, and every lead converted into it. Batched, bounded,
30
- * idempotent: a re-run finds nothing left to move.
31
- * 2. Then, in ONE transaction: the survivor takes the plan, every address
32
- * it now answers to is indexed at it, and the merged document is
33
- * deleted — with both records re-read inside the transaction, so two
34
- * merges racing on one pair cannot both succeed and a plan is never
35
- * computed on a document that changed under it.
36
- *
37
- * Children first because a process that dies between the two steps leaves
38
- * children pointing at a record that EXISTS — the survivor — while the merged
39
- * document is still there for a retry to fold in. The other order would
40
- * leave deals pointing at a deleted contact, which is a record nobody can
41
- * open from anywhere.
42
- *
43
- * ## What it does not move
44
- *
45
- * List memberships, suppression entries and the delivery log are keyed by
46
- * ADDRESS, not by contact id, and stay under the merged address; a capture
47
- * on that address resolves to the survivor through the index, which is the
48
- * fact those keys need. A company's contacts count drops only for a company
49
- * both records named — the survivor still names the rest.
50
- */ import { CONTACT_EMAIL_INDEX_COLLECTION, contactEmails } from "@aglyn/aglyn/app-utils/contacts";
51
- import { planContactMerge } from "@aglyn/aglyn/app-utils/contact-merge";
52
- import { CRM_COLLECTIONS } from "@aglyn/aglyn/app-utils/crm";
53
- import { personKey } from "@aglyn/aglyn/app-utils/person-key";
54
- import { FieldValue } from "firebase-admin/firestore";
55
- import { settleCompanyContactsCounts } from "./contact-company-link.js";
56
- import { recomputeCrmNextTaskAt } from "./crm-next-activity.js";
57
- import { restampCrmListFieldsAt } from "./crm-records.js";
58
- import { logHostActivity } from "./organizations.js";
59
- /** How many pointing rows one repoint pass reads — and one batch writes. */ const REPOINT_PAGE = 400;
60
- /** A ceiling on passes, so a query that keeps answering cannot loop forever. */ const REPOINT_PASSES = 25;
61
- /**
62
- * Point every row of `collection` whose `contactId` is `from` at `to`.
63
- *
64
- * Page, batch, repeat: each pass reads the rows still pointing at the
65
- * merged record — the ones a previous pass moved no longer match — so the
66
- * loop ends when a pass finds nothing. `updatedAt` moves with the pointer
67
- * because an integration syncing by `updatedAfter` has to see the change.
68
- */ async function repointContactId(firestore, collection, from, to) {
69
- let moved = 0;
70
- for(let pass = 0; pass < REPOINT_PASSES; pass += 1){
71
- const page = await collection.where('contactId', '==', from).limit(REPOINT_PAGE).get();
72
- if (page.empty) break;
73
- const batch = firestore.batch();
74
- for (const row of page.docs){
75
- batch.update(row.ref, {
76
- contactId: to,
77
- updatedAt: FieldValue.serverTimestamp()
78
- });
79
- }
80
- await batch.commit();
81
- moved += page.size;
82
- if (page.size < REPOINT_PAGE) break;
83
- }
84
- return moved;
85
- }
86
- /**
87
- * The leads converted into the merged contact, restamped at the survivor.
88
- *
89
- * A lead lives at `orgs/{orgId}/leads/{personKey(email)}` (AGL-3275), so there
90
- * is still no query — one document per address, asked for directly. This used
91
- * to be bounded by sites TIMES addresses because a lead lived under each site;
92
- * it is bounded by addresses now, and the sites drop out of the signature
93
- * because the collection no longer has one per site to walk.
94
- */ async function repointLeads(orgRef, emails, from, to) {
95
- let moved = 0;
96
- /*
97
- * ONE ROW PER ADDRESS, on the org (AGL-3275). This walked the sites because
98
- * there was a lead under each of them; re-pointing the same document once
99
- * per site would now be the same write repeated, and the second would find
100
- * `convertedContactId` already moved and skip — a silent undercount rather
101
- * than a wrong answer, but an undercount the caller reports to the operator.
102
- */ for (const email of emails){
103
- const key = personKey(email);
104
- if (!key) continue;
105
- const leadRef = orgRef.collection('leads').doc(key);
106
- const lead = await leadRef.get();
107
- if (!lead.exists || lead.get('convertedContactId') !== from) continue;
108
- await leadRef.update({
109
- convertedContactId: to,
110
- updatedAt: FieldValue.serverTimestamp()
111
- });
112
- moved += 1;
113
- }
114
- return moved;
115
- }
116
- function strings(value) {
117
- return Array.isArray(value) ? value.filter((item)=>typeof item === 'string' && item !== '') : [];
118
- }
119
- export async function mergeContacts(options) {
120
- var _mergedBefore_data, _survivorBefore_data, _contactEmails_, _plan_emails_;
121
- const { firestore, orgRef, survivorId, mergedId, actor, hostId } = options;
122
- if (!survivorId || !mergedId || survivorId === mergedId) {
123
- return {
124
- ok: false,
125
- reason: 'same-record'
126
- };
127
- }
128
- const contactsRef = orgRef.collection('contacts');
129
- const survivorRef = contactsRef.doc(survivorId);
130
- const mergedRef = contactsRef.doc(mergedId);
131
- const [survivorBefore, mergedBefore] = await Promise.all([
132
- survivorRef.get(),
133
- mergedRef.get()
134
- ]);
135
- if (!survivorBefore.exists) return {
136
- ok: false,
137
- reason: 'survivor-missing'
138
- };
139
- if (!mergedBefore.exists) return {
140
- ok: false,
141
- reason: 'merged-missing'
142
- };
143
- const mergedData = (_mergedBefore_data = mergedBefore.data()) != null ? _mergedBefore_data : {};
144
- const survivorData = (_survivorBefore_data = survivorBefore.data()) != null ? _survivorBefore_data : {};
145
- /*==========================================
146
- * 1. THE CHILDREN — before the parent, for the reason the header gives.
147
- *=========================================*/ const [deals, tasks, activities] = await Promise.all([
148
- repointContactId(firestore, orgRef.collection(CRM_COLLECTIONS.deals), mergedId, survivorId),
149
- repointContactId(firestore, orgRef.collection(CRM_COLLECTIONS.tasks), mergedId, survivorId),
150
- repointContactId(firestore, orgRef.collection(CRM_COLLECTIONS.activities), mergedId, survivorId)
151
- ]);
152
- const leadHosts = [
153
- ...new Set([
154
- ...strings(mergedData['capturedByHostIds']),
155
- ...strings(survivorData['capturedByHostIds']),
156
- ...strings([
157
- mergedData['hostId'],
158
- survivorData['hostId'],
159
- hostId
160
- ])
161
- ])
162
- ];
163
- const leads = await repointLeads(orgRef, contactEmails(mergedData), mergedId, survivorId);
164
- /*==========================================
165
- * 2. THE SWAP — one transaction over both documents and the index.
166
- *=========================================*/ const index = orgRef.collection(CONTACT_EMAIL_INDEX_COLLECTION);
167
- const swapped = await firestore.runTransaction(async (transaction)=>{
168
- var _survivor_data, _merged_data, _survivor_data1;
169
- const [survivor, merged] = await Promise.all([
170
- transaction.get(survivorRef),
171
- transaction.get(mergedRef)
172
- ]);
173
- if (!survivor.exists) return {
174
- reason: 'survivor-missing'
175
- };
176
- if (!merged.exists) return {
177
- reason: 'merged-missing'
178
- };
179
- const plan = planContactMerge((_survivor_data = survivor.data()) != null ? _survivor_data : {}, (_merged_data = merged.data()) != null ? _merged_data : {});
180
- transaction.set(survivorRef, _extends({}, plan.survivor, {
181
- updatedAt: FieldValue.serverTimestamp()
182
- }), {
183
- merge: true
184
- });
185
- for (const email of plan.emails){
186
- const key = personKey(email);
187
- if (!key) continue;
188
- transaction.set(index.doc(key), {
189
- email,
190
- contactId: survivorId,
191
- updatedAt: FieldValue.serverTimestamp()
192
- }, {
193
- merge: true
194
- });
195
- }
196
- transaction.delete(mergedRef);
197
- return {
198
- plan,
199
- survivor: (_survivor_data1 = survivor.data()) != null ? _survivor_data1 : {}
200
- };
201
- });
202
- if ('reason' in swapped) return {
203
- ok: false,
204
- reason: swapped.reason
205
- };
206
- const { plan } = swapped;
207
- // The survivor now carries both records' addresses, holders and facets —
208
- // what the Contacts list searches and filters by (AGL-3321).
209
- await restampCrmListFieldsAt(survivorRef, 'contacts');
210
- /*
211
- * The companies both records named counted two people and now count one
212
- * (AGL-2613). Settled after the swap the way the capture settles its
213
- * counts: a figure that could not move is logged, and the company page's
214
- * live aggregate corrects it.
215
- */ await settleCompanyContactsCounts(orgRef.collection(CRM_COLLECTIONS.companies), {
216
- companyId: null,
217
- mirror: null,
218
- counts: plan.companyCounts
219
- });
220
- /*
221
- * The survivor now owns the merged record's open tasks too, so its next
222
- * activity is the earliest of both (AGL-2661). Settled the same way: a
223
- * figure that could not move is logged, and the Fields section's
224
- * recompute corrects it.
225
- */ await recomputeCrmNextTaskAt(firestore, orgRef.id, [
226
- {
227
- contactId: survivorId
228
- }
229
- ]).catch((error)=>{
230
- console.error('contact merge next activity could not be recomputed', survivorId, error);
231
- });
232
- const mergedEmail = (_contactEmails_ = contactEmails(mergedData)[0]) != null ? _contactEmails_ : '';
233
- const survivorEmail = (_plan_emails_ = plan.emails[0]) != null ? _plan_emails_ : '';
234
- // The canonical name — the one identity every holder shares — for the
235
- // feed; the address stands in for a person nobody has named.
236
- const survivorName = typeof swapped.survivor['name'] === 'string' ? swapped.survivor['name'] : '';
237
- /*
238
- * THE TIMELINE NOTE, on the survivor: the merge is an event in this
239
- * person's history, and the record page is where a reader looks for one.
240
- * Filed under the calling site, or the survivor's first site for a door
241
- * with none; its own catch, because a note that could not be written must
242
- * not read as a merge that did not happen.
243
- */ try {
244
- var _survivorData_hostId;
245
- await orgRef.collection(CRM_COLLECTIONS.activities).add(_extends({
246
- kind: 'note',
247
- body: `Merged with ${mergedEmail}`,
248
- atMs: Date.now(),
249
- byUid: actor.uid
250
- }, options.actorName || actor.email ? {
251
- byName: options.actorName || actor.email
252
- } : {}, {
253
- contactId: survivorId,
254
- visibleTo: plan.visibleTo,
255
- hostId: hostId != null ? hostId : String((_survivorData_hostId = survivorData['hostId']) != null ? _survivorData_hostId : ''),
256
- createdByUid: actor.uid,
257
- createdAt: FieldValue.serverTimestamp(),
258
- updatedAt: FieldValue.serverTimestamp()
259
- }));
260
- } catch (error) {
261
- console.error('[contact-merge] timeline note failed', survivorId, error);
262
- }
263
- // The audit line (AGL-2622), in the feed of the site whose console did
264
- // the work. A door with no site — the API — has no host feed to write.
265
- if (hostId) {
266
- await logHostActivity(hostId, actor, `Merged with ${mergedEmail}`, {
267
- type: 'contact',
268
- id: survivorId,
269
- name: survivorName || survivorEmail
270
- });
271
- }
272
- return {
273
- ok: true,
274
- survivorId,
275
- survivorEmail,
276
- mergedId,
277
- mergedEmail,
278
- emails: plan.emails,
279
- repointed: {
280
- deals,
281
- tasks,
282
- activities,
283
- leads
284
- }
285
- };
286
- }
287
-
288
- //# sourceMappingURL=contact-merge.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/contact-merge.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * MERGING TWO CONTACTS — the write (AGL-2625).\n *\n * `planContactMerge` says what the survivor becomes; this performs it, and\n * moves everything that pointed at the merged record. One function behind\n * two doors — the console's `crm/contacts-merge` route and\n * `POST /v1/contacts/{id}/merge` — so the REST caller and the record page\n * cannot merge two different ways.\n *\n * ## The order is the crash-safety argument\n *\n * 1. Every deal, task and activity naming the merged contact is repointed\n * at the survivor, and every lead converted into it. Batched, bounded,\n * idempotent: a re-run finds nothing left to move.\n * 2. Then, in ONE transaction: the survivor takes the plan, every address\n * it now answers to is indexed at it, and the merged document is\n * deleted — with both records re-read inside the transaction, so two\n * merges racing on one pair cannot both succeed and a plan is never\n * computed on a document that changed under it.\n *\n * Children first because a process that dies between the two steps leaves\n * children pointing at a record that EXISTS — the survivor — while the merged\n * document is still there for a retry to fold in. The other order would\n * leave deals pointing at a deleted contact, which is a record nobody can\n * open from anywhere.\n *\n * ## What it does not move\n *\n * List memberships, suppression entries and the delivery log are keyed by\n * ADDRESS, not by contact id, and stay under the merged address; a capture\n * on that address resolves to the survivor through the index, which is the\n * fact those keys need. A company's contacts count drops only for a company\n * both records named — the survivor still names the rest.\n */\n\nimport {\n CONTACT_EMAIL_INDEX_COLLECTION,\n contactEmails,\n} from '@aglyn/aglyn/app-utils/contacts'\nimport type { HostActivityActor } from '@aglyn/aglyn/app-utils/activity-presenter'\nimport { planContactMerge } from '@aglyn/aglyn/app-utils/contact-merge'\nimport { CRM_COLLECTIONS } from '@aglyn/aglyn/app-utils/crm'\nimport { personKey } from '@aglyn/aglyn/app-utils/person-key'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { settleCompanyContactsCounts } from './contact-company-link'\nimport { recomputeCrmNextTaskAt } from './crm-next-activity'\nimport { restampCrmListFieldsAt } from './crm-records'\nimport { logHostActivity } from './organizations'\n\n/** How many pointing rows one repoint pass reads — and one batch writes. */\nconst REPOINT_PAGE = 400\n/** A ceiling on passes, so a query that keeps answering cannot loop forever. */\nconst REPOINT_PASSES = 25\n\nexport interface MergeContactsOptions {\n firestore: FirebaseFirestore.Firestore\n /** `orgs/{orgId}` — the org whose contacts both records are. */\n orgRef: FirebaseFirestore.DocumentReference\n /** The record that stays, and keeps its address as the identity. */\n survivorId: string\n /** The record folded in and deleted. */\n mergedId: string\n actor: HostActivityActor\n /**\n * The site whose console did the work: where the activity feed entry\n * lands, and the site a timeline note is filed under. `null` for a door\n * with no site — the REST API — which logs no host entry.\n */\n hostId: string | null\n /** The actor's display name for the timeline note, when the door knows it. */\n actorName?: string | null\n}\n\nexport interface MergeContactsRepointed {\n deals: number\n tasks: number\n activities: number\n leads: number\n}\n\nexport type MergeContactsResult =\n | {\n ok: true\n survivorId: string\n survivorEmail: string\n mergedId: string\n mergedEmail: string\n /** Every address the survivor answers to now, primary first. */\n emails: string[]\n repointed: MergeContactsRepointed\n }\n | { ok: false; reason: 'same-record' | 'survivor-missing' | 'merged-missing' }\n\n/**\n * Point every row of `collection` whose `contactId` is `from` at `to`.\n *\n * Page, batch, repeat: each pass reads the rows still pointing at the\n * merged record — the ones a previous pass moved no longer match — so the\n * loop ends when a pass finds nothing. `updatedAt` moves with the pointer\n * because an integration syncing by `updatedAfter` has to see the change.\n */\nasync function repointContactId(\n firestore: FirebaseFirestore.Firestore,\n collection: FirebaseFirestore.CollectionReference,\n from: string,\n to: string,\n): Promise<number> {\n let moved = 0\n for (let pass = 0; pass < REPOINT_PASSES; pass += 1) {\n const page = await collection\n .where('contactId', '==', from)\n .limit(REPOINT_PAGE)\n .get()\n if (page.empty) break\n const batch = firestore.batch()\n for (const row of page.docs) {\n batch.update(row.ref, {\n contactId: to,\n updatedAt: FieldValue.serverTimestamp(),\n })\n }\n await batch.commit()\n moved += page.size\n if (page.size < REPOINT_PAGE) break\n }\n return moved\n}\n\n/**\n * The leads converted into the merged contact, restamped at the survivor.\n *\n * A lead lives at `orgs/{orgId}/leads/{personKey(email)}` (AGL-3275), so there\n * is still no query — one document per address, asked for directly. This used\n * to be bounded by sites TIMES addresses because a lead lived under each site;\n * it is bounded by addresses now, and the sites drop out of the signature\n * because the collection no longer has one per site to walk.\n */\nasync function repointLeads(\n orgRef: FirebaseFirestore.DocumentReference,\n emails: readonly string[],\n from: string,\n to: string,\n): Promise<number> {\n let moved = 0\n /*\n * ONE ROW PER ADDRESS, on the org (AGL-3275). This walked the sites because\n * there was a lead under each of them; re-pointing the same document once\n * per site would now be the same write repeated, and the second would find\n * `convertedContactId` already moved and skip — a silent undercount rather\n * than a wrong answer, but an undercount the caller reports to the operator.\n */\n for (const email of emails) {\n const key = personKey(email)\n if (!key) continue\n const leadRef = orgRef.collection('leads').doc(key)\n const lead = await leadRef.get()\n if (!lead.exists || lead.get('convertedContactId') !== from) continue\n await leadRef.update({\n convertedContactId: to,\n updatedAt: FieldValue.serverTimestamp(),\n })\n moved += 1\n }\n return moved\n}\n\nfunction strings(value: unknown): string[] {\n return Array.isArray(value)\n ? value.filter((item): item is string => typeof item === 'string' && item !== '')\n : []\n}\n\nexport async function mergeContacts(\n options: MergeContactsOptions,\n): Promise<MergeContactsResult> {\n const { firestore, orgRef, survivorId, mergedId, actor, hostId } = options\n if (!survivorId || !mergedId || survivorId === mergedId) {\n return { ok: false, reason: 'same-record' }\n }\n const contactsRef = orgRef.collection('contacts')\n const survivorRef = contactsRef.doc(survivorId)\n const mergedRef = contactsRef.doc(mergedId)\n const [survivorBefore, mergedBefore] = await Promise.all([\n survivorRef.get(),\n mergedRef.get(),\n ])\n if (!survivorBefore.exists) return { ok: false, reason: 'survivor-missing' }\n if (!mergedBefore.exists) return { ok: false, reason: 'merged-missing' }\n const mergedData = (mergedBefore.data() ?? {}) as Record<string, unknown>\n const survivorData = (survivorBefore.data() ?? {}) as Record<string, unknown>\n\n /*==========================================\n * 1. THE CHILDREN — before the parent, for the reason the header gives.\n *=========================================*/\n const [deals, tasks, activities] = await Promise.all([\n repointContactId(firestore, orgRef.collection(CRM_COLLECTIONS.deals), mergedId, survivorId),\n repointContactId(firestore, orgRef.collection(CRM_COLLECTIONS.tasks), mergedId, survivorId),\n repointContactId(\n firestore,\n orgRef.collection(CRM_COLLECTIONS.activities),\n mergedId,\n survivorId,\n ),\n ])\n const leadHosts = [\n ...new Set([\n ...strings(mergedData['capturedByHostIds']),\n ...strings(survivorData['capturedByHostIds']),\n ...strings([mergedData['hostId'], survivorData['hostId'], hostId]),\n ]),\n ]\n const leads = await repointLeads(\n orgRef,\n contactEmails(mergedData),\n mergedId,\n survivorId,\n )\n\n /*==========================================\n * 2. THE SWAP — one transaction over both documents and the index.\n *=========================================*/\n const index = orgRef.collection(CONTACT_EMAIL_INDEX_COLLECTION)\n const swapped = await firestore.runTransaction(async (transaction) => {\n const [survivor, merged] = await Promise.all([\n transaction.get(survivorRef),\n transaction.get(mergedRef),\n ])\n if (!survivor.exists) return { reason: 'survivor-missing' as const }\n if (!merged.exists) return { reason: 'merged-missing' as const }\n const plan = planContactMerge(\n (survivor.data() ?? {}) as Record<string, unknown>,\n (merged.data() ?? {}) as Record<string, unknown>,\n )\n transaction.set(\n survivorRef,\n { ...plan.survivor, updatedAt: FieldValue.serverTimestamp() },\n { merge: true },\n )\n for (const email of plan.emails) {\n const key = personKey(email)\n if (!key) continue\n transaction.set(\n index.doc(key),\n { email, contactId: survivorId, updatedAt: FieldValue.serverTimestamp() },\n { merge: true },\n )\n }\n transaction.delete(mergedRef)\n return { plan, survivor: (survivor.data() ?? {}) as Record<string, unknown> }\n })\n if ('reason' in swapped) return { ok: false, reason: swapped.reason }\n const { plan } = swapped\n // The survivor now carries both records' addresses, holders and facets —\n // what the Contacts list searches and filters by (AGL-3321).\n await restampCrmListFieldsAt(survivorRef, 'contacts')\n\n /*\n * The companies both records named counted two people and now count one\n * (AGL-2613). Settled after the swap the way the capture settles its\n * counts: a figure that could not move is logged, and the company page's\n * live aggregate corrects it.\n */\n await settleCompanyContactsCounts(orgRef.collection(CRM_COLLECTIONS.companies), {\n companyId: null,\n mirror: null,\n counts: plan.companyCounts,\n })\n /*\n * The survivor now owns the merged record's open tasks too, so its next\n * activity is the earliest of both (AGL-2661). Settled the same way: a\n * figure that could not move is logged, and the Fields section's\n * recompute corrects it.\n */\n await recomputeCrmNextTaskAt(firestore, orgRef.id, [{ contactId: survivorId }]).catch(\n (error: unknown) => {\n console.error('contact merge next activity could not be recomputed', survivorId, error)\n },\n )\n\n const mergedEmail = contactEmails(mergedData)[0] ?? ''\n const survivorEmail = plan.emails[0] ?? ''\n // The canonical name — the one identity every holder shares — for the\n // feed; the address stands in for a person nobody has named.\n const survivorName =\n typeof swapped.survivor['name'] === 'string' ? swapped.survivor['name'] : ''\n\n /*\n * THE TIMELINE NOTE, on the survivor: the merge is an event in this\n * person's history, and the record page is where a reader looks for one.\n * Filed under the calling site, or the survivor's first site for a door\n * with none; its own catch, because a note that could not be written must\n * not read as a merge that did not happen.\n */\n try {\n await orgRef.collection(CRM_COLLECTIONS.activities).add({\n kind: 'note',\n body: `Merged with ${mergedEmail}`,\n atMs: Date.now(),\n byUid: actor.uid,\n ...(options.actorName || actor.email\n ? { byName: options.actorName || actor.email }\n : {}),\n contactId: survivorId,\n visibleTo: plan.visibleTo,\n hostId: hostId ?? String(survivorData['hostId'] ?? ''),\n createdByUid: actor.uid,\n createdAt: FieldValue.serverTimestamp(),\n updatedAt: FieldValue.serverTimestamp(),\n })\n } catch (error) {\n console.error('[contact-merge] timeline note failed', survivorId, error)\n }\n\n // The audit line (AGL-2622), in the feed of the site whose console did\n // the work. A door with no site — the API — has no host feed to write.\n if (hostId) {\n await logHostActivity(hostId, actor, `Merged with ${mergedEmail}`, {\n type: 'contact',\n id: survivorId,\n name: survivorName || survivorEmail,\n })\n }\n\n return {\n ok: true,\n survivorId,\n survivorEmail,\n mergedId,\n mergedEmail,\n emails: plan.emails,\n repointed: { deals, tasks, activities, leads },\n }\n}\n"],"names":["CONTACT_EMAIL_INDEX_COLLECTION","contactEmails","planContactMerge","CRM_COLLECTIONS","personKey","FieldValue","settleCompanyContactsCounts","recomputeCrmNextTaskAt","restampCrmListFieldsAt","logHostActivity","REPOINT_PAGE","REPOINT_PASSES","repointContactId","firestore","collection","from","to","moved","pass","page","where","limit","get","empty","batch","row","docs","update","ref","contactId","updatedAt","serverTimestamp","commit","size","repointLeads","orgRef","emails","email","key","leadRef","doc","lead","exists","convertedContactId","strings","value","Array","isArray","filter","item","mergeContacts","options","mergedBefore","survivorBefore","plan","survivorId","mergedId","actor","hostId","ok","reason","contactsRef","survivorRef","mergedRef","Promise","all","mergedData","data","survivorData","deals","tasks","activities","leadHosts","Set","leads","index","swapped","runTransaction","transaction","survivor","merged","set","merge","delete","companies","companyId","mirror","counts","companyCounts","id","catch","error","console","mergedEmail","survivorEmail","survivorName","add","kind","body","atMs","Date","now","byUid","uid","actorName","byName","visibleTo","String","createdByUid","createdAt","type","name","repointed"],"mappings":"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"}
@@ -1,35 +0,0 @@
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
- import type { FormDatasetBinding } from '@aglyn/aglyn/app-utils/form-dataset-binding';
18
- /**
19
- * The token a page carries for one form's binding on one site.
20
- *
21
- * Throws when `TOKEN_SIGNING_SECRET` is not configured, like every token this
22
- * secret signs; a caller that renders a page decides what an unsigned form
23
- * does.
24
- */
25
- export declare function signFormDatasetBinding(hostId: string, binding: FormDatasetBinding): string;
26
- /**
27
- * The binding a token carries, or null when it is not a valid token for
28
- * `hostId`.
29
- *
30
- * Null for anything else at all — not a string, oversized, another version, a
31
- * signature that does not match, a site other than this one, or no signing
32
- * secret — because the only safe reading of a binding that cannot be verified
33
- * is that there is none.
34
- */
35
- export declare function verifyFormDatasetBinding(hostId: string, token: unknown): FormDatasetBinding | null;
@@ -1,107 +0,0 @@
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
- */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
- import { createHmac } from "crypto";
18
- import { tokenSigningSecret } from "./media-signing.js";
19
- import { safeEqual } from "./safe-equal.js";
20
- /**
21
- * A form's dataset binding, signed into the page that renders the form.
22
- *
23
- * `/api/forms/submit` is public and unauthenticated, so nothing in its body
24
- * may choose which dataset a record lands in: a dataset id taken from the body
25
- * let anyone add rows to any dataset the site can see. The binding is instead
26
- * read off the form by the compose that renders the page, signed here, and
27
- * carried by the form; the route writes only where a valid signature says.
28
- *
29
- * What a token grants is what the published form already does. Anyone can read
30
- * one out of the page, and replaying it writes to that form's dataset with
31
- * that form's field map, for the site it was minted on — nothing a visitor
32
- * submitting the form could not already do.
33
- *
34
- * Signed with the shared, fail-closed `TOKEN_SIGNING_SECRET`, the secret the
35
- * other tenant tokens use, under its own `form-dataset-binding:` prefix so it
36
- * can never be replayed as any other kind of token. No expiry: a published
37
- * page can be served long after it rendered, and an expired token would drop
38
- * a live form's records. Rotating the secret, or the version, retires every
39
- * token at once.
40
- */ const VERSION = 'v1';
41
- const PREFIX = 'form-dataset-binding';
42
- /** Far above any real binding, so an oversized body is refused unparsed. */ const MAX_TOKEN_CHARS = 8192;
43
- const signature = (payload)=>createHmac('sha256', tokenSigningSecret()).update(`${PREFIX}:${VERSION}:${payload}`).digest('hex');
44
- /**
45
- * The token a page carries for one form's binding on one site.
46
- *
47
- * Throws when `TOKEN_SIGNING_SECRET` is not configured, like every token this
48
- * secret signs; a caller that renders a page decides what an unsigned form
49
- * does.
50
- */ export function signFormDatasetBinding(hostId, binding) {
51
- const body = _extends({
52
- h: hostId
53
- }, binding.datasetId ? {
54
- d: binding.datasetId
55
- } : {}, binding.datasetName ? {
56
- n: binding.datasetName
57
- } : {}, {
58
- m: binding.fieldMap
59
- });
60
- const payload = Buffer.from(JSON.stringify(body)).toString('base64url');
61
- return `${VERSION}.${payload}.${signature(payload)}`;
62
- }
63
- const text = (value, max)=>typeof value === 'string' ? value.trim().slice(0, max) : '';
64
- /**
65
- * The binding a token carries, or null when it is not a valid token for
66
- * `hostId`.
67
- *
68
- * Null for anything else at all — not a string, oversized, another version, a
69
- * signature that does not match, a site other than this one, or no signing
70
- * secret — because the only safe reading of a binding that cannot be verified
71
- * is that there is none.
72
- */ export function verifyFormDatasetBinding(hostId, token) {
73
- try {
74
- if (typeof token !== 'string' || token.length > MAX_TOKEN_CHARS) return null;
75
- const [version, payload, signed, extra] = token.split('.');
76
- if (version !== VERSION || !payload || !signed || extra !== undefined) {
77
- return null;
78
- }
79
- if (!safeEqual(signed, signature(payload))) return null;
80
- const body = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
81
- if (!hostId || (body == null ? void 0 : body.h) !== hostId) return null;
82
- const datasetId = text(body.d, 128);
83
- const datasetName = text(body.n, 60);
84
- if (!datasetId && !datasetName) return null;
85
- const fieldMap = {};
86
- if (body.m && typeof body.m === 'object' && !Array.isArray(body.m)) {
87
- for (const [key, value] of Object.entries(body.m)){
88
- const submittedKey = text(key, 64);
89
- const datasetFieldId = text(value, 64);
90
- if (submittedKey && datasetFieldId) {
91
- fieldMap[submittedKey] = datasetFieldId;
92
- }
93
- }
94
- }
95
- return _extends({}, datasetId ? {
96
- datasetId
97
- } : {}, datasetName ? {
98
- datasetName
99
- } : {}, {
100
- fieldMap
101
- });
102
- } catch (unused) {
103
- return null;
104
- }
105
- }
106
-
107
- //# sourceMappingURL=form-dataset-binding-token.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/form-dataset-binding-token.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { FormDatasetBinding } from '@aglyn/aglyn/app-utils/form-dataset-binding'\nimport { createHmac } from 'crypto'\nimport { tokenSigningSecret } from './media-signing'\nimport { safeEqual } from './safe-equal'\n\n/**\n * A form's dataset binding, signed into the page that renders the form.\n *\n * `/api/forms/submit` is public and unauthenticated, so nothing in its body\n * may choose which dataset a record lands in: a dataset id taken from the body\n * let anyone add rows to any dataset the site can see. The binding is instead\n * read off the form by the compose that renders the page, signed here, and\n * carried by the form; the route writes only where a valid signature says.\n *\n * What a token grants is what the published form already does. Anyone can read\n * one out of the page, and replaying it writes to that form's dataset with\n * that form's field map, for the site it was minted on — nothing a visitor\n * submitting the form could not already do.\n *\n * Signed with the shared, fail-closed `TOKEN_SIGNING_SECRET`, the secret the\n * other tenant tokens use, under its own `form-dataset-binding:` prefix so it\n * can never be replayed as any other kind of token. No expiry: a published\n * page can be served long after it rendered, and an expired token would drop\n * a live form's records. Rotating the secret, or the version, retires every\n * token at once.\n */\n\nconst VERSION = 'v1'\nconst PREFIX = 'form-dataset-binding'\n/** Far above any real binding, so an oversized body is refused unparsed. */\nconst MAX_TOKEN_CHARS = 8192\n\ninterface SignedBinding {\n h: string\n d?: string\n n?: string\n m: Record<string, string>\n}\n\nconst signature = (payload: string): string =>\n createHmac('sha256', tokenSigningSecret())\n .update(`${PREFIX}:${VERSION}:${payload}`)\n .digest('hex')\n\n/**\n * The token a page carries for one form's binding on one site.\n *\n * Throws when `TOKEN_SIGNING_SECRET` is not configured, like every token this\n * secret signs; a caller that renders a page decides what an unsigned form\n * does.\n */\nexport function signFormDatasetBinding(\n hostId: string,\n binding: FormDatasetBinding,\n): string {\n const body: SignedBinding = {\n h: hostId,\n ...(binding.datasetId ? { d: binding.datasetId } : {}),\n ...(binding.datasetName ? { n: binding.datasetName } : {}),\n m: binding.fieldMap,\n }\n const payload = Buffer.from(JSON.stringify(body)).toString('base64url')\n return `${VERSION}.${payload}.${signature(payload)}`\n}\n\nconst text = (value: unknown, max: number): string =>\n typeof value === 'string' ? value.trim().slice(0, max) : ''\n\n/**\n * The binding a token carries, or null when it is not a valid token for\n * `hostId`.\n *\n * Null for anything else at all — not a string, oversized, another version, a\n * signature that does not match, a site other than this one, or no signing\n * secret — because the only safe reading of a binding that cannot be verified\n * is that there is none.\n */\nexport function verifyFormDatasetBinding(\n hostId: string,\n token: unknown,\n): FormDatasetBinding | null {\n try {\n if (typeof token !== 'string' || token.length > MAX_TOKEN_CHARS) return null\n const [version, payload, signed, extra] = token.split('.')\n if (version !== VERSION || !payload || !signed || extra !== undefined) {\n return null\n }\n if (!safeEqual(signed, signature(payload))) return null\n const body = JSON.parse(\n Buffer.from(payload, 'base64url').toString('utf8'),\n ) as Partial<SignedBinding>\n if (!hostId || body?.h !== hostId) return null\n const datasetId = text(body.d, 128)\n const datasetName = text(body.n, 60)\n if (!datasetId && !datasetName) return null\n const fieldMap: Record<string, string> = {}\n if (body.m && typeof body.m === 'object' && !Array.isArray(body.m)) {\n for (const [key, value] of Object.entries(body.m)) {\n const submittedKey = text(key, 64)\n const datasetFieldId = text(value, 64)\n if (submittedKey && datasetFieldId) {\n fieldMap[submittedKey] = datasetFieldId\n }\n }\n }\n return {\n ...(datasetId ? { datasetId } : {}),\n ...(datasetName ? { datasetName } : {}),\n fieldMap,\n }\n } catch {\n return null\n }\n}\n"],"names":["createHmac","tokenSigningSecret","safeEqual","VERSION","PREFIX","MAX_TOKEN_CHARS","signature","payload","update","digest","signFormDatasetBinding","hostId","binding","body","h","datasetId","d","datasetName","n","m","fieldMap","Buffer","from","JSON","stringify","toString","text","value","max","trim","slice","verifyFormDatasetBinding","token","length","version","signed","extra","split","undefined","parse","Array","isArray","key","Object","entries","submittedKey","datasetFieldId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,SAASA,UAAU,QAAQ,SAAQ;AACnC,SAASC,kBAAkB,QAAQ,qBAAiB;AACpD,SAASC,SAAS,QAAQ,kBAAc;AAExC;;;;;;;;;;;;;;;;;;;;CAoBC,GAED,MAAMC,UAAU;AAChB,MAAMC,SAAS;AACf,0EAA0E,GAC1E,MAAMC,kBAAkB;AASxB,MAAMC,YAAY,CAACC,UACjBP,WAAW,UAAUC,sBAClBO,MAAM,CAAC,GAAGJ,OAAO,CAAC,EAAED,QAAQ,CAAC,EAAEI,SAAS,EACxCE,MAAM,CAAC;AAEZ;;;;;;CAMC,GACD,OAAO,SAASC,uBACdC,MAAc,EACdC,OAA2B;IAE3B,MAAMC,OAAsB;QAC1BC,GAAGH;OACCC,QAAQG,SAAS,GAAG;QAAEC,GAAGJ,QAAQG,SAAS;IAAC,IAAI,CAAC,GAChDH,QAAQK,WAAW,GAAG;QAAEC,GAAGN,QAAQK,WAAW;IAAC,IAAI,CAAC;QACxDE,GAAGP,QAAQQ,QAAQ;;IAErB,MAAMb,UAAUc,OAAOC,IAAI,CAACC,KAAKC,SAAS,CAACX,OAAOY,QAAQ,CAAC;IAC3D,OAAO,GAAGtB,QAAQ,CAAC,EAAEI,QAAQ,CAAC,EAAED,UAAUC,UAAU;AACtD;AAEA,MAAMmB,OAAO,CAACC,OAAgBC,MAC5B,OAAOD,UAAU,WAAWA,MAAME,IAAI,GAAGC,KAAK,CAAC,GAAGF,OAAO;AAE3D;;;;;;;;CAQC,GACD,OAAO,SAASG,yBACdpB,MAAc,EACdqB,KAAc;IAEd,IAAI;QACF,IAAI,OAAOA,UAAU,YAAYA,MAAMC,MAAM,GAAG5B,iBAAiB,OAAO;QACxE,MAAM,CAAC6B,SAAS3B,SAAS4B,QAAQC,MAAM,GAAGJ,MAAMK,KAAK,CAAC;QACtD,IAAIH,YAAY/B,WAAW,CAACI,WAAW,CAAC4B,UAAUC,UAAUE,WAAW;YACrE,OAAO;QACT;QACA,IAAI,CAACpC,UAAUiC,QAAQ7B,UAAUC,WAAW,OAAO;QACnD,MAAMM,OAAOU,KAAKgB,KAAK,CACrBlB,OAAOC,IAAI,CAACf,SAAS,aAAakB,QAAQ,CAAC;QAE7C,IAAI,CAACd,UAAUE,CAAAA,wBAAAA,KAAMC,CAAC,MAAKH,QAAQ,OAAO;QAC1C,MAAMI,YAAYW,KAAKb,KAAKG,CAAC,EAAE;QAC/B,MAAMC,cAAcS,KAAKb,KAAKK,CAAC,EAAE;QACjC,IAAI,CAACH,aAAa,CAACE,aAAa,OAAO;QACvC,MAAMG,WAAmC,CAAC;QAC1C,IAAIP,KAAKM,CAAC,IAAI,OAAON,KAAKM,CAAC,KAAK,YAAY,CAACqB,MAAMC,OAAO,CAAC5B,KAAKM,CAAC,GAAG;YAClE,KAAK,MAAM,CAACuB,KAAKf,MAAM,IAAIgB,OAAOC,OAAO,CAAC/B,KAAKM,CAAC,EAAG;gBACjD,MAAM0B,eAAenB,KAAKgB,KAAK;gBAC/B,MAAMI,iBAAiBpB,KAAKC,OAAO;gBACnC,IAAIkB,gBAAgBC,gBAAgB;oBAClC1B,QAAQ,CAACyB,aAAa,GAAGC;gBAC3B;YACF;QACF;QACA,OAAO,aACD/B,YAAY;YAAEA;QAAU,IAAI,CAAC,GAC7BE,cAAc;YAAEA;QAAY,IAAI,CAAC;YACrCG;;IAEJ,EAAE,eAAM;QACN,OAAO;IACT;AACF"}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/stripe-account-mode.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Whether a stored Connect linkage can actually take money HERE (AGL-2471).\n *\n * THE DEFECT. Production Firestore held three Connect linkages and all three\n * named TEST-mode accounts. One — `profiles/7AVEMtDa…`, `stripeAccountId:\n * acct_1TulDeRbL3B9Ioqz` — also carried `stripeChargesEnabled: true`, which\n * was the entire payments-readiness test every money door made:\n *\n * if (!accountId || !ownerProfile.get('stripeChargesEnabled')) …\n *\n * So three storefronts presented as payments-ready, minted a LIVE Checkout\n * session with a TEST-mode `payment_intent_data[transfer_data][destination]`,\n * and were refused by Stripe as a generic 502 at the shopper's checkout. A\n * merchant told they are ready to take money, who silently cannot.\n *\n * WHY THE ACCOUNT ID IS NOT THE EVIDENCE. `acct_1TulDeRbL3B9Ioqz` is a real\n * production value naming a TEST account. Stripe's account ids carry no mode\n * marker — live and test ids are the same shape — so no amount of reading the\n * string can tell them apart. What DOES tell them apart is Stripe itself:\n * retrieving that id with the live key answers\n *\n * 400 The account acct_1TulDeRbL3B9Ioqz was a test account created with a\n * testmode key, and therefore can only be used with testmode keys.\n *\n * The Account object, notably, has NO `livemode` field of its own (verified\n * against the live API: the payload carries `charges_enabled`,\n * `payouts_enabled`, `capabilities` and no `livemode`). So the mode is\n * recorded at the two moments Stripe does state it:\n *\n * 1. `account.updated` — the EVENT carries `livemode`, and it is Stripe's\n * own statement about the account the event is describing;\n * 2. onboarding — the account was just created or retrieved successfully\n * with the platform key, and Stripe hard-refuses cross-mode retrieval\n * (above), so the account's mode IS the key's mode; the key's mode is\n * then confirmed against the API via `resolvePlatformStripeMode`.\n *\n * WHY THE RULE IS ASYMMETRIC, AND NOT \"REFUSE ANYTHING UNPROVEN\". An absent\n * `stripeAccountLivemode` refuses only on a LIVE deployment. That is where\n * real money moves and where the defect was found, and a live deployment can\n * always re-establish the field: the merchant reconnects, or Stripe sends one\n * `account.updated`. Everywhere else — a test-key deployment, a developer\n * machine, a self-hosted install still in sandbox, the whole test suite — an\n * unverified linkage keeps the behaviour it had before, because no real money\n * can move there and Stripe enforces the mode boundary itself. Refusing\n * everywhere would have bought production nothing extra and broken every\n * non-production deployment.\n *\n * A PROVEN mismatch (`stripeAccountLivemode` recorded and disagreeing) is\n * refused in BOTH directions, because that costs nothing to detect and is\n * never right.\n *\n * WHAT IS SNIFFED, AND WHY THAT ONE IS FAIR. The SECRET KEY states its own\n * mode — `sk_live_`, `sk_test_`, `rk_live_`, `rk_test_` — and that is a\n * documented, stable Stripe invariant, unlike the account id. It is used only\n * to answer \"which mode is this deployment\", never \"which mode is this\n * account\", and even there `resolvePlatformStripeMode` prefers the API.\n *\n * HOW IT GOT THERE. A developer machine runs against PRODUCTION Firestore\n * (`FIREBASE_PROJECT_ID=aglyn-main` in every dev env file) while\n * `apps/console/.env.development.local` sets `STRIPE_SECRET_KEY` to the\n * `sk_test_` key by deliberate policy (AGL-1137: localhost must never touch\n * live Stripe). The connect route then creates a test-mode Express account\n * and writes it into the production database. Nothing about that is going to\n * change — the split key is the right call — so the gate has to be the thing\n * that notices.\n */\n\n/** Which Stripe world a key or an account belongs to. */\nexport type StripeMode = 'live' | 'test'\n\n/**\n * Why a stored linkage may not be charged against. Every value other than\n * `'ready'` is a refusal, and the mode ones are refusals the old two-field\n * test could not make.\n */\nexport type ConnectReadiness =\n /** Charge away. */\n | 'ready'\n /** No Connect account is stored at all — the onboarding call to action. */\n | 'not-connected'\n /** Stripe will not let this account take charges. */\n | 'charges-disabled'\n /**\n * The linkage predates AGL-2471 (or was written by something that did not\n * record mode), so its mode was never established — AND this deployment is\n * LIVE. This is the shape all three poisoned production records have.\n * The connect route and the `account.updated` sync both write the field, so\n * a genuine merchant self-heals on the next of either.\n */\n | 'mode-unverified'\n /** The account belongs to the OTHER Stripe world. The AGL-2471 defect. */\n | 'mode-mismatch'\n\nexport interface ConnectReadinessInput {\n /** `stripeAccountId` off the profile, whatever shape the document has. */\n accountId?: unknown\n /** `stripeChargesEnabled` off the profile. */\n chargesEnabled?: unknown\n /** `stripeAccountLivemode` off the profile — absent before AGL-2471. */\n accountLivemode?: unknown\n /** Defaults to the mode of `STRIPE_SECRET_KEY`. */\n platformMode?: StripeMode | undefined\n}\n\n/**\n * The mode a Stripe secret key states about itself, or `undefined` when the\n * string is not a Stripe secret key.\n *\n * Deliberately narrow: it matches `sk_`/`rk_` followed by `live`/`test`, and\n * refuses everything else rather than guessing. Handing it an `acct_…` id\n * returns `undefined` — the whole point is that account ids say nothing.\n */\nexport function platformStripeMode(\n key: string | undefined = process.env.STRIPE_SECRET_KEY,\n): StripeMode | undefined {\n const match = /^[sr]k_(live|test)_/.exec(String(key ?? '').trim())\n return match ? (match[1] as StripeMode) : undefined\n}\n\n/**\n * The platform's mode, asked of Stripe and only then of the key.\n *\n * `GET /v1/balance` is a singleton that carries `livemode`, so it states the\n * mode of whatever key made the call — no object has to be created to find\n * out. The key prefix is the fallback for a restricted key that cannot read\n * balance; without it, a self-hosted install with a scoped key could not\n * finish onboarding at all.\n */\nexport async function resolvePlatformStripeMode(\n key: string | undefined = process.env.STRIPE_SECRET_KEY,\n fetchImpl: typeof fetch = fetch,\n): Promise<StripeMode | undefined> {\n const fromKey = platformStripeMode(key)\n if (!key) return fromKey\n try {\n const response = await fetchImpl('https://api.stripe.com/v1/balance', {\n method: 'GET',\n headers: { Authorization: `Bearer ${key}` },\n })\n if (response.ok) {\n const payload = await response.json()\n if (typeof payload?.livemode === 'boolean') {\n return payload.livemode ? 'live' : 'test'\n }\n }\n } catch {\n // Network or transport failure — fall through to the key, which is the\n // same answer we would have recorded before this existed.\n }\n return fromKey\n}\n\n/**\n * Decides whether a stored Connect linkage may be charged against.\n *\n * Order matters: the two refusals that existed before AGL-2471 are asked\n * first, so an unconnected or restricted merchant still gets the answer they\n * always got, and only a linkage that WOULD have passed reaches the mode\n * question.\n */\nexport function connectReadiness(\n input: ConnectReadinessInput,\n): ConnectReadiness {\n const accountId =\n typeof input.accountId === 'string' ? input.accountId.trim() : ''\n if (!accountId) return 'not-connected'\n if (input.chargesEnabled !== true) return 'charges-disabled'\n const platformMode =\n 'platformMode' in input ? input.platformMode : platformStripeMode()\n // Three-valued on purpose, exactly as AGL-1997 reads `payoutsEnabled`: only\n // a literal boolean is a recorded answer. A string `'true'` or a `1` is a\n // field somebody else wrote, and inventing `true` from it would re-open the\n // hole this closes.\n if (typeof input.accountLivemode === 'boolean') {\n // A PROVEN mismatch is refused in either direction. It costs nothing to\n // detect and it is always wrong.\n if (!platformMode) return 'ready'\n return input.accountLivemode === (platformMode === 'live')\n ? 'ready'\n : 'mode-mismatch'\n }\n return platformMode === 'live' ? 'mode-unverified' : 'ready'\n}\n\n/**\n * The form every money door calls: `true` only when the linkage may be\n * charged, with the two mode refusals reported to the server log.\n *\n * The log line is the part the shopper's generic 409 cannot carry. Before\n * this, a mode mismatch surfaced as a 502 from Stripe with nothing anywhere\n * naming the cause; the sale is still refused the same way, but now somebody\n * can find out why.\n */\nexport function connectLinkageIsReady(\n input: ConnectReadinessInput,\n context?: { subject?: string },\n): boolean {\n const readiness = connectReadiness(input)\n if (readiness === 'mode-mismatch' || readiness === 'mode-unverified') {\n console.error(\n `[AGL-2471] Refusing a charge against Connect account ` +\n `${String(input.accountId)}${\n context?.subject ? ` (${context.subject})` : ''\n }: ${readiness}. The stored linkage is not verified for this ` +\n `deployment's Stripe mode; the merchant must re-onboard.`,\n )\n }\n return readiness === 'ready'\n}\n"],"names":["platformStripeMode","key","process","env","STRIPE_SECRET_KEY","match","exec","String","trim","undefined","resolvePlatformStripeMode","fetchImpl","fetch","fromKey","response","method","headers","Authorization","ok","payload","json","livemode","connectReadiness","input","accountId","chargesEnabled","platformMode","accountLivemode","connectLinkageIsReady","context","readiness","console","error","subject"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiEC,GAED,uDAAuD,GAqCvD;;;;;;;CAOC,GACD,OAAO,SAASA,mBACdC,MAA0BC,QAAQC,GAAG,CAACC,iBAAiB;IAEvD,MAAMC,QAAQ,sBAAsBC,IAAI,CAACC,OAAON,cAAAA,MAAO,IAAIO,IAAI;IAC/D,OAAOH,QAASA,KAAK,CAAC,EAAE,GAAkBI;AAC5C;AAEA;;;;;;;;CAQC,GACD,OAAO,eAAeC,0BACpBT,MAA0BC,QAAQC,GAAG,CAACC,iBAAiB,EACvDO,YAA0BC,KAAK;IAE/B,MAAMC,UAAUb,mBAAmBC;IACnC,IAAI,CAACA,KAAK,OAAOY;IACjB,IAAI;QACF,MAAMC,WAAW,MAAMH,UAAU,qCAAqC;YACpEI,QAAQ;YACRC,SAAS;gBAAEC,eAAe,CAAC,OAAO,EAAEhB,KAAK;YAAC;QAC5C;QACA,IAAIa,SAASI,EAAE,EAAE;YACf,MAAMC,UAAU,MAAML,SAASM,IAAI;YACnC,IAAI,QAAOD,2BAAAA,QAASE,QAAQ,MAAK,WAAW;gBAC1C,OAAOF,QAAQE,QAAQ,GAAG,SAAS;YACrC;QACF;IACF,EAAE,eAAM;IACN,uEAAuE;IACvE,0DAA0D;IAC5D;IACA,OAAOR;AACT;AAEA;;;;;;;CAOC,GACD,OAAO,SAASS,iBACdC,KAA4B;IAE5B,MAAMC,YACJ,OAAOD,MAAMC,SAAS,KAAK,WAAWD,MAAMC,SAAS,CAAChB,IAAI,KAAK;IACjE,IAAI,CAACgB,WAAW,OAAO;IACvB,IAAID,MAAME,cAAc,KAAK,MAAM,OAAO;IAC1C,MAAMC,eACJ,kBAAkBH,QAAQA,MAAMG,YAAY,GAAG1B;IACjD,4EAA4E;IAC5E,0EAA0E;IAC1E,4EAA4E;IAC5E,oBAAoB;IACpB,IAAI,OAAOuB,MAAMI,eAAe,KAAK,WAAW;QAC9C,wEAAwE;QACxE,iCAAiC;QACjC,IAAI,CAACD,cAAc,OAAO;QAC1B,OAAOH,MAAMI,eAAe,KAAMD,CAAAA,iBAAiB,MAAK,IACpD,UACA;IACN;IACA,OAAOA,iBAAiB,SAAS,oBAAoB;AACvD;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASE,sBACdL,KAA4B,EAC5BM,OAA8B;IAE9B,MAAMC,YAAYR,iBAAiBC;IACnC,IAAIO,cAAc,mBAAmBA,cAAc,mBAAmB;QACpEC,QAAQC,KAAK,CACX,CAAC,qDAAqD,CAAC,GACrD,GAAGzB,OAAOgB,MAAMC,SAAS,IACvBK,CAAAA,2BAAAA,QAASI,OAAO,IAAG,CAAC,EAAE,EAAEJ,QAAQI,OAAO,CAAC,CAAC,CAAC,GAAG,GAC9C,EAAE,EAAEH,UAAU,8CAA8C,CAAC,GAC9D,CAAC,uDAAuD,CAAC;IAE/D;IACA,OAAOA,cAAc;AACvB"}