attlaz-client 1.74.0 → 1.75.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/dist/Client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { OAuthClientToken } from './Http/OAuthClientToken.js';
2
- import { ITransport } from './Http/Transport/ITransport.js';
2
+ import { ITransport, ParseErrorHandler } from './Http/Transport/ITransport.js';
3
3
  import { OAuthClient } from './Http/Transport/OAuthClient.js';
4
4
  import { AccessTokenEndpoint } from './Service/AccessTokenEndpoint.js';
5
5
  import { AdapterConnectionEndpoint } from './Service/AdapterConnectionEndpoint.js';
@@ -59,6 +59,13 @@ export declare class Client {
59
59
  setPublicClient(clientId: string): void;
60
60
  setVersion(version: string | null): void;
61
61
  getHttpClient(): OAuthClient;
62
+ /**
63
+ * Set a handler for non-fatal response parse failures (schema drift). Defaults to
64
+ * console.error; wire this to your logger / error tracker to observe drift in production
65
+ * without breaking views. Pass null to restore the default. Set on the active transport,
66
+ * so it applies whether the client uses OAuth or a custom transport.
67
+ */
68
+ setParseErrorHandler(handler: ParseErrorHandler | null): void;
62
69
  authenticate(): Promise<boolean>;
63
70
  authenticate(username: string, password: string): Promise<boolean>;
64
71
  isAuthenticated(): boolean;
package/dist/Client.js CHANGED
@@ -122,6 +122,15 @@ export class Client {
122
122
  getHttpClient() {
123
123
  return this.httpClient;
124
124
  }
125
+ /**
126
+ * Set a handler for non-fatal response parse failures (schema drift). Defaults to
127
+ * console.error; wire this to your logger / error tracker to observe drift in production
128
+ * without breaking views. Pass null to restore the default. Set on the active transport,
129
+ * so it applies whether the client uses OAuth or a custom transport.
130
+ */
131
+ setParseErrorHandler(handler) {
132
+ this.transport.setParseErrorHandler(handler);
133
+ }
125
134
  async authenticate(username = null, password = null) {
126
135
  if (username === null || password === null) {
127
136
  return await this.httpClient.authenticate();
@@ -10,53 +10,16 @@ export class ClientError extends Error {
10
10
  }
11
11
  static fromError(error) {
12
12
  // Already a ClientError (or a subclass such as ApiError) — preserve it as-is.
13
- // Re-deriving here would discard its httpStatus, because this method inspects the
14
- // `.status`/`.code` shape of raw transport errors, not ClientError's own fields.
15
13
  if (error instanceof ClientError) {
16
14
  return error;
17
15
  }
18
- let clientError = new ClientError('Unknown error', HttpStatus.HTTP_INTERNAL_SERVER_ERROR);
19
- const xError = error;
20
- if (xError.status !== null && xError.status !== undefined) {
21
- const statusCode = xError.status;
22
- const { body } = xError;
23
- const clientErrorByStatus = this.byStatus(statusCode, body);
24
- if (clientErrorByStatus !== null) {
25
- clientError = clientErrorByStatus;
26
- }
27
- }
28
- else if (xError.code !== null && xError.code !== undefined) {
29
- switch (xError.code) {
30
- case 'EUNAVAILABLE':
31
- case 'ECONNREFUSED':
32
- case 'HTTP_UNAVAILABLE':
33
- clientError.httpStatus = HttpStatus.HTTP_UNAVAILABLE;
34
- break;
35
- case 'EAUTH':
36
- case 401:
37
- clientError.httpStatus = HttpStatus.HTTP_UNAUTHORIZED;
38
- break;
39
- case 'ESTATUS':
40
- if (error.message === 'HTTP status 503') {
41
- clientError.httpStatus = HttpStatus.HTTP_UNAVAILABLE;
42
- }
43
- break;
44
- case 500:
45
- clientError.httpStatus = HttpStatus.HTTP_INTERNAL_SERVER_ERROR;
46
- break;
47
- default:
48
- console.warn('[Client error] Error code `' + xError.code + '` not recognised');
49
- clientError.httpStatus = HttpStatus.HTTP_INTERNAL_SERVER_ERROR;
50
- }
51
- }
52
- else if (error instanceof TypeError) {
53
- // fetch throws TypeError on network failures (ECONNREFUSED, DNS, etc.)
54
- clientError.httpStatus = HttpStatus.HTTP_UNAVAILABLE;
55
- clientError.message = 'Service not available';
56
- return clientError;
57
- }
16
+ // The transport uses fetch, which throws TypeError on network failures (DNS, refused
17
+ // connection, TLS, etc.). Anything else reaching here is unexpected — surface it as a
18
+ // generic 500 while keeping the original name/stack for debugging.
19
+ const clientError = error instanceof TypeError
20
+ ? new ClientError('Service not available', HttpStatus.HTTP_UNAVAILABLE)
21
+ : new ClientError('Unknown error', HttpStatus.HTTP_INTERNAL_SERVER_ERROR);
58
22
  clientError.name = error.name;
59
- // TODO: only in debug mode
60
23
  clientError.stack = error.stack;
61
24
  return clientError;
62
25
  }
@@ -5,6 +5,4 @@ export declare class HttpClientResponse {
5
5
  body: any | null;
6
6
  headers: Headers;
7
7
  constructor(status: number, statusText: string);
8
- getContentType(): string | null;
9
- isJson(): boolean;
10
8
  }
@@ -1,4 +1,3 @@
1
- import { ContentTypeHelper } from './ContentTypeHelper.js';
2
1
  export class HttpClientResponse {
3
2
  status;
4
3
  statusText;
@@ -8,22 +7,4 @@ export class HttpClientResponse {
8
7
  this.status = status;
9
8
  this.statusText = statusText;
10
9
  }
11
- getContentType() {
12
- // TODO: content-type or Content-Type or both...?
13
- let contentType = this.headers['content-type'];
14
- if (contentType === null || contentType === undefined) {
15
- return null;
16
- }
17
- if (Array.isArray(contentType)) {
18
- [contentType] = contentType;
19
- }
20
- if (contentType === null || contentType === undefined) {
21
- return null;
22
- }
23
- const parsedContentType = ContentTypeHelper.formatContentType(contentType);
24
- return parsedContentType.type;
25
- }
26
- isJson() {
27
- return this.getContentType() === 'application/json';
28
- }
29
10
  }
@@ -1,5 +1,5 @@
1
1
  import { Parameters } from '../Data/Parameters.js';
2
- import { ITransport } from './ITransport.js';
2
+ import { ITransport, ParseErrorHandler } from './ITransport.js';
3
3
  export interface DirectTransportRoute {
4
4
  prefix: string;
5
5
  baseUrl: string;
@@ -27,6 +27,7 @@ export interface DirectTransportRoute {
27
27
  export declare class DirectTransport implements ITransport {
28
28
  private readonly clients;
29
29
  private readonly sortedRoutes;
30
+ private parseErrorHandler;
30
31
  /**
31
32
  * Creates the default session payload for internal service-to-service calls.
32
33
  */
@@ -35,4 +36,6 @@ export declare class DirectTransport implements ITransport {
35
36
  request<T>(action: string, parameters?: Parameters, method?: string, _signWithOauthToken?: boolean): Promise<T>;
36
37
  private resolveClient;
37
38
  isDebugEnabled(): boolean;
39
+ setParseErrorHandler(handler: ParseErrorHandler | null): void;
40
+ reportParseError(message: string, context: Record<string, unknown>): void;
38
41
  }
@@ -23,6 +23,7 @@ import { OAuthClient } from './OAuthClient.js';
23
23
  export class DirectTransport {
24
24
  clients = new Map();
25
25
  sortedRoutes;
26
+ parseErrorHandler = null;
26
27
  /**
27
28
  * Creates the default session payload for internal service-to-service calls.
28
29
  */
@@ -69,4 +70,14 @@ export class DirectTransport {
69
70
  isDebugEnabled() {
70
71
  return false;
71
72
  }
73
+ setParseErrorHandler(handler) {
74
+ this.parseErrorHandler = handler;
75
+ }
76
+ reportParseError(message, context) {
77
+ if (this.parseErrorHandler !== null) {
78
+ this.parseErrorHandler(message, context);
79
+ return;
80
+ }
81
+ console.error(message, context);
82
+ }
72
83
  }
@@ -1,5 +1,13 @@
1
1
  import { Parameters } from '../Data/Parameters.js';
2
+ /**
3
+ * Invoked when an endpoint fails to parse a response object (schema drift). Reporting is
4
+ * non-fatal — parsing still degrades gracefully. Defaults to console.error; set a custom handler
5
+ * (e.g. via Client.setParseErrorHandler) to route these to a logger / error tracker.
6
+ */
7
+ export type ParseErrorHandler = (message: string, context: Record<string, unknown>) => void;
2
8
  export interface ITransport {
3
9
  request: <T>(action: string, parameters: Parameters, method: string, signWithOauthToken: boolean) => Promise<T>;
4
10
  isDebugEnabled: () => boolean;
11
+ reportParseError: (message: string, context: Record<string, unknown>) => void;
12
+ setParseErrorHandler: (handler: ParseErrorHandler | null) => void;
5
13
  }
@@ -2,25 +2,26 @@ import { Headers } from '../Data/Headers.js';
2
2
  import { Parameters } from '../Data/Parameters.js';
3
3
  import { OAuthClientOptions } from '../OAuthClientOptions.js';
4
4
  import { OAuthClientToken } from '../OAuthClientToken.js';
5
- import { ITransport } from './ITransport.js';
5
+ import { ITransport, ParseErrorHandler } from './ITransport.js';
6
6
  export declare class OAuthClient implements ITransport {
7
7
  private readonly options;
8
8
  private debug;
9
9
  private oauthClientToken;
10
10
  private refreshTokenPromise;
11
11
  private version;
12
+ private parseErrorHandler;
12
13
  constructor(options: OAuthClientOptions);
13
14
  authenticate(username: string, password: string): Promise<boolean>;
14
15
  authenticate(): Promise<boolean>;
15
16
  refreshToken(): Promise<void>;
16
17
  private isPublicClient;
17
18
  /**
18
- * Direct token request for public clients that must not send client_secret.
19
- * The axios-oauth-client library always includes client_secret in the body,
20
- * so public clients use this method instead.
19
+ * Performs an OAuth token request (password, client_credentials or refresh_token grant) and
20
+ * returns the raw token response. On a non-ok response it throws a ClientError carrying the
21
+ * server's HTTP status and, when the body is JSON, its structured error message.
21
22
  */
22
23
  private requestToken;
23
- isTokenExpires(): boolean;
24
+ isTokenExpired(): boolean;
24
25
  request<T>(action: string, parameters?: Parameters, method?: string, signWithOauthToken?: boolean): Promise<T>;
25
26
  isAuthenticated(): boolean;
26
27
  getToken(): OAuthClientToken | null;
@@ -29,6 +30,8 @@ export declare class OAuthClient implements ITransport {
29
30
  enableDebug(): void;
30
31
  disableDebug(): void;
31
32
  isDebugEnabled(): boolean;
33
+ setParseErrorHandler(handler: ParseErrorHandler | null): void;
34
+ reportParseError(message: string, context: Record<string, unknown>): void;
32
35
  private defaultHeaders;
33
36
  getDefaultHeaders(): Headers;
34
37
  private initDefaultHeaders;
@@ -11,6 +11,7 @@ export class OAuthClient {
11
11
  oauthClientToken = null;
12
12
  refreshTokenPromise = null;
13
13
  version = null;
14
+ parseErrorHandler = null;
14
15
  constructor(options) {
15
16
  this.options = options;
16
17
  this.initDefaultHeaders();
@@ -96,9 +97,9 @@ export class OAuthClient {
96
97
  return this.options.clientSecret === null || this.options.clientSecret === '';
97
98
  }
98
99
  /**
99
- * Direct token request for public clients that must not send client_secret.
100
- * The axios-oauth-client library always includes client_secret in the body,
101
- * so public clients use this method instead.
100
+ * Performs an OAuth token request (password, client_credentials or refresh_token grant) and
101
+ * returns the raw token response. On a non-ok response it throws a ClientError carrying the
102
+ * server's HTTP status and, when the body is JSON, its structured error message.
102
103
  */
103
104
  async requestToken(params) {
104
105
  const response = await fetch(this.getApiEndpointUrl(this.options.accessTokenUri), {
@@ -107,64 +108,83 @@ export class OAuthClient {
107
108
  body: new URLSearchParams(params).toString(),
108
109
  });
109
110
  if (!response.ok) {
110
- throw new ClientError(response.statusText || 'Token request failed', response.status);
111
+ const clientError = new ClientError(response.statusText || 'Token request failed', response.status);
112
+ // The token endpoint bypasses HttpClient (and authenticate() bypasses Endpoint.toApiError),
113
+ // so without this the server's body is discarded and callers only see the generic HTTP
114
+ // statusText. Attach the body and surface its structured OAuth error message directly so a
115
+ // failed login shows e.g. an invalid-credentials reason. Mirrors HttpClient.request() +
116
+ // Endpoint.toApiError(). The refresh path replaces this message with a 401, so it's unaffected.
117
+ try {
118
+ const data = await response.json();
119
+ clientError.response = {
120
+ status: response.status,
121
+ statusText: response.statusText,
122
+ data,
123
+ };
124
+ const apiMessage = data?.error?.message;
125
+ if (apiMessage !== undefined && apiMessage !== null && apiMessage !== '') {
126
+ clientError.message = apiMessage;
127
+ }
128
+ }
129
+ catch {
130
+ // Response body is not JSON — leave response/message as-is
131
+ }
132
+ throw clientError;
111
133
  }
112
134
  return response.json();
113
135
  }
114
- isTokenExpires() {
136
+ isTokenExpired() {
115
137
  if (this.oauthClientToken === null) {
116
138
  throw new Error('No token defined');
117
139
  }
118
140
  return OAuthClientToken.isExpired(this.oauthClientToken);
119
141
  }
120
142
  async request(action, parameters = null, method = 'GET', signWithOauthToken = true) {
121
- if (signWithOauthToken && !this.isAuthenticated()) {
122
- throw new ClientError('Unable to perform request, access token not provided');
123
- }
124
- else {
125
- if (signWithOauthToken) {
126
- if (this.oauthClientToken === null) {
127
- throw new ClientError('Unable to perform request, access token not provided');
128
- }
129
- if (signWithOauthToken && OAuthClientToken.isExpired(this.oauthClientToken)) {
130
- if (this.refreshTokenPromise === null) {
131
- this.refreshTokenPromise = this.refreshToken();
132
- try {
133
- await this.refreshTokenPromise;
134
- }
135
- finally {
136
- // Always clear, even on failure, so a single failed refresh
137
- // doesn't poison every later request with the same rejected
138
- // promise (e.g. after the user re-authenticates).
139
- this.refreshTokenPromise = null;
140
- }
141
- }
142
- else {
143
+ if (signWithOauthToken) {
144
+ // A single null check both guards and narrows the token type. No access token at
145
+ // all is an auth failure (401), so consumers route it to the same sign-out path as
146
+ // a rejected refresh.
147
+ if (this.oauthClientToken === null) {
148
+ throw new ClientError('Unable to perform request, access token not provided', HttpStatus.HTTP_UNAUTHORIZED);
149
+ }
150
+ if (OAuthClientToken.isExpired(this.oauthClientToken)) {
151
+ if (this.refreshTokenPromise === null) {
152
+ this.refreshTokenPromise = this.refreshToken();
153
+ try {
143
154
  await this.refreshTokenPromise;
144
155
  }
145
- }
146
- }
147
- const requestData = this.createRequestData(action, parameters, method, signWithOauthToken);
148
- if (this.debug) {
149
- console.debug('[OAuthClient] Request: ' + requestData.method.toUpperCase() + ' ' + requestData.getFullUrl(), { headers: requestData.headers });
150
- }
151
- try {
152
- const response = await HttpClient.request(requestData);
153
- return response.body;
154
- }
155
- catch (error) {
156
- if (!(error instanceof ClientError)) {
157
- const clientError = ClientError.fromError(error);
158
- if (this.debug) {
159
- console.error('[Client] Error:', { error, clientError });
156
+ finally {
157
+ // Always clear, even on failure, so a single failed refresh doesn't
158
+ // poison every later request with the same rejected promise (e.g.
159
+ // after the user re-authenticates).
160
+ this.refreshTokenPromise = null;
160
161
  }
161
- throw clientError;
162
162
  }
163
+ else {
164
+ await this.refreshTokenPromise;
165
+ }
166
+ }
167
+ }
168
+ const requestData = this.createRequestData(action, parameters, method, signWithOauthToken);
169
+ if (this.debug) {
170
+ console.debug('[OAuthClient] Request: ' + requestData.method.toUpperCase() + ' ' + requestData.getFullUrl(), { headers: requestData.headers });
171
+ }
172
+ try {
173
+ const response = await HttpClient.request(requestData);
174
+ return response.body;
175
+ }
176
+ catch (error) {
177
+ if (!(error instanceof ClientError)) {
178
+ const clientError = ClientError.fromError(error);
163
179
  if (this.debug) {
164
- console.error('[Client] Error:', { error });
180
+ console.error('[Client] Error:', { error, clientError });
165
181
  }
166
- throw error;
182
+ throw clientError;
167
183
  }
184
+ if (this.debug) {
185
+ console.error('[Client] Error:', { error });
186
+ }
187
+ throw error;
168
188
  }
169
189
  }
170
190
  isAuthenticated() {
@@ -189,6 +209,16 @@ export class OAuthClient {
189
209
  isDebugEnabled() {
190
210
  return this.debug;
191
211
  }
212
+ setParseErrorHandler(handler) {
213
+ this.parseErrorHandler = handler;
214
+ }
215
+ reportParseError(message, context) {
216
+ if (this.parseErrorHandler !== null) {
217
+ this.parseErrorHandler(message, context);
218
+ return;
219
+ }
220
+ console.error(message, context);
221
+ }
192
222
  defaultHeaders = {};
193
223
  getDefaultHeaders() {
194
224
  return this.defaultHeaders;
@@ -262,22 +292,18 @@ export class OAuthClient {
262
292
  });
263
293
  }
264
294
  if (signWithOauthToken) {
295
+ // request() already ensured a present, non-expired token; this guard only narrows
296
+ // the type before signing.
265
297
  if (this.oauthClientToken === null) {
266
- throw new ClientError('Unable to perform request, access token not provided');
298
+ throw new ClientError('Unable to perform request, access token not provided', HttpStatus.HTTP_UNAUTHORIZED);
267
299
  }
268
- if (OAuthClientToken.isExpired(this.oauthClientToken)) {
269
- throw new Error('Unable to sign request, token is expired');
270
- }
271
- requestData = this.signRequest(requestData);
300
+ requestData = this.signRequest(requestData, this.oauthClientToken);
272
301
  }
273
302
  return requestData;
274
303
  }
275
- signRequest(requestObject) {
276
- if (this.oauthClientToken === null) {
277
- throw new ClientError('Unable to sign request, access token not provided');
278
- }
279
- const tokenType = this.oauthClientToken.token_type;
280
- const accessToken = this.oauthClientToken.access_token;
304
+ signRequest(requestObject, token) {
305
+ const tokenType = token.token_type;
306
+ const accessToken = token.access_token;
281
307
  if (tokenType !== undefined && tokenType.toLowerCase() === 'bearer') {
282
308
  requestObject.setHeader('Authorization', 'Bearer ' + accessToken);
283
309
  }
@@ -17,10 +17,6 @@ export class Endpoint {
17
17
  if (data === null || data === undefined) {
18
18
  return null;
19
19
  }
20
- if (typeof data === 'object' && '_formatted' in data) {
21
- // TODO: unset _formatted
22
- return data;
23
- }
24
20
  if (Array.isArray(data)) {
25
21
  const result = [];
26
22
  for (const value of data) {
@@ -118,14 +114,9 @@ export class Endpoint {
118
114
  /**
119
115
  * Parse errors
120
116
  */
121
- // TODO: temporary check until we know the API is fully upgraded
122
117
  if (requestResponse === null || requestResponse === undefined) {
123
118
  throw new ApiError('Unable to parse object: response is empty for action `[' + method + '] ' + action + '`', HttpStatus.HTTP_INTERNAL_SERVER_ERROR);
124
119
  }
125
- if ((Object.prototype.hasOwnProperty.call(requestResponse, 'data') && !Object.prototype.hasOwnProperty.call(requestResponse, 'id')) && requestResponse.data !== undefined) {
126
- console.error('Response for object "' + action + '" seem to still use old "data" property (this property is only used for collection requests)', { requestResponse });
127
- requestResponse = requestResponse.data;
128
- }
129
120
  result.setData(this.parseObject(requestResponse, parser));
130
121
  return result;
131
122
  }
@@ -170,13 +161,24 @@ export class Endpoint {
170
161
  return data;
171
162
  }
172
163
  parseObject(rawObject, parser) {
173
- // TODO: is it interesting to keep this wrapper, or only in develop mode?
174
- const wrappedData = ObjectWrapper.wrap(rawObject);
164
+ // Parse the raw object directly by default. With debug enabled the object is wrapped so
165
+ // that reads of properties the response doesn't contain get reported — useful for spotting
166
+ // schema drift, but too noisy (and a per-object Proxy cost) to run normally.
167
+ // Enable via client.getHttpClient().enableDebug().
168
+ const data = this.httpClient.isDebugEnabled() ? ObjectWrapper.wrap(rawObject) : rawObject;
175
169
  try {
176
- return parser(wrappedData);
170
+ return parser(data);
177
171
  }
178
172
  catch (error) {
179
- console.error('Unable to parse object', { object: rawObject, error });
173
+ // Report through the transport's parse-error handler (defaults to console.error;
174
+ // consumers can route it to a logger / error tracker via Client.setParseErrorHandler).
175
+ this.httpClient.reportParseError('Unable to parse object', { object: rawObject, error });
176
+ // In debug, surface schema drift loudly (dev/tests) by rethrowing. In production,
177
+ // degrade gracefully — return null so the caller skips the row / shows a not-found
178
+ // state, and a single unparseable object never breaks an entire view.
179
+ if (this.httpClient.isDebugEnabled()) {
180
+ throw error;
181
+ }
180
182
  }
181
183
  return null;
182
184
  }
package/dist/index.d.ts CHANGED
@@ -15,6 +15,7 @@ export { OAuthClientOptions } from './Http/OAuthClientOptions.js';
15
15
  export { OAuthClientToken } from './Http/OAuthClientToken.js';
16
16
  export { ClientError } from './Http/ClientError.js';
17
17
  export { ApiError } from './Model/Error/ApiError.js';
18
+ export type { ParseErrorHandler } from './Http/Transport/ITransport.js';
18
19
  export { HttpStatus } from './Http/HttpStatus.js';
19
20
  export { ContentTypeHelper } from './Http/ContentTypeHelper.js';
20
21
  /**
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "1.73.1";
1
+ export declare const VERSION = "1.74.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "1.73.1";
1
+ export const VERSION = "1.74.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "attlaz-client",
3
- "version": "1.74.0",
3
+ "version": "1.75.0",
4
4
  "description": "Javascript Client to access Attlaz API",
5
5
  "types": "./dist/index.d.ts",
6
6
  "main": "./dist/index.js",