@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/README.md +21 -9
- 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.mjs
CHANGED
|
@@ -1,277 +1,229 @@
|
|
|
1
1
|
// src/client.ts
|
|
2
2
|
var OwayError = class extends Error {
|
|
3
|
-
constructor(
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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(
|
|
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
|
|
35
|
-
tokenUrl: config.tokenUrl ||
|
|
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", "
|
|
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
|
|
57
|
-
|
|
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 = ["
|
|
69
|
-
const
|
|
70
|
-
for (const [
|
|
71
|
-
const
|
|
72
|
-
if (sensitive.some((s) =>
|
|
73
|
-
|
|
74
|
-
} else if (
|
|
75
|
-
|
|
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
|
-
|
|
99
|
+
out[k] = v;
|
|
78
100
|
}
|
|
79
101
|
}
|
|
80
|
-
return
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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).
|
|
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
|
|
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
|
-
|
|
161
|
+
Authorization: `Bearer ${token}`,
|
|
175
162
|
"x-request-id": requestId,
|
|
176
163
|
...options.headers
|
|
177
164
|
};
|
|
178
|
-
if (apiKey)
|
|
179
|
-
|
|
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
|
|
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 =
|
|
199
|
-
if (!
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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:
|
|
216
|
-
|
|
185
|
+
status: err.statusCode,
|
|
186
|
+
code: err.code,
|
|
187
|
+
requestId: err.requestId,
|
|
217
188
|
attempt: attempt + 1,
|
|
218
|
-
|
|
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
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
217
|
+
get(path, query, companyApiKey) {
|
|
266
218
|
return this.request("GET", path, { query, companyApiKey });
|
|
267
219
|
}
|
|
268
|
-
|
|
220
|
+
post(path, body, companyApiKey) {
|
|
269
221
|
return this.request("POST", path, { body, companyApiKey });
|
|
270
222
|
}
|
|
271
|
-
|
|
223
|
+
put(path, body, companyApiKey) {
|
|
272
224
|
return this.request("PUT", path, { body, companyApiKey });
|
|
273
225
|
}
|
|
274
|
-
|
|
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.
|
|
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": {
|