@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.mjs ADDED
@@ -0,0 +1,352 @@
1
+ // src/client.ts
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;
8
+ this.name = "OwayError";
9
+ }
10
+ /**
11
+ * Determines if this error represents a transient failure that should be retried
12
+ */
13
+ isRetryable() {
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;
23
+ }
24
+ };
25
+ var HttpClient = class {
26
+ constructor(config) {
27
+ this.accessToken = null;
28
+ this.tokenExpiry = 0;
29
+ this.tokenRefreshPromise = null;
30
+ if (!config.clientId || !config.clientSecret) {
31
+ throw new OwayError("clientId and clientSecret are required. Contact Oway Sales Engineering to obtain M2M credentials.");
32
+ }
33
+ this.config = {
34
+ baseUrl: config.baseUrl || process.env.OWAY_BASE_URL || "https://rest-api.sandbox.oway.io",
35
+ tokenUrl: config.tokenUrl || (config.baseUrl || process.env.OWAY_BASE_URL || "https://rest-api.sandbox.oway.io") + "/v1/auth/token",
36
+ maxRetries: config.maxRetries ?? 3,
37
+ timeout: config.timeout ?? 3e4,
38
+ debug: config.debug ?? false,
39
+ clientId: config.clientId,
40
+ clientSecret: config.clientSecret,
41
+ apiKey: config.apiKey,
42
+ // Optional default company API key
43
+ logger: config.logger
44
+ };
45
+ this.log("debug", "Oway SDK initialized", {
46
+ baseUrl: this.config.baseUrl,
47
+ authMode: "M2M",
48
+ hasDefaultApiKey: !!this.config.apiKey
49
+ });
50
+ }
51
+ /**
52
+ * Internal logging with sanitization
53
+ */
54
+ log(level, message, meta) {
55
+ 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
+ }
62
+ }
63
+ /**
64
+ * Sanitize objects for logging - remove sensitive fields
65
+ */
66
+ sanitizeForLogging(obj) {
67
+ 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);
76
+ } else {
77
+ sanitized[key] = value;
78
+ }
79
+ }
80
+ return sanitized;
81
+ }
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
+ async getAccessToken() {
87
+ if (this.tokenRefreshPromise) {
88
+ this.log("debug", "Waiting for token refresh in progress");
89
+ return this.tokenRefreshPromise;
90
+ }
91
+ if (this.accessToken && Date.now() < this.tokenExpiry - 5 * 60 * 1e3) {
92
+ return this.accessToken;
93
+ }
94
+ this.log("debug", "Refreshing access token");
95
+ this.tokenRefreshPromise = this.refreshToken();
96
+ try {
97
+ this.accessToken = await this.tokenRefreshPromise;
98
+ this.log("info", "Access token refreshed", {
99
+ expiresAt: new Date(this.tokenExpiry).toISOString()
100
+ });
101
+ return this.accessToken;
102
+ } finally {
103
+ this.tokenRefreshPromise = null;
104
+ }
105
+ }
106
+ /**
107
+ * Perform the actual token refresh using M2M credentials
108
+ */
109
+ async refreshToken() {
110
+ 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
+ })
121
+ });
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
+ this.tokenExpiry = Date.now() + data.expires_in * 1e3;
131
+ return data.access_token;
132
+ } catch (error) {
133
+ this.log("error", "Token refresh failed", {
134
+ error: error instanceof Error ? error.message : "Unknown error"
135
+ });
136
+ if (error instanceof OwayError) throw error;
137
+ throw new OwayError(
138
+ `Authentication failed: ${error instanceof Error ? error.message : "Unknown error"}`,
139
+ "AUTH_ERROR"
140
+ );
141
+ }
142
+ }
143
+ /**
144
+ * Generate a unique request ID
145
+ */
146
+ generateRequestId() {
147
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
148
+ const r = Math.random() * 16 | 0;
149
+ const v = c === "x" ? r : r & 3 | 8;
150
+ return v.toString(16);
151
+ });
152
+ }
153
+ /**
154
+ * Make an authenticated request to the Oway API
155
+ */
156
+ async request(method, path, options = {}) {
157
+ const token = await this.getAccessToken();
158
+ const url = new URL(path, this.config.baseUrl);
159
+ const requestId = options.requestId || this.generateRequestId();
160
+ if (options.query) {
161
+ Object.entries(options.query).forEach(([key, value]) => {
162
+ url.searchParams.append(key, String(value));
163
+ });
164
+ }
165
+ const apiKey = options.companyApiKey || this.config.companyApiKey || this.config.apiKey;
166
+ const headers = {
167
+ "Content-Type": "application/json",
168
+ "Authorization": `Bearer ${token}`,
169
+ "x-request-id": requestId,
170
+ ...options.headers
171
+ };
172
+ if (apiKey) {
173
+ headers["x-oway-api-key"] = apiKey;
174
+ }
175
+ this.log("debug", `${method} ${path}`, {
176
+ requestId,
177
+ hasBody: !!options.body,
178
+ query: options.query
179
+ });
180
+ let lastError = null;
181
+ for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
182
+ try {
183
+ const controller = new AbortController();
184
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
185
+ const response = await fetch(url.toString(), {
186
+ method,
187
+ headers,
188
+ body: options.body ? JSON.stringify(options.body) : void 0,
189
+ signal: controller.signal
190
+ });
191
+ clearTimeout(timeoutId);
192
+ const serverRequestId = response.headers.get("x-request-id") || requestId;
193
+ if (!response.ok) {
194
+ let errorData;
195
+ try {
196
+ errorData = await response.json();
197
+ } catch {
198
+ errorData = { message: response.statusText };
199
+ }
200
+ const error = new OwayError(
201
+ errorData.message || `Request failed with status ${response.status}`,
202
+ errorData.code || "API_ERROR",
203
+ response.status,
204
+ serverRequestId
205
+ );
206
+ this.log("warn", "Request failed", {
207
+ method,
208
+ path,
209
+ status: response.status,
210
+ requestId: serverRequestId,
211
+ attempt: attempt + 1,
212
+ isRetryable: error.isRetryable()
213
+ });
214
+ throw error;
215
+ }
216
+ this.log("debug", "Request successful", {
217
+ method,
218
+ path,
219
+ status: response.status,
220
+ requestId: serverRequestId
221
+ });
222
+ if (response.status === 204) {
223
+ return {};
224
+ }
225
+ return await response.json();
226
+ } catch (error) {
227
+ lastError = error;
228
+ if (error instanceof OwayError && !error.isRetryable()) {
229
+ this.log("error", "Non-retryable error", {
230
+ requestId,
231
+ code: error.code,
232
+ statusCode: error.statusCode
233
+ });
234
+ throw error;
235
+ }
236
+ if (attempt === this.config.maxRetries) {
237
+ this.log("error", "Max retries exceeded", {
238
+ requestId,
239
+ attempts: attempt + 1
240
+ });
241
+ break;
242
+ }
243
+ const delay = Math.pow(2, attempt) * 1e3;
244
+ this.log("warn", "Retrying request", {
245
+ requestId,
246
+ attempt: attempt + 1,
247
+ maxRetries: this.config.maxRetries,
248
+ delayMs: delay
249
+ });
250
+ await new Promise((resolve) => setTimeout(resolve, delay));
251
+ }
252
+ }
253
+ this.log("error", "Request failed", {
254
+ requestId,
255
+ error: lastError instanceof Error ? lastError.message : "Unknown error"
256
+ });
257
+ throw lastError || new OwayError("Request failed after retries", "MAX_RETRIES_EXCEEDED", void 0, requestId);
258
+ }
259
+ async get(path, query, companyApiKey) {
260
+ return this.request("GET", path, { query, companyApiKey });
261
+ }
262
+ async post(path, body, companyApiKey) {
263
+ return this.request("POST", path, { body, companyApiKey });
264
+ }
265
+ async put(path, body, companyApiKey) {
266
+ return this.request("PUT", path, { body, companyApiKey });
267
+ }
268
+ async delete(path, companyApiKey) {
269
+ return this.request("DELETE", path, { companyApiKey });
270
+ }
271
+ };
272
+
273
+ // src/resources/quotes.ts
274
+ var Quotes = class {
275
+ constructor(client) {
276
+ this.client = client;
277
+ }
278
+ /**
279
+ * Request a shipping quote
280
+ * @param params Quote parameters
281
+ * @param companyApiKey Optional: Specify company API key for multi-tenant integrations
282
+ */
283
+ async create(params, companyApiKey) {
284
+ return this.client.post("/v1/shipper/quote", params, companyApiKey);
285
+ }
286
+ /**
287
+ * Retrieve a quote by ID
288
+ * @param quoteId Quote ID
289
+ * @param companyApiKey Optional: Specify company API key for multi-tenant integrations
290
+ */
291
+ async retrieve(quoteId, companyApiKey) {
292
+ return this.client.get(`/v1/shipper/quote/${quoteId}`, void 0, companyApiKey);
293
+ }
294
+ };
295
+
296
+ // src/resources/shipments.ts
297
+ var Shipments = class {
298
+ constructor(client) {
299
+ this.client = client;
300
+ }
301
+ async create(params, companyApiKey) {
302
+ return this.client.post("/v1/shipper/shipment", params, companyApiKey);
303
+ }
304
+ async retrieve(orderNumber, companyApiKey) {
305
+ return this.client.get(`/v1/shipper/shipment/${orderNumber}`, void 0, companyApiKey);
306
+ }
307
+ async confirm(orderNumber, companyApiKey) {
308
+ return this.client.put(`/v1/shipper/shipment/${orderNumber}/confirm`, void 0, companyApiKey);
309
+ }
310
+ async cancel(orderNumber, companyApiKey) {
311
+ return this.client.put(`/v1/shipper/shipment/${orderNumber}/cancel`, void 0, companyApiKey);
312
+ }
313
+ async tracking(orderNumber, companyApiKey) {
314
+ return this.client.get(`/v1/shipper/shipment/${orderNumber}/tracking`, void 0, companyApiKey);
315
+ }
316
+ async document(orderNumber, documentType, companyApiKey) {
317
+ return this.client.get(`/v1/shipper/shipment/${orderNumber}/document/${documentType}`, void 0, companyApiKey);
318
+ }
319
+ async invoice(orderNumber, companyApiKey) {
320
+ return this.client.get(`/v1/shipper/shipment/${orderNumber}/invoice`, void 0, companyApiKey);
321
+ }
322
+ };
323
+
324
+ // src/environments.ts
325
+ var OwayEnvironments = {
326
+ /**
327
+ * Sandbox environment for development and testing
328
+ * Safe to use - no real shipments created
329
+ */
330
+ SANDBOX: "https://rest-api.sandbox.oway.io",
331
+ /**
332
+ * Production environment for live traffic
333
+ * Real shipments will be created and billed
334
+ */
335
+ PRODUCTION: "https://rest-api.oway.io"
336
+ };
337
+
338
+ // src/index.ts
339
+ var Oway = class {
340
+ constructor(config) {
341
+ this.client = new HttpClient(config);
342
+ this.quotes = new Quotes(this.client);
343
+ this.shipments = new Shipments(this.client);
344
+ }
345
+ };
346
+ var index_default = Oway;
347
+ export {
348
+ Oway,
349
+ OwayEnvironments,
350
+ OwayError,
351
+ index_default as default
352
+ };
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@oway/sdk",
3
+ "version": "0.1.0",
4
+ "description": "Official Oway JavaScript/TypeScript SDK",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "generate": "openapi-typescript ../../openapi/spec.json -o src/generated/schema.ts",
21
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
22
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
23
+ "test": "vitest run",
24
+ "test:watch": "vitest",
25
+ "lint": "eslint src --ext .ts",
26
+ "prepublishOnly": "npm run build"
27
+ },
28
+ "keywords": [
29
+ "oway",
30
+ "sdk",
31
+ "typescript",
32
+ "javascript",
33
+ "shipping",
34
+ "logistics",
35
+ "freight"
36
+ ],
37
+ "author": "Oway",
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/Oway-Inc/oway-sdk.git",
42
+ "directory": "packages/typescript"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^20.11.0",
49
+ "@vitest/coverage-v8": "^1.6.1",
50
+ "openapi-typescript": "^7.0.0",
51
+ "tsup": "^8.0.1",
52
+ "typescript": "^5.3.3",
53
+ "vitest": "^1.2.0"
54
+ },
55
+ "engines": {
56
+ "node": ">=18.0.0"
57
+ }
58
+ }