@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/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
+ });