@proveanything/smartlinks 1.6.1 → 1.6.3

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.
@@ -0,0 +1,581 @@
1
+ # Communications (Comms & Broadcasts)
2
+
3
+ This guide covers the full communications surface of the SDK: transactional sends, multi-channel broadcasts, consent management, push registration, and analytics.
4
+
5
+ ---
6
+
7
+ ## What You Can Do
8
+
9
+ - **Transactional Send** — Fire a single targeted message to one contact without creating a broadcast record.
10
+ - **Broadcasts** — Define and enqueue multi-channel campaigns (email, push, SMS, wallet); preview and test before sending.
11
+ - **Comms Settings** — Configure topics and unsubscribe behaviour per collection.
12
+ - **Consent & Preferences** — Record opt-ins/outs and per-subject preferences.
13
+ - **Method Registration** — Register email, SMS, and Web Push contact methods.
14
+ - **Analytics** — Query delivery events, recipient lists, and log custom events.
15
+
16
+ ---
17
+
18
+ ## Transactional Send
19
+
20
+ Sends a single message to one contact using a template. No broadcast record is created. The send is logged to the contact's communication history with `sourceType: 'transactional'`.
21
+
22
+ **Endpoint:** `POST /admin/collection/:collectionId/comm/send`
23
+
24
+ ```typescript
25
+ import { comms } from '@proveanything/smartlinks'
26
+ import type { TransactionalSendRequest } from '@proveanything/smartlinks'
27
+
28
+ const result = await comms.sendTransactional('collectionId', {
29
+ contactId: 'e4f2a1b0-...',
30
+ templateId: 'warranty-update',
31
+ channel: 'preferred', // 'email' | 'sms' | 'push' | 'wallet' | 'preferred' (default)
32
+ props: { claimRef: 'CLM-0042', decision: 'approved' },
33
+ include: {
34
+ productId: 'prod-abc123', // hydrate product context into template
35
+ appCase: 'c9d1e2f3-...', // hydrate an app case record
36
+ },
37
+ ref: 'warranty-decision-notification', // arbitrary string for your own tracking
38
+ appId: 'warrantyApp',
39
+ })
40
+
41
+ if (result.ok) {
42
+ console.log(`Sent via ${result.channel}`, result.messageId)
43
+ } else {
44
+ console.error('Send failed:', result.error)
45
+ }
46
+ ```
47
+
48
+ ### Request Body
49
+
50
+ | Field | Type | Description |
51
+ |-------|------|-------------|
52
+ | `contactId` | `string` | **Required.** Contact to send to. |
53
+ | `templateId` | `string` | **Required.** Template to render. |
54
+ | `channel` | `'email' \| 'sms' \| 'push' \| 'wallet' \| 'preferred'` | Channel to send on. `'preferred'` (default) picks the contact's best available channel. |
55
+ | `props` | `Record<string, unknown>` | Extra variables merged into template rendering. |
56
+ | `include` | object | Hydration flags — see table below. |
57
+ | `ref` | `string` | Arbitrary reference string stored with the event. |
58
+ | `appId` | `string` | App identifier, used for context/logging. |
59
+
60
+ **`include` fields:**
61
+
62
+ | Field | Type | Description |
63
+ |-------|------|-------------|
64
+ | `collection` | `boolean` | Include collection data in template context. |
65
+ | `productId` | `string` | Hydrate a specific product. |
66
+ | `proofId` | `string` | Hydrate a specific proof. |
67
+ | `user` | `boolean` | Include the requesting user's data. |
68
+ | `appCase` | `string` | Hydrate an app case record by ID. |
69
+ | `appThread` | `string` | Hydrate an app thread record by ID. |
70
+ | `appRecord` | `string` | Hydrate an app record by ID. |
71
+
72
+ ### Response
73
+
74
+ ```typescript
75
+ // Success
76
+ { ok: true; channel: 'email' | 'sms' | 'push' | 'wallet'; messageId?: string }
77
+
78
+ // Failure
79
+ { ok: false; error: string }
80
+ ```
81
+
82
+ ---
83
+
84
+ ## Broadcasts
85
+
86
+ Broadcasts are multi-channel campaigns defined and managed under a collection. Create a broadcast record in the platform first, then use the SDK to preview, test, and enqueue.
87
+
88
+ **Import:**
89
+ ```typescript
90
+ import { broadcasts } from '@proveanything/smartlinks'
91
+ import type { BroadcastSendRequest } from '@proveanything/smartlinks'
92
+ ```
93
+
94
+ ### Preview
95
+
96
+ Render a broadcast template for a given channel without sending.
97
+
98
+ ```typescript
99
+ const preview = await broadcasts.preview('collectionId', 'broadcastId', {
100
+ email: 'user@example.com', // optional: override recipient email for preview
101
+ channelOverride: 'push', // force a specific channel
102
+ props: { productId: 'prod_123' },
103
+ hydrate: true,
104
+ include: { product: true, proof: true }
105
+ })
106
+ // preview.channel — which channel was rendered
107
+ // preview.payload / preview.subject / preview.body — channel-specific render output
108
+ ```
109
+
110
+ ### Test Send
111
+
112
+ Send a single test message to a contact.
113
+
114
+ ```typescript
115
+ await broadcasts.sendTest('collectionId', 'broadcastId', {
116
+ contactId: 'contact_123',
117
+ props: { promo: 'JAN' }
118
+ })
119
+ ```
120
+
121
+ ### Enqueue Production Send
122
+
123
+ Enqueue a background send to all recipients. When `channel` is omitted the broadcast's own mode setting drives channel selection:
124
+ - `preferred` — picks the best single channel per recipient.
125
+ - `channels` / `all` — sends on every enabled channel.
126
+
127
+ ```typescript
128
+ const body: BroadcastSendRequest = {
129
+ pageSize: 200,
130
+ hydrate: true,
131
+ include: { product: true, proof: true, user: true }
132
+ }
133
+ await broadcasts.send('collectionId', 'broadcastId', body)
134
+ ```
135
+
136
+ ### Manual Send
137
+
138
+ ```typescript
139
+ await broadcasts.sendManual('collectionId', 'broadcastId', { /* overrides */ })
140
+ ```
141
+
142
+ ### Topic Targeting and Consent
143
+
144
+ A broadcast must declare a `topic` under `broadcast.data.topic` (e.g. `newsletter`, `critical`). The topic key must match one defined in your collection's comms settings, and consent enforcement uses it at send time.
145
+
146
+ Consent resolution order (per recipient, per channel):
147
+ 1. `preferences._default.topicsByChannel[channel][topic]`
148
+ 2. `preferences._default.channels[channel]`
149
+ 3. `preferences._default.topics[topic]`
150
+ 4. Subject-specific preferences (when hydrating a subject context)
151
+
152
+ Minimal broadcast data shape:
153
+ ```json
154
+ {
155
+ "data": {
156
+ "topic": "newsletter",
157
+ "channelSettings": {
158
+ "mode": "preferred",
159
+ "channels": [
160
+ { "channel": "email", "enabled": true, "priority": 1, "templateId": "tmpl_newsletter_email" },
161
+ { "channel": "push", "enabled": true, "priority": 2 }
162
+ ]
163
+ }
164
+ }
165
+ }
166
+ ```
167
+
168
+ ### Append Recipients and Events
169
+
170
+ ```typescript
171
+ // Append a single delivery event
172
+ await broadcasts.append('collectionId', {
173
+ broadcastId: 'broadcastId',
174
+ contactId: 'contact_123',
175
+ channel: 'email',
176
+ eventType: 'delivered',
177
+ outcome: 'success'
178
+ })
179
+
180
+ // Append bulk events
181
+ await broadcasts.appendBulk('collectionId', {
182
+ params: { broadcastId: 'broadcastId', channel: 'push' },
183
+ ids: ['contact_1', 'contact_2'],
184
+ idField: 'contactId'
185
+ })
186
+
187
+ // List recipients
188
+ const recips = await broadcasts.recipients('collectionId', 'broadcastId', { limit: 50 })
189
+ ```
190
+
191
+ ---
192
+
193
+ ## Admin Comms Settings
194
+
195
+ Configure unsubscribe behaviour and topics per collection.
196
+
197
+ **Base path:** `admin/collection/:collectionId/comm.settings`
198
+
199
+ ### Get Settings
200
+
201
+ ```typescript
202
+ import { comms } from '@proveanything/smartlinks'
203
+ const { settings } = await comms.getSettings('collectionId', { includeSecret: true })
204
+ // settings.unsub — { requireToken, hasSecret }
205
+ // settings.topics — map of topic key → config
206
+ ```
207
+
208
+ ### Patch Settings
209
+
210
+ ```typescript
211
+ await comms.patchSettings('collectionId', {
212
+ unsub: { requireToken: true, secret: '<your-secret>' },
213
+ topics: { /* ... */ }
214
+ })
215
+ ```
216
+
217
+ To clear the unsubscribe secret, send `secret: ''`.
218
+
219
+ ### Topics Config
220
+
221
+ Each topic key maps to a config object:
222
+
223
+ ```json
224
+ {
225
+ "newsletter": {
226
+ "label": "Newsletter",
227
+ "description": "Occasional updates and stories",
228
+ "classification": "marketing",
229
+ "defaults": {
230
+ "policy": "opt-in",
231
+ "byChannel": { "email": "opt-in", "push": "opt-in" },
232
+ "channels": { "email": true, "push": true, "sms": false }
233
+ },
234
+ "rules": {
235
+ "allowChannels": ["email", "sms", "push"],
236
+ "allowUnsubscribe": true
237
+ },
238
+ "required": false
239
+ },
240
+ "critical": {
241
+ "label": "Critical Notices",
242
+ "classification": "transactional",
243
+ "defaults": {
244
+ "policy": "opt-out",
245
+ "channels": { "email": true, "push": true }
246
+ },
247
+ "rules": { "allowChannels": ["email", "push"], "allowUnsubscribe": false },
248
+ "required": true
249
+ }
250
+ }
251
+ ```
252
+
253
+ - `classification`: `'transactional'` or `'marketing'` — used by the UI when no explicit policy is set.
254
+ - `defaults.policy`: `'opt-in'` or `'opt-out'` — fallback policy for channels not overridden by `byChannel`.
255
+ - `required: true` and `rules.allowUnsubscribe: false` — use for critical/mandatory messages.
256
+
257
+ ### Unsubscribe Tokens
258
+
259
+ If `unsub.requireToken` is true, public unsubscribe calls must include a `token` query param.
260
+
261
+ Token generation (topic opt-out):
262
+ ```
263
+ basis = "${contactId}:${topic}"
264
+ token = sha256(basis + ":" + unsub.secret) // hex
265
+ ```
266
+
267
+ Token generation (channel opt-out):
268
+ ```
269
+ basis = "${contactId}:channel:${channel}"
270
+ token = sha256(basis + ":" + unsub.secret)
271
+ ```
272
+
273
+ Browser helper:
274
+ ```typescript
275
+ async function sha256Hex(input: string): Promise<string> {
276
+ const data = new TextEncoder().encode(input)
277
+ const hash = await crypto.subtle.digest('SHA-256', data)
278
+ return Array.from(new Uint8Array(hash))
279
+ .map(b => b.toString(16).padStart(2, '0'))
280
+ .join('')
281
+ }
282
+
283
+ const token = await sha256Hex(`${contactId}:${topic}:${secret}`)
284
+ ```
285
+
286
+ ---
287
+
288
+ ## Public Comms API
289
+
290
+ All public endpoints live under `public/collection/:collectionId/comm/*` and do not require an API key.
291
+
292
+ ### Get Topics
293
+
294
+ ```typescript
295
+ const { topics } = await comms.getPublicTopics('collectionId')
296
+ ```
297
+
298
+ Each topic carries UI metadata:
299
+ - `classification`: `'transactional'` | `'marketing'`
300
+ - `defaults.policy`: `'opt-in'` | `'opt-out'`
301
+ - `defaults.byChannel[channel]`: per-channel override
302
+
303
+ Helper to resolve effective policy for a toggle UI:
304
+ ```typescript
305
+ type Policy = 'opt-in' | 'opt-out'
306
+
307
+ function effectivePolicy(topic: any, channel: 'email' | 'sms' | 'push' | 'wallet'): Policy {
308
+ return (
309
+ topic?.defaults?.byChannel?.[channel] ??
310
+ topic?.defaults?.policy ??
311
+ (topic?.classification === 'marketing' ? 'opt-in' : 'opt-out')
312
+ ) as Policy
313
+ }
314
+ ```
315
+
316
+ ### Consent (Default)
317
+
318
+ Record a contact's default channel and topic opt-ins.
319
+
320
+ ```typescript
321
+ await comms.upsertConsent('collectionId', {
322
+ contactId: 'contact_123',
323
+ channels: { email: true, push: true },
324
+ topics: { newsletter: true, critical: true },
325
+ topicsByChannel: { email: { newsletter: true }, push: { critical: true } }
326
+ })
327
+ ```
328
+
329
+ ### Preferences (Subject-Specific)
330
+
331
+ Record consent specific to a product, proof, or other subject. Omit `subject` to update defaults.
332
+
333
+ ```typescript
334
+ await comms.upsertPreferences('collectionId', {
335
+ contactId: 'contact_123',
336
+ subject: { type: 'product', id: 'prod_1' },
337
+ channels: { email: true },
338
+ topics: { updates: true }
339
+ })
340
+ ```
341
+
342
+ ### Subscribe / Unsubscribe (Subject)
343
+
344
+ Manage subscriptions to a specific subject (e.g. product updates, proof alerts).
345
+
346
+ ```typescript
347
+ // Subscribe
348
+ const { subscriptionId } = await comms.subscribe('collectionId', {
349
+ contactId: 'contact_123',
350
+ subject: { type: 'proof', id: 'prf_1', productId: 'prod_1' },
351
+ subscribe: true,
352
+ source: 'api'
353
+ })
354
+
355
+ // Unsubscribe (same call, subscribe: false)
356
+ await comms.subscribe('collectionId', {
357
+ contactId: 'contact_123',
358
+ subject: { type: 'proof', id: 'prf_1', productId: 'prod_1' },
359
+ subscribe: false,
360
+ source: 'api'
361
+ })
362
+ ```
363
+
364
+ ### Check Subscription
365
+
366
+ ```typescript
367
+ const r = await comms.checkSubscription('collectionId', {
368
+ contactId: 'contact_123',
369
+ subjectType: 'proof',
370
+ subjectId: 'prf_1',
371
+ productId: 'prod_1'
372
+ })
373
+ // r.subscribed === true | false
374
+ ```
375
+
376
+ ### Resolve Subscriptions
377
+
378
+ Find contacts that are subscribed to a subject using identity hints.
379
+
380
+ ```typescript
381
+ const res = await comms.resolveSubscriptions('collectionId', {
382
+ subject: { type: 'product', id: 'prod_1' },
383
+ hints: { userId: 'user_1', email: 'user@example.com' }
384
+ })
385
+ ```
386
+
387
+ ### List and Register Methods
388
+
389
+ ```typescript
390
+ // List registered methods for a contact
391
+ const { methods } = await comms.listMethods('collectionId', {
392
+ contactId: 'contact_123',
393
+ type: 'email' // optional filter: 'email' | 'sms' | 'push'
394
+ })
395
+
396
+ // Register email
397
+ await comms.registerEmail('collectionId', {
398
+ contactId: 'contact_123',
399
+ email: 'user@example.com'
400
+ })
401
+
402
+ // Register SMS
403
+ await comms.registerSms('collectionId', {
404
+ contactId: 'contact_123',
405
+ phone: '+12065550100'
406
+ })
407
+ ```
408
+
409
+ ### Unsubscribe (Public Link / One-Click)
410
+
411
+ ```typescript
412
+ await comms.unsubscribe('collectionId', {
413
+ contactId: 'contact_123',
414
+ topic: 'newsletter',
415
+ token: '<sha256-hex-when-requireToken-is-true>'
416
+ })
417
+ ```
418
+
419
+ Omit `topic` to unsubscribe from all. Omit `channel` to apply across all channels.
420
+
421
+ ---
422
+
423
+ ## Push Registration
424
+
425
+ Web Push requires a service worker and the browser's Push API.
426
+
427
+ ### 1. Fetch the VAPID Public Key
428
+
429
+ ```typescript
430
+ import { comms } from '@proveanything/smartlinks'
431
+ const { publicKey } = await comms.getPushVapidPublicKey('collectionId')
432
+ ```
433
+
434
+ ### 2. Subscribe in the Service Worker
435
+
436
+ ```typescript
437
+ const registration = await navigator.serviceWorker.ready
438
+ const subscription = await registration.pushManager.subscribe({
439
+ userVisibleOnly: true,
440
+ applicationServerKey: urlBase64ToUint8Array(publicKey),
441
+ })
442
+ const subJson = subscription.toJSON() as any
443
+ ```
444
+
445
+ ### 3. Register the Method
446
+
447
+ ```typescript
448
+ await comms.registerPush('collectionId', {
449
+ contactId: 'contact_123',
450
+ endpoint: subscription.endpoint,
451
+ keys: subJson?.keys,
452
+ meta: { userId: 'user_1' }
453
+ })
454
+ ```
455
+
456
+ ### VAPID Key Utility
457
+
458
+ ```typescript
459
+ function urlBase64ToUint8Array(base64String: string): Uint8Array {
460
+ const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
461
+ const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
462
+ const rawData = atob(base64)
463
+ const output = new Uint8Array(rawData.length)
464
+ for (let i = 0; i < rawData.length; i++) output[i] = rawData.charCodeAt(i)
465
+ return output
466
+ }
467
+ ```
468
+
469
+ ---
470
+
471
+ ## Analytics
472
+
473
+ ### Query Events by User / Contact
474
+
475
+ ```typescript
476
+ const events = await comms.queryByUser('collectionId', {
477
+ userId: 'user_123',
478
+ limit: 100
479
+ })
480
+ ```
481
+
482
+ ### Recipient IDs for a Source
483
+
484
+ ```typescript
485
+ const ids = await comms.queryRecipientIds('collectionId', {
486
+ broadcastId: 'br_456'
487
+ })
488
+ ```
489
+
490
+ ### Recipients Without an Action
491
+
492
+ ```typescript
493
+ const withoutOpen = await comms.queryRecipientsWithoutAction('collectionId', {
494
+ broadcastId: 'br_456',
495
+ actionId: 'open'
496
+ })
497
+ ```
498
+
499
+ ### Recipients With an Action
500
+
501
+ ```typescript
502
+ const withClick = await comms.queryRecipientsWithAction('collectionId', {
503
+ broadcastId: 'br_456',
504
+ actionId: 'click',
505
+ includeOutcome: true // returns RecipientWithOutcome[] instead of RecipientId[]
506
+ })
507
+ ```
508
+
509
+ ### Log Events
510
+
511
+ ```typescript
512
+ // Single event
513
+ await comms.logCommunicationEvent('collectionId', {
514
+ eventType: 'opened',
515
+ channel: 'email',
516
+ contactId: 'contact_123'
517
+ })
518
+
519
+ // Bulk events (same params applied to all IDs)
520
+ await comms.logBulkCommunicationEvents('collectionId', {
521
+ params: { broadcastId: 'br_456', channel: 'email', eventType: 'delivered' },
522
+ ids: ['contact_1', 'contact_2', 'contact_3'],
523
+ idField: 'contactId'
524
+ })
525
+ ```
526
+
527
+ ---
528
+
529
+ ## End-to-End Admin Workflow
530
+
531
+ A typical sequence from setup to delivery:
532
+
533
+ ```typescript
534
+ import { comms, broadcasts } from '@proveanything/smartlinks'
535
+
536
+ // 1. Configure topics and unsubscribe rules
537
+ await comms.patchSettings('collectionId', {
538
+ unsub: { requireToken: true, secret: process.env.UNSUB_SECRET },
539
+ topics: {
540
+ newsletter: {
541
+ label: 'Newsletter', classification: 'marketing',
542
+ defaults: { policy: 'opt-in', channels: { email: true, push: true } },
543
+ rules: { allowChannels: ['email', 'push', 'sms'], allowUnsubscribe: true }
544
+ }
545
+ }
546
+ })
547
+
548
+ // 2. Register contact methods (done at contact creation / profile update)
549
+ await comms.registerEmail('collectionId', { contactId: 'c_123', email: 'user@example.com' })
550
+
551
+ // 3. Record consent (done when user confirms opt-in)
552
+ await comms.upsertConsent('collectionId', {
553
+ contactId: 'c_123',
554
+ channels: { email: true }, topics: { newsletter: true }
555
+ })
556
+
557
+ // 4a. Transactional: send a one-off triggered message
558
+ const result = await comms.sendTransactional('collectionId', {
559
+ contactId: 'c_123', templateId: 'order-confirmed',
560
+ channel: 'email', props: { orderId: 'ORD-001' }
561
+ })
562
+
563
+ // 4b. Broadcast: preview → test → enqueue
564
+ const preview = await broadcasts.preview('collectionId', 'br_spring', {
565
+ channelOverride: 'email', hydrate: true, include: { product: true }
566
+ })
567
+ await broadcasts.sendTest('collectionId', 'br_spring', { contactId: 'c_123' })
568
+ await broadcasts.send('collectionId', 'br_spring', { pageSize: 200, hydrate: true })
569
+ ```
570
+
571
+ ---
572
+
573
+ ## Prerequisites
574
+
575
+ | Feature | Requirement |
576
+ |---------|-------------|
577
+ | Email | SendGrid credentials (env or collection comms settings) |
578
+ | Push | `VAPID_PUBLIC_KEY`, `VAPID_PRIVATE_KEY` env vars |
579
+ | SMS | Twilio credentials (env or collection comms settings) |
580
+ | Wallet | Google Wallet issuer + service account |
581
+ | Hydration | Set `hydrate: true` and populate `include` on broadcast/transactional sends |
@@ -275,4 +275,68 @@ export interface SubscriptionsResolveResponse {
275
275
  anyMethods: boolean;
276
276
  anyWalletForSubject: boolean;
277
277
  }
278
+ /**
279
+ * Send a single message to one contact using a template.
280
+ * No broadcast record is created; the send is logged directly to the
281
+ * contact's communication history with sourceType: 'transactional'.
282
+ *
283
+ * POST /admin/collection/:collectionId/comm/send
284
+ */
285
+ export interface TransactionalSendRequest {
286
+ /** CRM contact UUID */
287
+ contactId: string;
288
+ /** Firestore template ID */
289
+ templateId: string;
290
+ /**
291
+ * Channel to send on. Defaults to 'preferred', which auto-selects the
292
+ * contact's best available channel respecting consent, suppression, and
293
+ * template availability (priority: email → push → sms → wallet).
294
+ */
295
+ channel?: 'email' | 'sms' | 'push' | 'wallet' | 'preferred';
296
+ /** Extra Liquid variables merged into the top-level render context */
297
+ props?: Record<string, unknown>;
298
+ /** Context objects to hydrate into the Liquid template */
299
+ include?: {
300
+ /** Hydrate {{ collection }}. Default: true */
301
+ collection?: boolean;
302
+ /** Hydrate {{ product }} from this product ID */
303
+ productId?: string;
304
+ /** Hydrate {{ proof }} (requires productId) */
305
+ proofId?: string;
306
+ /** Hydrate {{ user }} from contact.userId */
307
+ user?: boolean;
308
+ /** Hydrate {{ appCase }} from this case UUID */
309
+ appCase?: string;
310
+ /** Hydrate {{ appThread }} from this thread UUID */
311
+ appThread?: string;
312
+ /** Hydrate {{ appRecord }} from this record UUID */
313
+ appRecord?: string;
314
+ };
315
+ /** Arbitrary label stored on the comms-events row (e.g. 'warranty-step-2') */
316
+ ref?: string;
317
+ /** App context stored on the comms-events row */
318
+ appId?: string;
319
+ }
320
+ export interface TransactionalSendResponse {
321
+ ok: true;
322
+ /** The channel the message was actually sent on */
323
+ channel: 'email' | 'sms' | 'push' | 'wallet';
324
+ /** Provider message ID (email/SMS); absent for push/wallet */
325
+ messageId?: string;
326
+ }
327
+ export interface TransactionalSendError {
328
+ ok: false;
329
+ /**
330
+ * Error code. Known values:
331
+ * - `transactional.contact_not_found`
332
+ * - `transactional.template_not_found`
333
+ * - `transactional.no_channel_available`
334
+ * - `transactional.email_missing`
335
+ * - `transactional.phone_missing`
336
+ * - `transactional.no_push_methods`
337
+ * - `transactional.no_wallet_methods`
338
+ */
339
+ error: string;
340
+ }
341
+ export type TransactionalSendResult = TransactionalSendResponse | TransactionalSendError;
278
342
  export {};