@signalhousellc/sdk 1.0.68 → 1.1.0

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,240 @@
1
+ /** @typedef {import('../SignalHouseSDK.js').RequestOptions} RequestOptions */
2
+ /**
3
+ * @template T
4
+ * @typedef {import('../SignalHouseSDK.js').SdkResponse<T>} SdkResponse
5
+ */
6
+
7
+ /**
8
+ * Sender registrations for every jurisdiction and type: the catalog of what a region offers, retail quotes,
9
+ * customer submissions and staff review operations. `type` selects the shape of `data`
10
+ * (for VIRTUAL_LONG_CODE: quantity, useCases and the optional voice dossier).
11
+ */
12
+ export class Registrations {
13
+ constructor(client, enableAdmin) {
14
+ this.client = client;
15
+ this.enableAdmin = enableAdmin;
16
+
17
+ if (enableAdmin) {
18
+ this.admin = {
19
+ /**
20
+ * Approve or reject a registration awaiting Signal House review (signalhouse only)
21
+ * @async
22
+ * @roles signalhouse
23
+ * @param {Object} params - The parameters for the review decision
24
+ * @param {string} params.registrationId - The registration ID (e.g. GB8T9GTD5AZY)
25
+ * @param {("APPROVE"|"REJECT")} params.action - The review decision
26
+ * @param {string} [params.reason] - Why the registration was rejected; required when action is REJECT
27
+ * @param {RequestOptions} [params.options] - Additional options for the request
28
+ * @returns {Promise<SdkResponse<any>>} A promise that resolves to the updated registration
29
+ */
30
+ reviewRegistration: async ({ registrationId, action, reason, options = {} }) => {
31
+ this.client._require({ registrationId, action });
32
+ const safeId = encodeURIComponent(registrationId);
33
+ return this.client(`/registration/${safeId}/review`, {
34
+ method: "POST",
35
+ body: { action, reason },
36
+ ...options,
37
+ });
38
+ },
39
+
40
+ /**
41
+ * Save corrections to the type's correctable fields for a registration the provider sent back for
42
+ * more information; owner, quantity, bundle and provider order never change (signalhouse only)
43
+ * @async
44
+ * @roles signalhouse
45
+ * @param {Object} params - The parameters for the correction
46
+ * @param {string} params.registrationId - The registration ID (e.g. GB8T9GTD5AZY)
47
+ * @param {string} params.reason - Why the registration was corrected
48
+ * @param {Object} params.data - The type's correctable fields (for VIRTUAL_LONG_CODE: useCases and, for SMS + Voice, voice)
49
+ * @param {RequestOptions} [params.options] - Additional options for the request
50
+ * @returns {Promise<SdkResponse<any>>} A promise that resolves to the updated registration
51
+ */
52
+ correctRegistration: async ({ registrationId, reason, data, options = {} }) => {
53
+ this.client._require({ registrationId, reason, data });
54
+ const safeId = encodeURIComponent(registrationId);
55
+ return this.client(`/registration/${safeId}/corrections`, {
56
+ method: "POST",
57
+ body: { reason, data },
58
+ ...options,
59
+ });
60
+ },
61
+
62
+ /**
63
+ * Allocate a provider-owned number to a registration through the staff recovery path (signalhouse only)
64
+ * @async
65
+ * @roles signalhouse
66
+ * @param {Object} params - The parameters for the allocation
67
+ * @param {string} params.registrationId - The registration ID (e.g. GB8T9GTD5AZY)
68
+ * @param {string} params.phoneNumber - The E.164 number (10-15 digits, optional leading +) to allocate
69
+ * @param {RequestOptions} [params.options] - Additional options for the request
70
+ * @returns {Promise<SdkResponse<any>>} A promise that resolves to the updated registration
71
+ */
72
+ addNumber: async ({ registrationId, phoneNumber, options = {} }) => {
73
+ this.client._require({ registrationId, phoneNumber });
74
+ const safeId = encodeURIComponent(registrationId);
75
+ return this.client(`/registration/${safeId}/numbers`, {
76
+ method: "POST",
77
+ body: { phoneNumber },
78
+ ...options,
79
+ });
80
+ },
81
+
82
+ /**
83
+ * Link an existing provider order to a registration parked in RECONCILIATION_REQUIRED (signalhouse only)
84
+ * @async
85
+ * @roles signalhouse
86
+ * @param {Object} params - The parameters for the reconciliation
87
+ * @param {string} params.registrationId - The registration ID (e.g. GB8T9GTD5AZY)
88
+ * @param {string} params.externalId - The provider's identifier for the existing order
89
+ * @param {RequestOptions} [params.options] - Additional options for the request
90
+ * @returns {Promise<SdkResponse<any>>} A promise that resolves to the updated registration
91
+ */
92
+ reconcile: async ({ registrationId, externalId, options = {} }) => {
93
+ this.client._require({ registrationId, externalId });
94
+ const safeId = encodeURIComponent(registrationId);
95
+ return this.client(`/registration/${safeId}/reconcile`, {
96
+ method: "POST",
97
+ body: { externalId },
98
+ ...options,
99
+ });
100
+ },
101
+
102
+ /**
103
+ * Download the completed carrier application as DOCX bytes (signalhouse only)
104
+ * @async
105
+ * @roles signalhouse
106
+ * @param {Object} params - The parameters for the download
107
+ * @param {string} params.registrationId - The registration ID (e.g. GB8T9GTD5AZY)
108
+ * @param {RequestOptions} [params.options] - Additional options for the request
109
+ * @returns {Promise<SdkResponse<ArrayBuffer>>} A promise that resolves to the DOCX document as an ArrayBuffer
110
+ */
111
+ getApplication: async ({ registrationId, options = {} }) => {
112
+ this.client._require({ registrationId });
113
+ const safeId = encodeURIComponent(registrationId);
114
+ return this.client(`/registration/${safeId}/application`, {
115
+ method: "GET",
116
+ responseType: "arraybuffer",
117
+ ...options,
118
+ });
119
+ },
120
+ };
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Get the registration types a region offers and whether each can be acquired right now
126
+ * @async
127
+ * @roles api, admin, developer, billing, user
128
+ * @param {Object} params - The parameters for the catalog
129
+ * @param {string} params.region - ISO-2 jurisdiction (US, GB or CA; UK is accepted for GB)
130
+ * @param {RequestOptions} [params.options] - Additional options for the request
131
+ * @returns {Promise<SdkResponse<any>>} A promise that resolves to { region, jurisdiction, sendingRequiresRegistration, registeredThrough, provisioning, types }
132
+ */
133
+ async getCatalog({ region, options = {} }) {
134
+ this.client._require({ region });
135
+ const queryString = this.client._getQueryString({ region });
136
+ return this.client(`/registration/catalog${queryString}`, { method: "GET", ...options });
137
+ }
138
+
139
+ /**
140
+ * Get retail quotes for a registration type: one quote per capability bundle with per-number setupAmount
141
+ * and monthlyAmount, the totals for the quantity (integer microdollars) and etaDays.
142
+ * Unavailable (400) while the type cannot be acquired in the region.
143
+ * @async
144
+ * @roles api, admin, developer, billing, user
145
+ * @param {Object} params - The parameters for the quote
146
+ * @param {string} params.region - ISO-2 jurisdiction (UK is accepted for GB)
147
+ * @param {string} params.type - Registration type (e.g. VIRTUAL_LONG_CODE)
148
+ * @param {number} [params.quantity=1] - Number of numbers to quote (1-100)
149
+ * @param {string} [params.groupId] - Group to price for; staff only
150
+ * @param {RequestOptions} [params.options] - Additional options for the request
151
+ * @returns {Promise<SdkResponse<any>>} A promise that resolves to { data: [quotes] }
152
+ */
153
+ async getQuotes({ region, type, quantity = 1, groupId, options = {} }) {
154
+ this.client._require({ region, type });
155
+ const queryString = this.client._getQueryString({ region, type, quantity, groupId });
156
+ return this.client(`/registration/quotes${queryString}`, { method: "GET", ...options });
157
+ }
158
+
159
+ /**
160
+ * List registrations for an authorized group; staff may omit groupId to read the review queue
161
+ * @async
162
+ * @roles api, admin, developer, billing, user
163
+ * @param {Object} [params] - The parameters for filtering the registrations
164
+ * @param {string} [params.groupId] - Filter by group ID
165
+ * @param {string} [params.subgroupId] - Filter by subgroup ID
166
+ * @param {string|string[]} [params.region] - Filter by one ISO-2 jurisdiction or a list
167
+ * @param {string} [params.type] - Filter by registration type
168
+ * @param {string} [params.kind] - Filter by kind (identity, program, sender)
169
+ * @param {string|string[]} [params.status] - Filter by one registration status or a list
170
+ * @param {string} [params.parentRegistrationId] - Filter by the parent registration ID
171
+ * @param {string} [params.phoneNumber] - Filter to registrations holding this phone number
172
+ * @param {string} [params.sortBy] - Sort field: createdAt (default), subgroupId, registrationId, region or status
173
+ * @param {string} [params.sortOrder] - Sort direction: asc or desc (default)
174
+ * @param {number} [params.page] - Page number (default 1)
175
+ * @param {number} [params.limit] - Registrations per page (default 25, max 100)
176
+ * @param {RequestOptions} [params.options] - Additional options for the request
177
+ * @returns {Promise<SdkResponse<any>>} A promise that resolves to { data: [registrations], totalCount, page, limit }
178
+ */
179
+ async getRegistrations({ groupId, subgroupId, region, type, kind, status, parentRegistrationId, phoneNumber, sortBy, sortOrder, page, limit, options = {} } = {}) {
180
+ const queryString = this.client._getQueryString({ groupId, subgroupId, region, type, kind, status, parentRegistrationId, phoneNumber, sortBy, sortOrder, page, limit });
181
+ return this.client(`/registration${queryString}`, { method: "GET", ...options });
182
+ }
183
+
184
+ /**
185
+ * Get a single registration with its review outcome, the type's public data and its status history
186
+ * @async
187
+ * @roles api, admin, developer, billing, user
188
+ * @param {Object} params - The parameters for getting the registration
189
+ * @param {string} params.registrationId - The registration ID (e.g. GB8T9GTD5AZY)
190
+ * @param {RequestOptions} [params.options] - Additional options for the request
191
+ * @returns {Promise<SdkResponse<any>>} A promise that resolves to the registration
192
+ */
193
+ async getRegistration({ registrationId, options = {} }) {
194
+ this.client._require({ registrationId });
195
+ const safeId = encodeURIComponent(registrationId);
196
+ return this.client(`/registration/${safeId}`, { method: "GET", ...options });
197
+ }
198
+
199
+ /**
200
+ * Cancel a registration. Allowed from SIGNAL_HOUSE_REVIEW, SIGNAL_HOUSE_APPROVED, PENDING_PROVIDER, NEEDS_INFORMATION,
201
+ * PAYMENT_REQUIRED and APPROVED (409 otherwise). Numbers already allocated stay on the account and stop sending.
202
+ * @async
203
+ * @roles api, admin, developer, user
204
+ * @param {Object} params - The parameters for cancelling the registration
205
+ * @param {string} params.registrationId - The registration ID (e.g. GB8T9GTD5AZY)
206
+ * @param {RequestOptions} [params.options] - Additional options for the request
207
+ * @returns {Promise<SdkResponse<any>>} A promise that resolves to the cancelled registration
208
+ */
209
+ async cancelRegistration({ registrationId, options = {} }) {
210
+ this.client._require({ registrationId });
211
+ const safeId = encodeURIComponent(registrationId);
212
+ return this.client(`/registration/${safeId}`, { method: "DELETE", ...options });
213
+ }
214
+
215
+ /**
216
+ * Submit a registration. `type` selects the shape of `data`: for VIRTUAL_LONG_CODE it is { quantity (1-100),
217
+ * useCases (1-10 descriptions), voice? } where voice is required exactly when capabilities include VOICE.
218
+ * Reuse the same clientRequestId only when retrying an identical payload (409 otherwise).
219
+ * Unavailable (400) while the type cannot be acquired in the region.
220
+ * @async
221
+ * @roles api, admin, developer, billing, user
222
+ * @param {Object} params - The parameters for the registration
223
+ * @param {string} params.region - ISO-2 jurisdiction (UK is accepted for GB)
224
+ * @param {string} params.type - Registration type (e.g. VIRTUAL_LONG_CODE)
225
+ * @param {string} params.subgroupId - Subgroup that will own the registration and any allocated numbers
226
+ * @param {string} params.clientRequestId - Idempotency key (UUID)
227
+ * @param {string[]} params.capabilities - One of the type's bundles, e.g. ["SMS"] or ["SMS", "VOICE"]
228
+ * @param {Object} params.data - The type's submission fields
229
+ * @param {RequestOptions} [params.options] - Additional options for the request
230
+ * @returns {Promise<SdkResponse<any>>} A promise that resolves to the created registration
231
+ */
232
+ async createRegistration({ region, type, subgroupId, clientRequestId, capabilities, data, options = {} }) {
233
+ this.client._require({ region, type, subgroupId, clientRequestId, capabilities, data });
234
+ return this.client("/registration", {
235
+ method: "POST",
236
+ body: { region, type, subgroupId, clientRequestId, capabilities, data },
237
+ ...options,
238
+ });
239
+ }
240
+ }
@@ -26,9 +26,11 @@
26
26
 
27
27
  /**
28
28
  * @typedef {Object} CreateServiceUserData
29
- * @property {string} groupId - The ID of the group to which the service user belongs (required)
29
+ * @property {string} groupId - The ID of the group to which the service user belongs (required for customer API keys)
30
30
  * @property {string} name - The name of the service user (required)
31
- * @property {string} role - The role of the service user (admin, developer, billing, user) (required)
31
+ * @property {string} role - The role of the service user: "api" for a customer API key (required)
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
+ * @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.
32
34
  */
33
35
 
34
36
  /**
@@ -47,6 +49,8 @@
47
49
  * @property {string} [country] - The country of the user's address (optional)
48
50
  * @property {string} [postalCode] - The postal code of the user's address (optional)
49
51
  * @property {string} [status] - The status of the user (e.g., "active", "inactive")
52
+ * @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
+ * @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.
50
54
  */
51
55
 
52
56
  export class Users {
@@ -37,8 +37,10 @@ export class Webhooks {
37
37
  * plaintext HMAC-SHA256 secret used to sign every delivery to this endpoint, sent as the
38
38
  * `X-SignalHouse-Signature` header alongside `X-SignalHouse-Timestamp` (the signed string is
39
39
  * `${timestamp}.${rawRequestBody}`) and `X-SignalHouse-Delivery-Id` (stable across retries of the
40
- * same event, for receiver-side dedup). Store this secret now: it is never returned again by any
41
- * read (SHGHL-2944).
40
+ * same event, for receiver-side dedup), plus `X-SignalHouse-Signature-V1` (`t=<ms>,v1=<hex>[,v1=<hex>]`,
41
+ * every currently valid signature). Store this secret now: it is never returned again by any read
42
+ * (SHGHL-2944). If it's lost, call `rotateWebhookSecret`. Verify deliveries with
43
+ * `verifyWebhookSignature` from `@signalhousellc/sdk/webhooks` (Node only).
42
44
  * @async
43
45
  * @roles api, admin, developer
44
46
  * @param {Object} params - The parameters for creating a webhook
@@ -53,9 +55,11 @@ export class Webhooks {
53
55
  }
54
56
 
55
57
  /**
56
- * Update an existing webhook with the specified data. Like every other read of a webhook, the
57
- * response carries `hasSigningSecret` (boolean), never the signing secret itself — see
58
- * `createWebhook` (SHGHL-2944).
58
+ * Update an existing webhook with the specified data. `updateData` may include `name`, `url`,
59
+ * `subscribedEvents` and (SHGHL-3486) `authType`, `apiHeaderPrefix` and `credentials` (`{ key, secret? }`)
60
+ * 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
62
+ * response carries `hasSigningSecret` and `hasCredentials` (booleans), never a secret or credential.
59
63
  * @async
60
64
  * @roles api, admin, developer
61
65
  * @param {Object} params - The parameters for updating a webhook
@@ -71,6 +75,28 @@ export class Webhooks {
71
75
  return this.client(`/webhook/${safeId}`, { method: "PUT", body: updateData, ...options });
72
76
  }
73
77
 
78
+ /**
79
+ * Rotate a webhook's signing secret, or give a webhook created before signing existed its first one
80
+ * (SHGHL-3486). The response includes the new plaintext `signingSecret`, returned this one time only.
81
+ * The replaced secret keeps working for `graceSeconds` (default 86400, max 604800, 0 = stop now):
82
+ * during that window each delivery's `X-SignalHouse-Signature-V1` header carries a signature for both,
83
+ * and `previousSecretExpiresAt` in the response says when the old one stops. Not available to OAuth
84
+ * connector tokens.
85
+ * @async
86
+ * @roles api, admin, developer
87
+ * @param {Object} params - The parameters for rotating the secret
88
+ * @param {string} params.id - The ID of the webhook
89
+ * @param {number} [params.graceSeconds] - How long the replaced secret stays valid
90
+ * @param {RequestOptions} [params.options] - Additional options for the request
91
+ * @throws {Error} Throws an error if the id parameter is missing
92
+ * @returns {Promise<SdkResponse<any>>} The webhook, including the new plaintext `signingSecret` (this one time only)
93
+ */
94
+ async rotateWebhookSecret({ id, graceSeconds, options = {} }) {
95
+ this.client._require({ id });
96
+ const safeId = encodeURIComponent(id);
97
+ return this.client(`/webhook/${safeId}/rotate-secret`, { method: "POST", body: graceSeconds === undefined ? {} : { graceSeconds }, ...options });
98
+ }
99
+
74
100
  /**
75
101
  * Delete a webhook by its ID (mark as inactive)
76
102
  * @async
@@ -0,0 +1,66 @@
1
+ import crypto from "node:crypto";
2
+
3
+ // Node-only helpers for receiving Signal House webhooks (SHGHL-3486). Kept out of the main entry point,
4
+ // which also runs in browsers, because it depends on node:crypto. Import from "@signalhousellc/sdk/webhooks".
5
+
6
+ const DEFAULT_TOLERANCE_MS = 5 * 60 * 1000;
7
+
8
+ /**
9
+ * Verify that a webhook delivery came from Signal House and is recent.
10
+ *
11
+ * Checks `X-SignalHouse-Signature-V1` (`t=<ms>,v1=<hex>[,v1=<hex>]`, which carries every currently valid
12
+ * signature during a secret rotation) when present, otherwise the original `X-SignalHouse-Signature` +
13
+ * `X-SignalHouse-Timestamp` pair. Each signature is a hex HMAC-SHA256 of `${timestamp}.${rawBody}`. Comparisons
14
+ * are constant-time.
15
+ *
16
+ * @param {Object} params
17
+ * @param {string|Buffer} params.rawBody - The exact request body as received, before any JSON parsing.
18
+ * @param {Object} params.headers - Request headers (any casing; e.g. Express `req.headers`).
19
+ * @param {string} [params.secret] - Your endpoint's signing secret.
20
+ * @param {string[]} [params.secrets] - Several secrets to accept, e.g. old and new while you roll over.
21
+ * @param {number} [params.toleranceMs=300000] - Reject deliveries whose timestamp is further than this from now.
22
+ * @param {number} [params.now=Date.now()] - Current time in ms (for tests).
23
+ * @returns {{valid: boolean, reason?: string}} - `valid` true when a signature matches within the time window;
24
+ * otherwise `reason` is one of `no_secret`, `no_signature`, `bad_timestamp`, `stale_timestamp`, `bad_signature`.
25
+ */
26
+ export function verifyWebhookSignature({ rawBody, headers, secret, secrets, toleranceMs = DEFAULT_TOLERANCE_MS, now = Date.now() }) {
27
+ const candidates = (secrets ?? (secret ? [secret] : [])).filter(Boolean);
28
+ if (candidates.length === 0) return { valid: false, reason: "no_secret" };
29
+
30
+ const header = (name) => {
31
+ if (!headers) return undefined;
32
+ const match = Object.keys(headers).find((key) => key.toLowerCase() === name);
33
+ const value = match === undefined ? undefined : headers[match];
34
+ return Array.isArray(value) ? value[0] : value;
35
+ };
36
+
37
+ let timestamp;
38
+ let signatures = [];
39
+ const v1 = header("x-signalhouse-signature-v1");
40
+ if (v1) {
41
+ const parts = String(v1).split(",").map((part) => part.trim());
42
+ timestamp = parts.find((part) => part.startsWith("t="))?.slice(2);
43
+ signatures = parts.filter((part) => part.startsWith("v1=")).map((part) => part.slice(3)).filter(Boolean);
44
+ }
45
+ // No usable V1 (absent, or mangled by an intermediary): fall back to the original header pair
46
+ if (signatures.length === 0) {
47
+ timestamp = header("x-signalhouse-timestamp");
48
+ const legacy = header("x-signalhouse-signature");
49
+ signatures = legacy ? [String(legacy)] : [];
50
+ }
51
+ if (signatures.length === 0) return { valid: false, reason: "no_signature" };
52
+
53
+ const sentAt = Number(timestamp);
54
+ if (!timestamp || !Number.isFinite(sentAt)) return { valid: false, reason: "bad_timestamp" };
55
+ if (Math.abs(now - sentAt) > toleranceMs) return { valid: false, reason: "stale_timestamp" };
56
+
57
+ const body = Buffer.isBuffer(rawBody) ? rawBody.toString("utf8") : String(rawBody ?? "");
58
+ for (const candidate of candidates) {
59
+ const expected = Buffer.from(crypto.createHmac("sha256", candidate).update(`${timestamp}.${body}`).digest("hex"), "utf8");
60
+ for (const signature of signatures) {
61
+ const given = Buffer.from(signature, "utf8");
62
+ if (given.length === expected.length && crypto.timingSafeEqual(given, expected)) return { valid: true };
63
+ }
64
+ }
65
+ return { valid: false, reason: "bad_signature" };
66
+ }