@proteos/sdk 0.49.0 → 0.50.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proteos/sdk",
3
- "version": "0.49.0",
3
+ "version": "0.50.1",
4
4
  "license": "Apache-2.0",
5
5
  "description": "TypeScript SDK for the Proteos platform",
6
6
  "repository": {
@@ -88,6 +88,12 @@ export const PLATFORM_ENTITIES: readonly PlatformEntity[] = [
88
88
  { slug: 'contact-groups', name: 'Contact Groups' },
89
89
  // Tone-of-voice synthesis: per-user setups + generated instruction profiles.
90
90
  { slug: 'tone-profiles', name: 'Tone Profiles' },
91
+ // Outbound send constraints (windows, connection limits, frequency caps) +
92
+ // their preset catalog.
93
+ { slug: 'sending-rules', name: 'Sending Rules' },
94
+ // The channel_action ledger: acts through a connection that are neither a
95
+ // message nor a reaction (LinkedIn invitations, profile visits, InMail).
96
+ { slug: 'channel-actions', name: 'Channel Actions' },
91
97
  // Connectors (connector-service). `connections` above is shared; this is the
92
98
  // manifest catalog.
93
99
  { slug: 'connectors', name: 'Connectors' },
@@ -2,14 +2,18 @@ import type { ProteosClient } from '../client.js'
2
2
  import type {
3
3
  AcceptMistranscribedTermRequest,
4
4
  AgentListener,
5
+ ApplySendingLimitPresetRequest,
5
6
  AttachContactAddressRequest,
6
7
  BlockContactRequest,
8
+ CallTokenResponse,
9
+ ChannelAction,
7
10
  Connection,
8
11
  Contact,
9
12
  ContactAddress,
10
13
  ContactErasureRequest,
11
14
  ContactGroup,
12
15
  ContactMergeProposal,
16
+ ContactRecordLink,
13
17
  Conversation,
14
18
  ConversationFilter,
15
19
  ConversationFilterEvent,
@@ -17,13 +21,12 @@ import type {
17
21
  CreateAgentListenerRequest,
18
22
  CreateConnectionRequest,
19
23
  CreateContactGroupRequest,
20
- CreateContactRequest,
21
- ContactRecordLink,
22
24
  CreateContactRecordLinkRequest,
23
- ListContactRecordLinksQuery,
25
+ CreateContactRequest,
24
26
  CreateConversationFilterRequest,
25
27
  CreateConversationTypeRequest,
26
28
  CreateGlossaryTermRequest,
29
+ CreateSendingRuleRequest,
27
30
  CreateToneProfileSetupRequest,
28
31
  CreateTranscriptionRequest,
29
32
  DeleteConnectionQuery,
@@ -32,10 +35,12 @@ import type {
32
35
  InstallConnectionRequest,
33
36
  InstallConnectionResponse,
34
37
  ListAgentListenersQuery,
38
+ ListChannelActionsQuery,
35
39
  ListConnectionsQuery,
36
40
  ListContactAddressesQuery,
37
41
  ListContactGroupsQuery,
38
42
  ListContactMergeProposalsQuery,
43
+ ListContactRecordLinksQuery,
39
44
  ListContactsQuery,
40
45
  ListConversationFilterEventsQuery,
41
46
  ListConversationFiltersQuery,
@@ -47,21 +52,27 @@ import type {
47
52
  ListReactionsResponse,
48
53
  ListResponse,
49
54
  ListRoomsQuery,
55
+ ListSendingLimitPresetsQuery,
56
+ ListSendingRulesQuery,
50
57
  ListToneProfileSetupsQuery,
51
58
  ListToneProfilesQuery,
52
59
  ListTranscriptionsQuery,
53
- CallTokenResponse,
54
60
  MaterializeTranscriptionRequest,
55
- MintCallTokenRequest,
56
- PhoneNumber,
57
- UpdatePhoneNumberRequest,
58
61
  MergeContactsRequest,
59
62
  Message,
63
+ MintCallTokenRequest,
60
64
  MistranscribedTerm,
65
+ PerformChannelActionRequest,
66
+ PhoneNumber,
61
67
  Reaction,
62
68
  RecordPermissionEventRequest,
63
69
  ResolveToneProfileQuery,
70
+ RespondChannelActionRequest,
64
71
  Room,
72
+ SendEligibility,
73
+ SendEligibilityRequest,
74
+ SendingLimitPreset,
75
+ SendingRule,
65
76
  SendMessageRequest,
66
77
  SyncConnectionRequest,
67
78
  ToneProfile,
@@ -77,6 +88,8 @@ import type {
77
88
  UpdateConversationTypeRequest,
78
89
  UpdateDraftRequest,
79
90
  UpdateGlossaryTermRequest,
91
+ UpdatePhoneNumberRequest,
92
+ UpdateSendingRuleRequest,
80
93
  UpdateTranscriptionRequest,
81
94
  } from './types.js'
82
95
  import { type VoiceService, VoiceServiceImpl } from './voice.js'
@@ -102,6 +115,13 @@ export class ConversationClient {
102
115
  readonly agentListeners: AgentListenerService
103
116
  /** Ingest-time filter rules (drop-with-audit) + their event trail. */
104
117
  readonly conversationFilters: ConversationFilterService
118
+ /** Outbound send constraints: windows, connection limits, frequency caps + presets. */
119
+ readonly sendingRules: SendingRuleService
120
+ /**
121
+ * Channel actions: acts performed through a connection that are neither a
122
+ * message nor a reaction — LinkedIn invitations, profile visits, InMail.
123
+ */
124
+ readonly channelActions: ChannelActionService
105
125
  /** Per-org glossary: custom vocabulary that boosts transcription accuracy. */
106
126
  readonly glossaryTerms: GlossaryTermService
107
127
  /** Conversation taxonomy: the types the pre-summary classifier assigns. */
@@ -126,6 +146,8 @@ export class ConversationClient {
126
146
  this.messages = new MessageServiceImpl(client)
127
147
  this.agentListeners = new AgentListenerServiceImpl(client)
128
148
  this.conversationFilters = new ConversationFilterServiceImpl(client)
149
+ this.sendingRules = new SendingRuleServiceImpl(client)
150
+ this.channelActions = new ChannelActionServiceImpl(client)
129
151
  this.glossaryTerms = new GlossaryTermServiceImpl(client)
130
152
  this.conversationTypes = new ConversationTypeServiceImpl(client)
131
153
  this.contactGroups = new ContactGroupServiceImpl(client)
@@ -369,7 +391,9 @@ class ContactServiceImpl implements ContactService {
369
391
  return this.client.request('POST', `${CONVERSATION_BASE_PATH}/contact-record-links`, request)
370
392
  }
371
393
 
372
- listRecordLinks(query: ListContactRecordLinksQuery = {}): Promise<ListResponse<ContactRecordLink>> {
394
+ listRecordLinks(
395
+ query: ListContactRecordLinksQuery = {},
396
+ ): Promise<ListResponse<ContactRecordLink>> {
373
397
  return this.client.requestWithQuery(
374
398
  'GET',
375
399
  `${CONVERSATION_BASE_PATH}/contact-record-links`,
@@ -549,6 +573,12 @@ export interface MessageService {
549
573
  /** One message with its read-time projections (reactions, attachments). */
550
574
  get(messageId: string): Promise<Message>
551
575
  send(request: SendMessageRequest): Promise<Message>
576
+ /**
577
+ * Dry-run of the sending gate: same addressing as send, no content, nothing
578
+ * minted. Answers whether the send may go out now and, when held, the
579
+ * earliest instant it may (`earliest_allowed_at`).
580
+ */
581
+ checkSendEligibility(request: SendEligibilityRequest): Promise<SendEligibility>
552
582
  /**
553
583
  * Store an outbound message for human review (status=draft) — same request
554
584
  * shape as send, nothing reaches the connector until sendDraft. Originate
@@ -600,6 +630,14 @@ class MessageServiceImpl implements MessageService {
600
630
  return this.client.request('POST', `${CONVERSATION_BASE_PATH}/messages/send`, request)
601
631
  }
602
632
 
633
+ checkSendEligibility(request: SendEligibilityRequest): Promise<SendEligibility> {
634
+ return this.client.request(
635
+ 'POST',
636
+ `${CONVERSATION_BASE_PATH}/messages/send-eligibility`,
637
+ request,
638
+ )
639
+ }
640
+
603
641
  draft(request: SendMessageRequest): Promise<Message> {
604
642
  return this.client.request('POST', `${CONVERSATION_BASE_PATH}/messages/draft`, request)
605
643
  }
@@ -1136,7 +1174,10 @@ export interface CallService {
1136
1174
  request: UpdatePhoneNumberRequest,
1137
1175
  ): Promise<{ data: PhoneNumber[] }>
1138
1176
  /** Point one number at the platform. Returns the refreshed list. */
1139
- connectPhoneNumber(connectionId: string, numberExternalId: string): Promise<{ data: PhoneNumber[] }>
1177
+ connectPhoneNumber(
1178
+ connectionId: string,
1179
+ numberExternalId: string,
1180
+ ): Promise<{ data: PhoneNumber[] }>
1140
1181
  /** Release one number (restores its previous handler). Returns the refreshed list. */
1141
1182
  disconnectPhoneNumber(
1142
1183
  connectionId: string,
@@ -1199,3 +1240,130 @@ export type {
1199
1240
  VoiceService,
1200
1241
  VoiceTranscriptionStream,
1201
1242
  } from './voice.js'
1243
+
1244
+ /**
1245
+ * Sending rules — outbound send constraints (windows, connection limits,
1246
+ * frequency caps), the static limit-preset catalog and its apply expansion.
1247
+ * A denied send/reply/sendDraft fails with a ProteosError whose `details`
1248
+ * carry `earliest_allowed_at` (429) or the blocked contact (403).
1249
+ */
1250
+ export interface SendingRuleService {
1251
+ list(query?: ListSendingRulesQuery): Promise<ListResponse<SendingRule>>
1252
+ get(id: string): Promise<SendingRule>
1253
+ create(request: CreateSendingRuleRequest): Promise<SendingRule>
1254
+ update(id: string, request: UpdateSendingRuleRequest): Promise<SendingRule>
1255
+ delete(id: string): Promise<void>
1256
+ /** The static preset catalog, optionally narrowed to one connector. */
1257
+ listPresets(query?: ListSendingLimitPresetsQuery): Promise<{ data: SendingLimitPreset[] }>
1258
+ /**
1259
+ * Expand a preset into limit rules linked to the given connections,
1260
+ * replacing the limit rules they were linked to before.
1261
+ */
1262
+ applyPreset(request: ApplySendingLimitPresetRequest): Promise<{ data: SendingRule[] }>
1263
+ }
1264
+
1265
+ class SendingRuleServiceImpl implements SendingRuleService {
1266
+ constructor(private readonly client: ProteosClient) {}
1267
+
1268
+ list(query: ListSendingRulesQuery = {}): Promise<ListResponse<SendingRule>> {
1269
+ return this.client.requestWithQuery('GET', `${CONVERSATION_BASE_PATH}/sending-rules`, query)
1270
+ }
1271
+
1272
+ get(id: string): Promise<SendingRule> {
1273
+ return this.client.request(
1274
+ 'GET',
1275
+ `${CONVERSATION_BASE_PATH}/sending-rules/${encodeURIComponent(id)}`,
1276
+ )
1277
+ }
1278
+
1279
+ create(request: CreateSendingRuleRequest): Promise<SendingRule> {
1280
+ return this.client.request('POST', `${CONVERSATION_BASE_PATH}/sending-rules`, request)
1281
+ }
1282
+
1283
+ update(id: string, request: UpdateSendingRuleRequest): Promise<SendingRule> {
1284
+ return this.client.request(
1285
+ 'PATCH',
1286
+ `${CONVERSATION_BASE_PATH}/sending-rules/${encodeURIComponent(id)}`,
1287
+ request,
1288
+ )
1289
+ }
1290
+
1291
+ async delete(id: string): Promise<void> {
1292
+ await this.client.request(
1293
+ 'DELETE',
1294
+ `${CONVERSATION_BASE_PATH}/sending-rules/${encodeURIComponent(id)}`,
1295
+ )
1296
+ }
1297
+
1298
+ listPresets(query: ListSendingLimitPresetsQuery = {}): Promise<{ data: SendingLimitPreset[] }> {
1299
+ return this.client.requestWithQuery(
1300
+ 'GET',
1301
+ `${CONVERSATION_BASE_PATH}/sending-rules/presets`,
1302
+ query,
1303
+ )
1304
+ }
1305
+
1306
+ applyPreset(request: ApplySendingLimitPresetRequest): Promise<{ data: SendingRule[] }> {
1307
+ return this.client.request(
1308
+ 'POST',
1309
+ `${CONVERSATION_BASE_PATH}/sending-rules/apply-preset`,
1310
+ request,
1311
+ )
1312
+ }
1313
+ }
1314
+
1315
+ /**
1316
+ * Channel actions — the ledger of acts performed through a connection that
1317
+ * are neither a message nor a reaction (LinkedIn invitations, profile visits,
1318
+ * InMail). A denied perform fails like a denied send (ProteosError with
1319
+ * `details.earliest_allowed_at`); a provider refusal carries its own code
1320
+ * (already_connected, already_invited_recently, invitation_already_received,
1321
+ * connection_limit_reached, insufficient_inmail_credits, inmail_not_allowed,
1322
+ * not_connected_with_recipient). An invitation is pre-flighted with one silent
1323
+ * profile read before anything is sent, so those refusals never mint a
1324
+ * phantom act.
1325
+ */
1326
+ export interface ChannelActionService {
1327
+ list(query?: ListChannelActionsQuery): Promise<ListResponse<ChannelAction>>
1328
+ get(id: string): Promise<ChannelAction>
1329
+ /** Performs one act; resolves to the row in its post-perform status. */
1330
+ perform(request: PerformChannelActionRequest): Promise<ChannelAction>
1331
+ /** Withdraws a performed outbound act (an invitation). */
1332
+ cancel(id: string): Promise<ChannelAction>
1333
+ /** Answers an inbound act (a received invitation). */
1334
+ respond(id: string, request: RespondChannelActionRequest): Promise<ChannelAction>
1335
+ }
1336
+
1337
+ class ChannelActionServiceImpl implements ChannelActionService {
1338
+ constructor(private readonly client: ProteosClient) {}
1339
+
1340
+ list(query: ListChannelActionsQuery = {}): Promise<ListResponse<ChannelAction>> {
1341
+ return this.client.requestWithQuery('GET', `${CONVERSATION_BASE_PATH}/channel-actions`, query)
1342
+ }
1343
+
1344
+ get(id: string): Promise<ChannelAction> {
1345
+ return this.client.request(
1346
+ 'GET',
1347
+ `${CONVERSATION_BASE_PATH}/channel-actions/${encodeURIComponent(id)}`,
1348
+ )
1349
+ }
1350
+
1351
+ perform(request: PerformChannelActionRequest): Promise<ChannelAction> {
1352
+ return this.client.request('POST', `${CONVERSATION_BASE_PATH}/channel-actions`, request)
1353
+ }
1354
+
1355
+ cancel(id: string): Promise<ChannelAction> {
1356
+ return this.client.request(
1357
+ 'POST',
1358
+ `${CONVERSATION_BASE_PATH}/channel-actions/${encodeURIComponent(id)}/cancel`,
1359
+ )
1360
+ }
1361
+
1362
+ respond(id: string, request: RespondChannelActionRequest): Promise<ChannelAction> {
1363
+ return this.client.request(
1364
+ 'POST',
1365
+ `${CONVERSATION_BASE_PATH}/channel-actions/${encodeURIComponent(id)}/respond`,
1366
+ request,
1367
+ )
1368
+ }
1369
+ }
@@ -290,6 +290,14 @@ export interface Connection {
290
290
  supports_reactions: boolean
291
291
  /** The capability descriptor; absent when unsupported. */
292
292
  reactions?: ReactionCapability
293
+ /**
294
+ * Computed on read like reactions: the channel actions the connector
295
+ * performs through this connection (invitation, profile_visit, inmail, …).
296
+ * A separate list from reactions — a reaction toggles an edge on a
297
+ * message, an action is a performed act with its own lifecycle. Absent
298
+ * when the connector performs none.
299
+ */
300
+ actions?: ChannelActionCapability[]
293
301
  /**
294
302
  * Computed on read like supports_reactions: who operates the integration
295
303
  * (native | unipile). Absent when the connector is not registered in this
@@ -1220,6 +1228,14 @@ export interface Contact {
1220
1228
  /** Merge tombstone redirect (set when status is 'merged'). */
1221
1229
  merged_into_contact_id?: string
1222
1230
  source: ContactSource
1231
+ /** IANA zone name (Europe/Berlin); absent = unknown. Filled from directory sweeps while empty. */
1232
+ timezone?: string
1233
+ /**
1234
+ * BCP-47 language tag with optional region (de, de-CH, pt-BR); absent =
1235
+ * unknown. Named locale, not language: the region carries formatting
1236
+ * conventions on top of the language.
1237
+ */
1238
+ locale?: string
1223
1239
  /** ContactGroup membership (one group per contact); absent = unassigned. */
1224
1240
  group_key?: string
1225
1241
  /**
@@ -1347,6 +1363,10 @@ export interface UpdateContactRequest {
1347
1363
  name?: string
1348
1364
  status?: 'active' | 'archived'
1349
1365
  has_legal_hold?: boolean
1366
+ /** IANA zone name; normalized server-side, 400 contact_timezone_invalid when unparseable; '' clears. */
1367
+ timezone?: string
1368
+ /** BCP-47 language tag; normalized server-side, 400 contact_locale_invalid when unparseable; '' clears. */
1369
+ locale?: string
1350
1370
  /**
1351
1371
  * Assigns the contact to a contact group ('' clears). A PATCH assignment is
1352
1372
  * stamped group_source='manual' — tone synthesis never overrides it.
@@ -1362,6 +1382,10 @@ export interface UpdateContactRequest {
1362
1382
  export interface CreateContactRequest {
1363
1383
  name: string
1364
1384
  addresses: AttachContactAddressRequest[]
1385
+ /** IANA zone name; optional, validated as on update. */
1386
+ timezone?: string
1387
+ /** BCP-47 language tag; optional, validated as on update. */
1388
+ locale?: string
1365
1389
  }
1366
1390
 
1367
1391
  export interface AttachContactAddressRequest {
@@ -1610,3 +1634,269 @@ export interface DispatchMeetingBotRequest {
1610
1634
  */
1611
1635
  language?: string
1612
1636
  }
1637
+
1638
+ // ── Sending rules ─────────────────────────────────────────────────────────────
1639
+
1640
+ /**
1641
+ * Discriminates a sending rule: window (WHEN sending is allowed, recipient-
1642
+ * local weekday ranges), limit (HOW MUCH one connection may send per rolling
1643
+ * period), frequency_cap (HOW OFTEN one contact may be contacted per rolling
1644
+ * period).
1645
+ */
1646
+ export type SendingRuleType = 'window' | 'limit' | 'frequency_cap'
1647
+ /** Rolling lookback ("last 24 hours from now") — never a calendar day. */
1648
+ export type SendingPeriod = 'rolling_24h' | 'rolling_7d' | 'rolling_30d'
1649
+ /**
1650
+ * ONE kind of act performed through a channel connection — shared by a
1651
+ * limit's `action` (what it counts), a channel action's `action_type` (what
1652
+ * was performed) and the eligibility check. `message` is the plain send
1653
+ * (valid on a limit, never on a channel action row).
1654
+ */
1655
+ export type ChannelActionType = 'message' | 'invitation' | 'inmail' | 'profile_visit'
1656
+ export type Weekday =
1657
+ | 'monday'
1658
+ | 'tuesday'
1659
+ | 'wednesday'
1660
+ | 'thursday'
1661
+ | 'friday'
1662
+ | 'saturday'
1663
+ | 'sunday'
1664
+
1665
+ /** One open range on one weekday, "HH:MM" wall-clock, from < until, same day. */
1666
+ export interface WindowDay {
1667
+ day: Weekday
1668
+ from: string
1669
+ until: string
1670
+ }
1671
+ export interface WindowRuleConfig {
1672
+ days: WindowDay[]
1673
+ /** IANA zone used for contacts without a timezone. */
1674
+ fallback_timezone: string
1675
+ }
1676
+ export interface LimitRuleConfig {
1677
+ action: ChannelActionType
1678
+ max_count: number
1679
+ period: SendingPeriod
1680
+ /** Minimum spacing between consecutive sends; 0/absent = none. */
1681
+ min_gap_seconds?: number
1682
+ }
1683
+ export interface FrequencyCapRuleConfig {
1684
+ max_count: number
1685
+ period: SendingPeriod
1686
+ }
1687
+ export type SendingRuleConfig = WindowRuleConfig | LimitRuleConfig | FrequencyCapRuleConfig
1688
+
1689
+ /**
1690
+ * One outbound send constraint, defined once and LINKED to any number of
1691
+ * connections and/or channels (both empty = org-wide). Per rule type the most
1692
+ * specific tier wins at send time: names the connection > names the channel >
1693
+ * org-wide. Replies skip the rule when `is_reply_exempt`.
1694
+ */
1695
+ export interface SendingRule {
1696
+ id: string
1697
+ org_id: string
1698
+ name: string
1699
+ connection_ids: string[]
1700
+ channels: Channel[]
1701
+ rule_type: SendingRuleType
1702
+ rule_config?: SendingRuleConfig
1703
+ is_enabled: boolean
1704
+ is_reply_exempt: boolean
1705
+ created_at: string
1706
+ created_by: UserRef
1707
+ updated_at: string
1708
+ updated_by: UserRef
1709
+ }
1710
+
1711
+ export interface CreateSendingRuleRequest {
1712
+ name?: string
1713
+ connection_ids?: string[]
1714
+ channels?: Channel[]
1715
+ rule_type: SendingRuleType
1716
+ rule_config: Record<string, unknown>
1717
+ /** Defaults to true. */
1718
+ is_enabled?: boolean
1719
+ /** Defaults per type: window + frequency_cap true, limit false. */
1720
+ is_reply_exempt?: boolean
1721
+ }
1722
+
1723
+ export interface UpdateSendingRuleRequest {
1724
+ name?: string
1725
+ /** Replaces the stored links wholesale when present. */
1726
+ connection_ids?: string[]
1727
+ channels?: Channel[]
1728
+ /** rule_type and rule_config must be sent together. */
1729
+ rule_type?: SendingRuleType
1730
+ rule_config?: Record<string, unknown>
1731
+ is_enabled?: boolean
1732
+ is_reply_exempt?: boolean
1733
+ }
1734
+
1735
+ export interface ListSendingRulesQuery extends PaginationQuery {
1736
+ /** Rules LINKED to this connection. */
1737
+ connection_id?: string
1738
+ channel?: Channel
1739
+ rule_type?: SendingRuleType
1740
+ is_enabled?: boolean
1741
+ }
1742
+
1743
+ /** A recommended limit bundle for one class of sender account (static catalog). */
1744
+ export interface SendingLimitPreset {
1745
+ key: string
1746
+ name: string
1747
+ description: string
1748
+ connector_keys: ConnectorKey[]
1749
+ is_recommended: boolean
1750
+ rules: { rule_type: 'limit'; rule_config: LimitRuleConfig }[]
1751
+ }
1752
+
1753
+ export interface ListSendingLimitPresetsQuery {
1754
+ connector_key?: ConnectorKey
1755
+ }
1756
+
1757
+ export interface ApplySendingLimitPresetRequest {
1758
+ connection_ids: string[]
1759
+ preset_key: string
1760
+ }
1761
+
1762
+ /** Dry-run twin of SendMessageRequest: addressing only, nothing minted. */
1763
+ export interface SendEligibilityRequest {
1764
+ conversation_id?: string
1765
+ reply_to_message_id?: string
1766
+ connection_id?: string
1767
+ to?: SendRecipient[]
1768
+ cc?: SendRecipient[]
1769
+ bcc?: SendRecipient[]
1770
+ /**
1771
+ * Widens the check to a channel action (invitation, profile_visit, inmail):
1772
+ * originate mode only, the first `to` recipient is the target. Absent =
1773
+ * message.
1774
+ */
1775
+ action_type?: ChannelActionType
1776
+ }
1777
+
1778
+ /**
1779
+ * "May this send go out now?" — `reason` is the error code a real send would
1780
+ * fail with (sending_window_closed | sending_limit_reached |
1781
+ * frequency_cap_reached | contact_blocked | contact_opted_out);
1782
+ * `earliest_allowed_at` is set for the temporal three.
1783
+ */
1784
+ export interface SendEligibility {
1785
+ is_allowed: boolean
1786
+ reason?: string
1787
+ rule_id?: string
1788
+ rule_type?: SendingRuleType
1789
+ earliest_allowed_at?: string
1790
+ contact_id?: string
1791
+ contact_address_id?: string
1792
+ }
1793
+
1794
+ // ── Channel actions ───────────────────────────────────────────────────────────
1795
+
1796
+ /**
1797
+ * Lifecycle of a channel action. Execution: pending → performed | failed.
1798
+ * Outcome (invitations): performed → accepted | declined | withdrawn |
1799
+ * expired; an inbound received invitation starts pending and ends
1800
+ * accepted | declined | expired.
1801
+ */
1802
+ export type ChannelActionStatus =
1803
+ | 'pending'
1804
+ | 'performed'
1805
+ | 'failed'
1806
+ | 'accepted'
1807
+ | 'declined'
1808
+ | 'withdrawn'
1809
+ | 'expired'
1810
+ /** Our answer to an inbound channel action (a received invitation). */
1811
+ export type ChannelActionResponse = 'accept' | 'decline'
1812
+ /** What an action type acts on: a person on the channel, or an external object. */
1813
+ export type ChannelActionTargetKind = 'contact-address' | 'external'
1814
+
1815
+ /**
1816
+ * One action type a connector performs, projected onto `connection.actions`.
1817
+ */
1818
+ export interface ChannelActionCapability {
1819
+ action_type: ChannelActionType
1820
+ target_kind: ChannelActionTargetKind
1821
+ /** The act can be withdrawn after performing (an invitation). */
1822
+ is_cancelable: boolean
1823
+ /** An inbound act of this type can be answered (accept / decline). */
1824
+ is_respondable: boolean
1825
+ /** Performing also sends a message that lands as a Message + Conversation (InMail). */
1826
+ is_message_minting: boolean
1827
+ /** Bound of the free-text note the act carries (LinkedIn invitation: 300). */
1828
+ max_note_length?: number
1829
+ }
1830
+
1831
+ export interface InvitationParams {
1832
+ note?: string
1833
+ email?: string
1834
+ }
1835
+ export type ProfileVisitParams = Record<string, never>
1836
+ export interface InmailParams {
1837
+ subject?: string
1838
+ content: ContentBlock[]
1839
+ }
1840
+ export type ChannelActionParams = InvitationParams | ProfileVisitParams | InmailParams
1841
+
1842
+ /**
1843
+ * One act performed through a channel connection that is neither a message
1844
+ * nor a reaction — a LinkedIn invitation, a profile visit, an InMail (which
1845
+ * ALSO mints a message, see message_id). A LEDGER row: appended, transitioned
1846
+ * along its lifecycle, never toggled. `direction` outbound = we acted;
1847
+ * inbound = someone acted on us (a received invitation you may answer).
1848
+ * `created_by` is the performer on outbound rows (user or agent), the system
1849
+ * on ingested inbound rows; `updated_by` who accepted / declined / withdrew.
1850
+ */
1851
+ export interface ChannelAction {
1852
+ id: string
1853
+ org_id: string
1854
+ connection_id: string
1855
+ connector_key: ConnectorKey
1856
+ channel: Channel
1857
+ action_type: ChannelActionType
1858
+ direction: MessageDirection
1859
+ status: ChannelActionStatus
1860
+ contact_id?: string
1861
+ contact_address_id?: string
1862
+ /** The provider-side identity acted on (a LinkedIn member id). */
1863
+ target_external_id: string
1864
+ contact: ContactRef
1865
+ /** An InMail's minted message; an invitation's note-chat once accepted. */
1866
+ message_id?: string
1867
+ conversation_id?: string
1868
+ params?: ChannelActionParams
1869
+ /** The provider handle (Unipile invitation id) — cancel / respond key. */
1870
+ external_action_id?: string
1871
+ error?: string
1872
+ occurred_at: string
1873
+ resolved_at?: string
1874
+ /** Provider enrichment: invitation usage %, network_distance, … */
1875
+ metadata: Record<string, unknown>
1876
+ created_at: string
1877
+ created_by: UserRef
1878
+ updated_at: string
1879
+ updated_by: UserRef
1880
+ }
1881
+
1882
+ /** Performs one act. `target` is the person acted on (kind contact-address + the connector-side external id). */
1883
+ export interface PerformChannelActionRequest {
1884
+ connection_id: string
1885
+ action_type: ChannelActionType
1886
+ target: SendRecipient
1887
+ /** Per-type input: invitation {note?, email?}; profile_visit {}; inmail {subject?, content}. */
1888
+ params?: Record<string, unknown>
1889
+ }
1890
+
1891
+ export interface RespondChannelActionRequest {
1892
+ response: ChannelActionResponse
1893
+ }
1894
+
1895
+ export interface ListChannelActionsQuery extends PaginationQuery {
1896
+ channel?: Channel
1897
+ connection_id?: string
1898
+ action_type?: ChannelActionType
1899
+ direction?: MessageDirection
1900
+ status?: ChannelActionStatus
1901
+ contact_id?: string
1902
+ }
package/src/errors.ts CHANGED
@@ -23,12 +23,24 @@ export class ProteosError extends Error {
23
23
  readonly httpStatus: number
24
24
  /** API error code (e.g., 'not_found', 'unauthorized') */
25
25
  readonly code: ErrorCodeType | string
26
-
27
- constructor(message: string, httpStatus: number, code: ErrorCodeType | string) {
26
+ /**
27
+ * Optional machine-readable payload beside the message e.g. a denied send's
28
+ * `earliest_allowed_at` / `rule_id`, the offending `contact_id`. Absent on
29
+ * most errors.
30
+ */
31
+ readonly details: Record<string, unknown> | undefined
32
+
33
+ constructor(
34
+ message: string,
35
+ httpStatus: number,
36
+ code: ErrorCodeType | string,
37
+ details?: Record<string, unknown>,
38
+ ) {
28
39
  super(message)
29
40
  this.name = 'ProteosError'
30
41
  this.httpStatus = httpStatus
31
42
  this.code = code
43
+ this.details = details
32
44
 
33
45
  // V8-specific API for cleaner stack traces; not present in all runtimes
34
46
  const v8Capture = (Error as unknown as { captureStackTrace?: (t: object, c: unknown) => void })
@@ -112,6 +124,7 @@ export function getDefaultErrorCode(httpStatus: number): ErrorCodeType | string
112
124
  export interface ApiErrorResponse {
113
125
  code?: string
114
126
  message?: string
127
+ details?: Record<string, unknown>
115
128
  }
116
129
 
117
130
  /**
@@ -121,6 +134,7 @@ export async function parseErrorResponse(response: Response): Promise<ProteosErr
121
134
  const httpStatus = response.status
122
135
  let code: string = getDefaultErrorCode(httpStatus)
123
136
  let message = 'Unknown error'
137
+ let details: Record<string, unknown> | undefined
124
138
 
125
139
  try {
126
140
  const body = await response.text()
@@ -136,6 +150,9 @@ export async function parseErrorResponse(response: Response): Promise<ProteosErr
136
150
  } else {
137
151
  message = body || `HTTP ${httpStatus}`
138
152
  }
153
+ if (json.details && typeof json.details === 'object') {
154
+ details = json.details
155
+ }
139
156
  } catch {
140
157
  // Not JSON, use body as message
141
158
  message = body.trim() || `HTTP ${httpStatus}`
@@ -144,5 +161,5 @@ export async function parseErrorResponse(response: Response): Promise<ProteosErr
144
161
  message = `HTTP ${httpStatus}`
145
162
  }
146
163
 
147
- return new ProteosError(message, httpStatus, code)
164
+ return new ProteosError(message, httpStatus, code, details)
148
165
  }