@superblocksteam/sabs-client 0.258.0 → 0.260.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.
package/src/sabs.test.ts CHANGED
@@ -807,10 +807,8 @@ describe('sabs service', () => {
807
807
 
808
808
  const anyBuildId = 'anyBuildId';
809
809
 
810
- const expectedError = 'sabs service request failed: request failed\n[500] internal server error: "failed to process request"';
811
-
812
810
  const sabs = new SabsClient('http://localhost:3000');
813
- await expect(sabs.status({ buildId: anyBuildId, accessToken: anyAccessToken })).rejects.toThrow(expectedError);
811
+ await expect(sabs.status({ buildId: anyBuildId, accessToken: anyAccessToken })).rejects.toThrow();
814
812
  });
815
813
 
816
814
  test('re-raises error when error is not axios error', async () => {
@@ -819,10 +817,291 @@ describe('sabs service', () => {
819
817
 
820
818
  const anyBuildId = 'anyBuildId';
821
819
 
822
- const expectedError = 'sabs service request failed: unexpected error';
820
+ const sabs = new SabsClient('http://localhost:3000');
821
+ await expect(sabs.status({ buildId: anyBuildId, accessToken: anyAccessToken })).rejects.toThrow();
822
+ });
823
+ });
824
+
825
+ describe('backward compatibility', () => {
826
+ test('errors can be caught as generic Error instances (backward compatibility)', async () => {
827
+ const mockAxios = jest.spyOn(axios, 'request');
828
+ mockAxios.mockRejectedValue(
829
+ new AxiosError('Service unavailable', 'ECONNREFUSED', undefined, undefined, {
830
+ headers: {},
831
+ config: {
832
+ headers: new AxiosHeaders()
833
+ },
834
+ status: 503,
835
+ statusText: 'Service Unavailable',
836
+ data: { message: 'Server temporarily unavailable' }
837
+ })
838
+ );
839
+
840
+ const sabs = new SabsClient('http://localhost:3000');
841
+ const applicationId = 'test-app';
842
+ const organizationId = 'test-org';
843
+ const directoryHash = 'abc123';
844
+
845
+ // This simulates how existing applications might handle errors
846
+ try {
847
+ const result = await sabs.build({
848
+ directoryHash,
849
+ meta: new ApplicationMetadata({
850
+ id: applicationId,
851
+ organizationId
852
+ }),
853
+ buildKey: anyBuildKey,
854
+ accessToken: anyAccessToken
855
+ });
856
+
857
+ // Should not reach here
858
+ expect(result).toBeUndefined();
859
+ } catch (error) {
860
+ // Legacy error handling - should still work with new error types
861
+ expect(error).toBeInstanceOf(Error);
862
+ expect(error.message).toContain('SABS API Error (503)');
863
+
864
+ // Verify we can still access basic Error properties
865
+ expect(typeof error.message).toBe('string');
866
+ expect(error.name).toBeDefined();
867
+
868
+ // This is how existing code might log and re-throw
869
+ const loggedError = {
870
+ error,
871
+ applicationId,
872
+ organizationId,
873
+ directoryHash,
874
+ message: error.message
875
+ };
876
+
877
+ expect(loggedError.error).toBe(error);
878
+ expect(loggedError.message).toBe(error.message);
879
+
880
+ // Re-throwing as generic Error should work
881
+ expect(() => {
882
+ throw new Error('Unable to launch build');
883
+ }).toThrow('Unable to launch build');
884
+ }
885
+ });
886
+
887
+ test('new error types provide additional functionality when accessed', async () => {
888
+ const mockAxios = jest.spyOn(axios, 'request');
889
+ mockAxios.mockRejectedValue(
890
+ new AxiosError('Unauthorized', 'UNAUTHORIZED', undefined, undefined, {
891
+ headers: {},
892
+ config: {
893
+ headers: new AxiosHeaders()
894
+ },
895
+ status: 401,
896
+ statusText: 'Unauthorized',
897
+ data: { message: 'Invalid token' }
898
+ })
899
+ );
823
900
 
824
901
  const sabs = new SabsClient('http://localhost:3000');
825
- await expect(sabs.status({ buildId: anyBuildId, accessToken: anyAccessToken })).rejects.toThrow(expectedError);
902
+
903
+ try {
904
+ await sabs.status({ buildId: 'test-build', accessToken: 'invalid-token' });
905
+ } catch (error) {
906
+ // Backward compatible - can still catch as Error
907
+ expect(error).toBeInstanceOf(Error);
908
+
909
+ // But applications can now also check for specific error types
910
+ const { UnauthorizedError } = await import('./errors');
911
+ expect(error).toBeInstanceOf(UnauthorizedError);
912
+
913
+ // And access the status code if needed
914
+ if ('status' in error) {
915
+ expect(error.status).toBe(401);
916
+ }
917
+ }
918
+ });
919
+
920
+ describe('response data handling', () => {
921
+ test('handles responseData with message property', async () => {
922
+ const mockAxios = jest.spyOn(axios, 'request');
923
+ mockAxios.mockRejectedValue(
924
+ new AxiosError('Bad Request', 'BAD_REQUEST', undefined, undefined, {
925
+ headers: {},
926
+ config: {
927
+ headers: new AxiosHeaders()
928
+ },
929
+ status: 400,
930
+ statusText: 'Bad Request',
931
+ data: { message: 'Directory hash is required' }
932
+ })
933
+ );
934
+
935
+ const sabs = new SabsClient('http://localhost:3000');
936
+
937
+ try {
938
+ await sabs.build({
939
+ directoryHash: 'test', // Use non-empty value to avoid client-side validation
940
+ meta: new ApplicationMetadata({ id: 'test', organizationId: 'test' }),
941
+ buildKey: 'test',
942
+ accessToken: 'test'
943
+ });
944
+ } catch (error) {
945
+ expect(error).toBeInstanceOf(Error);
946
+ expect(error.message).toBe('SABS API Error (400): Directory hash is required');
947
+ }
948
+ });
949
+
950
+ test('handles responseData as string', async () => {
951
+ const mockAxios = jest.spyOn(axios, 'request');
952
+ mockAxios.mockRejectedValue(
953
+ new AxiosError('Internal Server Error', 'INTERNAL_SERVER_ERROR', undefined, undefined, {
954
+ headers: {},
955
+ config: {
956
+ headers: new AxiosHeaders()
957
+ },
958
+ status: 500,
959
+ statusText: 'Internal Server Error',
960
+ data: 'Database connection failed'
961
+ })
962
+ );
963
+
964
+ const sabs = new SabsClient('http://localhost:3000');
965
+
966
+ try {
967
+ await sabs.build({
968
+ directoryHash: 'test',
969
+ meta: new ApplicationMetadata({ id: 'test', organizationId: 'test' }),
970
+ buildKey: 'test',
971
+ accessToken: 'test'
972
+ });
973
+ } catch (error) {
974
+ expect(error).toBeInstanceOf(Error);
975
+ expect(error.message).toBe('SABS API Error (500): Database connection failed');
976
+ }
977
+ });
978
+
979
+ test('handles null responseData', async () => {
980
+ const mockAxios = jest.spyOn(axios, 'request');
981
+ mockAxios.mockRejectedValue(
982
+ new AxiosError('Service Unavailable', 'SERVICE_UNAVAILABLE', undefined, undefined, {
983
+ headers: {},
984
+ config: {
985
+ headers: new AxiosHeaders()
986
+ },
987
+ status: 503,
988
+ statusText: 'Service Unavailable',
989
+ data: null
990
+ })
991
+ );
992
+
993
+ const sabs = new SabsClient('http://localhost:3000');
994
+
995
+ try {
996
+ await sabs.build({
997
+ directoryHash: 'test',
998
+ meta: new ApplicationMetadata({ id: 'test', organizationId: 'test' }),
999
+ buildKey: 'test',
1000
+ accessToken: 'test'
1001
+ });
1002
+ } catch (error) {
1003
+ expect(error).toBeInstanceOf(Error);
1004
+ expect(error.message).toBe('SABS API Error (503): Service Unavailable (503)');
1005
+ }
1006
+ });
1007
+
1008
+ test('handles undefined responseData', async () => {
1009
+ const mockAxios = jest.spyOn(axios, 'request');
1010
+ mockAxios.mockRejectedValue(
1011
+ new AxiosError('Gateway Timeout', 'GATEWAY_TIMEOUT', undefined, undefined, {
1012
+ headers: {},
1013
+ config: {
1014
+ headers: new AxiosHeaders()
1015
+ },
1016
+ status: 504,
1017
+ statusText: 'Gateway Timeout',
1018
+ data: undefined
1019
+ })
1020
+ );
1021
+
1022
+ const sabs = new SabsClient('http://localhost:3000');
1023
+
1024
+ try {
1025
+ await sabs.build({
1026
+ directoryHash: 'test',
1027
+ meta: new ApplicationMetadata({ id: 'test', organizationId: 'test' }),
1028
+ buildKey: 'test',
1029
+ accessToken: 'test'
1030
+ });
1031
+ } catch (error) {
1032
+ expect(error).toBeInstanceOf(Error);
1033
+ expect(error.message).toBe('SABS API Error (504): Gateway Timeout (504)');
1034
+ }
1035
+ });
1036
+
1037
+ test('handles responseData object without message property', async () => {
1038
+ const mockAxios = jest.spyOn(axios, 'request');
1039
+ mockAxios.mockRejectedValue(
1040
+ new AxiosError('Bad Request', 'BAD_REQUEST', undefined, undefined, {
1041
+ headers: {},
1042
+ config: {
1043
+ headers: new AxiosHeaders()
1044
+ },
1045
+ status: 400,
1046
+ statusText: 'Bad Request',
1047
+ data: { error: 'validation_failed', details: ['field is required'] }
1048
+ })
1049
+ );
1050
+
1051
+ const sabs = new SabsClient('http://localhost:3000');
1052
+
1053
+ try {
1054
+ await sabs.build({
1055
+ directoryHash: 'test',
1056
+ meta: new ApplicationMetadata({ id: 'test', organizationId: 'test' }),
1057
+ buildKey: 'test',
1058
+ accessToken: 'test'
1059
+ });
1060
+ } catch (error) {
1061
+ expect(error).toBeInstanceOf(Error);
1062
+ expect(error.message).toBe('SABS API Error (400): Bad Request (400)');
1063
+ }
1064
+ });
1065
+
1066
+ test('all errors have consistent HttpError interface', async () => {
1067
+ const responseData = { code: 'TIMEOUT', retryAfter: 30 };
1068
+ const mockAxios = jest.spyOn(axios, 'request');
1069
+ mockAxios.mockRejectedValue(
1070
+ new AxiosError('Request timeout', 'TIMEOUT', undefined, undefined, {
1071
+ headers: {},
1072
+ config: {
1073
+ headers: new AxiosHeaders()
1074
+ },
1075
+ status: 500,
1076
+ statusText: 'Internal Server Error',
1077
+ data: responseData
1078
+ })
1079
+ );
1080
+
1081
+ const sabs = new SabsClient('http://localhost:3000');
1082
+
1083
+ try {
1084
+ await sabs.build({
1085
+ directoryHash: 'test',
1086
+ meta: new ApplicationMetadata({ id: 'test', organizationId: 'test' }),
1087
+ buildKey: 'test',
1088
+ accessToken: 'test'
1089
+ });
1090
+ } catch (error) {
1091
+ expect(error).toBeInstanceOf(Error);
1092
+ expect(error.message).toBe('SABS API Error (500): Internal Server Error (500)');
1093
+
1094
+ // All error types now have consistent HttpError interface
1095
+ expect('status' in error).toBe(true);
1096
+ expect('title' in error).toBe(true);
1097
+ if ('status' in error) {
1098
+ expect(error.status).toBe(500);
1099
+ }
1100
+ if ('title' in error) {
1101
+ expect(error.title).toBe('Internal server error');
1102
+ }
1103
+ }
1104
+ });
826
1105
  });
827
1106
  });
828
1107
  });
package/src/sabs.ts CHANGED
@@ -17,6 +17,17 @@ import {
17
17
  } from '@superblocksteam/sabs-types';
18
18
  import axios, { AxiosRequestConfig, RawAxiosRequestHeaders } from 'axios';
19
19
 
20
+ import { createErrorFromStatusCode, createClientError, createNetworkError } from './errors';
21
+
22
+ /**
23
+ * SABS (Superblocks Application Build System) TypeScript Client
24
+ *
25
+ * Provides methods to interact with the SABS API for building and managing applications.
26
+ *
27
+ * All error types inherit from the standard `HttpError` class that extends the standard `Error` class,
28
+ * ensuring backward compatibility with existing error handling code that catches generic `Error` instances
29
+ * used in earlier versions of the client library.
30
+ */
20
31
  export class SabsClient {
21
32
  private readonly baseUrl: string;
22
33
 
@@ -24,6 +35,20 @@ export class SabsClient {
24
35
  this.baseUrl = baseUrl;
25
36
  }
26
37
 
38
+ /**
39
+ * Start a new build for an application
40
+ *
41
+ * @param params - Build parameters
42
+ * @param params.directoryHash - Hash of the application directory to build
43
+ * @param params.meta - Application metadata (ID and organization ID)
44
+ * @param params.buildKey - Secret build key for authentication
45
+ * @param params.accessToken - JWT access token for authorization
46
+ * @returns Promise resolving to build information including build ID
47
+ * @throws BadRequestError if the directory hash, application metadata, application ID, organization ID, build key, or access token are empty or invalid
48
+ * @throws ForbiddenError if the access token is invalid or missing the required scopes
49
+ * @throws InternalServerError if the service has an unexpected error while performing this request
50
+ * @throws HttpError if the request fails for an unknown reason
51
+ */
27
52
  public async build({
28
53
  directoryHash,
29
54
  meta,
@@ -36,22 +61,22 @@ export class SabsClient {
36
61
  accessToken?: string;
37
62
  }): Promise<BuildResponse> {
38
63
  if (!directoryHash || directoryHash.length === 0) {
39
- throw new Error('Directory hash is required');
64
+ throw createClientError('Directory hash is required');
40
65
  }
41
66
  if (!meta) {
42
- throw new Error('Application metadata is required');
67
+ throw createClientError('Application metadata is required');
43
68
  }
44
69
  if (!meta.id || meta.id.length === 0) {
45
- throw new Error('Application ID is required');
70
+ throw createClientError('Application ID is required');
46
71
  }
47
72
  if (!meta.organizationId || meta.organizationId.length === 0) {
48
- throw new Error('Organization ID is required');
73
+ throw createClientError('Organization ID is required');
49
74
  }
50
75
  if (!buildKey || buildKey.length === 0) {
51
- throw new Error('Build key is required');
76
+ throw createClientError('Build key is required');
52
77
  }
53
78
  if (!accessToken || accessToken.length === 0) {
54
- throw new Error('Access token is required');
79
+ throw createClientError('Access token is required');
55
80
  }
56
81
 
57
82
  const data = new BuildRequest({
@@ -70,12 +95,25 @@ export class SabsClient {
70
95
  );
71
96
  }
72
97
 
98
+ /**
99
+ * Get the status of a build
100
+ *
101
+ * @param params - Status query parameters
102
+ * @param params.buildId - ID of the build to check
103
+ * @param params.accessToken - JWT access token for authorization
104
+ * @returns Promise resolving to build status information
105
+ * @throws BadRequestError if the build ID or access token is empty or invalid
106
+ * @throws UnauthorizedError if the access token is invalid or missing the required scopes
107
+ * @throws NotFoundError if the build ID is invalid
108
+ * @throws InternalServerError if the service has an unexpected error while performing this request
109
+ * @throws HttpError if the request fails for an unknown reason
110
+ */
73
111
  public async status({ buildId, accessToken }: { buildId: string; accessToken?: string }): Promise<StatusResponse> {
74
112
  if (!buildId || buildId.length === 0) {
75
- throw new Error('Build ID is required');
113
+ throw createClientError('Build ID is required');
76
114
  }
77
115
  if (!accessToken || accessToken.length === 0) {
78
- throw new Error('Access token is required');
116
+ throw createClientError('Access token is required');
79
117
  }
80
118
 
81
119
  return this.executeRequest<StatusResponse>(
@@ -87,6 +125,21 @@ export class SabsClient {
87
125
  );
88
126
  }
89
127
 
128
+ /**
129
+ * Get the status of multiple builds at once
130
+ *
131
+ * @param params - Bulk status query parameters
132
+ * @param params.organizationId - Organization ID
133
+ * @param params.applicationId - Application ID
134
+ * @param params.directoryHashes - Array of directory hashes to check
135
+ * @param params.accessToken - JWT access token for authorization
136
+ * @returns Promise resolving to multiple build status information
137
+ * @throws BadRequestError if the organization ID, application ID, directory hashes, or access token are empty or invalid
138
+ * @throws UnauthorizedError if the access token is invalid or missing the required scopes
139
+ * @throws NotFoundError if the organization ID or application ID is invalid
140
+ * @throws InternalServerError if the service has an unexpected error while performing this request
141
+ * @throws HttpError if the request fails for an unknown reason
142
+ */
90
143
  public async bulkStatus({
91
144
  organizationId,
92
145
  applicationId,
@@ -99,16 +152,16 @@ export class SabsClient {
99
152
  accessToken?: string;
100
153
  }): Promise<BulkStatusResponse> {
101
154
  if (!organizationId || organizationId.length === 0) {
102
- throw new Error('Organization ID is required');
155
+ throw createClientError('Organization ID is required');
103
156
  }
104
157
  if (!applicationId || applicationId.length === 0) {
105
- throw new Error('Application ID is required');
158
+ throw createClientError('Application ID is required');
106
159
  }
107
160
  if (!directoryHashes || directoryHashes.length === 0) {
108
- throw new Error('Directory hashes are required');
161
+ throw createClientError('Directory hashes are required');
109
162
  }
110
163
  if (!accessToken || accessToken.length === 0) {
111
- throw new Error('Access token is required');
164
+ throw createClientError('Access token is required');
112
165
  }
113
166
 
114
167
  const data = new BulkStatusRequest({
@@ -127,6 +180,21 @@ export class SabsClient {
127
180
  );
128
181
  }
129
182
 
183
+ /**
184
+ * List all builds for a specific application and directory
185
+ *
186
+ * @param params - List query parameters
187
+ * @param params.organizationId - Organization ID
188
+ * @param params.applicationId - Application ID
189
+ * @param params.directoryHash - Hash of the application directory
190
+ * @param params.accessToken - JWT access token for authorization
191
+ * @returns Promise resolving to list of builds
192
+ * @throws BadRequestError if the organization ID, application ID, directory hash, or access token are empty or invalid
193
+ * @throws UnauthorizedError if the access token is invalid or missing the required scopes
194
+ * @throws NotFoundError if the organization ID or application ID is invalid
195
+ * @throws InternalServerError if the service has an unexpected error while performing this request
196
+ * @throws HttpError if the request fails for an unknown reason
197
+ */
130
198
  public async list({
131
199
  organizationId,
132
200
  applicationId,
@@ -139,16 +207,16 @@ export class SabsClient {
139
207
  accessToken?: string;
140
208
  }): Promise<ListResponse> {
141
209
  if (!organizationId || organizationId.length === 0) {
142
- throw new Error('Organization ID is required');
210
+ throw createClientError('Organization ID is required');
143
211
  }
144
212
  if (!applicationId || applicationId.length === 0) {
145
- throw new Error('Application ID is required');
213
+ throw createClientError('Application ID is required');
146
214
  }
147
215
  if (!directoryHash || directoryHash.length === 0) {
148
- throw new Error('Directory hash is required');
216
+ throw createClientError('Directory hash is required');
149
217
  }
150
218
  if (!accessToken || accessToken.length === 0) {
151
- throw new Error('Access token is required');
219
+ throw createClientError('Access token is required');
152
220
  }
153
221
 
154
222
  const data = new ListRequest({
@@ -167,6 +235,22 @@ export class SabsClient {
167
235
  );
168
236
  }
169
237
 
238
+ /**
239
+ * Terminate a running build with a final status
240
+ *
241
+ * @param params - Termination parameters
242
+ * @param params.buildId - ID of the build to terminate
243
+ * @param params.status - Final status of the build
244
+ * @param params.buildKey - Secret build key for authentication
245
+ * @param params.error - Optional error message if build failed
246
+ * @param params.accessToken - JWT access token for authorization
247
+ * @returns Promise resolving to termination confirmation
248
+ * @throws BadRequestError if the build ID, build status, build key, or access token are empty or invalid
249
+ * @throws UnauthorizedError if the access token is invalid or missing the required scopes
250
+ * @throws NotFoundError if the build ID is invalid
251
+ * @throws InternalServerError if the service has an unexpected error while performing this request
252
+ * @throws HttpError if the request fails for an unknown reason
253
+ */
170
254
  public async terminate({
171
255
  buildId,
172
256
  status,
@@ -181,16 +265,16 @@ export class SabsClient {
181
265
  accessToken?: string;
182
266
  }): Promise<TerminateResponse> {
183
267
  if (!buildId || buildId.length === 0) {
184
- throw new Error('Build ID is required');
268
+ throw createClientError('Build ID is required');
185
269
  }
186
270
  if (!status) {
187
- throw new Error('Build status is required');
271
+ throw createClientError('Build status is required');
188
272
  }
189
273
  if (!buildKey || buildKey.length === 0) {
190
- throw new Error('Build key is required');
274
+ throw createClientError('Build key is required');
191
275
  }
192
276
  if (!accessToken || accessToken.length === 0) {
193
- throw new Error('Access token is required');
277
+ throw createClientError('Access token is required');
194
278
  }
195
279
 
196
280
  const data = new TerminateRequest({
@@ -210,6 +294,21 @@ export class SabsClient {
210
294
  );
211
295
  }
212
296
 
297
+ /**
298
+ * Create a new live edit session for real-time development
299
+ *
300
+ * @param params - Live edit creation parameters
301
+ * @param params.applicationId - Application ID
302
+ * @param params.organizationId - Organization ID
303
+ * @param params.branch - Git branch name
304
+ * @param params.expiresIn - Session duration in seconds
305
+ * @param params.accessToken - JWT access token for authorization
306
+ * @returns Promise resolving to live edit session information
307
+ * @throws BadRequestError if the application ID, organization ID, branch, or access token are empty or invalid, or expiresIn is not greater than 0
308
+ * @throws UnauthorizedError if the access token is invalid or missing the required scopes
309
+ * @throws InternalServerError if the service has an unexpected error while performing this request
310
+ * @throws HttpError if the request fails for an unknown reason
311
+ */
213
312
  public async createLiveEdit({
214
313
  applicationId,
215
314
  organizationId,
@@ -224,16 +323,19 @@ export class SabsClient {
224
323
  accessToken: string;
225
324
  }): Promise<CreateLiveEditResponse> {
226
325
  if (!applicationId || applicationId.length === 0) {
227
- throw new Error('Application ID is required');
326
+ throw createClientError('Application ID is required');
228
327
  }
229
328
  if (!organizationId || organizationId.length === 0) {
230
- throw new Error('Organization ID is required');
329
+ throw createClientError('Organization ID is required');
231
330
  }
232
331
  if (!branch || branch.length === 0) {
233
- throw new Error('Branch is required');
332
+ throw createClientError('Branch is required');
234
333
  }
235
334
  if (!accessToken || accessToken.length === 0) {
236
- throw new Error('Access token is required');
335
+ throw createClientError('Access token is required');
336
+ }
337
+ if (!expiresIn || expiresIn <= 0) {
338
+ throw createClientError('Expires in is required and must be greater than 0');
237
339
  }
238
340
 
239
341
  const data = new CreateLiveEditRequest({
@@ -256,6 +358,19 @@ export class SabsClient {
256
358
  );
257
359
  }
258
360
 
361
+ /**
362
+ * Terminate an active live edit session
363
+ *
364
+ * @param params - Live edit termination parameters
365
+ * @param params.liveEditId - ID of the live edit session to terminate
366
+ * @param params.accessToken - JWT access token for authorization
367
+ * @returns Promise resolving to termination confirmation
368
+ * @throws BadRequestError if the live edit ID or access token are empty or invalid
369
+ * @throws UnauthorizedError if the access token is invalid or missing the required scopes
370
+ * @throws NotFoundError if the live edit ID is invalid
371
+ * @throws InternalServerError if the service has an unexpected error while performing this request
372
+ * @throws HttpError if the request fails for an unknown reason
373
+ */
259
374
  public async terminateLiveEdit({
260
375
  liveEditId,
261
376
  accessToken
@@ -264,10 +379,10 @@ export class SabsClient {
264
379
  accessToken: string;
265
380
  }): Promise<TerminateLiveEditResponse> {
266
381
  if (!liveEditId || liveEditId.length === 0) {
267
- throw new Error('Live edit ID is required');
382
+ throw createClientError('Live edit ID is required');
268
383
  }
269
384
  if (!accessToken || accessToken.length === 0) {
270
- throw new Error('Access token is required');
385
+ throw createClientError('Access token is required');
271
386
  }
272
387
 
273
388
  const data = new TerminateLiveEditRequest({
@@ -300,12 +415,26 @@ export class SabsClient {
300
415
  });
301
416
  return response.data;
302
417
  } catch (error) {
303
- let errMsg = `sabs service request failed: ${error.message}`;
304
418
  if (axios.isAxiosError(error)) {
305
- errMsg += `\n[${error.response?.status}] ${error.response?.statusText}: ${JSON.stringify(error.response?.data)}`;
306
- }
419
+ const statusCode = error.response?.status ?? 500;
420
+ const statusText = error.response?.statusText ?? 'Unknown Error';
421
+ const responseData = error.response?.data;
307
422
 
308
- throw new Error(errMsg);
423
+ let message: string;
424
+ if (responseData && typeof responseData === 'object' && responseData.message) {
425
+ message = responseData.message;
426
+ } else if (responseData && typeof responseData === 'string') {
427
+ message = responseData;
428
+ } else {
429
+ message = `${statusText} (${statusCode})`;
430
+ }
431
+
432
+ throw createErrorFromStatusCode(statusCode, message);
433
+ } else {
434
+ // Network error or other non-HTTP error
435
+ const message = error instanceof Error ? error.message : 'Unknown error occurred';
436
+ throw createNetworkError(message, error instanceof Error ? error : undefined);
437
+ }
309
438
  }
310
439
  }
311
440
  }