@ripwords/myinvois-client 0.2.39 → 0.2.40
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/{apiQueue-DoIYEzN4.js → apiQueue-CrR6pgYn.js} +5 -11
- package/dist/{apiQueue-kVoJdrS-.cjs → apiQueue-Dj0xtcGe.cjs} +6 -12
- package/dist/{apiQueue-kVoJdrS-.cjs.map → apiQueue-Dj0xtcGe.cjs.map} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/index23.cjs +1 -1
- package/dist/utils/apiQueue.js +1 -1
- package/package.json +1 -1
|
@@ -163,23 +163,17 @@ function categorizeRequest(path, method = "GET") {
|
|
|
163
163
|
const cleanPath = path.toLowerCase();
|
|
164
164
|
const isPost = method?.toUpperCase() === "POST";
|
|
165
165
|
if (cleanPath.includes("/documentsubmissions")) return isPost ? "submitDocuments" : "getSubmission";
|
|
166
|
-
if (cleanPath.includes("/
|
|
167
|
-
if (cleanPath.endsWith("/cancel")) return "cancelDocument";
|
|
168
|
-
if (cleanPath.endsWith("/reject")) return "rejectDocument";
|
|
169
|
-
if (cleanPath.endsWith("/details")) return "getDocumentDetails";
|
|
170
|
-
if (cleanPath.includes("/recent")) return "getRecentDocuments";
|
|
171
|
-
return method === "GET" ? "getDocument" : "searchDocuments";
|
|
172
|
-
}
|
|
173
|
-
if (cleanPath.includes("/searchtin")) return "searchTin";
|
|
174
|
-
if (cleanPath.includes("/qrcode")) return "taxpayerQr";
|
|
175
|
-
if (cleanPath.includes("/connect/token")) return "loginTaxpayer";
|
|
166
|
+
if (cleanPath.includes("/documents/recent")) return "getRecentDocuments";
|
|
176
167
|
if (cleanPath.includes("/documents/search")) return "searchDocuments";
|
|
168
|
+
if (cleanPath.includes("/documents/state/") && cleanPath.endsWith("/state")) return "cancelDocument";
|
|
177
169
|
if (/\/documents\/[^/]+\/raw$/.test(cleanPath)) return "getDocument";
|
|
178
170
|
if (/\/documents\/[^/]+\/details$/.test(cleanPath)) return "getDocumentDetails";
|
|
179
|
-
if (cleanPath.includes("/documents/state/")) return isPost ? "cancelDocument" : "getDocument";
|
|
180
171
|
if (cleanPath.includes("/taxpayer/search/tin")) return "searchTin";
|
|
181
172
|
if (cleanPath.includes("/taxpayer/validate/")) return "searchTin";
|
|
182
173
|
if (cleanPath.includes("/taxpayer/qrcode")) return "taxpayerQr";
|
|
174
|
+
if (cleanPath.includes("/searchtin")) return "searchTin";
|
|
175
|
+
if (cleanPath.includes("/qrcode")) return "taxpayerQr";
|
|
176
|
+
if (cleanPath.includes("/connect/token")) return "loginTaxpayer";
|
|
183
177
|
return "default";
|
|
184
178
|
}
|
|
185
179
|
|
|
@@ -164,23 +164,17 @@ function categorizeRequest(path, method = "GET") {
|
|
|
164
164
|
const cleanPath = path.toLowerCase();
|
|
165
165
|
const isPost = method?.toUpperCase() === "POST";
|
|
166
166
|
if (cleanPath.includes("/documentsubmissions")) return isPost ? "submitDocuments" : "getSubmission";
|
|
167
|
-
if (cleanPath.includes("/
|
|
168
|
-
if (cleanPath.endsWith("/cancel")) return "cancelDocument";
|
|
169
|
-
if (cleanPath.endsWith("/reject")) return "rejectDocument";
|
|
170
|
-
if (cleanPath.endsWith("/details")) return "getDocumentDetails";
|
|
171
|
-
if (cleanPath.includes("/recent")) return "getRecentDocuments";
|
|
172
|
-
return method === "GET" ? "getDocument" : "searchDocuments";
|
|
173
|
-
}
|
|
174
|
-
if (cleanPath.includes("/searchtin")) return "searchTin";
|
|
175
|
-
if (cleanPath.includes("/qrcode")) return "taxpayerQr";
|
|
176
|
-
if (cleanPath.includes("/connect/token")) return "loginTaxpayer";
|
|
167
|
+
if (cleanPath.includes("/documents/recent")) return "getRecentDocuments";
|
|
177
168
|
if (cleanPath.includes("/documents/search")) return "searchDocuments";
|
|
169
|
+
if (cleanPath.includes("/documents/state/") && cleanPath.endsWith("/state")) return "cancelDocument";
|
|
178
170
|
if (/\/documents\/[^/]+\/raw$/.test(cleanPath)) return "getDocument";
|
|
179
171
|
if (/\/documents\/[^/]+\/details$/.test(cleanPath)) return "getDocumentDetails";
|
|
180
|
-
if (cleanPath.includes("/documents/state/")) return isPost ? "cancelDocument" : "getDocument";
|
|
181
172
|
if (cleanPath.includes("/taxpayer/search/tin")) return "searchTin";
|
|
182
173
|
if (cleanPath.includes("/taxpayer/validate/")) return "searchTin";
|
|
183
174
|
if (cleanPath.includes("/taxpayer/qrcode")) return "taxpayerQr";
|
|
175
|
+
if (cleanPath.includes("/searchtin")) return "searchTin";
|
|
176
|
+
if (cleanPath.includes("/qrcode")) return "taxpayerQr";
|
|
177
|
+
if (cleanPath.includes("/connect/token")) return "loginTaxpayer";
|
|
184
178
|
return "default";
|
|
185
179
|
}
|
|
186
180
|
|
|
@@ -197,4 +191,4 @@ Object.defineProperty(exports, 'queueRequest', {
|
|
|
197
191
|
return queueRequest;
|
|
198
192
|
}
|
|
199
193
|
});
|
|
200
|
-
//# sourceMappingURL=apiQueue-
|
|
194
|
+
//# sourceMappingURL=apiQueue-Dj0xtcGe.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apiQueue-kVoJdrS-.cjs","names":["RATE_LIMITS: Record<ApiCategory, RateLimitConfig>","config: RateLimitConfig","delay: number","fn: () => Promise<T>","debug: boolean","category?: ApiCategory","category: ApiCategory","path: string","method: string"],"sources":["../src/utils/apiQueue.ts"],"sourcesContent":["// A very small utility that provides per-endpoint request queuing with fixed-window rate-limits.\n// The goal is to make sure that we never exceed the vendor-defined limits while also ensuring\n// that every request is eventually executed.\n//\n// NOTE: This is intentionally minimal – no external dependencies are introduced.\n// If you need more advanced features (persistence, jitter, etc.) consider a library such as `bottleneck`.\n\n/*\nRate-limit specification (per 60-second window)\n----------------------------------------------\nLogin as Taxpayer System : 12\nLogin as Intermediary System : 12\nSubmit Documents : 100\nGet Submission : 300\nCancel Document : 12\nReject Document : 12\nGet Document : 60\nGet Document Details : 125\nGet Recent Documents : 12\nSearch Documents : 12\nSearch Taxpayer's TIN : 60\nTaxpayer's QR Code : 60\n*/\n\nexport type ApiCategory =\n | 'loginTaxpayer'\n | 'loginIntermediary'\n | 'submitDocuments'\n | 'getSubmission'\n | 'cancelDocument'\n | 'rejectDocument'\n | 'getDocument'\n | 'getDocumentDetails'\n | 'getRecentDocuments'\n | 'searchDocuments'\n | 'searchTin'\n | 'taxpayerQr'\n | 'default'\n\ninterface RateLimitConfig {\n limit: number\n windowMs: number\n}\n\nconst WINDOW = 60_000 // 60 seconds\n\n// Hard-coded limits based on the specification above.\nconst RATE_LIMITS: Record<ApiCategory, RateLimitConfig> = {\n loginTaxpayer: { limit: 12, windowMs: WINDOW },\n loginIntermediary: { limit: 12, windowMs: WINDOW },\n submitDocuments: { limit: 100, windowMs: WINDOW },\n getSubmission: { limit: 300, windowMs: WINDOW },\n cancelDocument: { limit: 12, windowMs: WINDOW },\n rejectDocument: { limit: 12, windowMs: WINDOW },\n getDocument: { limit: 60, windowMs: WINDOW },\n getDocumentDetails: { limit: 125, windowMs: WINDOW },\n getRecentDocuments: { limit: 12, windowMs: WINDOW },\n searchDocuments: { limit: 12, windowMs: WINDOW },\n searchTin: { limit: 60, windowMs: WINDOW },\n taxpayerQr: { limit: 60, windowMs: WINDOW },\n default: { limit: 10_000, windowMs: WINDOW }, // effectively no limit\n}\n\n/**\n * A token-bucket style rate-limiter with queuing.\n * Uses a sliding window approach to allow bursts while respecting overall limits.\n * Each category gets its own instance so limits remain isolated.\n */\nclass RateLimiter {\n private readonly limit: number\n private readonly windowMs: number\n private readonly minInterval: number\n\n private queue: Array<() => void> = []\n private nextAvailable = 0 // timestamp (ms) when the next request can be executed\n private timer: NodeJS.Timeout | null = null\n private requestTimes: number[] = [] // Track request timestamps for sliding window\n private isProcessing = false // Prevent race conditions in drainQueue\n\n constructor(config: RateLimitConfig) {\n this.limit = config.limit\n this.windowMs = config.windowMs\n // Use a more reasonable interval that allows bursts while preventing 429s\n // Allow bursts up to 50% of the limit, then space out remaining requests\n const baseInterval = Math.ceil((this.windowMs / this.limit) * 0.5) // 50% of even spacing\n const isTestEnv = process.env.NODE_ENV === 'test'\n const forceReal = process.env.APIQUEUE_REAL_INTERVAL === 'true'\n // In unit-test envs we use minimal spacing unless explicitly forced back on.\n // This prevents test failures while still maintaining some rate limiting\n this.minInterval =\n isTestEnv && !forceReal ? Math.min(10, baseInterval) : baseInterval\n }\n\n private drainQueue() {\n // Prevent race conditions by ensuring only one drainQueue runs at a time\n if (this.isProcessing || this.queue.length === 0) {\n return\n }\n\n this.isProcessing = true\n\n try {\n const now = Date.now()\n\n // Clean up old request times outside the window\n this.requestTimes = this.requestTimes.filter(\n time => now - time < this.windowMs,\n )\n\n // Check if we can make another request within the rate limit\n if (this.requestTimes.length >= this.limit) {\n // We've hit the limit, schedule for when the oldest request expires\n const oldestRequest = Math.min(...this.requestTimes)\n const nextAvailable = oldestRequest + this.windowMs\n\n this.scheduleNextDrain(nextAvailable - now)\n return\n }\n\n // Check minimum interval constraint\n if (now < this.nextAvailable) {\n // Too early – schedule when we're allowed to execute next\n this.scheduleNextDrain(this.nextAvailable - now)\n return\n }\n\n // Execute the next queued task\n const next = this.queue.shift()!\n const requestStartTime = Date.now()\n this.requestTimes.push(requestStartTime)\n this.nextAvailable = requestStartTime + this.minInterval\n\n // Execute the request immediately\n next()\n } finally {\n this.isProcessing = false\n }\n\n // After resetting isProcessing, check if there are more requests\n // and schedule the next drain with appropriate delay\n if (this.queue.length > 0) {\n // Calculate delay until we can process the next request\n const now = Date.now()\n const delay = Math.max(1, this.nextAvailable - now)\n\n // Use scheduleNextDrain to ensure only one timer is active\n this.scheduleNextDrain(delay)\n }\n }\n\n private scheduleNextDrain(delay: number) {\n if (this.timer) {\n clearTimeout(this.timer)\n }\n\n this.timer = setTimeout(\n () => {\n this.timer = null\n this.drainQueue()\n },\n Math.max(0, delay),\n )\n }\n\n get queueSize() {\n return this.queue.length\n }\n\n // Cleanup method to prevent memory leaks\n cleanup() {\n if (this.timer) {\n clearTimeout(this.timer)\n this.timer = null\n }\n this.queue = []\n this.requestTimes = []\n this.isProcessing = false\n }\n\n schedule<T>(\n fn: () => Promise<T>,\n debug: boolean = false,\n category?: ApiCategory,\n ): Promise<T> {\n return new Promise((resolve, reject) => {\n const execute = () => {\n if (debug && category) {\n console.log(\n `[apiQueue] ▶️ Executing request (${category}). Remaining queue: ${this.queue.length}`,\n )\n }\n try {\n const result = fn()\n if (result && typeof (result as any).then === 'function') {\n ;(result as Promise<T>).then(resolve).catch(reject)\n } else {\n resolve(result as T)\n }\n } catch (err) {\n reject(err)\n }\n }\n\n if (debug && category) {\n console.log(\n `[apiQueue] ⏳ Queued request (${category}). Queue length before push: ${this.queue.length}`,\n )\n }\n\n this.queue.push(execute)\n this.drainQueue()\n })\n }\n}\n\n// A shared registry of limiters keyed by category\nconst limiterRegistry = new Map<ApiCategory, RateLimiter>()\n\nfunction getLimiter(category: ApiCategory): RateLimiter {\n if (!limiterRegistry.has(category)) {\n limiterRegistry.set(category, new RateLimiter(RATE_LIMITS[category]))\n }\n // Non-null because we just set it if missing.\n return limiterRegistry.get(category) as RateLimiter\n}\n\n/**\n * Public helper to schedule a request according to the category's limits.\n */\nexport function queueRequest<T>(\n category: ApiCategory,\n fn: () => Promise<T>,\n debug: boolean = false,\n): Promise<T> {\n const limiter = getLimiter(category)\n return limiter.schedule(fn, debug, category)\n}\n\n/**\n * Very naive path-based category detection. If no matcher fits, the `default` category\n * (effectively unlimited) is returned. Adjust these heuristics as your API surface evolves.\n */\nexport function categorizeRequest(\n path: string,\n method: string = 'GET',\n): ApiCategory {\n const cleanPath = path.toLowerCase()\n const isPost = method?.toUpperCase() === 'POST'\n\n if (cleanPath.includes('/documentsubmissions')) {\n return isPost ? 'submitDocuments' : 'getSubmission'\n }\n\n if (cleanPath.includes('/documentmanagement')) {\n if (cleanPath.endsWith('/cancel')) return 'cancelDocument'\n if (cleanPath.endsWith('/reject')) return 'rejectDocument'\n if (cleanPath.endsWith('/details')) return 'getDocumentDetails'\n if (cleanPath.includes('/recent')) return 'getRecentDocuments'\n // Fallbacks inside document management\n return method === 'GET' ? 'getDocument' : 'searchDocuments'\n }\n\n if (cleanPath.includes('/searchtin')) return 'searchTin'\n if (cleanPath.includes('/qrcode')) return 'taxpayerQr'\n if (cleanPath.includes('/connect/token')) {\n // Distinguish between taxpayer & intermediary based on path hint if possible\n return 'loginTaxpayer'\n }\n\n // -----------------------------\n // New path matchers (v1.0 endpoints)\n // -----------------------------\n\n // Search Documents\n if (cleanPath.includes('/documents/search')) {\n return 'searchDocuments'\n }\n\n // Document raw content\n if (/\\/documents\\/[^/]+\\/raw$/.test(cleanPath)) {\n return 'getDocument'\n }\n\n // Document details\n if (/\\/documents\\/[^/]+\\/details$/.test(cleanPath)) {\n return 'getDocumentDetails'\n }\n\n // Document state actions (cancel/reject)\n if (cleanPath.includes('/documents/state/')) {\n return isPost ? 'cancelDocument' : 'getDocument'\n }\n\n // Taxpayer TIN search & validation share same limit bucket\n if (cleanPath.includes('/taxpayer/search/tin')) return 'searchTin'\n if (cleanPath.includes('/taxpayer/validate/')) return 'searchTin'\n\n // Taxpayer QR code info\n if (cleanPath.includes('/taxpayer/qrcode')) return 'taxpayerQr'\n\n return 'default'\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"apiQueue-Dj0xtcGe.cjs","names":["RATE_LIMITS: Record<ApiCategory, RateLimitConfig>","config: RateLimitConfig","delay: number","fn: () => Promise<T>","debug: boolean","category?: ApiCategory","category: ApiCategory","path: string","method: string"],"sources":["../src/utils/apiQueue.ts"],"sourcesContent":["// A very small utility that provides per-endpoint request queuing with fixed-window rate-limits.\n// The goal is to make sure that we never exceed the vendor-defined limits while also ensuring\n// that every request is eventually executed.\n//\n// NOTE: This is intentionally minimal – no external dependencies are introduced.\n// If you need more advanced features (persistence, jitter, etc.) consider a library such as `bottleneck`.\n\n/*\nRate-limit specification (per 60-second window)\n----------------------------------------------\nLogin as Taxpayer System : 12\nLogin as Intermediary System : 12\nSubmit Documents : 100\nGet Submission : 300\nCancel Document : 12\nReject Document : 12\nGet Document : 60\nGet Document Details : 125\nGet Recent Documents : 12\nSearch Documents : 12\nSearch Taxpayer's TIN : 60\nTaxpayer's QR Code : 60\n*/\n\nexport type ApiCategory =\n | 'loginTaxpayer'\n | 'loginIntermediary'\n | 'submitDocuments'\n | 'getSubmission'\n | 'cancelDocument'\n | 'rejectDocument'\n | 'getDocument'\n | 'getDocumentDetails'\n | 'getRecentDocuments'\n | 'searchDocuments'\n | 'searchTin'\n | 'taxpayerQr'\n | 'default'\n\ninterface RateLimitConfig {\n limit: number\n windowMs: number\n}\n\nconst WINDOW = 60_000 // 60 seconds\n\n// Hard-coded limits based on the specification above.\nconst RATE_LIMITS: Record<ApiCategory, RateLimitConfig> = {\n loginTaxpayer: { limit: 12, windowMs: WINDOW },\n loginIntermediary: { limit: 12, windowMs: WINDOW },\n submitDocuments: { limit: 100, windowMs: WINDOW },\n getSubmission: { limit: 300, windowMs: WINDOW },\n cancelDocument: { limit: 12, windowMs: WINDOW },\n rejectDocument: { limit: 12, windowMs: WINDOW },\n getDocument: { limit: 60, windowMs: WINDOW },\n getDocumentDetails: { limit: 125, windowMs: WINDOW },\n getRecentDocuments: { limit: 12, windowMs: WINDOW },\n searchDocuments: { limit: 12, windowMs: WINDOW },\n searchTin: { limit: 60, windowMs: WINDOW },\n taxpayerQr: { limit: 60, windowMs: WINDOW },\n default: { limit: 10_000, windowMs: WINDOW }, // effectively no limit\n}\n\n/**\n * A token-bucket style rate-limiter with queuing.\n * Uses a sliding window approach to allow bursts while respecting overall limits.\n * Each category gets its own instance so limits remain isolated.\n */\nclass RateLimiter {\n private readonly limit: number\n private readonly windowMs: number\n private readonly minInterval: number\n\n private queue: Array<() => void> = []\n private nextAvailable = 0 // timestamp (ms) when the next request can be executed\n private timer: NodeJS.Timeout | null = null\n private requestTimes: number[] = [] // Track request timestamps for sliding window\n private isProcessing = false // Prevent race conditions in drainQueue\n\n constructor(config: RateLimitConfig) {\n this.limit = config.limit\n this.windowMs = config.windowMs\n // Use a more reasonable interval that allows bursts while preventing 429s\n // Allow bursts up to 50% of the limit, then space out remaining requests\n const baseInterval = Math.ceil((this.windowMs / this.limit) * 0.5) // 50% of even spacing\n const isTestEnv = process.env.NODE_ENV === 'test'\n const forceReal = process.env.APIQUEUE_REAL_INTERVAL === 'true'\n // In unit-test envs we use minimal spacing unless explicitly forced back on.\n // This prevents test failures while still maintaining some rate limiting\n this.minInterval =\n isTestEnv && !forceReal ? Math.min(10, baseInterval) : baseInterval\n }\n\n private drainQueue() {\n // Prevent race conditions by ensuring only one drainQueue runs at a time\n if (this.isProcessing || this.queue.length === 0) {\n return\n }\n\n this.isProcessing = true\n\n try {\n const now = Date.now()\n\n // Clean up old request times outside the window\n this.requestTimes = this.requestTimes.filter(\n time => now - time < this.windowMs,\n )\n\n // Check if we can make another request within the rate limit\n if (this.requestTimes.length >= this.limit) {\n // We've hit the limit, schedule for when the oldest request expires\n const oldestRequest = Math.min(...this.requestTimes)\n const nextAvailable = oldestRequest + this.windowMs\n\n this.scheduleNextDrain(nextAvailable - now)\n return\n }\n\n // Check minimum interval constraint\n if (now < this.nextAvailable) {\n // Too early – schedule when we're allowed to execute next\n this.scheduleNextDrain(this.nextAvailable - now)\n return\n }\n\n // Execute the next queued task\n const next = this.queue.shift()!\n const requestStartTime = Date.now()\n this.requestTimes.push(requestStartTime)\n this.nextAvailable = requestStartTime + this.minInterval\n\n // Execute the request immediately\n next()\n } finally {\n this.isProcessing = false\n }\n\n // After resetting isProcessing, check if there are more requests\n // and schedule the next drain with appropriate delay\n if (this.queue.length > 0) {\n // Calculate delay until we can process the next request\n const now = Date.now()\n const delay = Math.max(1, this.nextAvailable - now)\n\n // Use scheduleNextDrain to ensure only one timer is active\n this.scheduleNextDrain(delay)\n }\n }\n\n private scheduleNextDrain(delay: number) {\n if (this.timer) {\n clearTimeout(this.timer)\n }\n\n this.timer = setTimeout(\n () => {\n this.timer = null\n this.drainQueue()\n },\n Math.max(0, delay),\n )\n }\n\n get queueSize() {\n return this.queue.length\n }\n\n // Cleanup method to prevent memory leaks\n cleanup() {\n if (this.timer) {\n clearTimeout(this.timer)\n this.timer = null\n }\n this.queue = []\n this.requestTimes = []\n this.isProcessing = false\n }\n\n schedule<T>(\n fn: () => Promise<T>,\n debug: boolean = false,\n category?: ApiCategory,\n ): Promise<T> {\n return new Promise((resolve, reject) => {\n const execute = () => {\n if (debug && category) {\n console.log(\n `[apiQueue] ▶️ Executing request (${category}). Remaining queue: ${this.queue.length}`,\n )\n }\n try {\n const result = fn()\n if (result && typeof (result as any).then === 'function') {\n ;(result as Promise<T>).then(resolve).catch(reject)\n } else {\n resolve(result as T)\n }\n } catch (err) {\n reject(err)\n }\n }\n\n if (debug && category) {\n console.log(\n `[apiQueue] ⏳ Queued request (${category}). Queue length before push: ${this.queue.length}`,\n )\n }\n\n this.queue.push(execute)\n this.drainQueue()\n })\n }\n}\n\n// A shared registry of limiters keyed by category\nconst limiterRegistry = new Map<ApiCategory, RateLimiter>()\n\nfunction getLimiter(category: ApiCategory): RateLimiter {\n if (!limiterRegistry.has(category)) {\n limiterRegistry.set(category, new RateLimiter(RATE_LIMITS[category]))\n }\n // Non-null because we just set it if missing.\n return limiterRegistry.get(category) as RateLimiter\n}\n\n/**\n * Public helper to schedule a request according to the category's limits.\n */\nexport function queueRequest<T>(\n category: ApiCategory,\n fn: () => Promise<T>,\n debug: boolean = false,\n): Promise<T> {\n const limiter = getLimiter(category)\n return limiter.schedule(fn, debug, category)\n}\n\n/**\n * Very naive path-based category detection. If no matcher fits, the `default` category\n * (effectively unlimited) is returned. Adjust these heuristics as your API surface evolves.\n */\nexport function categorizeRequest(\n path: string,\n method: string = 'GET',\n): ApiCategory {\n const cleanPath = path.toLowerCase()\n const isPost = method?.toUpperCase() === 'POST'\n\n if (cleanPath.includes('/documentsubmissions')) {\n return isPost ? 'submitDocuments' : 'getSubmission'\n }\n\n // -----------------------------\n // v1.0 API endpoint matchers\n // -----------------------------\n\n // Get Recent Documents - /api/v1.0/documents/recent\n if (cleanPath.includes('/documents/recent')) {\n return 'getRecentDocuments'\n }\n\n // Search Documents - /api/v1.0/documents/search\n if (cleanPath.includes('/documents/search')) {\n return 'searchDocuments'\n }\n\n // Document state actions (cancel/reject) - PUT /api/v1.0/documents/state/{uuid}/state\n // Both cancel and reject use the same endpoint, differentiated only by request body\n if (cleanPath.includes('/documents/state/') && cleanPath.endsWith('/state')) {\n // Both cancelDocument and rejectDocument share the same rate limit (12 RPM)\n // Use cancelDocument category for both since they share the same bucket\n return 'cancelDocument'\n }\n\n // Document raw content - /api/v1.0/documents/{uuid}/raw\n if (/\\/documents\\/[^/]+\\/raw$/.test(cleanPath)) {\n return 'getDocument'\n }\n\n // Document details - /api/v1.0/documents/{uuid}/details\n if (/\\/documents\\/[^/]+\\/details$/.test(cleanPath)) {\n return 'getDocumentDetails'\n }\n\n // Taxpayer TIN search & validation share same limit bucket\n if (cleanPath.includes('/taxpayer/search/tin')) return 'searchTin'\n if (cleanPath.includes('/taxpayer/validate/')) return 'searchTin'\n\n // Taxpayer QR code info\n if (cleanPath.includes('/taxpayer/qrcode')) return 'taxpayerQr'\n\n // Legacy matchers (kept for backward compatibility)\n if (cleanPath.includes('/searchtin')) return 'searchTin'\n if (cleanPath.includes('/qrcode')) return 'taxpayerQr'\n if (cleanPath.includes('/connect/token')) {\n return 'loginTaxpayer'\n }\n\n return 'default'\n}\n"],"mappings":"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"}
|
package/dist/index.cjs
CHANGED
|
@@ -8,7 +8,7 @@ const require_platformLogin = require('./platformLogin-Ch6hFKoU.cjs');
|
|
|
8
8
|
const require_taxpayerValidation = require('./taxpayerValidation-D_jGaVty.cjs');
|
|
9
9
|
const require_certificate = require('./certificate-CWmfCPdt.cjs');
|
|
10
10
|
const require_getBaseUrl = require('./getBaseUrl-D0G4GZmp.cjs');
|
|
11
|
-
const require_apiQueue = require('./apiQueue-
|
|
11
|
+
const require_apiQueue = require('./apiQueue-Dj0xtcGe.cjs');
|
|
12
12
|
|
|
13
13
|
//#region src/index.ts
|
|
14
14
|
var MyInvoisClient = class MyInvoisClient {
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { platformLogin } from "./platformLogin-CqI9OLYP.js";
|
|
|
8
8
|
import { taxpayerQRCode, tinSearch, verifyTin } from "./taxpayerValidation-y6P-Es0S.js";
|
|
9
9
|
import { extractCertificateInfo, getPemFromP12, validateKeyPair } from "./certificate-COwqszxD.js";
|
|
10
10
|
import { getBaseUrl } from "./getBaseUrl-D7nUmoYb.js";
|
|
11
|
-
import { categorizeRequest, queueRequest } from "./apiQueue-
|
|
11
|
+
import { categorizeRequest, queueRequest } from "./apiQueue-CrR6pgYn.js";
|
|
12
12
|
|
|
13
13
|
//#region src/index.ts
|
|
14
14
|
var MyInvoisClient = class MyInvoisClient {
|
package/dist/index23.cjs
CHANGED
package/dist/utils/apiQueue.js
CHANGED