@signalhousellc/sdk 1.4.1 → 1.4.2

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.1",
3
+ "version": "1.4.2",
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
@@ -1,5 +1,9 @@
1
1
  import axios from "axios";
2
2
 
3
+ // Bounds every request made by this SDK. Chosen well above typical p99 latency so it only trips on
4
+ // a genuinely hung connection, not a slow-but-completing request.
5
+ const REQUEST_TIMEOUT_MS = 30000;
6
+
3
7
  /**
4
8
  * @typedef {Object} RequestOptions
5
9
  * @property {string} [token] - An optional bearer token to include in the request for authentication
@@ -100,8 +104,10 @@ export class SignalHouseSDK {
100
104
  * @returns {Function} A function to make API requests
101
105
  */
102
106
  _createClient(isMultipart = false) {
103
- // Create an Axios instance with the base URL and default headers
104
- const instance = axios.create({ baseURL: this.baseUrl, headers: isMultipart ? {} : { "Content-Type": "application/json" } });
107
+ // Create an Axios instance with the base URL and default headers. `timeout` bounds every
108
+ // request so a hung connection (server-side lock contention, a stalled upstream call) rejects
109
+ // instead of leaving the caller's promise unsettled forever with no error and no feedback.
110
+ const instance = axios.create({ baseURL: this.baseUrl, timeout: REQUEST_TIMEOUT_MS, headers: isMultipart ? {} : { "Content-Type": "application/json" } });
105
111
 
106
112
  // Add a request interceptor to include the API key in the Authorization header
107
113
  instance.interceptors.request.use((/** @type {import('axios').InternalAxiosRequestConfig & { token?: string, idempotencyKey?: string }} */ config) => {
@@ -122,8 +128,17 @@ export class SignalHouseSDK {
122
128
  return { success: true, data: response.data, status: response.status };
123
129
  }),
124
130
 
125
- // Non-2xx responses
131
+ // Non-2xx responses, plus timeouts and network failures (no `error.response` at all) - those
132
+ // get a clear message here rather than surfacing axios's internal error.message verbatim.
126
133
  (error) => {
134
+ if (!error.response) {
135
+ const isTimeout = error.code === "ECONNABORTED" || /timeout/i.test(error.message || "");
136
+ return {
137
+ success: false,
138
+ error: isTimeout ? "Request timed out. Please try again." : "Network error. Please check your connection and try again.",
139
+ status: null,
140
+ };
141
+ }
127
142
  return {
128
143
  success: false,
129
144
  error: error.response?.data?.message || error.response?.data || error.message || 500,
@@ -36,6 +36,7 @@
36
36
  * @property {string} privacyPolicyLink - A URL linking to the privacy policy for the campaign (required, must be at most 2048 characters)
37
37
  * @property {string} termsAndConditionsLink - A URL linking to the terms and conditions for the campaign (required, must be at most 2048 characters)
38
38
  * @property {string} [embeddedLinkSample] - A sample of the embedded link used in the campaign (optional, must be at most 255 characters)
39
+ * @property {string[]} [sampleMediaIds] - Up to 5 mediaIds from uploadSampleMedia: sample MMS media shown to carrier and TCR reviewers (10DLC only)
39
40
  * @property {Array<string>} phoneNumbers - An array of phone numbers associated with the campaign (required, each number must be at least 10 digits and contain only digits, a maximum of 5000 numbers is allowed)
40
41
  */
41
42
 
@@ -70,6 +71,7 @@
70
71
  * @property {string} [privacyPolicyLink] - A URL linking to the privacy policy for the campaign (optional, must be at most 2048 characters if provided)
71
72
  * @property {string} [termsAndConditionsLink] - A URL linking to the terms and conditions for the campaign (optional, must be at most 2048 characters if provided)
72
73
  * @property {string} [embeddedLinkSample] - A sample of the embedded link used in the campaign (optional, must be at most 255 characters if provided)
74
+ * @property {string[]} [sampleMediaIds] - Up to 5 mediaIds from uploadSampleMedia: sample MMS media shown to carrier and TCR reviewers (10DLC only)
73
75
  * @property {Object} [tollFree] - Toll-Free editable fields, used when updating a Toll-Free campaign. Mirrors the CreateTollFreeCampaignData `tollFree` object with every field optional (useCase, messageVolume, programSummary (40-500), exampleMessage (≤1000), customerCareEmail, optInImageURLs, optIns, multiNumberReason (≤500), useCases, channels, landingId). Phone numbers cannot be changed via update — Toll-Free numbers are locked to their campaign.
74
76
  */
75
77
 
@@ -529,6 +531,43 @@ export class Campaigns {
529
531
  return this.client(`/campaign/short-code/artifact/${safeArtifactId}`, { method: 'GET', ...options });
530
532
  }
531
533
 
534
+ /**
535
+ * Upload a sample media file (JPEG, PNG, GIF, MP4 or MP3, up to 5MB) for a 10DLC campaign. Reference the
536
+ * returned mediaId in sampleMediaIds on campaign create or update; it is attached to the TCR campaign for
537
+ * carrier review
538
+ * @async
539
+ * @roles api, admin, developer, user
540
+ * @param {Object} params - The parameters for uploading the sample media
541
+ * @param {Blob|File} params.file - The media file to upload, provided as a Blob/File
542
+ * @param {string} [params.groupId] - The group the file belongs to. Required with a staff (signalhouse_*) key, which has no active group; otherwise optional and must be your own group
543
+ * @param {RequestOptions} [params.options] - Additional options for the request
544
+ * @throws {Error} Throws an error if the file parameter is missing
545
+ * @returns {Promise<SdkResponse<any>>} The response from the server containing the mediaId, fileName, mimeType and size
546
+ */
547
+ async uploadSampleMedia({ file, groupId, options = {} }) {
548
+ this.client._require({ file });
549
+ const formData = new FormData();
550
+ if (groupId) formData.append('groupId', groupId);
551
+ formData.append('file', file);
552
+ return this.multipartClient(`/campaign/sample-media`, { method: 'POST', body: formData, ...options });
553
+ }
554
+
555
+ /**
556
+ * Download a campaign sample media file your group owns
557
+ * @async
558
+ * @roles api, admin, developer, user
559
+ * @param {Object} params - The parameters for downloading the sample media
560
+ * @param {string} params.mediaId - The mediaId returned by uploadSampleMedia
561
+ * @param {RequestOptions} [params.options] - Additional options for the request
562
+ * @throws {Error} Throws an error if the mediaId parameter is missing
563
+ * @returns {Promise<SdkResponse<any>>} The response from the server containing the file bytes
564
+ */
565
+ async readSampleMedia({ mediaId, options = {} }) {
566
+ this.client._require({ mediaId });
567
+ const safeMediaId = encodeURIComponent(mediaId);
568
+ return this.client(`/campaign/sample-media/${safeMediaId}`, { method: 'GET', ...options });
569
+ }
570
+
532
571
  /**
533
572
  * Upload an opt-in proof image to be hosted on-platform, returning a publicly-fetchable URL suitable for a
534
573
  * Toll-Free campaign's optInImageURLs or a Short Code campaign's shortCode.screenshotUrl