@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.
Files changed (50) hide show
  1. package/README.md +43 -0
  2. package/dist/capabilities/auth.d.ts +39 -0
  3. package/dist/capabilities/auth.d.ts.map +1 -0
  4. package/dist/capabilities/auth.js +130 -0
  5. package/dist/capabilities/close.d.ts +8 -0
  6. package/dist/capabilities/close.d.ts.map +1 -0
  7. package/dist/capabilities/close.js +70 -0
  8. package/dist/capabilities/get-shipment-details.d.ts +63 -0
  9. package/dist/capabilities/get-shipment-details.d.ts.map +1 -0
  10. package/dist/capabilities/get-shipment-details.js +97 -0
  11. package/dist/capabilities/index.d.ts +10 -0
  12. package/dist/capabilities/index.d.ts.map +1 -0
  13. package/dist/capabilities/index.js +9 -0
  14. package/dist/capabilities/label.d.ts +33 -0
  15. package/dist/capabilities/label.d.ts.map +1 -0
  16. package/dist/capabilities/label.js +328 -0
  17. package/dist/capabilities/parcels.d.ts +33 -0
  18. package/dist/capabilities/parcels.d.ts.map +1 -0
  19. package/dist/capabilities/parcels.js +284 -0
  20. package/dist/capabilities/pickup-points.d.ts +41 -0
  21. package/dist/capabilities/pickup-points.d.ts.map +1 -0
  22. package/dist/capabilities/pickup-points.js +294 -0
  23. package/dist/capabilities/track.d.ts +72 -0
  24. package/dist/capabilities/track.d.ts.map +1 -0
  25. package/dist/capabilities/track.js +331 -0
  26. package/dist/index.d.ts +83 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +142 -0
  29. package/dist/mappers/label.d.ts +67 -0
  30. package/dist/mappers/label.d.ts.map +1 -0
  31. package/dist/mappers/label.js +83 -0
  32. package/dist/mappers/shipment.d.ts +110 -0
  33. package/dist/mappers/shipment.d.ts.map +1 -0
  34. package/dist/mappers/shipment.js +258 -0
  35. package/dist/mappers/tracking.d.ts +60 -0
  36. package/dist/mappers/tracking.d.ts.map +1 -0
  37. package/dist/mappers/tracking.js +187 -0
  38. package/dist/utils/httpUtils.d.ts +36 -0
  39. package/dist/utils/httpUtils.d.ts.map +1 -0
  40. package/dist/utils/httpUtils.js +76 -0
  41. package/dist/utils/oauthFallback.d.ts +47 -0
  42. package/dist/utils/oauthFallback.d.ts.map +1 -0
  43. package/dist/utils/oauthFallback.js +250 -0
  44. package/dist/utils/resolveBaseUrl.d.ts +75 -0
  45. package/dist/utils/resolveBaseUrl.d.ts.map +1 -0
  46. package/dist/utils/resolveBaseUrl.js +65 -0
  47. package/dist/validation.d.ts +1890 -0
  48. package/dist/validation.d.ts.map +1 -0
  49. package/dist/validation.js +726 -0
  50. 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