@ripwords/myinvois-client 0.2.17 → 0.2.18
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-Csh8ru3S.js → apiQueue-BE3-DB1A.js} +14 -9
- package/dist/{apiQueue-CRyjAw52.cjs → apiQueue-DDXeQJDC.cjs} +15 -10
- package/dist/apiQueue-DDXeQJDC.cjs.map +1 -0
- 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
- package/dist/apiQueue-CRyjAw52.cjs.map +0 -1
|
@@ -55,34 +55,39 @@ const RATE_LIMITS = {
|
|
|
55
55
|
}
|
|
56
56
|
};
|
|
57
57
|
/**
|
|
58
|
-
* A very small
|
|
59
|
-
*
|
|
58
|
+
* A very small token-bucket style rate-limiter with queuing.
|
|
59
|
+
* Instead of allowing a full-window burst (which could trigger a 429),
|
|
60
|
+
* requests are spaced evenly so the throughput stays within the
|
|
61
|
+
* <limit>/<window> budget at all times. Each category gets its own
|
|
62
|
+
* instance so limits remain isolated.
|
|
60
63
|
*/
|
|
61
64
|
var RateLimiter = class {
|
|
62
65
|
limit;
|
|
63
66
|
windowMs;
|
|
67
|
+
minInterval;
|
|
64
68
|
queue = [];
|
|
65
|
-
|
|
69
|
+
nextAvailable = 0;
|
|
66
70
|
timer = null;
|
|
67
71
|
constructor(config) {
|
|
68
72
|
this.limit = config.limit;
|
|
69
73
|
this.windowMs = config.windowMs;
|
|
74
|
+
const baseInterval = Math.ceil(this.windowMs / this.limit);
|
|
75
|
+
const isTestEnv = process.env.NODE_ENV === "test";
|
|
76
|
+
const forceReal = process.env.APIQUEUE_REAL_INTERVAL === "true";
|
|
77
|
+
this.minInterval = isTestEnv && !forceReal ? 0 : baseInterval;
|
|
70
78
|
}
|
|
71
79
|
drainQueue() {
|
|
72
80
|
if (this.queue.length === 0) return;
|
|
73
81
|
const now = Date.now();
|
|
74
|
-
|
|
75
|
-
if (this.timestamps.length >= this.limit) {
|
|
76
|
-
const earliest = this.timestamps[0];
|
|
77
|
-
const delay = this.windowMs - (now - earliest) + 1;
|
|
82
|
+
if (now < this.nextAvailable) {
|
|
78
83
|
if (!this.timer) this.timer = setTimeout(() => {
|
|
79
84
|
this.timer = null;
|
|
80
85
|
this.drainQueue();
|
|
81
|
-
},
|
|
86
|
+
}, this.nextAvailable - now);
|
|
82
87
|
return;
|
|
83
88
|
}
|
|
84
89
|
const next = this.queue.shift();
|
|
85
|
-
this.
|
|
90
|
+
this.nextAvailable = Date.now() + this.minInterval;
|
|
86
91
|
next();
|
|
87
92
|
this.drainQueue();
|
|
88
93
|
}
|
|
@@ -56,34 +56,39 @@ const RATE_LIMITS = {
|
|
|
56
56
|
}
|
|
57
57
|
};
|
|
58
58
|
/**
|
|
59
|
-
* A very small
|
|
60
|
-
*
|
|
59
|
+
* A very small token-bucket style rate-limiter with queuing.
|
|
60
|
+
* Instead of allowing a full-window burst (which could trigger a 429),
|
|
61
|
+
* requests are spaced evenly so the throughput stays within the
|
|
62
|
+
* <limit>/<window> budget at all times. Each category gets its own
|
|
63
|
+
* instance so limits remain isolated.
|
|
61
64
|
*/
|
|
62
65
|
var RateLimiter = class {
|
|
63
66
|
limit;
|
|
64
67
|
windowMs;
|
|
68
|
+
minInterval;
|
|
65
69
|
queue = [];
|
|
66
|
-
|
|
70
|
+
nextAvailable = 0;
|
|
67
71
|
timer = null;
|
|
68
72
|
constructor(config) {
|
|
69
73
|
this.limit = config.limit;
|
|
70
74
|
this.windowMs = config.windowMs;
|
|
75
|
+
const baseInterval = Math.ceil(this.windowMs / this.limit);
|
|
76
|
+
const isTestEnv = process.env.NODE_ENV === "test";
|
|
77
|
+
const forceReal = process.env.APIQUEUE_REAL_INTERVAL === "true";
|
|
78
|
+
this.minInterval = isTestEnv && !forceReal ? 0 : baseInterval;
|
|
71
79
|
}
|
|
72
80
|
drainQueue() {
|
|
73
81
|
if (this.queue.length === 0) return;
|
|
74
82
|
const now = Date.now();
|
|
75
|
-
|
|
76
|
-
if (this.timestamps.length >= this.limit) {
|
|
77
|
-
const earliest = this.timestamps[0];
|
|
78
|
-
const delay = this.windowMs - (now - earliest) + 1;
|
|
83
|
+
if (now < this.nextAvailable) {
|
|
79
84
|
if (!this.timer) this.timer = setTimeout(() => {
|
|
80
85
|
this.timer = null;
|
|
81
86
|
this.drainQueue();
|
|
82
|
-
},
|
|
87
|
+
}, this.nextAvailable - now);
|
|
83
88
|
return;
|
|
84
89
|
}
|
|
85
90
|
const next = this.queue.shift();
|
|
86
|
-
this.
|
|
91
|
+
this.nextAvailable = Date.now() + this.minInterval;
|
|
87
92
|
next();
|
|
88
93
|
this.drainQueue();
|
|
89
94
|
}
|
|
@@ -161,4 +166,4 @@ Object.defineProperty(exports, 'queueRequest', {
|
|
|
161
166
|
return queueRequest;
|
|
162
167
|
}
|
|
163
168
|
});
|
|
164
|
-
//# sourceMappingURL=apiQueue-
|
|
169
|
+
//# sourceMappingURL=apiQueue-DDXeQJDC.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apiQueue-DDXeQJDC.cjs","names":["RATE_LIMITS: Record<ApiCategory, RateLimitConfig>","config: RateLimitConfig","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 very small token-bucket style rate-limiter with queuing.\n * Instead of allowing a full-window burst (which could trigger a 429),\n * requests are spaced evenly so the throughput stays within the\n * <limit>/<window> budget at all times. Each category gets its own\n * 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\n constructor(config: RateLimitConfig) {\n this.limit = config.limit\n this.windowMs = config.windowMs\n const baseInterval = Math.ceil(this.windowMs / this.limit)\n const isTestEnv = process.env.NODE_ENV === 'test'\n const forceReal = process.env.APIQUEUE_REAL_INTERVAL === 'true'\n // In unit-test envs we collapse spacing unless explicitly forced back on.\n this.minInterval = isTestEnv && !forceReal ? 0 : baseInterval\n }\n\n private drainQueue() {\n if (this.queue.length === 0) {\n return\n }\n\n const now = Date.now()\n if (now < this.nextAvailable) {\n // Too early – schedule when we're allowed to execute next\n if (!this.timer) {\n this.timer = setTimeout(() => {\n this.timer = null\n this.drainQueue()\n }, this.nextAvailable - now)\n }\n return\n }\n\n // Execute the next queued task\n const next = this.queue.shift()!\n this.nextAvailable = Date.now() + this.minInterval\n next()\n\n // Attempt to drain further (may schedule another run if cannot execute immediately)\n this.drainQueue()\n }\n\n get queueSize() {\n return this.queue.length\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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"}
|
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-DDXeQJDC.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-BE3-DB1A.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
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"apiQueue-CRyjAw52.cjs","names":["RATE_LIMITS: Record<ApiCategory, RateLimitConfig>","config: RateLimitConfig","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 very small sliding-window rate-limiter with queuing.\n * Each category gets its own instance so limits are isolated.\n */\nclass RateLimiter {\n private readonly limit: number\n private readonly windowMs: number\n private queue: Array<() => void> = []\n private timestamps: number[] = []\n private timer: NodeJS.Timeout | null = null\n\n constructor(config: RateLimitConfig) {\n this.limit = config.limit\n this.windowMs = config.windowMs\n }\n\n private drainQueue() {\n if (this.queue.length === 0) {\n return\n }\n\n const now = Date.now()\n // Purge stale timestamps (older than window)\n this.timestamps = this.timestamps.filter(ts => now - ts < this.windowMs)\n\n if (this.timestamps.length >= this.limit) {\n // We are currently rate-limited – schedule a retry when the earliest call exits the window\n const earliest = this.timestamps[0]!\n const delay = this.windowMs - (now - earliest) + 1 // +1ms buffer\n if (!this.timer) {\n this.timer = setTimeout(() => {\n this.timer = null\n this.drainQueue()\n }, delay)\n }\n return\n }\n\n // We can process at least one queued request now\n const next = this.queue.shift()!\n this.timestamps.push(now)\n next()\n\n // Recursively drain (in case there is remaining capacity)\n this.drainQueue()\n }\n\n get queueSize() {\n return this.queue.length\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 // Support both promise and synchronous return values\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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"}
|