@signalhousellc/sdk 1.4.0 → 1.4.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": "@signalhousellc/sdk",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Signal House SDK for use with the Signal House platform",
5
5
  "type": "module",
6
6
  "main": "src/SignalHouseSDK.js",
File without changes
@@ -17,14 +17,17 @@ description: >-
17
17
  **Deliveries to a webhook endpoint are signed.** Every endpoint created with `POST /webhook` gets a
18
18
  signing secret, returned in plaintext **exactly once**, as `signingSecret` in that create response.
19
19
  Every later read reports only `hasSigningSecret: true|false`. Store the secret at creation; nothing
20
- can hand it back, and a lost secret means creating a new endpoint.
20
+ can hand it back. If it is lost or exposed, rotate it: `POST /webhook/:id/rotate-secret` returns a new
21
+ secret once, and the old one keeps working for a grace window (`graceSeconds`, default 24h, max 7
22
+ days, `0` for none) so the receiver can switch over without dropping deliveries.
21
23
 
22
24
  A signed delivery carries:
23
25
 
24
26
  | Header | Value |
25
27
  |---|---|
26
- | `X-SignalHouse-Signature` | hex HMAC-SHA256, keyed with the signing secret, over `${timestamp}.${rawBody}` |
28
+ | `X-SignalHouse-Signature` | hex HMAC-SHA256, keyed with the signing secret, over `${timestamp}.${rawBody}`. During a rotation grace window it stays on the **previous** secret and switches when the window ends |
27
29
  | `X-SignalHouse-Timestamp` | the `timestamp` in that string: Unix time in **milliseconds** |
30
+ | `X-SignalHouse-Signature-V1` | `t=<ms>,v1=<hex>[,v1=<hex>]`: one signature per currently valid secret, newest first. During a rotation grace window it carries both the new and the previous secret's signature; accept a match on any |
28
31
  | `X-SignalHouse-Delivery-Id` | a UUID for this event to this endpoint, the same on every retry |
29
32
 
30
33
  A correct verifier:
@@ -37,12 +40,18 @@ A correct verifier:
37
40
  header is milliseconds, not seconds.
38
41
  4. **Rejects an unsigned request to an endpoint that has a secret.** Otherwise a forger simply
39
42
  leaves the headers off.
43
+ 5. **Prefers `X-SignalHouse-Signature-V1`**, so a secret rotation never breaks verification.
44
+
45
+ In Node, `verifyWebhookSignature` from `@signalhousellc/sdk/webhooks` does all of this and returns
46
+ `{ valid, reason }` without throwing. Other languages implement the five steps directly; the
47
+ Webhook Signing guide has a Python version.
40
48
 
41
49
  What is still **not** verifiable:
42
50
 
43
- - **Endpoints created before signing shipped (2026-09-03) are sent unsigned.** They report
44
- `hasSigningSecret: false`, and there is no way to add a secret to an existing endpoint. To get
45
- signing, create a new endpoint, store its secret, then delete the old one.
51
+ - **Endpoints created before signing shipped (2026-09-03) are sent unsigned** until they get a
52
+ secret. They report `hasSigningSecret: false`. Call `POST /webhook/:id/rotate-secret` once (or use
53
+ *Create signing secret* in the portal's webhook edit view) to issue their first secret; deliveries
54
+ are signed from then on.
46
55
  - **Per-number inbound URLs are never signed** (see below).
47
56
 
48
57
  For anything unsigned, the old rules still apply: do not treat the request as authenticated, and
@@ -50,9 +59,10 @@ never take an irreversible or financial action on webhook content alone. Read th
50
59
  state back from the API first. Do not put secrets in a webhook URL as a substitute for
51
60
  verification; URLs end up in proxy logs and support screenshots.
52
61
 
53
- The `authType` setting (`NONE`, `BASIC`, `API_KEY`, `OAUTH2`) is a different thing: it
54
- authenticates *Signal House to the receiver's endpoint*. The signature is what proves origin and
55
- integrity. `authType` is a second layer, not a substitute.
62
+ The `authType` setting (`NONE`, `BASIC`, `API_KEY`, `OAUTH2`, `HMAC`) chooses what *else* Signal
63
+ House sends to the receiver. Signing does not depend on it: every endpoint with a secret is signed
64
+ whatever `authType` says. `BASIC`/`API_KEY`/`OAUTH2` add credentials as a second layer; `HMAC` sends
65
+ none and declares that the receiver authenticates by the signature alone.
56
66
 
57
67
  ## Retry behaviour, and when an event is gone
58
68
 
@@ -100,7 +110,7 @@ So a correct receiver:
100
110
 
101
111
  Webhooks are created per group, and numbers can additionally carry their own inbound URL.
102
112
  `authType` picks how Signal House authenticates to the endpoint, and `credentials` is
103
- `{ key, secret }`, required whenever `authType` is not `NONE`.
113
+ `{ key, secret }`, required whenever `authType` is not `NONE` or `HMAC` (and rejected with those two).
104
114
 
105
115
  | authType | What is sent |
106
116
  |---|---|
@@ -108,10 +118,10 @@ Webhooks are created per group, and numbers can additionally carry their own inb
108
118
  | `BASIC` | HTTP Basic; `key` is the username, `secret` is the password |
109
119
  | `API_KEY` | `key` holds the key; `secret` optional |
110
120
  | `OAUTH2` | `secret` is the access token sent as the Authorization value; `key` is required at create but unused in outgoing headers |
121
+ | `HMAC` | no credentials and no Authorization header; the receiver verifies the signature headers. Rejected for an endpoint with no signing secret until it has one |
111
122
 
112
- Verify the signature on every endpoint that has one. On top of that, `NONE` is fine for an
113
- endpoint that is already unreachable from the public internet; for anything else a real `authType`
114
- is cheap defense in depth.
123
+ Verify the signature on every endpoint that has one. `HMAC` (or `NONE`) is fine for a receiver
124
+ that verifies signatures; `BASIC` or `API_KEY` on top is cheap defense in depth.
115
125
 
116
126
  **The table above does not cover per-number inbound URLs.** A number can carry
117
127
  `primaryInboundWebhookUrl` and `secondaryInboundWebhookUrl`, and inbound messages are posted to
@@ -124,7 +134,9 @@ signed group endpoint instead.
124
134
 
125
135
  ## Where to look it up
126
136
 
127
- - **Event types, payload shape and the signing scheme:** the Webhooks reference,
137
+ - **The signing scheme, verification and rotation:** the Webhook Signing guide,
138
+ https://app2.signalhouse.io/docs/webhook-signing.
139
+ - **Event types and payload shape:** the Webhooks reference,
128
140
  https://app2.signalhouse.io/docs/webhooks. The event list changes independently of this skill;
129
141
  read it there rather than reciting it here.
130
142
  - **Whether an event actually happened:** `GET /notification`
@@ -143,6 +143,24 @@
143
143
  * @property {boolean} [allowGreetingInterruption] - Whether a caller may talk over the greeting.
144
144
  * @property {string[]} [keyterms] - Words the transcriber is biased toward — names, jargon, unusual spellings.
145
145
  * @property {Object|null} [backgroundSound] - Ambient audio bed as `{ preset, volume }`; null = silence.
146
+ * @property {AgentWorkflow|null} [workflow] - Conversation workflow graph. Voice channel only — sending one on webchat or sms is a 400. null = no workflow.
147
+ */
148
+
149
+ /**
150
+ * A conversation workflow graph, in the voice runtime's own shape. Nodes and edges are keyed
151
+ * maps rather than arrays: an edge names its endpoints by node key, and a node's `edge_order`
152
+ * names its outgoing edges in the order their conditions are evaluated.
153
+ *
154
+ * Node bodies are deliberately not enumerated here. Node types are defined by the runtime and
155
+ * new ones appear without an SDK release, so the SDK passes them through untouched. The API
156
+ * validates the envelope — an entry point under the node key `start_node` typed `start` (the
157
+ * runtime looks it up by that exact key), no second `start` node, at most 200 nodes, and every
158
+ * edge endpoint naming a node that exists — and rejects a graph that fails any of those.
159
+ * @typedef {Object} AgentWorkflow
160
+ * @property {Object<string, Object>} nodes - Nodes keyed by node id. Each carries at least `type` and `position` ({ x, y }).
161
+ * @property {Object<string, Object>} edges - Edges keyed by edge id. Each carries `source` and `target` node ids.
162
+ * @property {Object<string, Object>} [subgraphs] - Nested subgraphs keyed by subgraph id.
163
+ * @property {boolean} [prevent_subagent_loops] - Stop a subagent handing control back to one that already ran.
146
164
  */
147
165
 
148
166
  export class Agents {
@@ -234,14 +252,16 @@ export class Agents {
234
252
  }
235
253
 
236
254
  /**
237
- * Delete (inactivate) an agent profile by its id.
255
+ * Permanently delete an agent profile by its id.
256
+ *
257
+ * Permanently deletes the agent, its channel settings, and any tools and knowledge scoped to it, plus their provider-side counterparts. Tools and knowledge shared across the account are untouched. Conversation records survive — a transcript outlives the agent that produced it.
238
258
  * @async
239
259
  * @roles api, admin, developer
240
260
  * @param {Object} params - The parameters for deleting an agent profile.
241
261
  * @param {string} params.id - The id of the agent profile to delete.
242
262
  * @param {RequestOptions} [params.options] - Additional options for the request.
243
263
  * @throws {Error} Throws an error if the id parameter is missing.
244
- * @returns {Promise<SdkResponse<any>>} - The inactivated agent profile returned from the server.
264
+ * @returns {Promise<Object>} - The deleted agent profile, as it was.
245
265
  */
246
266
  async deleteAgentProfile({ id, options = {} }) {
247
267
  this.client._require({ id });
@@ -300,11 +300,12 @@ export class Messages {
300
300
  * @param {("day"|"exact")} [params.dateBounds] - Which contract binds startDate/endDate. "day": each bound is widened to its whole UTC day. "exact": a timestamp is bound as the instant it names and a bare date is its whole UTC day. Omitted: "day".
301
301
  * @param {string|string[]} [params.region] - ISO-2 country filter: US, GB or CA (any case of UK/GB/GBR/United Kingdom is accepted for GB); one value, an array, or a comma-separated string; omitted = every region. Only accepted where the environment serves hourly analytics; elsewhere any value is rejected with a 400.
302
302
  * @param {Object[]|string} [params.regionScopes] - Array of OR-ed branches `{ region, channels, brandId?, campaignId?, phoneNumber? }` (channels among tenDLC, virtualLongCode, tollFree, shortCode, p2p); `[]` matches nothing. Sent as JSON unless already a string. Only accepted where the environment serves hourly analytics; elsewhere any value is rejected with a 400.
303
+ * @param {string} [params.errorCode] - Narrow the breakdown to a single failure reason: a numeric Infobip carrier code (as a string, e.g. "607"), the Signal House campaign-opt-out code "OUT", or a no-code failure class (other, moderation, att_limit, p2p_stop, campaign_opt_out, dnc_blocked, bind_hold_expired). Omitted = every reason.
303
304
  * @param {RequestOptions} [params.options]
304
305
  * @returns {Promise<SdkResponse<any>>} - { rows, totalCount, totalErrors: { sms, mms, p2p, all }, page, limit }
305
306
  */
306
- async getAnalyticsByErrorCode({ groupId, subgroupId, brandId, campaignId, registrationId, phoneNumber, carrier, carrierFamily, startDate, endDate, page, limit, channel, region, regionScopes, dateBounds, options = {} }) {
307
- const filters = { groupId, subgroupId, brandId, campaignId, registrationId, phoneNumber, carrier, carrierFamily, startDate, endDate, page, limit, channel, region, regionScopes, dateBounds };
307
+ async getAnalyticsByErrorCode({ groupId, subgroupId, brandId, campaignId, registrationId, phoneNumber, carrier, carrierFamily, startDate, endDate, page, limit, channel, region, regionScopes, dateBounds, errorCode, options = {} }) {
308
+ const filters = { groupId, subgroupId, brandId, campaignId, registrationId, phoneNumber, carrier, carrierFamily, startDate, endDate, page, limit, channel, region, regionScopes, dateBounds, errorCode };
308
309
  const queryString = this.client._getQueryString(withRegionScopes(filters));
309
310
  return this.client(`/message/analytics/by-error-code${queryString}`, { method: "GET", ...options });
310
311
  }
@@ -31,6 +31,7 @@
31
31
  * @property {string} role - The role of the service user: "api" for a customer API key (required)
32
32
  * @property {string[]} [allowedIps] - Only accept this API key from these IPv4/IPv6 addresses or CIDR ranges (max 100). Omit or pass [] for no IP restriction. Customer API keys only.
33
33
  * @property {string[]} [allowedCountries] - Only accept this API key from these countries, as ISO 3166-1 alpha-2 codes such as "US" (max 250). Omit or pass [] for no country restriction. Customer API keys only.
34
+ * @property {string[]} [allowedDestinationCountries] - Only let this API key send messages to these countries, as ISO 3166-1 alpha-2 codes such as "US" (max 250). Sends to any other country are rejected with a 403 DESTINATION_COUNTRY_NOT_ALLOWED. Omit or pass [] for any country. Customer API keys only.
34
35
  */
35
36
 
36
37
  /**
@@ -51,6 +52,7 @@
51
52
  * @property {string} [status] - The status of the user (e.g., "active", "inactive")
52
53
  * @property {string[]} [allowedIps] - Customer API keys only: replace the key's allowed IPv4/IPv6 addresses or CIDR ranges ([] removes the restriction). Requires an interactive admin or developer session; an API key cannot change its own restrictions.
53
54
  * @property {string[]} [allowedCountries] - Customer API keys only: replace the key's allowed ISO 3166-1 alpha-2 country codes ([] removes the restriction). Requires an interactive admin or developer session.
55
+ * @property {string[]} [allowedDestinationCountries] - Customer API keys only: replace the countries the key may send messages to ([] allows any country). Requires an interactive admin or developer session.
54
56
  */
55
57
 
56
58
  export class Users {
@@ -40,7 +40,9 @@ export class Webhooks {
40
40
  * same event, for receiver-side dedup), plus `X-SignalHouse-Signature-V1` (`t=<ms>,v1=<hex>[,v1=<hex>]`,
41
41
  * every currently valid signature). Store this secret now: it is never returned again by any read
42
42
  * (SHGHL-2944). If it's lost, call `rotateWebhookSecret`. Verify deliveries with
43
- * `verifyWebhookSignature` from `@signalhousellc/sdk/webhooks` (Node only).
43
+ * `verifyWebhookSignature` from `@signalhousellc/sdk/webhooks` (Node only). `webhookData.authType` is one of
44
+ * `NONE` (default), `BASIC`, `API_KEY`, `OAUTH2` or `HMAC`; `HMAC` (SHGHL-3595) sends no credentials, so the
45
+ * receiver authenticates by the signature alone, and must be sent without `credentials` or `apiHeaderPrefix`.
44
46
  * @async
45
47
  * @roles api, admin, developer
46
48
  * @param {Object} params - The parameters for creating a webhook
@@ -58,7 +60,9 @@ export class Webhooks {
58
60
  * Update an existing webhook with the specified data. `updateData` may include `name`, `url`,
59
61
  * `subscribedEvents` and (SHGHL-3486) `authType`, `apiHeaderPrefix` and `credentials` (`{ key, secret? }`)
60
62
  * to change the endpoint's own credentials in place: credentials are required when switching to an auth
61
- * type that needs them, and omitted credentials keep the stored ones. Like every read of a webhook, the
63
+ * type that needs them, and omitted credentials keep the stored ones. Switching to `NONE` or `HMAC` clears
64
+ * the stored credentials; `HMAC` is rejected for an endpoint with no signing secret (call
65
+ * `rotateWebhookSecret` first). Like every read of a webhook, the
62
66
  * response carries `hasSigningSecret` and `hasCredentials` (booleans), never a secret or credential.
63
67
  * @async
64
68
  * @roles api, admin, developer
@@ -9,12 +9,13 @@
9
9
  * @property {string} name - Profile name (required).
10
10
  * @property {string[]} subgroupIds - Subgroups this profile spans (required, at least one).
11
11
  * @property {string} [region] - Region/POP label.
12
- * @property {"FORWARD"|"WEBRTC"|"SIP_TRUNK"|"SIP_PROFILE"|"CALL_CONTROL"} [routeAction] - Inbound call action (default "FORWARD"). Deliver the call to a PSTN number (FORWARD), ring the profile's subgroup registered softphones (WEBRTC), send to a SIP trunk (SIP_TRUNK), ring a registered SIP endpoint (SIP_PROFILE), or hand the call to your programmable call-control webhook that returns SHML (CALL_CONTROL).
12
+ * @property {"FORWARD"|"WEBRTC"|"SIP_TRUNK"|"SIP_PROFILE"|"CALL_CONTROL"|"AI"} [routeAction] - Inbound call action (default "FORWARD"). Deliver the call to a PSTN number (FORWARD), ring the profile's subgroup registered softphones (WEBRTC), send to a SIP trunk (SIP_TRUNK), ring a registered SIP endpoint (SIP_PROFILE), hand the call to your programmable call-control webhook that returns SHML (CALL_CONTROL), or answer it with an AI agent (AI).
13
13
  * @property {string} [forwardToE164] - Destination number, required when routeAction is "FORWARD".
14
14
  * @property {number} [forwardAfterSeconds] - Ring seconds before the action fires.
15
15
  * @property {string} [routeSipTrunkId] - SIP trunk id, required when routeAction is "SIP_TRUNK".
16
16
  * @property {string} [routeSipProfileId] - SIP endpoint id, required when routeAction is "SIP_PROFILE".
17
17
  * @property {string} [webhookUrl] - Your inbound voice webhook (SHML program URL), required when routeAction is "CALL_CONTROL". Inbound calls to this profile's numbers are driven by the SHML your webhook returns.
18
+ * @property {string} [routeAgentProfileId] - AI agent profile id (AGP…), required when routeAction is "AI". The agent's prompt, voice, model and tools are read when the call lands, so editing the agent takes effect on the next call with nothing to re-save here.
18
19
  * @property {boolean} [recordingEnabled] - Record calls on this profile.
19
20
  * @property {boolean} [enabled] - Active state (default true).
20
21
  */
@@ -24,12 +25,13 @@
24
25
  * @property {string} [name] - Profile name.
25
26
  * @property {string[]} [subgroupIds] - Subgroups this profile spans.
26
27
  * @property {string|null} [region] - Region/POP label.
27
- * @property {"FORWARD"|"WEBRTC"|"SIP_TRUNK"|"SIP_PROFILE"|"CALL_CONTROL"} [routeAction] - Inbound call action (FORWARD | WEBRTC | SIP_TRUNK | SIP_PROFILE | CALL_CONTROL).
28
+ * @property {"FORWARD"|"WEBRTC"|"SIP_TRUNK"|"SIP_PROFILE"|"CALL_CONTROL"|"AI"} [routeAction] - Inbound call action (FORWARD | WEBRTC | SIP_TRUNK | SIP_PROFILE | CALL_CONTROL | AI).
28
29
  * @property {string|null} [forwardToE164] - FORWARD destination.
29
30
  * @property {number|null} [forwardAfterSeconds] - Ring seconds before the action fires.
30
31
  * @property {string|null} [routeSipTrunkId] - SIP trunk id (required when routeAction is "SIP_TRUNK").
31
32
  * @property {string|null} [routeSipProfileId] - SIP endpoint id (required when routeAction is "SIP_PROFILE").
32
33
  * @property {string|null} [webhookUrl] - Inbound voice webhook / SHML program URL (required when routeAction is "CALL_CONTROL"). Pass null to clear.
34
+ * @property {string|null} [routeAgentProfileId] - AI agent profile id (AGP…), required when routeAction is "AI". Pass null to clear.
33
35
  * @property {boolean} [recordingEnabled] - Record calls on this profile.
34
36
  * @property {boolean} [enabled] - Active state.
35
37
  */