@shopickup/adapters-mpl 0.0.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 +43 -0
- package/dist/capabilities/auth.d.ts +39 -0
- package/dist/capabilities/auth.d.ts.map +1 -0
- package/dist/capabilities/auth.js +130 -0
- package/dist/capabilities/close.d.ts +8 -0
- package/dist/capabilities/close.d.ts.map +1 -0
- package/dist/capabilities/close.js +70 -0
- package/dist/capabilities/get-shipment-details.d.ts +63 -0
- package/dist/capabilities/get-shipment-details.d.ts.map +1 -0
- package/dist/capabilities/get-shipment-details.js +97 -0
- package/dist/capabilities/index.d.ts +10 -0
- package/dist/capabilities/index.d.ts.map +1 -0
- package/dist/capabilities/index.js +9 -0
- package/dist/capabilities/label.d.ts +33 -0
- package/dist/capabilities/label.d.ts.map +1 -0
- package/dist/capabilities/label.js +328 -0
- package/dist/capabilities/parcels.d.ts +33 -0
- package/dist/capabilities/parcels.d.ts.map +1 -0
- package/dist/capabilities/parcels.js +284 -0
- package/dist/capabilities/pickup-points.d.ts +41 -0
- package/dist/capabilities/pickup-points.d.ts.map +1 -0
- package/dist/capabilities/pickup-points.js +294 -0
- package/dist/capabilities/track.d.ts +72 -0
- package/dist/capabilities/track.d.ts.map +1 -0
- package/dist/capabilities/track.js +331 -0
- package/dist/index.d.ts +83 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +142 -0
- package/dist/mappers/label.d.ts +67 -0
- package/dist/mappers/label.d.ts.map +1 -0
- package/dist/mappers/label.js +83 -0
- package/dist/mappers/shipment.d.ts +110 -0
- package/dist/mappers/shipment.d.ts.map +1 -0
- package/dist/mappers/shipment.js +258 -0
- package/dist/mappers/tracking.d.ts +60 -0
- package/dist/mappers/tracking.d.ts.map +1 -0
- package/dist/mappers/tracking.js +187 -0
- package/dist/utils/httpUtils.d.ts +36 -0
- package/dist/utils/httpUtils.d.ts.map +1 -0
- package/dist/utils/httpUtils.js +76 -0
- package/dist/utils/oauthFallback.d.ts +47 -0
- package/dist/utils/oauthFallback.d.ts.map +1 -0
- package/dist/utils/oauthFallback.js +250 -0
- package/dist/utils/resolveBaseUrl.d.ts +75 -0
- package/dist/utils/resolveBaseUrl.d.ts.map +1 -0
- package/dist/utils/resolveBaseUrl.js +65 -0
- package/dist/validation.d.ts +1890 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +726 -0
- package/package.json +69 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MPL Tracking Mapper
|
|
3
|
+
* Converts MPL C-code tracking records to canonical TrackingUpdate format
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Map MPL tracking status codes (C9) to canonical TrackingStatus
|
|
7
|
+
*
|
|
8
|
+
* C9 contains the tracking status code in Hungarian.
|
|
9
|
+
* Common values (from carrier documentation):
|
|
10
|
+
* - BEÉRKEZETT / RECEIVED / etc.
|
|
11
|
+
*/
|
|
12
|
+
function mapStatusCode(c9) {
|
|
13
|
+
if (!c9)
|
|
14
|
+
return 'PENDING';
|
|
15
|
+
const statusMap = {
|
|
16
|
+
// Hungarian versions
|
|
17
|
+
'BEÉRKEZETT': 'PENDING',
|
|
18
|
+
'FELDOLGOZÁS': 'PENDING',
|
|
19
|
+
'SZÁLLÍTÁS': 'IN_TRANSIT',
|
|
20
|
+
'KÉZBESÍTÉS_ALATT': 'OUT_FOR_DELIVERY',
|
|
21
|
+
'KÉZBESÍTVE': 'DELIVERED',
|
|
22
|
+
'VISSZAKÜLDVE': 'RETURNED',
|
|
23
|
+
'HIBA': 'EXCEPTION',
|
|
24
|
+
'FELDOLGOZÁS ALATT': 'PENDING',
|
|
25
|
+
'CSOMAG FELDOLGOZÁSA ALATT': 'PENDING',
|
|
26
|
+
// English versions
|
|
27
|
+
'RECEIVED': 'PENDING',
|
|
28
|
+
'PROCESSING': 'PENDING',
|
|
29
|
+
'IN_TRANSIT': 'IN_TRANSIT',
|
|
30
|
+
'IN_DELIVERY': 'OUT_FOR_DELIVERY',
|
|
31
|
+
'OUT_FOR_DELIVERY': 'OUT_FOR_DELIVERY',
|
|
32
|
+
'DELIVERED': 'DELIVERED',
|
|
33
|
+
'RETURNED': 'RETURNED',
|
|
34
|
+
'ERROR': 'EXCEPTION',
|
|
35
|
+
'EXCEPTION': 'EXCEPTION',
|
|
36
|
+
'PENDING': 'PENDING',
|
|
37
|
+
// German versions
|
|
38
|
+
'EMPFANGEN': 'PENDING',
|
|
39
|
+
'VERARBEITUNG': 'PENDING',
|
|
40
|
+
'TRANSPORT': 'IN_TRANSIT',
|
|
41
|
+
'AUSLIEFERUNG': 'OUT_FOR_DELIVERY',
|
|
42
|
+
'GELIEFERT': 'DELIVERED',
|
|
43
|
+
'ZURÜCKGEGEBEN': 'RETURNED',
|
|
44
|
+
'FEHLER': 'EXCEPTION',
|
|
45
|
+
};
|
|
46
|
+
return statusMap[c9.toUpperCase().trim()] || 'PENDING';
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Parse date/time from MPL format
|
|
50
|
+
* Expects format like: "2025-01-27 14:30:00" or similar
|
|
51
|
+
*/
|
|
52
|
+
function parseTimestamp(timestamp) {
|
|
53
|
+
if (!timestamp)
|
|
54
|
+
return null;
|
|
55
|
+
try {
|
|
56
|
+
const parsed = new Date(timestamp);
|
|
57
|
+
if (!isNaN(parsed.getTime())) {
|
|
58
|
+
return parsed;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Parse weight from string (C5)
|
|
68
|
+
* Expects format like "1.5 kg" or "1500" (grams)
|
|
69
|
+
*/
|
|
70
|
+
function parseWeight(weightStr) {
|
|
71
|
+
if (!weightStr)
|
|
72
|
+
return undefined;
|
|
73
|
+
try {
|
|
74
|
+
// Remove units (kg, g, etc.)
|
|
75
|
+
const numStr = weightStr.replace(/[^\d.,]/g, '').replace(',', '.');
|
|
76
|
+
const weight = parseFloat(numStr);
|
|
77
|
+
return isNaN(weight) ? undefined : weight;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Build event history from a single C-code record
|
|
85
|
+
* For 'last' state, returns single event from latest status
|
|
86
|
+
* For 'all' state, caller is responsible for handling multiple records
|
|
87
|
+
*/
|
|
88
|
+
function buildTrackingEvent(record) {
|
|
89
|
+
const timestamp = parseTimestamp(record.c10) || new Date();
|
|
90
|
+
const status = mapStatusCode(record.c9);
|
|
91
|
+
// Build location object from C-codes
|
|
92
|
+
const location = (record.c8 || record.c11) ? {
|
|
93
|
+
city: record.c11 || record.c8,
|
|
94
|
+
} : undefined;
|
|
95
|
+
return {
|
|
96
|
+
timestamp,
|
|
97
|
+
status,
|
|
98
|
+
location,
|
|
99
|
+
description: record.c12 || record.c6 || 'No description',
|
|
100
|
+
carrierStatusCode: record.c9,
|
|
101
|
+
raw: record,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Convert MPL C-code tracking record to canonical TrackingUpdate
|
|
106
|
+
*
|
|
107
|
+
* @param record - MPL tracking record with C-codes
|
|
108
|
+
* @param includeFinancialData - If true, include weight/size/value (Registered endpoint)
|
|
109
|
+
* @returns Canonical TrackingUpdate
|
|
110
|
+
*/
|
|
111
|
+
export function mapMPLTrackingToCanonical(record, includeFinancialData = false) {
|
|
112
|
+
// Validate critical field
|
|
113
|
+
if (!record.c1) {
|
|
114
|
+
throw new Error('Invalid tracking record: missing c1 (Consignment ID)');
|
|
115
|
+
}
|
|
116
|
+
// Build base tracking update
|
|
117
|
+
const trackingUpdate = {
|
|
118
|
+
trackingNumber: record.c1,
|
|
119
|
+
status: mapStatusCode(record.c9),
|
|
120
|
+
lastUpdate: parseTimestamp(record.c10) || null,
|
|
121
|
+
events: [buildTrackingEvent(record)],
|
|
122
|
+
rawCarrierResponse: {
|
|
123
|
+
record,
|
|
124
|
+
// Include financial data in response if available
|
|
125
|
+
...(includeFinancialData && {
|
|
126
|
+
weight: record.c5,
|
|
127
|
+
dimensions: {
|
|
128
|
+
length: record.c41,
|
|
129
|
+
width: record.c42,
|
|
130
|
+
height: record.c43,
|
|
131
|
+
},
|
|
132
|
+
value: record.c58,
|
|
133
|
+
}),
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
return trackingUpdate;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Build multiple events from complete tracking history
|
|
140
|
+
* Used when state='all' is requested
|
|
141
|
+
*
|
|
142
|
+
* Note: MPL API appears to return multiple records in trackAndTrace array
|
|
143
|
+
* when state='all'. Each record represents an event in the history.
|
|
144
|
+
*/
|
|
145
|
+
export function mapMPLTrackingHistoryToCanonical(records, includeFinancialData = false) {
|
|
146
|
+
if (records.length === 0) {
|
|
147
|
+
throw new Error('No tracking records provided');
|
|
148
|
+
}
|
|
149
|
+
// Get base info from first/main record (usually latest)
|
|
150
|
+
const mainRecord = records[0];
|
|
151
|
+
const trackingNumber = mainRecord.c1;
|
|
152
|
+
if (!trackingNumber) {
|
|
153
|
+
throw new Error('Invalid tracking records: missing c1 (Consignment ID)');
|
|
154
|
+
}
|
|
155
|
+
// Build event history from all records
|
|
156
|
+
const events = records.map(record => buildTrackingEvent(record));
|
|
157
|
+
// Get latest status from first record (most recent)
|
|
158
|
+
const latestStatus = mapStatusCode(mainRecord.c9);
|
|
159
|
+
const lastUpdate = parseTimestamp(mainRecord.c10) || null;
|
|
160
|
+
const trackingUpdate = {
|
|
161
|
+
trackingNumber,
|
|
162
|
+
status: latestStatus,
|
|
163
|
+
lastUpdate,
|
|
164
|
+
events,
|
|
165
|
+
rawCarrierResponse: {
|
|
166
|
+
records,
|
|
167
|
+
// Include financial data in response if available
|
|
168
|
+
...(includeFinancialData && {
|
|
169
|
+
weight: mainRecord.c5,
|
|
170
|
+
dimensions: {
|
|
171
|
+
length: mainRecord.c41,
|
|
172
|
+
width: mainRecord.c42,
|
|
173
|
+
height: mainRecord.c43,
|
|
174
|
+
},
|
|
175
|
+
value: mainRecord.c58,
|
|
176
|
+
}),
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
return trackingUpdate;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Determine if record has financial data (Registered vs Guest)
|
|
183
|
+
* Registered includes C5 (weight), Guest excludes it
|
|
184
|
+
*/
|
|
185
|
+
export function isRegisteredRecord(record) {
|
|
186
|
+
return !!(record.c5 || record.c41 || record.c42 || record.c58);
|
|
187
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MPL HTTP Client Utilities
|
|
3
|
+
* Thin utilities for building MPL API requests
|
|
4
|
+
*/
|
|
5
|
+
import type { MPLCredentials } from '../validation.js';
|
|
6
|
+
/**
|
|
7
|
+
* Build standard MPL auth headers
|
|
8
|
+
* Supports both OAuth2 Bearer token and API Key authentication
|
|
9
|
+
* Uses "Authorization" header with appropriate scheme
|
|
10
|
+
*
|
|
11
|
+
* @param credentials MPL credentials (OAuth2 or API Key)
|
|
12
|
+
* @param accountingCode Customer code provided by Magyar Posta Zrt.
|
|
13
|
+
* @param requestId Optional GUID for request tracking; auto-generated if omitted
|
|
14
|
+
* @returns Headers object with Authorization, Content-Type, X-Accounting-Code, and X-Request-ID
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildMPLHeaders(credentials: MPLCredentials, accountingCode: string, requestId?: string): Record<string, string>;
|
|
17
|
+
/**
|
|
18
|
+
* Check if an error response indicates Basic authentication is disabled
|
|
19
|
+
*
|
|
20
|
+
* When Basic auth is disabled at the MPL account level, the API returns a 401 with:
|
|
21
|
+
* {
|
|
22
|
+
* "fault": {
|
|
23
|
+
* "faultstring": "Basic authentication is not enabled for this proxy or client.",
|
|
24
|
+
* "detail": {
|
|
25
|
+
* "errorcode": "RaiseFault.BasicAuthNotEnabled"
|
|
26
|
+
* }
|
|
27
|
+
* }
|
|
28
|
+
* }
|
|
29
|
+
*
|
|
30
|
+
* This helper detects this specific error to trigger OAuth token exchange fallback.
|
|
31
|
+
*
|
|
32
|
+
* @param body Response body (likely parsed JSON)
|
|
33
|
+
* @returns true if the error is "Basic auth not enabled"
|
|
34
|
+
*/
|
|
35
|
+
export declare function isBasicAuthDisabledError(body: unknown): boolean;
|
|
36
|
+
//# sourceMappingURL=httpUtils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"httpUtils.d.ts","sourceRoot":"","sources":["../../src/utils/httpUtils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,cAAc,EAA8B,MAAM,kBAAkB,CAAC;AAEnF;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC3B,WAAW,EAAE,cAAc,EAC3B,cAAc,EAAE,MAAM,EACtB,SAAS,CAAC,EAAE,MAAM,GACnB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA8BxB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAkB/D"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MPL HTTP Client Utilities
|
|
3
|
+
* Thin utilities for building MPL API requests
|
|
4
|
+
*/
|
|
5
|
+
import { randomUUID } from 'crypto';
|
|
6
|
+
/**
|
|
7
|
+
* Build standard MPL auth headers
|
|
8
|
+
* Supports both OAuth2 Bearer token and API Key authentication
|
|
9
|
+
* Uses "Authorization" header with appropriate scheme
|
|
10
|
+
*
|
|
11
|
+
* @param credentials MPL credentials (OAuth2 or API Key)
|
|
12
|
+
* @param accountingCode Customer code provided by Magyar Posta Zrt.
|
|
13
|
+
* @param requestId Optional GUID for request tracking; auto-generated if omitted
|
|
14
|
+
* @returns Headers object with Authorization, Content-Type, X-Accounting-Code, and X-Request-ID
|
|
15
|
+
*/
|
|
16
|
+
export function buildMPLHeaders(credentials, accountingCode, requestId) {
|
|
17
|
+
const authType = credentials.authType;
|
|
18
|
+
const guid = requestId || randomUUID();
|
|
19
|
+
const headers = {
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
};
|
|
22
|
+
// Add X-Request-ID (required by MPL API)
|
|
23
|
+
headers["X-Request-ID"] = guid;
|
|
24
|
+
// Add X-Accounting-Code if provided
|
|
25
|
+
if (accountingCode) {
|
|
26
|
+
headers["X-Accounting-Code"] = accountingCode;
|
|
27
|
+
}
|
|
28
|
+
// Add Authorization header based on auth type
|
|
29
|
+
if (authType === 'oauth2') {
|
|
30
|
+
const { oAuth2Token } = credentials;
|
|
31
|
+
headers["Authorization"] = `Bearer ${oAuth2Token}`;
|
|
32
|
+
}
|
|
33
|
+
else if (authType === 'apiKey') {
|
|
34
|
+
const { apiKey, apiSecret } = credentials;
|
|
35
|
+
const basicAuth = Buffer.from(`${apiKey}:${apiSecret}`).toString("base64");
|
|
36
|
+
headers["Authorization"] = `Basic ${basicAuth}`;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
// This should never happen due to type safety, but added for completeness
|
|
40
|
+
throw new Error(`Unsupported MPL auth type: ${authType}`);
|
|
41
|
+
}
|
|
42
|
+
return headers;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Check if an error response indicates Basic authentication is disabled
|
|
46
|
+
*
|
|
47
|
+
* When Basic auth is disabled at the MPL account level, the API returns a 401 with:
|
|
48
|
+
* {
|
|
49
|
+
* "fault": {
|
|
50
|
+
* "faultstring": "Basic authentication is not enabled for this proxy or client.",
|
|
51
|
+
* "detail": {
|
|
52
|
+
* "errorcode": "RaiseFault.BasicAuthNotEnabled"
|
|
53
|
+
* }
|
|
54
|
+
* }
|
|
55
|
+
* }
|
|
56
|
+
*
|
|
57
|
+
* This helper detects this specific error to trigger OAuth token exchange fallback.
|
|
58
|
+
*
|
|
59
|
+
* @param body Response body (likely parsed JSON)
|
|
60
|
+
* @returns true if the error is "Basic auth not enabled"
|
|
61
|
+
*/
|
|
62
|
+
export function isBasicAuthDisabledError(body) {
|
|
63
|
+
if (!body || typeof body !== 'object')
|
|
64
|
+
return false;
|
|
65
|
+
const gatewayError = body;
|
|
66
|
+
const fault = gatewayError.fault;
|
|
67
|
+
if (!fault || typeof fault !== 'object')
|
|
68
|
+
return false;
|
|
69
|
+
const faultString = fault.faultstring || '';
|
|
70
|
+
const errorCode = fault.detail?.errorcode || '';
|
|
71
|
+
// Check for the specific error message
|
|
72
|
+
const hasBasicAuthMessage = typeof faultString === 'string' &&
|
|
73
|
+
faultString.includes('Basic authentication is not enabled');
|
|
74
|
+
const hasBasicAuthCode = errorCode === 'RaiseFault.BasicAuthNotEnabled';
|
|
75
|
+
return hasBasicAuthMessage || hasBasicAuthCode;
|
|
76
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Fallback HTTP Client Wrapper
|
|
3
|
+
*
|
|
4
|
+
* Wraps an HTTP client to automatically handle the case where Basic auth is disabled
|
|
5
|
+
* at the MPL account level. When a 401 is received with the "Basic auth not enabled" error,
|
|
6
|
+
* this wrapper:
|
|
7
|
+
*
|
|
8
|
+
* 1. Exchanges API credentials for an OAuth2 Bearer token
|
|
9
|
+
* 2. Retries the original request with the new Bearer token
|
|
10
|
+
* 3. Returns the retried response
|
|
11
|
+
*
|
|
12
|
+
* This allows integrators to use Basic auth normally, but automatically falls back
|
|
13
|
+
* to OAuth when Basic auth is not available, without modifying adapter code.
|
|
14
|
+
*
|
|
15
|
+
* Pattern:
|
|
16
|
+
* ```typescript
|
|
17
|
+
* const baseHttpClient = createAxiosHttpClient();
|
|
18
|
+
* const wrappedHttpClient = withOAuthFallback(
|
|
19
|
+
* baseHttpClient,
|
|
20
|
+
* credentials,
|
|
21
|
+
* accountingCode,
|
|
22
|
+
* resolveOAuthUrl,
|
|
23
|
+
* logger
|
|
24
|
+
* );
|
|
25
|
+
* // Now calls to wrappedHttpClient.post(...) handle OAuth fallback automatically
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
import type { HttpClient } from "@shopickup/core";
|
|
29
|
+
import type { Logger } from "@shopickup/core";
|
|
30
|
+
import type { MPLCredentials } from "../validation.js";
|
|
31
|
+
import type { ResolveOAuthUrl } from "./resolveBaseUrl.js";
|
|
32
|
+
/**
|
|
33
|
+
* Create an HTTP client wrapper that automatically exchanges credentials
|
|
34
|
+
* to OAuth2 Bearer token when Basic auth is disabled
|
|
35
|
+
*
|
|
36
|
+
* @param baseHttpClient The underlying HTTP client to wrap
|
|
37
|
+
* @param credentials API credentials (apiKey + apiSecret for Basic auth)
|
|
38
|
+
* @param accountingCode Customer code from Magyar Posta
|
|
39
|
+
* @param resolveOAuthUrl Function to resolve OAuth token endpoint URL (test vs. production)
|
|
40
|
+
* @param logger Optional logger for debugging
|
|
41
|
+
* @param useTestApi Optional flag to use sandbox/test API for OAuth token exchange (defaults to false/production)
|
|
42
|
+
* @returns Wrapped HttpClient with OAuth fallback
|
|
43
|
+
*/
|
|
44
|
+
export declare function withOAuthFallback(baseHttpClient: HttpClient, credentials: Extract<MPLCredentials, {
|
|
45
|
+
authType: 'apiKey';
|
|
46
|
+
}>, accountingCode: string, resolveOAuthUrl: ResolveOAuthUrl, logger?: Logger, useTestApi?: boolean): HttpClient;
|
|
47
|
+
//# sourceMappingURL=oauthFallback.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauthFallback.d.ts","sourceRoot":"","sources":["../../src/utils/oauthFallback.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAkC,MAAM,iBAAiB,CAAC;AAElF,OAAO,KAAK,EAAE,MAAM,EAAkB,MAAM,iBAAiB,CAAC;AAC9D,OAAO,KAAK,EAAE,cAAc,EAA6B,MAAM,kBAAkB,CAAC;AAGlF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAkB3D;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAC/B,cAAc,EAAE,UAAU,EAC1B,WAAW,EAAE,OAAO,CAAC,cAAc,EAAE;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,EAC5D,cAAc,EAAE,MAAM,EACtB,eAAe,EAAE,eAAe,EAChC,MAAM,CAAC,EAAE,MAAM,EACf,UAAU,GAAE,OAAe,GAC1B,UAAU,CA4OZ"}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Fallback HTTP Client Wrapper
|
|
3
|
+
*
|
|
4
|
+
* Wraps an HTTP client to automatically handle the case where Basic auth is disabled
|
|
5
|
+
* at the MPL account level. When a 401 is received with the "Basic auth not enabled" error,
|
|
6
|
+
* this wrapper:
|
|
7
|
+
*
|
|
8
|
+
* 1. Exchanges API credentials for an OAuth2 Bearer token
|
|
9
|
+
* 2. Retries the original request with the new Bearer token
|
|
10
|
+
* 3. Returns the retried response
|
|
11
|
+
*
|
|
12
|
+
* This allows integrators to use Basic auth normally, but automatically falls back
|
|
13
|
+
* to OAuth when Basic auth is not available, without modifying adapter code.
|
|
14
|
+
*
|
|
15
|
+
* Pattern:
|
|
16
|
+
* ```typescript
|
|
17
|
+
* const baseHttpClient = createAxiosHttpClient();
|
|
18
|
+
* const wrappedHttpClient = withOAuthFallback(
|
|
19
|
+
* baseHttpClient,
|
|
20
|
+
* credentials,
|
|
21
|
+
* accountingCode,
|
|
22
|
+
* resolveOAuthUrl,
|
|
23
|
+
* logger
|
|
24
|
+
* );
|
|
25
|
+
* // Now calls to wrappedHttpClient.post(...) handle OAuth fallback automatically
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
import { CarrierError } from "@shopickup/core";
|
|
29
|
+
import { exchangeAuthToken } from "../capabilities/auth.js";
|
|
30
|
+
import { isBasicAuthDisabledError, buildMPLHeaders } from "./httpUtils.js";
|
|
31
|
+
/**
|
|
32
|
+
* Check if an error is an HttpError with 401 "Basic auth disabled" status
|
|
33
|
+
*/
|
|
34
|
+
function isBasicAuthDisabledHttpError(err) {
|
|
35
|
+
if (!err || typeof err !== 'object')
|
|
36
|
+
return false;
|
|
37
|
+
const errObj = err;
|
|
38
|
+
// Check if it's an HTTP error with 401 status
|
|
39
|
+
// HttpError has structure: { status?: number, response?: { data: unknown } }
|
|
40
|
+
if (errObj.status === 401 && errObj.response?.data) {
|
|
41
|
+
return isBasicAuthDisabledError(errObj.response.data);
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Create an HTTP client wrapper that automatically exchanges credentials
|
|
47
|
+
* to OAuth2 Bearer token when Basic auth is disabled
|
|
48
|
+
*
|
|
49
|
+
* @param baseHttpClient The underlying HTTP client to wrap
|
|
50
|
+
* @param credentials API credentials (apiKey + apiSecret for Basic auth)
|
|
51
|
+
* @param accountingCode Customer code from Magyar Posta
|
|
52
|
+
* @param resolveOAuthUrl Function to resolve OAuth token endpoint URL (test vs. production)
|
|
53
|
+
* @param logger Optional logger for debugging
|
|
54
|
+
* @param useTestApi Optional flag to use sandbox/test API for OAuth token exchange (defaults to false/production)
|
|
55
|
+
* @returns Wrapped HttpClient with OAuth fallback
|
|
56
|
+
*/
|
|
57
|
+
export function withOAuthFallback(baseHttpClient, credentials, accountingCode, resolveOAuthUrl, logger, useTestApi = false) {
|
|
58
|
+
let cachedOAuthToken = null;
|
|
59
|
+
/**
|
|
60
|
+
* Internal helper to handle 401 "Basic auth disabled" error
|
|
61
|
+
* Exchanges credentials for OAuth token and retries the request
|
|
62
|
+
*/
|
|
63
|
+
async function handleBasicAuthDisabled(method, url, data, config) {
|
|
64
|
+
try {
|
|
65
|
+
logger?.info('[OAuth Fallback] Basic auth disabled detected, attempting to exchange credentials', {
|
|
66
|
+
url,
|
|
67
|
+
method,
|
|
68
|
+
accountingCode: accountingCode.substring(0, 4) + '****' // mask for logging
|
|
69
|
+
});
|
|
70
|
+
// Exchange credentials for OAuth token (or use cached token)
|
|
71
|
+
let oauthToken;
|
|
72
|
+
if (cachedOAuthToken && cachedOAuthToken.issued_at) {
|
|
73
|
+
// Check if token is still valid (with 30-second buffer)
|
|
74
|
+
const expiresAt = cachedOAuthToken.issued_at + (cachedOAuthToken.expires_in * 1000) - 30000;
|
|
75
|
+
if (Date.now() < expiresAt) {
|
|
76
|
+
logger?.debug('[OAuth Fallback] Using cached OAuth token', {
|
|
77
|
+
expiresInSeconds: cachedOAuthToken.expires_in,
|
|
78
|
+
remainingMs: expiresAt - Date.now(),
|
|
79
|
+
});
|
|
80
|
+
oauthToken = cachedOAuthToken.access_token;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
logger?.info('[OAuth Fallback] Cached OAuth token expired, exchanging for new one', {
|
|
84
|
+
expiresInSeconds: cachedOAuthToken.expires_in,
|
|
85
|
+
});
|
|
86
|
+
// Token expired, exchange again
|
|
87
|
+
const exchanged = await exchangeAuthToken({ credentials, options: { useTestApi } }, { http: baseHttpClient, logger }, resolveOAuthUrl, accountingCode);
|
|
88
|
+
logger?.info('[OAuth Fallback] Successfully exchanged credentials for new OAuth token', {
|
|
89
|
+
expiresInSeconds: exchanged.expires_in,
|
|
90
|
+
});
|
|
91
|
+
cachedOAuthToken = exchanged;
|
|
92
|
+
oauthToken = exchanged.access_token;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
// No cached token, exchange now
|
|
97
|
+
logger?.info('[OAuth Fallback] No cached token, exchanging API credentials for OAuth token');
|
|
98
|
+
const exchanged = await exchangeAuthToken({ credentials, options: { useTestApi } }, { http: baseHttpClient, logger }, resolveOAuthUrl, accountingCode);
|
|
99
|
+
logger?.info('[OAuth Fallback] Successfully exchanged credentials for OAuth token', {
|
|
100
|
+
expiresInSeconds: exchanged.expires_in,
|
|
101
|
+
});
|
|
102
|
+
cachedOAuthToken = exchanged;
|
|
103
|
+
oauthToken = exchanged.access_token;
|
|
104
|
+
}
|
|
105
|
+
// Build new headers with OAuth token using object literal with 'any' type
|
|
106
|
+
// to work around TypeScript's discriminated union inference issue
|
|
107
|
+
const oauthCredentialsObj = {
|
|
108
|
+
authType: 'oauth2',
|
|
109
|
+
oAuth2Token: oauthToken,
|
|
110
|
+
};
|
|
111
|
+
const newHeaders = {
|
|
112
|
+
...(config?.headers || {}),
|
|
113
|
+
...buildMPLHeaders(oauthCredentialsObj, accountingCode),
|
|
114
|
+
};
|
|
115
|
+
const newConfig = {
|
|
116
|
+
...config,
|
|
117
|
+
headers: newHeaders,
|
|
118
|
+
};
|
|
119
|
+
logger?.info('[OAuth Fallback] Retrying original request with OAuth Bearer token', {
|
|
120
|
+
url,
|
|
121
|
+
method
|
|
122
|
+
});
|
|
123
|
+
// Retry the original request with new OAuth credentials
|
|
124
|
+
const retryResponse = await baseHttpClient[method](url, data, newConfig);
|
|
125
|
+
logger?.info('[OAuth Fallback] Request succeeded after OAuth fallback', {
|
|
126
|
+
url,
|
|
127
|
+
method,
|
|
128
|
+
status: retryResponse.status
|
|
129
|
+
});
|
|
130
|
+
return retryResponse;
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
// OAuth exchange failed or retry failed
|
|
134
|
+
// Fail-fast: don't retry again
|
|
135
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
136
|
+
logger?.error('[OAuth Fallback] OAuth fallback failed', {
|
|
137
|
+
url,
|
|
138
|
+
method,
|
|
139
|
+
error: errorMessage,
|
|
140
|
+
errorType: err instanceof Error ? err.constructor.name : typeof err,
|
|
141
|
+
});
|
|
142
|
+
if (err instanceof CarrierError) {
|
|
143
|
+
throw err;
|
|
144
|
+
}
|
|
145
|
+
throw new CarrierError(`OAuth fallback failed: ${errorMessage}`, 'Transient', { raw: err });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
async get(url, config) {
|
|
150
|
+
try {
|
|
151
|
+
const response = await baseHttpClient.get(url, config);
|
|
152
|
+
// Check for "Basic auth disabled" error in successful response
|
|
153
|
+
if (response.status === 401 && isBasicAuthDisabledError(response.body)) {
|
|
154
|
+
logger?.info('[OAuth Fallback] Intercepted 401 response (Basic auth disabled)', { url });
|
|
155
|
+
return handleBasicAuthDisabled('get', url, undefined, config);
|
|
156
|
+
}
|
|
157
|
+
return response;
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
// Check if error is a 401 "Basic auth disabled" error
|
|
161
|
+
if (isBasicAuthDisabledHttpError(err)) {
|
|
162
|
+
logger?.info('[OAuth Fallback] Intercepted 401 error (Basic auth disabled)', { url });
|
|
163
|
+
return handleBasicAuthDisabled('get', url, undefined, config);
|
|
164
|
+
}
|
|
165
|
+
// Re-throw other errors
|
|
166
|
+
throw err;
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
async post(url, data, config) {
|
|
170
|
+
try {
|
|
171
|
+
const response = await baseHttpClient.post(url, data, config);
|
|
172
|
+
// Check for "Basic auth disabled" error in successful response
|
|
173
|
+
if (response.status === 401 && isBasicAuthDisabledError(response.body)) {
|
|
174
|
+
logger?.info('[OAuth Fallback] Intercepted 401 response (Basic auth disabled)', { url });
|
|
175
|
+
return handleBasicAuthDisabled('post', url, data, config);
|
|
176
|
+
}
|
|
177
|
+
return response;
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
// Check if error is a 401 "Basic auth disabled" error
|
|
181
|
+
if (isBasicAuthDisabledHttpError(err)) {
|
|
182
|
+
logger?.info('[OAuth Fallback] Intercepted 401 error (Basic auth disabled)', { url });
|
|
183
|
+
return handleBasicAuthDisabled('post', url, data, config);
|
|
184
|
+
}
|
|
185
|
+
// Re-throw other errors
|
|
186
|
+
throw err;
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
async put(url, data, config) {
|
|
190
|
+
try {
|
|
191
|
+
const response = await baseHttpClient.put(url, data, config);
|
|
192
|
+
// Check for "Basic auth disabled" error in successful response
|
|
193
|
+
if (response.status === 401 && isBasicAuthDisabledError(response.body)) {
|
|
194
|
+
logger?.info('[OAuth Fallback] Intercepted 401 response (Basic auth disabled)', { url });
|
|
195
|
+
return handleBasicAuthDisabled('put', url, data, config);
|
|
196
|
+
}
|
|
197
|
+
return response;
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
// Check if error is a 401 "Basic auth disabled" error
|
|
201
|
+
if (isBasicAuthDisabledHttpError(err)) {
|
|
202
|
+
logger?.info('[OAuth Fallback] Intercepted 401 error (Basic auth disabled)', { url });
|
|
203
|
+
return handleBasicAuthDisabled('put', url, data, config);
|
|
204
|
+
}
|
|
205
|
+
// Re-throw other errors
|
|
206
|
+
throw err;
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
async patch(url, data, config) {
|
|
210
|
+
try {
|
|
211
|
+
const response = await baseHttpClient.patch(url, data, config);
|
|
212
|
+
// Check for "Basic auth disabled" error in successful response
|
|
213
|
+
if (response.status === 401 && isBasicAuthDisabledError(response.body)) {
|
|
214
|
+
logger?.info('[OAuth Fallback] Intercepted 401 response (Basic auth disabled)', { url });
|
|
215
|
+
return handleBasicAuthDisabled('patch', url, data, config);
|
|
216
|
+
}
|
|
217
|
+
return response;
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
// Check if error is a 401 "Basic auth disabled" error
|
|
221
|
+
if (isBasicAuthDisabledHttpError(err)) {
|
|
222
|
+
logger?.info('[OAuth Fallback] Intercepted 401 error (Basic auth disabled)', { url });
|
|
223
|
+
return handleBasicAuthDisabled('patch', url, data, config);
|
|
224
|
+
}
|
|
225
|
+
// Re-throw other errors
|
|
226
|
+
throw err;
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
async delete(url, config) {
|
|
230
|
+
try {
|
|
231
|
+
const response = await baseHttpClient.delete(url, config);
|
|
232
|
+
// Check for "Basic auth disabled" error in successful response
|
|
233
|
+
if (response.status === 401 && isBasicAuthDisabledError(response.body)) {
|
|
234
|
+
logger?.info('[OAuth Fallback] Intercepted 401 response (Basic auth disabled)', { url });
|
|
235
|
+
return handleBasicAuthDisabled('delete', url, undefined, config);
|
|
236
|
+
}
|
|
237
|
+
return response;
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
// Check if error is a 401 "Basic auth disabled" error
|
|
241
|
+
if (isBasicAuthDisabledHttpError(err)) {
|
|
242
|
+
logger?.info('[OAuth Fallback] Intercepted 401 error (Basic auth disabled)', { url });
|
|
243
|
+
return handleBasicAuthDisabled('delete', url, undefined, config);
|
|
244
|
+
}
|
|
245
|
+
// Re-throw other errors
|
|
246
|
+
throw err;
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Factory that creates a resolver function bound to production and test base URLs.
|
|
3
|
+
*
|
|
4
|
+
* This removes duplication of base URL strings across capability files while keeping
|
|
5
|
+
* per-call decision logic explicit and testable.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const resolveBaseUrl = createResolveBaseUrl(prodUrl, testUrl);
|
|
9
|
+
* const url = resolveBaseUrl(req.options); // returns test or prod based on useTestApi flag
|
|
10
|
+
*
|
|
11
|
+
* @param prodBaseUrl Production API base URL
|
|
12
|
+
* @param testBaseUrl Test/sandbox API base URL
|
|
13
|
+
* @returns A pure resolver function that accepts request options and returns the appropriate URL
|
|
14
|
+
*/
|
|
15
|
+
export declare function createResolveBaseUrl(prodBaseUrl: string, testBaseUrl: string): (opts?: {
|
|
16
|
+
useTestApi?: boolean;
|
|
17
|
+
}) => string;
|
|
18
|
+
/**
|
|
19
|
+
* Factory that creates a resolver function for OAuth2 token endpoints.
|
|
20
|
+
*
|
|
21
|
+
* MPL OAuth2 endpoints are separate from the main API:
|
|
22
|
+
* - Production: https://core.api.posta.hu/oauth2/token
|
|
23
|
+
* - Test/Sandbox: https://sandbox.api.posta.hu/oauth2/token
|
|
24
|
+
*
|
|
25
|
+
* Usage:
|
|
26
|
+
* const resolveOAuthUrl = createResolveOAuthUrl(
|
|
27
|
+
* 'https://core.api.posta.hu/oauth2/token',
|
|
28
|
+
* 'https://sandbox.api.posta.hu/oauth2/token'
|
|
29
|
+
* );
|
|
30
|
+
* const url = resolveOAuthUrl(req.options); // returns test or prod endpoint
|
|
31
|
+
*
|
|
32
|
+
* @param prodOAuthUrl Production OAuth2 token endpoint
|
|
33
|
+
* @param testOAuthUrl Test/sandbox OAuth2 token endpoint
|
|
34
|
+
* @returns A pure resolver function that accepts request options and returns the appropriate OAuth URL
|
|
35
|
+
*/
|
|
36
|
+
export declare function createResolveOAuthUrl(prodOAuthUrl: string, testOAuthUrl: string): (opts?: {
|
|
37
|
+
useTestApi?: boolean;
|
|
38
|
+
}) => string;
|
|
39
|
+
/**
|
|
40
|
+
* Factory that creates a resolver function for tracking endpoints.
|
|
41
|
+
*
|
|
42
|
+
* MPL tracking endpoints are separate from the main API:
|
|
43
|
+
* - Production: https://core.api.posta.hu/nyomkovetes
|
|
44
|
+
* - Test/Sandbox: https://sandbox.api.posta.hu/nyomkovetes
|
|
45
|
+
*
|
|
46
|
+
* Usage:
|
|
47
|
+
* const resolveTrackingUrl = createResolveTrackingUrl(
|
|
48
|
+
* 'https://core.api.posta.hu/nyomkovetes',
|
|
49
|
+
* 'https://sandbox.api.posta.hu/nyomkovetes'
|
|
50
|
+
* );
|
|
51
|
+
* const url = resolveTrackingUrl(req.options); // returns test or prod endpoint
|
|
52
|
+
*
|
|
53
|
+
* @param prodTrackingBaseUrl Production tracking API base URL
|
|
54
|
+
* @param testTrackingBaseUrl Test/sandbox tracking API base URL
|
|
55
|
+
* @returns A pure resolver function that accepts request options and returns the appropriate tracking URL
|
|
56
|
+
*/
|
|
57
|
+
export declare function createResolveTrackingUrl(prodTrackingBaseUrl: string, testTrackingBaseUrl: string): (opts?: {
|
|
58
|
+
useTestApi?: boolean;
|
|
59
|
+
}) => string;
|
|
60
|
+
/**
|
|
61
|
+
* Type definition for the resolver function returned by createResolveBaseUrl.
|
|
62
|
+
* Useful for typing capability function parameters.
|
|
63
|
+
*/
|
|
64
|
+
export type ResolveBaseUrl = ReturnType<typeof createResolveBaseUrl>;
|
|
65
|
+
/**
|
|
66
|
+
* Type definition for the resolver function returned by createResolveOAuthUrl.
|
|
67
|
+
* Useful for typing capability function parameters.
|
|
68
|
+
*/
|
|
69
|
+
export type ResolveOAuthUrl = ReturnType<typeof createResolveOAuthUrl>;
|
|
70
|
+
/**
|
|
71
|
+
* Type definition for the resolver function returned by createResolveTrackingUrl.
|
|
72
|
+
* Useful for typing capability function parameters.
|
|
73
|
+
*/
|
|
74
|
+
export type ResolveTrackingUrl = ReturnType<typeof createResolveTrackingUrl>;
|
|
75
|
+
//# sourceMappingURL=resolveBaseUrl.d.ts.map
|