@oway/sdk 0.1.2 → 0.2.1
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/README.md +25 -13
- package/dist/index.d.mts +2769 -834
- package/dist/index.d.ts +2769 -834
- package/dist/index.js +139 -187
- package/dist/index.mjs +139 -187
- package/package.json +5 -1
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(
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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(
|
|
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
|
|
64
|
-
tokenUrl: config.tokenUrl ||
|
|
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", "
|
|
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
|
|
86
|
-
|
|
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 = ["
|
|
98
|
-
const
|
|
99
|
-
for (const [
|
|
100
|
-
const
|
|
101
|
-
if (sensitive.some((s) =>
|
|
102
|
-
|
|
103
|
-
} else if (
|
|
104
|
-
|
|
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
|
-
|
|
128
|
+
out[k] = v;
|
|
107
129
|
}
|
|
108
130
|
}
|
|
109
|
-
return
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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).
|
|
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
|
|
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
|
-
|
|
190
|
+
Authorization: `Bearer ${token}`,
|
|
204
191
|
"x-request-id": requestId,
|
|
205
192
|
...options.headers
|
|
206
193
|
};
|
|
207
|
-
if (apiKey)
|
|
208
|
-
|
|
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
|
|
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 =
|
|
228
|
-
if (!
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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:
|
|
245
|
-
|
|
214
|
+
status: err.statusCode,
|
|
215
|
+
code: err.code,
|
|
216
|
+
requestId: err.requestId,
|
|
246
217
|
attempt: attempt + 1,
|
|
247
|
-
|
|
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
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
246
|
+
get(path, query, companyApiKey) {
|
|
295
247
|
return this.request("GET", path, { query, companyApiKey });
|
|
296
248
|
}
|
|
297
|
-
|
|
249
|
+
post(path, body, companyApiKey) {
|
|
298
250
|
return this.request("POST", path, { body, companyApiKey });
|
|
299
251
|
}
|
|
300
|
-
|
|
252
|
+
put(path, body, companyApiKey) {
|
|
301
253
|
return this.request("PUT", path, { body, companyApiKey });
|
|
302
254
|
}
|
|
303
|
-
|
|
255
|
+
delete(path, companyApiKey) {
|
|
304
256
|
return this.request("DELETE", path, { companyApiKey });
|
|
305
257
|
}
|
|
306
258
|
};
|