@goodandready/dsh-key-rotation 0.7.30 → 0.7.32
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/LICENSE +21 -21
- package/README.md +254 -172
- package/README.ru.md +254 -0
- package/README.zh.md +215 -0
- package/cordis.patch.yml +6 -6
- package/lib/agent-budget.js +68 -68
- package/lib/bucket.js +129 -52
- package/lib/canary.js +63 -56
- package/lib/client-helpers.js +21 -21
- package/lib/client.js +1156 -1063
- package/lib/concurrency.js +73 -72
- package/lib/heal.js +35 -35
- package/lib/histogram.js +66 -66
- package/lib/incident.js +76 -76
- package/lib/index.js +1850 -1836
- package/lib/pool.js +264 -237
- package/lib/quota-window.js +45 -45
- package/lib/quota.js +39 -39
- package/lib/region.js +50 -50
- package/lib/sandbox.js +117 -117
- package/lib/shadow.js +81 -81
- package/lib/usage-report.js +79 -49
- package/lib/webhook.js +193 -133
- package/package.json +58 -58
package/lib/pool.js
CHANGED
|
@@ -1,237 +1,264 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (!
|
|
28
|
-
if (
|
|
29
|
-
const
|
|
30
|
-
if (
|
|
31
|
-
try {
|
|
32
|
-
const
|
|
33
|
-
if (host
|
|
34
|
-
return
|
|
35
|
-
} catch {
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
/\
|
|
45
|
-
/\
|
|
46
|
-
/\
|
|
47
|
-
/\b
|
|
48
|
-
/\
|
|
49
|
-
/\
|
|
50
|
-
/\
|
|
51
|
-
/\b
|
|
52
|
-
/\
|
|
53
|
-
/\
|
|
54
|
-
/\b(?:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
return
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/** Parse
|
|
198
|
-
export function
|
|
199
|
-
if (typeof
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (!
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
return
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (!
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
1
|
+
// lib/pool.js — pure pool arithmetic and selection logic for dsh-key-rotation.
|
|
2
|
+
// Isolated from cordis/dsh runtime so unit tests run in vanilla Node.js.
|
|
3
|
+
|
|
4
|
+
/** Number of characters shown in masked key tails. */
|
|
5
|
+
export const KEY_TAIL_CHARS = 5;
|
|
6
|
+
|
|
7
|
+
/** Mask key to show only the last KEY_TAIL_CHARS. */
|
|
8
|
+
export function keyTail(key) {
|
|
9
|
+
if (typeof key !== 'string') return '';
|
|
10
|
+
if (key.length <= KEY_TAIL_CHARS) return key;
|
|
11
|
+
return key.slice(-KEY_TAIL_CHARS);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Check if an IP address is a loopback address. */
|
|
15
|
+
export function isLoopbackAddress(ip) {
|
|
16
|
+
if (!ip || typeof ip !== 'string') return false;
|
|
17
|
+
if (ip === '127.0.0.1' || ip === '::1' || ip === '::ffff:127.0.0.1') return true;
|
|
18
|
+
if (ip.startsWith('127.')) return true;
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Check if an incoming HTTP request is from a trusted local bridge origin. */
|
|
23
|
+
export function isTrustedBridgeRequest(req) {
|
|
24
|
+
const remoteAddress = req?.socket?.remoteAddress;
|
|
25
|
+
if (!isLoopbackAddress(remoteAddress)) return false;
|
|
26
|
+
const origin = req?.headers?.origin;
|
|
27
|
+
if (!origin) return true;
|
|
28
|
+
if (req?.headers?.['sec-fetch-site'] === 'cross-site') return false;
|
|
29
|
+
const hostHeader = req?.headers?.host;
|
|
30
|
+
if (!hostHeader) return false;
|
|
31
|
+
try {
|
|
32
|
+
const originUrl = new URL(origin);
|
|
33
|
+
if (originUrl.host !== hostHeader) return false;
|
|
34
|
+
return isLoopbackAddress(originUrl.hostname) || originUrl.hostname === 'localhost';
|
|
35
|
+
} catch (_) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Regex matching error phrases that warrant switching to the next key. */
|
|
41
|
+
export const SWITCHABLE_MESSAGE_PATTERN = new RegExp([
|
|
42
|
+
/\b(?:quota|usage[\s_-]+limit|rate[\s_-]?limit)\b/i,
|
|
43
|
+
/\binsufficient[\s_-]+(?:quota|balance|credits?)\b/i,
|
|
44
|
+
/\bout[\s_-]+of[\s_-]+(?:credits?|budget)\b/i,
|
|
45
|
+
/\b(?:exceeded|exhausted)[\s_-]+(?:quota|limit|budget)\b/i,
|
|
46
|
+
/\bbilling\b/i,
|
|
47
|
+
/\b429\b|\b5\d\d\b/i,
|
|
48
|
+
/\btime(?:d)?\s*out\b|timeout/i,
|
|
49
|
+
/\b(?:network|connection|socket|fetch|ECONN[A-Z]+)\b/i,
|
|
50
|
+
/\bother side closed|premature close|stream ended (?:before|without)\b/i,
|
|
51
|
+
/\b401\b|\b403\b/i,
|
|
52
|
+
/\b(?:invalid|expired|revoked|unauthorized)[\s_-]+(?:api[\s_-]?key|token)\b/i,
|
|
53
|
+
/\bapi[\s_-]?key[\s_-]+(?:is[\s_-]+)?(?:invalid|expired|revoked|unauthorized)\b/i,
|
|
54
|
+
/\b(?:authentication|unauthorized|not[\s_-]+authorized)\b/i,
|
|
55
|
+
].map((r) => r.source).join('|'), 'i');
|
|
56
|
+
|
|
57
|
+
/** Default switch codes used by lib/index.js. */
|
|
58
|
+
export const DEFAULT_SWITCH_CODES = [
|
|
59
|
+
'QUOTA', 'RATE_LIMIT', 'SERVER', 'TIMEOUT', 'TRANSPORT',
|
|
60
|
+
'EMPTY_RESPONSE', 'UNKNOWN_MODEL', 'AUTH',
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
/** Soft failure codes: transient infrastructure drops where progressive exponential penalty is unwarranted. */
|
|
64
|
+
export const SOFT_FAILURE_CODES = new Set([
|
|
65
|
+
'SERVER', 'TIMEOUT', 'TRANSPORT', 'EMPTY_RESPONSE', '500', '502', '503', '504',
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
/** True if failure code or message represents a soft/transient drop. */
|
|
69
|
+
export function isSoftFailure(code, message = '') {
|
|
70
|
+
if (code && SOFT_FAILURE_CODES.has(String(code).toUpperCase())) return true;
|
|
71
|
+
if (/502|503|504|timeout|econnreset|econnrefused|socket hang up/i.test(message)) return true;
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Ref name validator. Same rule lib/index.js enforces in PUT/DELETE /key. */
|
|
76
|
+
const REF_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
77
|
+
export function isValidRef(ref) {
|
|
78
|
+
return typeof ref === 'string' && REF_RE.test(ref);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Apply full jitter to a duration (+- factor, e.g. +-12.5%). */
|
|
82
|
+
export function applyJitter(ms, factor = 0.125) {
|
|
83
|
+
if (!Number.isFinite(ms) || ms <= 0) return ms;
|
|
84
|
+
const spread = (Math.random() * 2 - 1) * factor; // -factor .. +factor
|
|
85
|
+
return Math.max(100, Math.round(ms * (1 + spread)));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Pick the next healthy ref in a round-robin pool. */
|
|
89
|
+
export function pickNext(pool, now, refsCount = pool.refs.length) {
|
|
90
|
+
if (refsCount === 0) return undefined;
|
|
91
|
+
const start = pool.state.pointer ?? 0;
|
|
92
|
+
for (let i = 0; i < refsCount; i++) {
|
|
93
|
+
const index = (start + i) % refsCount;
|
|
94
|
+
const candidate = pool.refs[index];
|
|
95
|
+
const until = pool.state.failedUntil.get(candidate);
|
|
96
|
+
if (until !== undefined && until > now) continue;
|
|
97
|
+
return candidate;
|
|
98
|
+
}
|
|
99
|
+
return undefined; // all cooled
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Apply a failed-key cooldown to pool state. Pure: returns next state shape. */
|
|
103
|
+
export function applyCooldown(pool, ref, cooldownMs, now = Date.now()) {
|
|
104
|
+
return {
|
|
105
|
+
...pool,
|
|
106
|
+
state: {
|
|
107
|
+
...pool.state,
|
|
108
|
+
failedUntil: new Map(pool.state.failedUntil).set(ref, now + cooldownMs),
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Backoff calculation with soft/hard failure support.
|
|
115
|
+
* failCount 1 => baseMs, 2 => baseMs*2, 3 => baseMs*4, capped at baseMs*8 (or maxMs).
|
|
116
|
+
* Soft failures use a short base cooldown without exponential multiplier.
|
|
117
|
+
*/
|
|
118
|
+
export function computeBackoff(baseMs, failCount, maxMs, isSoft = false) {
|
|
119
|
+
if (isSoft) {
|
|
120
|
+
return Math.min(baseMs, 10000);
|
|
121
|
+
}
|
|
122
|
+
const cap = maxMs ?? baseMs * 8;
|
|
123
|
+
if (failCount <= 1) return Math.min(baseMs, cap);
|
|
124
|
+
const backoff = baseMs * (1 << (failCount - 1)); // 2^(n-1)
|
|
125
|
+
return Math.min(backoff, cap);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Record a failure for `ref` in `pool.state`. */
|
|
129
|
+
export function recordFailure(pool, ref, now, baseMs, maxMs, isSoft = false, jitter = false) {
|
|
130
|
+
if (!pool.state.failCounts) pool.state.failCounts = new Map();
|
|
131
|
+
let next = pool.state.failCounts.get(ref) ?? 0;
|
|
132
|
+
if (!isSoft) {
|
|
133
|
+
next += 1;
|
|
134
|
+
pool.state.failCounts.set(ref, next);
|
|
135
|
+
}
|
|
136
|
+
let backoff = computeBackoff(baseMs, next || 1, maxMs, isSoft);
|
|
137
|
+
if (jitter) {
|
|
138
|
+
backoff = applyJitter(backoff);
|
|
139
|
+
}
|
|
140
|
+
pool.state.failedUntil.set(ref, now + backoff);
|
|
141
|
+
return backoff;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Record a success for `ref`. */
|
|
145
|
+
export function recordSuccess(pool, ref, now = Date.now()) {
|
|
146
|
+
if (pool.state.failCounts) pool.state.failCounts.delete(ref);
|
|
147
|
+
pool.state.failedUntil.delete(ref);
|
|
148
|
+
if (!pool.state.lastSuccessAt) pool.state.lastSuccessAt = new Map();
|
|
149
|
+
pool.state.lastSuccessAt.set(ref, now);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Decay penalty failCounts for stable keys that haven't failed in decayIntervalMs. */
|
|
153
|
+
export function decayPenalties(pool, now = Date.now(), decayIntervalMs = 3600_000) {
|
|
154
|
+
if (!pool?.state?.failCounts || !pool?.state?.lastSuccessAt) return 0;
|
|
155
|
+
let decayed = 0;
|
|
156
|
+
for (const [ref, count] of [...pool.state.failCounts.entries()]) {
|
|
157
|
+
if (count <= 0) {
|
|
158
|
+
pool.state.failCounts.delete(ref);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const lastOk = pool.state.lastSuccessAt.get(ref);
|
|
162
|
+
if (lastOk && now - lastOk >= decayIntervalMs) {
|
|
163
|
+
const next = count - 1;
|
|
164
|
+
if (next <= 0) {
|
|
165
|
+
pool.state.failCounts.delete(ref);
|
|
166
|
+
} else {
|
|
167
|
+
pool.state.failCounts.set(ref, next);
|
|
168
|
+
}
|
|
169
|
+
pool.state.lastSuccessAt.set(ref, now);
|
|
170
|
+
decayed++;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return decayed;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Return env value for ref if present in process.env, else undefined. */
|
|
177
|
+
export function envValue(ref) {
|
|
178
|
+
const v = typeof process !== 'undefined' ? process.env?.[ref] : undefined;
|
|
179
|
+
return typeof v === 'string' && v.length > 0 ? v : undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Sweep expired cooldown entries from poolState. Returns count of cleared refs. */
|
|
183
|
+
export function sweepExpired(poolState, now = Date.now()) {
|
|
184
|
+
let cleared = 0;
|
|
185
|
+
for (const st of poolState.values()) {
|
|
186
|
+
for (const [ref, until] of [...st.failedUntil.entries()]) {
|
|
187
|
+
if (until <= now) {
|
|
188
|
+
st.failedUntil.delete(ref);
|
|
189
|
+
st.failCounts?.delete(ref);
|
|
190
|
+
cleared++;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return cleared;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Parse Retry-After value from a header string or message. */
|
|
198
|
+
export function parseRetryAfter(value) {
|
|
199
|
+
if (typeof value !== 'string' || !value) return undefined;
|
|
200
|
+
const m = value.match(/retry-after\s*[:=]\s*(.+)/i);
|
|
201
|
+
const raw = m ? m[1].trim().split(/[\n\r;]/)[0].trim() : value.trim();
|
|
202
|
+
if (/^\d+$/.test(raw)) {
|
|
203
|
+
const sec = Number(raw);
|
|
204
|
+
if (sec >= 0 && sec <= 86400 * 7) return sec * 1000;
|
|
205
|
+
}
|
|
206
|
+
const ts = Date.parse(raw);
|
|
207
|
+
if (!Number.isNaN(ts)) {
|
|
208
|
+
const diff = ts - Date.now();
|
|
209
|
+
if (diff > 0 && diff < 86400 * 7 * 1000) return diff;
|
|
210
|
+
}
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Pick a key pool for a (provider, model) pair. */
|
|
215
|
+
export function selectPool(modelPoolByProvider, providerToPool, provider, model) {
|
|
216
|
+
const byModel = modelPoolByProvider && modelPoolByProvider.get(provider);
|
|
217
|
+
const base = providerToPool && providerToPool.get(provider);
|
|
218
|
+
if (!byModel || !model) return base ?? null;
|
|
219
|
+
if (byModel.has(model)) return byModel.get(model);
|
|
220
|
+
let best = null;
|
|
221
|
+
for (const key of byModel.keys()) {
|
|
222
|
+
if (model.startsWith(key) && key.length > (best ? best.length : 0)) best = key;
|
|
223
|
+
}
|
|
224
|
+
return (best ? byModel.get(best) : base) ?? null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Parse an expiry value (timestamp ms or ISO date string) to epoch ms. */
|
|
228
|
+
export function parseExpiry(v) {
|
|
229
|
+
if (typeof v === 'number' && v > 0) return v;
|
|
230
|
+
if (typeof v === 'string' && v.length > 0) { const ts = Date.parse(v); return Number.isNaN(ts) ? undefined : ts; }
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Compute a 0..100 health score for a pool based on its runtime state. */
|
|
235
|
+
export function computeHealthScore(state) {
|
|
236
|
+
if (!state || typeof state !== 'object') return 100;
|
|
237
|
+
const switches = state.switches ?? 0;
|
|
238
|
+
const exhaustions = state.exhaustionCount ?? 0;
|
|
239
|
+
const broken = state.brokenUntil ? state.brokenUntil.size : 0;
|
|
240
|
+
return Math.max(0, Math.min(100, 100 - (switches * 5) - (exhaustions * 10) - (broken * 15)));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Extract rate-limit info from an object that may carry response headers. */
|
|
244
|
+
export function extractRateLimit(headers) {
|
|
245
|
+
if (!headers || typeof headers !== 'object') return null;
|
|
246
|
+
const get = (name) => {
|
|
247
|
+
const v = headers[name] ?? headers[name.toLowerCase()] ?? headers[name.toUpperCase()];
|
|
248
|
+
if (v === undefined || v === null) return undefined;
|
|
249
|
+
return Number(String(v));
|
|
250
|
+
};
|
|
251
|
+
const remaining = get('X-RateLimit-Remaining');
|
|
252
|
+
const limit = get('X-RateLimit-Limit');
|
|
253
|
+
const reset = get('X-RateLimit-Reset');
|
|
254
|
+
if (remaining === undefined && limit === undefined) return null;
|
|
255
|
+
return { remaining, limit, reset };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** True if remaining is below the given threshold fraction of limit (e.g. 0.1). */
|
|
259
|
+
export function isRateLimited(rate, threshold = 0.1) {
|
|
260
|
+
if (!rate) return false;
|
|
261
|
+
if (rate.remaining === undefined) return false;
|
|
262
|
+
if (rate.limit && rate.limit > 0) return rate.remaining < rate.limit * threshold;
|
|
263
|
+
return rate.remaining <= 0;
|
|
264
|
+
}
|
package/lib/quota-window.js
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
// quota-window.js — calendar-based quota reset windows (issue #197).
|
|
2
|
-
|
|
3
|
-
export const QUOTA_WINDOW_TYPES = ['midnight_utc', 'midnight_pst', 'rolling_24h'];
|
|
4
|
-
|
|
5
|
-
export function nextQuotaReset(quotaResetWindow, now) {
|
|
6
|
-
now = now || Date.now();
|
|
7
|
-
if (!quotaResetWindow || typeof quotaResetWindow !== 'object') return null;
|
|
8
|
-
const type = quotaResetWindow.type;
|
|
9
|
-
const hour = Number.isFinite(quotaResetWindow.hour) ? quotaResetWindow.hour : 0;
|
|
10
|
-
|
|
11
|
-
if (type === 'rolling_24h') {
|
|
12
|
-
const d = new Date(now);
|
|
13
|
-
d.setUTCHours(d.getUTCHours() + 24, 0, 0, 0);
|
|
14
|
-
return d.getTime();
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (type === 'midnight_utc') {
|
|
18
|
-
const d = new Date(now);
|
|
19
|
-
d.setUTCHours(hour, 0, 0, 0);
|
|
20
|
-
if (d.getTime() <= now) d.setUTCDate(d.getUTCDate() + 1);
|
|
21
|
-
return d.getTime();
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (type === 'midnight_pst') {
|
|
25
|
-
const PST_OFFSET_MS = 8 * 3600_000;
|
|
26
|
-
const shifted = now + PST_OFFSET_MS;
|
|
27
|
-
const d = new Date(shifted);
|
|
28
|
-
d.setUTCHours(hour, 0, 0, 0);
|
|
29
|
-
if (d.getTime() <= shifted) d.setUTCDate(d.getUTCDate() + 1);
|
|
30
|
-
return d.getTime() - PST_OFFSET_MS;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function poolResetAt(pool, quotaResetWindow, now) {
|
|
37
|
-
return nextQuotaReset(quotaResetWindow, now);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function isBlockedUntilReset(failedUntil, resetAt, now) {
|
|
41
|
-
now = now || Date.now();
|
|
42
|
-
if (!Number.isFinite(failedUntil)) return false;
|
|
43
|
-
if (resetAt === null || resetAt === undefined) return false;
|
|
44
|
-
return failedUntil >= resetAt;
|
|
45
|
-
}
|
|
1
|
+
// quota-window.js — calendar-based quota reset windows (issue #197).
|
|
2
|
+
|
|
3
|
+
export const QUOTA_WINDOW_TYPES = ['midnight_utc', 'midnight_pst', 'rolling_24h'];
|
|
4
|
+
|
|
5
|
+
export function nextQuotaReset(quotaResetWindow, now) {
|
|
6
|
+
now = now || Date.now();
|
|
7
|
+
if (!quotaResetWindow || typeof quotaResetWindow !== 'object') return null;
|
|
8
|
+
const type = quotaResetWindow.type;
|
|
9
|
+
const hour = Number.isFinite(quotaResetWindow.hour) ? quotaResetWindow.hour : 0;
|
|
10
|
+
|
|
11
|
+
if (type === 'rolling_24h') {
|
|
12
|
+
const d = new Date(now);
|
|
13
|
+
d.setUTCHours(d.getUTCHours() + 24, 0, 0, 0);
|
|
14
|
+
return d.getTime();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (type === 'midnight_utc') {
|
|
18
|
+
const d = new Date(now);
|
|
19
|
+
d.setUTCHours(hour, 0, 0, 0);
|
|
20
|
+
if (d.getTime() <= now) d.setUTCDate(d.getUTCDate() + 1);
|
|
21
|
+
return d.getTime();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (type === 'midnight_pst') {
|
|
25
|
+
const PST_OFFSET_MS = -8 * 3600_000; // PST is UTC-8
|
|
26
|
+
const shifted = now + PST_OFFSET_MS;
|
|
27
|
+
const d = new Date(shifted);
|
|
28
|
+
d.setUTCHours(hour, 0, 0, 0);
|
|
29
|
+
if (d.getTime() <= shifted) d.setUTCDate(d.getUTCDate() + 1);
|
|
30
|
+
return d.getTime() - PST_OFFSET_MS;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function poolResetAt(pool, quotaResetWindow, now) {
|
|
37
|
+
return nextQuotaReset(quotaResetWindow, now);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isBlockedUntilReset(failedUntil, resetAt, now) {
|
|
41
|
+
now = now || Date.now();
|
|
42
|
+
if (!Number.isFinite(failedUntil)) return false;
|
|
43
|
+
if (resetAt === null || resetAt === undefined) return false;
|
|
44
|
+
return failedUntil >= resetAt;
|
|
45
|
+
}
|