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