@oway/sdk 0.1.2 → 0.2.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/index.js CHANGED
@@ -29,278 +29,230 @@ module.exports = __toCommonJS(index_exports);
29
29
 
30
30
  // src/client.ts
31
31
  var OwayError = class extends Error {
32
- constructor(message, code, statusCode, requestId) {
33
- super(message);
34
- this.code = code;
35
- this.statusCode = statusCode;
36
- this.requestId = requestId;
32
+ constructor(opts) {
33
+ super(opts.message);
37
34
  this.name = "OwayError";
35
+ this.statusCode = opts.statusCode;
36
+ this.code = opts.code;
37
+ this.requestId = opts.requestId;
38
+ this.violations = opts.violations ?? [];
39
+ this.rawBody = opts.rawBody;
38
40
  }
39
- /**
40
- * Determines if this error represents a transient failure that should be retried
41
- */
41
+ /** True for well-known transient HTTP status codes (408/429/500/502/503/504). */
42
42
  isRetryable() {
43
43
  if (!this.statusCode) return false;
44
- if (this.statusCode === 429) return true;
45
- if (this.statusCode === 503) return true;
46
- if (this.statusCode === 500) return true;
47
- if (this.statusCode === 501) return false;
48
- if (this.statusCode === 502) return true;
49
- if (this.statusCode === 504) return true;
50
- if (this.statusCode >= 500) return true;
51
- return false;
44
+ return [408, 429, 500, 502, 503, 504].includes(this.statusCode);
45
+ }
46
+ /** 4xx response. */
47
+ isClientError() {
48
+ return !!this.statusCode && this.statusCode >= 400 && this.statusCode < 500;
49
+ }
50
+ /** 5xx response. */
51
+ isServerError() {
52
+ return !!this.statusCode && this.statusCode >= 500 && this.statusCode < 600;
52
53
  }
53
54
  };
55
+ function parseHttpError(status, requestId, rawBody) {
56
+ let problem;
57
+ try {
58
+ problem = rawBody ? JSON.parse(rawBody) : void 0;
59
+ } catch {
60
+ }
61
+ const message = problem?.detail || problem?.title || `Request failed with status ${status}`;
62
+ return new OwayError({
63
+ message,
64
+ statusCode: status,
65
+ code: problem?.reason,
66
+ requestId,
67
+ violations: problem?.violations,
68
+ rawBody
69
+ });
70
+ }
71
+ function randomRequestId() {
72
+ if (typeof globalThis !== "undefined" && globalThis.crypto?.randomUUID) {
73
+ return globalThis.crypto.randomUUID();
74
+ }
75
+ const buf = new Uint8Array(16);
76
+ for (let i = 0; i < 16; i++) buf[i] = Math.floor(Math.random() * 256);
77
+ return Array.from(buf, (b) => b.toString(16).padStart(2, "0")).join("");
78
+ }
79
+ function backoffDelayMs(attempt) {
80
+ const base = Math.min(Math.pow(2, attempt) * 1e3, 3e4);
81
+ return Math.floor(Math.random() * (base + 1));
82
+ }
54
83
  var HttpClient = class {
55
84
  constructor(config) {
56
85
  this.accessToken = null;
57
86
  this.tokenExpiry = 0;
58
87
  this.tokenRefreshPromise = null;
59
88
  if (!config.clientId || !config.clientSecret) {
60
- throw new OwayError("clientId and clientSecret are required. Contact Oway Sales Engineering to obtain M2M credentials.");
89
+ throw new OwayError({
90
+ message: "clientId and clientSecret are required",
91
+ code: "CONFIG_MISSING_CREDENTIALS"
92
+ });
61
93
  }
94
+ const baseUrl = config.baseUrl || (typeof process !== "undefined" ? process.env?.OWAY_BASE_URL : void 0) || "https://api.sandbox.oway.io";
62
95
  this.config = {
63
- baseUrl: config.baseUrl || process.env.OWAY_BASE_URL || "https://api.sandbox.oway.io",
64
- tokenUrl: config.tokenUrl || (config.baseUrl || process.env.OWAY_BASE_URL || "https://api.sandbox.oway.io") + "/v1/auth/token",
96
+ baseUrl,
97
+ tokenUrl: config.tokenUrl || `${baseUrl}/v1/auth/token`,
65
98
  maxRetries: config.maxRetries ?? 3,
66
99
  timeout: config.timeout ?? 3e4,
67
100
  debug: config.debug ?? false,
68
101
  clientId: config.clientId,
69
102
  clientSecret: config.clientSecret,
70
103
  apiKey: config.apiKey,
71
- // Optional default company API key
72
104
  logger: config.logger
73
105
  };
74
- this.log("debug", "Oway SDK initialized", {
106
+ this.log("debug", "sdk initialized", {
75
107
  baseUrl: this.config.baseUrl,
76
- authMode: "M2M",
77
108
  hasDefaultApiKey: !!this.config.apiKey
78
109
  });
79
110
  }
80
- /**
81
- * Internal logging with sanitization
82
- */
83
111
  log(level, message, meta) {
112
+ if (!this.config.logger) return;
84
113
  if (!this.config.debug && level === "debug") return;
85
- const sanitized = meta ? this.sanitizeForLogging(meta) : void 0;
86
- if (this.config.logger) {
87
- this.config.logger[level](message, sanitized);
88
- } else if (this.config.debug && level !== "debug") {
89
- console[level === "error" ? "error" : "log"](`[Oway ${level}]`, message, sanitized);
90
- }
114
+ const safe = meta ? this.sanitize(meta) : void 0;
115
+ this.config.logger[level](message, safe);
91
116
  }
92
- /**
93
- * Sanitize objects for logging - remove sensitive fields
94
- */
95
- sanitizeForLogging(obj) {
117
+ sanitize(obj) {
96
118
  if (!obj || typeof obj !== "object") return obj;
97
- const sensitive = ["apiKey", "token", "authorization", "password", "secret"];
98
- const sanitized = Array.isArray(obj) ? [] : {};
99
- for (const [key, value] of Object.entries(obj)) {
100
- const lowerKey = key.toLowerCase();
101
- if (sensitive.some((s) => lowerKey.includes(s))) {
102
- sanitized[key] = "[REDACTED]";
103
- } else if (value && typeof value === "object") {
104
- sanitized[key] = this.sanitizeForLogging(value);
119
+ const sensitive = ["apikey", "token", "authorization", "password", "secret", "clientsecret"];
120
+ const out = Array.isArray(obj) ? [] : {};
121
+ for (const [k, v] of Object.entries(obj)) {
122
+ const lower = k.toLowerCase();
123
+ if (sensitive.some((s) => lower.includes(s))) {
124
+ out[k] = "[REDACTED]";
125
+ } else if (v && typeof v === "object") {
126
+ out[k] = this.sanitize(v);
105
127
  } else {
106
- sanitized[key] = value;
128
+ out[k] = v;
107
129
  }
108
130
  }
109
- return sanitized;
131
+ return out;
110
132
  }
111
- /**
112
- * Get or refresh the access token using the API key
113
- * Handles concurrent requests by queuing them behind a single refresh
114
- */
115
133
  async getAccessToken() {
116
- if (this.tokenRefreshPromise) {
117
- this.log("debug", "Waiting for token refresh in progress");
118
- return this.tokenRefreshPromise;
119
- }
134
+ if (this.tokenRefreshPromise) return this.tokenRefreshPromise;
120
135
  if (this.accessToken && Date.now() < this.tokenExpiry - 5 * 60 * 1e3) {
121
136
  return this.accessToken;
122
137
  }
123
- this.log("debug", "Refreshing access token");
124
138
  this.tokenRefreshPromise = this.refreshToken();
125
139
  try {
126
140
  this.accessToken = await this.tokenRefreshPromise;
127
- this.log("info", "Access token refreshed", {
128
- expiresAt: new Date(this.tokenExpiry).toISOString()
129
- });
130
141
  return this.accessToken;
131
142
  } finally {
132
143
  this.tokenRefreshPromise = null;
133
144
  }
134
145
  }
135
- /**
136
- * Perform the actual token refresh using M2M credentials
137
- */
138
146
  async refreshToken() {
147
+ this.log("debug", "refreshing access token");
148
+ const resp = await fetch(this.config.tokenUrl, {
149
+ method: "POST",
150
+ headers: { "Content-Type": "application/json" },
151
+ body: JSON.stringify({
152
+ clientId: this.config.clientId,
153
+ clientSecret: this.config.clientSecret
154
+ })
155
+ });
156
+ const text = await resp.text();
157
+ if (!resp.ok) {
158
+ throw parseHttpError(resp.status, resp.headers.get("x-request-id") ?? void 0, text);
159
+ }
160
+ let parsed;
139
161
  try {
140
- this.log("debug", "Refreshing M2M access token");
141
- const response = await fetch(this.config.tokenUrl, {
142
- method: "POST",
143
- headers: {
144
- "Content-Type": "application/json"
145
- },
146
- body: JSON.stringify({
147
- clientId: this.config.clientId,
148
- clientSecret: this.config.clientSecret
149
- })
162
+ parsed = JSON.parse(text);
163
+ } catch {
164
+ throw new OwayError({
165
+ message: "token response was not valid JSON",
166
+ code: "AUTH_INVALID_RESPONSE",
167
+ statusCode: resp.status
150
168
  });
151
- if (!response.ok) {
152
- throw new OwayError(
153
- "Failed to obtain access token",
154
- "AUTH_FAILED",
155
- response.status
156
- );
157
- }
158
- const data = await response.json();
159
- if (!data.accessToken || !data.expiresIn) {
160
- throw new OwayError(
161
- "Invalid token response: missing accessToken or expiresIn",
162
- "AUTH_INVALID_RESPONSE"
163
- );
164
- }
165
- this.tokenExpiry = Date.now() + data.expiresIn * 1e3;
166
- return data.accessToken;
167
- } catch (error) {
168
- this.log("error", "Token refresh failed", {
169
- error: error instanceof Error ? error.message : "Unknown error"
169
+ }
170
+ if (!parsed.accessToken || !parsed.expiresIn) {
171
+ throw new OwayError({
172
+ message: "token response missing accessToken or expiresIn",
173
+ code: "AUTH_INVALID_RESPONSE",
174
+ statusCode: resp.status
170
175
  });
171
- if (error instanceof OwayError) throw error;
172
- throw new OwayError(
173
- `Authentication failed: ${error instanceof Error ? error.message : "Unknown error"}`,
174
- "AUTH_ERROR"
175
- );
176
176
  }
177
+ this.tokenExpiry = Date.now() + parsed.expiresIn * 1e3;
178
+ return parsed.accessToken;
177
179
  }
178
- /**
179
- * Generate a unique request ID
180
- */
181
- generateRequestId() {
182
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
183
- const r = Math.random() * 16 | 0;
184
- const v = c === "x" ? r : r & 3 | 8;
185
- return v.toString(16);
186
- });
187
- }
188
- /**
189
- * Make an authenticated request to the Oway API
190
- */
191
180
  async request(method, path, options = {}) {
192
181
  const token = await this.getAccessToken();
193
182
  const url = new URL(path, this.config.baseUrl);
194
- const requestId = options.requestId || this.generateRequestId();
195
183
  if (options.query) {
196
- Object.entries(options.query).forEach(([key, value]) => {
197
- url.searchParams.append(key, String(value));
198
- });
184
+ for (const [k, v] of Object.entries(options.query)) url.searchParams.append(k, String(v));
199
185
  }
200
- const apiKey = options.companyApiKey || this.config.companyApiKey || this.config.apiKey;
186
+ const requestId = options.requestId || randomRequestId();
187
+ const apiKey = options.companyApiKey ?? this.config.apiKey;
201
188
  const headers = {
202
189
  "Content-Type": "application/json",
203
- "Authorization": `Bearer ${token}`,
190
+ Authorization: `Bearer ${token}`,
204
191
  "x-request-id": requestId,
205
192
  ...options.headers
206
193
  };
207
- if (apiKey) {
208
- headers["x-oway-api-key"] = apiKey;
209
- }
210
- this.log("debug", `${method} ${path}`, {
211
- requestId,
212
- hasBody: !!options.body,
213
- query: options.query
214
- });
215
- let lastError = null;
194
+ if (apiKey) headers["x-oway-api-key"] = apiKey;
195
+ let lastErr = null;
216
196
  for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
197
+ const controller = new AbortController();
198
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
217
199
  try {
218
- const controller = new AbortController();
219
- const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
220
- const response = await fetch(url.toString(), {
200
+ const resp = await fetch(url.toString(), {
221
201
  method,
222
202
  headers,
223
- body: options.body ? JSON.stringify(options.body) : void 0,
203
+ body: options.body !== void 0 ? JSON.stringify(options.body) : void 0,
224
204
  signal: controller.signal
225
205
  });
226
206
  clearTimeout(timeoutId);
227
- const serverRequestId = response.headers.get("x-request-id") || requestId;
228
- if (!response.ok) {
229
- let errorData;
230
- try {
231
- errorData = await response.json();
232
- } catch {
233
- errorData = { message: response.statusText };
234
- }
235
- const error = new OwayError(
236
- errorData.message || `Request failed with status ${response.status}`,
237
- errorData.code || "API_ERROR",
238
- response.status,
239
- serverRequestId
240
- );
241
- this.log("warn", "Request failed", {
207
+ const serverRequestId = resp.headers.get("x-request-id") ?? requestId;
208
+ if (!resp.ok) {
209
+ const rawBody = await resp.text();
210
+ const err = parseHttpError(resp.status, serverRequestId, rawBody);
211
+ this.log("warn", "request failed", {
242
212
  method,
243
213
  path,
244
- status: response.status,
245
- requestId: serverRequestId,
214
+ status: err.statusCode,
215
+ code: err.code,
216
+ requestId: err.requestId,
246
217
  attempt: attempt + 1,
247
- isRetryable: error.isRetryable()
248
- });
249
- throw error;
250
- }
251
- this.log("debug", "Request successful", {
252
- method,
253
- path,
254
- status: response.status,
255
- requestId: serverRequestId
256
- });
257
- if (response.status === 204) {
258
- return {};
259
- }
260
- return await response.json();
261
- } catch (error) {
262
- lastError = error;
263
- if (error instanceof OwayError && !error.isRetryable()) {
264
- this.log("error", "Non-retryable error", {
265
- requestId,
266
- code: error.code,
267
- statusCode: error.statusCode
218
+ retryable: err.isRetryable()
268
219
  });
269
- throw error;
220
+ if (!err.isRetryable() || attempt === this.config.maxRetries) throw err;
221
+ lastErr = err;
222
+ } else {
223
+ if (resp.status === 204) return {};
224
+ return await resp.json();
270
225
  }
271
- if (attempt === this.config.maxRetries) {
272
- this.log("error", "Max retries exceeded", {
273
- requestId,
274
- attempts: attempt + 1
275
- });
276
- break;
226
+ } catch (e) {
227
+ clearTimeout(timeoutId);
228
+ if (e instanceof OwayError) {
229
+ if (!e.isRetryable() || attempt === this.config.maxRetries) throw e;
230
+ lastErr = e;
231
+ } else {
232
+ lastErr = e;
233
+ if (attempt === this.config.maxRetries) throw lastErr;
277
234
  }
278
- const delay = Math.pow(2, attempt) * 1e3;
279
- this.log("warn", "Retrying request", {
280
- requestId,
281
- attempt: attempt + 1,
282
- maxRetries: this.config.maxRetries,
283
- delayMs: delay
284
- });
285
- await new Promise((resolve) => setTimeout(resolve, delay));
286
235
  }
236
+ const delay = backoffDelayMs(attempt);
237
+ this.log("warn", "retrying", { attempt: attempt + 1, delay });
238
+ await new Promise((r) => setTimeout(r, delay));
287
239
  }
288
- this.log("error", "Request failed", {
289
- requestId,
290
- error: lastError instanceof Error ? lastError.message : "Unknown error"
240
+ throw lastErr || new OwayError({
241
+ message: "request failed after retries",
242
+ code: "MAX_RETRIES_EXCEEDED",
243
+ requestId
291
244
  });
292
- throw lastError || new OwayError("Request failed after retries", "MAX_RETRIES_EXCEEDED", void 0, requestId);
293
245
  }
294
- async get(path, query, companyApiKey) {
246
+ get(path, query, companyApiKey) {
295
247
  return this.request("GET", path, { query, companyApiKey });
296
248
  }
297
- async post(path, body, companyApiKey) {
249
+ post(path, body, companyApiKey) {
298
250
  return this.request("POST", path, { body, companyApiKey });
299
251
  }
300
- async put(path, body, companyApiKey) {
252
+ put(path, body, companyApiKey) {
301
253
  return this.request("PUT", path, { body, companyApiKey });
302
254
  }
303
- async delete(path, companyApiKey) {
255
+ delete(path, companyApiKey) {
304
256
  return this.request("DELETE", path, { companyApiKey });
305
257
  }
306
258
  };