@oway/sdk 0.1.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 +224 -0
- package/dist/index.d.mts +1410 -0
- package/dist/index.d.ts +1410 -0
- package/dist/index.js +381 -0
- package/dist/index.mjs +352 -0
- package/package.json +58 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
Oway: () => Oway,
|
|
24
|
+
OwayEnvironments: () => OwayEnvironments,
|
|
25
|
+
OwayError: () => OwayError,
|
|
26
|
+
default: () => index_default
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
|
|
30
|
+
// src/client.ts
|
|
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;
|
|
37
|
+
this.name = "OwayError";
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Determines if this error represents a transient failure that should be retried
|
|
41
|
+
*/
|
|
42
|
+
isRetryable() {
|
|
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;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
var HttpClient = class {
|
|
55
|
+
constructor(config) {
|
|
56
|
+
this.accessToken = null;
|
|
57
|
+
this.tokenExpiry = 0;
|
|
58
|
+
this.tokenRefreshPromise = null;
|
|
59
|
+
if (!config.clientId || !config.clientSecret) {
|
|
60
|
+
throw new OwayError("clientId and clientSecret are required. Contact Oway Sales Engineering to obtain M2M credentials.");
|
|
61
|
+
}
|
|
62
|
+
this.config = {
|
|
63
|
+
baseUrl: config.baseUrl || process.env.OWAY_BASE_URL || "https://rest-api.sandbox.oway.io",
|
|
64
|
+
tokenUrl: config.tokenUrl || (config.baseUrl || process.env.OWAY_BASE_URL || "https://rest-api.sandbox.oway.io") + "/v1/auth/token",
|
|
65
|
+
maxRetries: config.maxRetries ?? 3,
|
|
66
|
+
timeout: config.timeout ?? 3e4,
|
|
67
|
+
debug: config.debug ?? false,
|
|
68
|
+
clientId: config.clientId,
|
|
69
|
+
clientSecret: config.clientSecret,
|
|
70
|
+
apiKey: config.apiKey,
|
|
71
|
+
// Optional default company API key
|
|
72
|
+
logger: config.logger
|
|
73
|
+
};
|
|
74
|
+
this.log("debug", "Oway SDK initialized", {
|
|
75
|
+
baseUrl: this.config.baseUrl,
|
|
76
|
+
authMode: "M2M",
|
|
77
|
+
hasDefaultApiKey: !!this.config.apiKey
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Internal logging with sanitization
|
|
82
|
+
*/
|
|
83
|
+
log(level, message, meta) {
|
|
84
|
+
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
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Sanitize objects for logging - remove sensitive fields
|
|
94
|
+
*/
|
|
95
|
+
sanitizeForLogging(obj) {
|
|
96
|
+
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);
|
|
105
|
+
} else {
|
|
106
|
+
sanitized[key] = value;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return sanitized;
|
|
110
|
+
}
|
|
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
|
+
async getAccessToken() {
|
|
116
|
+
if (this.tokenRefreshPromise) {
|
|
117
|
+
this.log("debug", "Waiting for token refresh in progress");
|
|
118
|
+
return this.tokenRefreshPromise;
|
|
119
|
+
}
|
|
120
|
+
if (this.accessToken && Date.now() < this.tokenExpiry - 5 * 60 * 1e3) {
|
|
121
|
+
return this.accessToken;
|
|
122
|
+
}
|
|
123
|
+
this.log("debug", "Refreshing access token");
|
|
124
|
+
this.tokenRefreshPromise = this.refreshToken();
|
|
125
|
+
try {
|
|
126
|
+
this.accessToken = await this.tokenRefreshPromise;
|
|
127
|
+
this.log("info", "Access token refreshed", {
|
|
128
|
+
expiresAt: new Date(this.tokenExpiry).toISOString()
|
|
129
|
+
});
|
|
130
|
+
return this.accessToken;
|
|
131
|
+
} finally {
|
|
132
|
+
this.tokenRefreshPromise = null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Perform the actual token refresh using M2M credentials
|
|
137
|
+
*/
|
|
138
|
+
async refreshToken() {
|
|
139
|
+
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
|
+
})
|
|
150
|
+
});
|
|
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
|
+
this.tokenExpiry = Date.now() + data.expires_in * 1e3;
|
|
160
|
+
return data.access_token;
|
|
161
|
+
} catch (error) {
|
|
162
|
+
this.log("error", "Token refresh failed", {
|
|
163
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
164
|
+
});
|
|
165
|
+
if (error instanceof OwayError) throw error;
|
|
166
|
+
throw new OwayError(
|
|
167
|
+
`Authentication failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
168
|
+
"AUTH_ERROR"
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Generate a unique request ID
|
|
174
|
+
*/
|
|
175
|
+
generateRequestId() {
|
|
176
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
177
|
+
const r = Math.random() * 16 | 0;
|
|
178
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
179
|
+
return v.toString(16);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Make an authenticated request to the Oway API
|
|
184
|
+
*/
|
|
185
|
+
async request(method, path, options = {}) {
|
|
186
|
+
const token = await this.getAccessToken();
|
|
187
|
+
const url = new URL(path, this.config.baseUrl);
|
|
188
|
+
const requestId = options.requestId || this.generateRequestId();
|
|
189
|
+
if (options.query) {
|
|
190
|
+
Object.entries(options.query).forEach(([key, value]) => {
|
|
191
|
+
url.searchParams.append(key, String(value));
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
const apiKey = options.companyApiKey || this.config.companyApiKey || this.config.apiKey;
|
|
195
|
+
const headers = {
|
|
196
|
+
"Content-Type": "application/json",
|
|
197
|
+
"Authorization": `Bearer ${token}`,
|
|
198
|
+
"x-request-id": requestId,
|
|
199
|
+
...options.headers
|
|
200
|
+
};
|
|
201
|
+
if (apiKey) {
|
|
202
|
+
headers["x-oway-api-key"] = apiKey;
|
|
203
|
+
}
|
|
204
|
+
this.log("debug", `${method} ${path}`, {
|
|
205
|
+
requestId,
|
|
206
|
+
hasBody: !!options.body,
|
|
207
|
+
query: options.query
|
|
208
|
+
});
|
|
209
|
+
let lastError = null;
|
|
210
|
+
for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
|
|
211
|
+
try {
|
|
212
|
+
const controller = new AbortController();
|
|
213
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
214
|
+
const response = await fetch(url.toString(), {
|
|
215
|
+
method,
|
|
216
|
+
headers,
|
|
217
|
+
body: options.body ? JSON.stringify(options.body) : void 0,
|
|
218
|
+
signal: controller.signal
|
|
219
|
+
});
|
|
220
|
+
clearTimeout(timeoutId);
|
|
221
|
+
const serverRequestId = response.headers.get("x-request-id") || requestId;
|
|
222
|
+
if (!response.ok) {
|
|
223
|
+
let errorData;
|
|
224
|
+
try {
|
|
225
|
+
errorData = await response.json();
|
|
226
|
+
} catch {
|
|
227
|
+
errorData = { message: response.statusText };
|
|
228
|
+
}
|
|
229
|
+
const error = new OwayError(
|
|
230
|
+
errorData.message || `Request failed with status ${response.status}`,
|
|
231
|
+
errorData.code || "API_ERROR",
|
|
232
|
+
response.status,
|
|
233
|
+
serverRequestId
|
|
234
|
+
);
|
|
235
|
+
this.log("warn", "Request failed", {
|
|
236
|
+
method,
|
|
237
|
+
path,
|
|
238
|
+
status: response.status,
|
|
239
|
+
requestId: serverRequestId,
|
|
240
|
+
attempt: attempt + 1,
|
|
241
|
+
isRetryable: error.isRetryable()
|
|
242
|
+
});
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
this.log("debug", "Request successful", {
|
|
246
|
+
method,
|
|
247
|
+
path,
|
|
248
|
+
status: response.status,
|
|
249
|
+
requestId: serverRequestId
|
|
250
|
+
});
|
|
251
|
+
if (response.status === 204) {
|
|
252
|
+
return {};
|
|
253
|
+
}
|
|
254
|
+
return await response.json();
|
|
255
|
+
} catch (error) {
|
|
256
|
+
lastError = error;
|
|
257
|
+
if (error instanceof OwayError && !error.isRetryable()) {
|
|
258
|
+
this.log("error", "Non-retryable error", {
|
|
259
|
+
requestId,
|
|
260
|
+
code: error.code,
|
|
261
|
+
statusCode: error.statusCode
|
|
262
|
+
});
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
if (attempt === this.config.maxRetries) {
|
|
266
|
+
this.log("error", "Max retries exceeded", {
|
|
267
|
+
requestId,
|
|
268
|
+
attempts: attempt + 1
|
|
269
|
+
});
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
const delay = Math.pow(2, attempt) * 1e3;
|
|
273
|
+
this.log("warn", "Retrying request", {
|
|
274
|
+
requestId,
|
|
275
|
+
attempt: attempt + 1,
|
|
276
|
+
maxRetries: this.config.maxRetries,
|
|
277
|
+
delayMs: delay
|
|
278
|
+
});
|
|
279
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
this.log("error", "Request failed", {
|
|
283
|
+
requestId,
|
|
284
|
+
error: lastError instanceof Error ? lastError.message : "Unknown error"
|
|
285
|
+
});
|
|
286
|
+
throw lastError || new OwayError("Request failed after retries", "MAX_RETRIES_EXCEEDED", void 0, requestId);
|
|
287
|
+
}
|
|
288
|
+
async get(path, query, companyApiKey) {
|
|
289
|
+
return this.request("GET", path, { query, companyApiKey });
|
|
290
|
+
}
|
|
291
|
+
async post(path, body, companyApiKey) {
|
|
292
|
+
return this.request("POST", path, { body, companyApiKey });
|
|
293
|
+
}
|
|
294
|
+
async put(path, body, companyApiKey) {
|
|
295
|
+
return this.request("PUT", path, { body, companyApiKey });
|
|
296
|
+
}
|
|
297
|
+
async delete(path, companyApiKey) {
|
|
298
|
+
return this.request("DELETE", path, { companyApiKey });
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// src/resources/quotes.ts
|
|
303
|
+
var Quotes = class {
|
|
304
|
+
constructor(client) {
|
|
305
|
+
this.client = client;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Request a shipping quote
|
|
309
|
+
* @param params Quote parameters
|
|
310
|
+
* @param companyApiKey Optional: Specify company API key for multi-tenant integrations
|
|
311
|
+
*/
|
|
312
|
+
async create(params, companyApiKey) {
|
|
313
|
+
return this.client.post("/v1/shipper/quote", params, companyApiKey);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Retrieve a quote by ID
|
|
317
|
+
* @param quoteId Quote ID
|
|
318
|
+
* @param companyApiKey Optional: Specify company API key for multi-tenant integrations
|
|
319
|
+
*/
|
|
320
|
+
async retrieve(quoteId, companyApiKey) {
|
|
321
|
+
return this.client.get(`/v1/shipper/quote/${quoteId}`, void 0, companyApiKey);
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// src/resources/shipments.ts
|
|
326
|
+
var Shipments = class {
|
|
327
|
+
constructor(client) {
|
|
328
|
+
this.client = client;
|
|
329
|
+
}
|
|
330
|
+
async create(params, companyApiKey) {
|
|
331
|
+
return this.client.post("/v1/shipper/shipment", params, companyApiKey);
|
|
332
|
+
}
|
|
333
|
+
async retrieve(orderNumber, companyApiKey) {
|
|
334
|
+
return this.client.get(`/v1/shipper/shipment/${orderNumber}`, void 0, companyApiKey);
|
|
335
|
+
}
|
|
336
|
+
async confirm(orderNumber, companyApiKey) {
|
|
337
|
+
return this.client.put(`/v1/shipper/shipment/${orderNumber}/confirm`, void 0, companyApiKey);
|
|
338
|
+
}
|
|
339
|
+
async cancel(orderNumber, companyApiKey) {
|
|
340
|
+
return this.client.put(`/v1/shipper/shipment/${orderNumber}/cancel`, void 0, companyApiKey);
|
|
341
|
+
}
|
|
342
|
+
async tracking(orderNumber, companyApiKey) {
|
|
343
|
+
return this.client.get(`/v1/shipper/shipment/${orderNumber}/tracking`, void 0, companyApiKey);
|
|
344
|
+
}
|
|
345
|
+
async document(orderNumber, documentType, companyApiKey) {
|
|
346
|
+
return this.client.get(`/v1/shipper/shipment/${orderNumber}/document/${documentType}`, void 0, companyApiKey);
|
|
347
|
+
}
|
|
348
|
+
async invoice(orderNumber, companyApiKey) {
|
|
349
|
+
return this.client.get(`/v1/shipper/shipment/${orderNumber}/invoice`, void 0, companyApiKey);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
// src/environments.ts
|
|
354
|
+
var OwayEnvironments = {
|
|
355
|
+
/**
|
|
356
|
+
* Sandbox environment for development and testing
|
|
357
|
+
* Safe to use - no real shipments created
|
|
358
|
+
*/
|
|
359
|
+
SANDBOX: "https://rest-api.sandbox.oway.io",
|
|
360
|
+
/**
|
|
361
|
+
* Production environment for live traffic
|
|
362
|
+
* Real shipments will be created and billed
|
|
363
|
+
*/
|
|
364
|
+
PRODUCTION: "https://rest-api.oway.io"
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
// src/index.ts
|
|
368
|
+
var Oway = class {
|
|
369
|
+
constructor(config) {
|
|
370
|
+
this.client = new HttpClient(config);
|
|
371
|
+
this.quotes = new Quotes(this.client);
|
|
372
|
+
this.shipments = new Shipments(this.client);
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
var index_default = Oway;
|
|
376
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
377
|
+
0 && (module.exports = {
|
|
378
|
+
Oway,
|
|
379
|
+
OwayEnvironments,
|
|
380
|
+
OwayError
|
|
381
|
+
});
|