@omnicross/core 0.1.2 → 0.1.4
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/NOTICE +57 -57
- package/README.md +15 -15
- package/dist/auth/GeminiCodeAssistProjectResolver.cjs +6 -1
- package/dist/auth/GeminiCodeAssistProjectResolver.js +6 -1
- package/dist/chunk-3QFXFCHG.js +33 -0
- package/dist/chunk-4IH4EL7M.cjs +9 -0
- package/dist/chunk-4YINUUKQ.cjs +33 -0
- package/dist/chunk-5ENKBSWO.js +311 -0
- package/dist/chunk-6RPZADX3.cjs +92 -0
- package/dist/chunk-7Y47UCF2.js +142 -0
- package/dist/chunk-AEOZIDEB.cjs +32 -0
- package/dist/chunk-APYG5QD7.cjs +50 -0
- package/dist/{chunk-MNYKI4CI.js → chunk-BPKCU575.js} +29 -0
- package/dist/{chunk-SN3YWBX7.cjs → chunk-DKPXN34V.cjs} +2108 -538
- package/dist/{chunk-4NBS6KPV.cjs → chunk-DQ5VAYZ6.cjs} +7 -8
- package/dist/chunk-E3UCVHLP.cjs +142 -0
- package/dist/chunk-FCR77GYM.cjs +33 -0
- package/dist/{chunk-74TMJA7Z.js → chunk-HRRYG2AX.js} +8 -4
- package/dist/chunk-HUZZZ3JL.js +32 -0
- package/dist/{chunk-KUU2RNG6.cjs → chunk-HYN75H6G.cjs} +3 -0
- package/dist/{chunk-QOCNX236.js → chunk-IAXWICZU.js} +24 -1
- package/dist/{chunk-JO5NLTLY.js → chunk-IK3KE3AU.js} +3 -0
- package/dist/{chunk-V5KPWNYX.cjs → chunk-JMJBSACD.cjs} +3 -3
- package/dist/chunk-K5NM7VAH.js +33 -0
- package/dist/{chunk-N3V2J5ZO.cjs → chunk-KFI44N2R.cjs} +8 -4
- package/dist/chunk-L74VDN4A.cjs +32 -0
- package/dist/chunk-OCEUVQHW.cjs +112 -0
- package/dist/chunk-OE6TDIWW.js +22 -0
- package/dist/chunk-OZFM4X3S.js +18 -0
- package/dist/{chunk-LKZJEL6E.cjs → chunk-Q27JY5RG.cjs} +6 -4
- package/dist/{chunk-SPVWWUHX.cjs → chunk-QWBWGODS.cjs} +24 -1
- package/dist/{chunk-2FEVTJWG.js → chunk-QYTT6GE3.js} +1929 -359
- package/dist/{chunk-UYPEN5XE.cjs → chunk-RGTR7CIO.cjs} +6 -6
- package/dist/chunk-RPII3E6Z.cjs +145 -0
- package/dist/{chunk-XBSYYZIY.cjs → chunk-SNYBEXLB.cjs} +31 -2
- package/dist/{chunk-2DCNB7DF.cjs → chunk-SU2GCLAC.cjs} +30 -10
- package/dist/{chunk-5HTVET6E.js → chunk-TIER4SUC.js} +24 -4
- package/dist/chunk-TKJMZBAH.js +32 -0
- package/dist/{chunk-ZSVQT3PW.js → chunk-UBWEBKRC.js} +5 -6
- package/dist/chunk-UHGVBFU3.js +385 -0
- package/dist/chunk-UIBD2YY5.cjs +22 -0
- package/dist/chunk-UNEFIWXI.cjs +32 -0
- package/dist/chunk-VACWOIRE.cjs +385 -0
- package/dist/{chunk-O466Y272.js → chunk-VPBAUQ2M.js} +7 -7
- package/dist/chunk-VQY5W4YW.cjs +311 -0
- package/dist/chunk-WLYIIEAF.js +50 -0
- package/dist/chunk-WNY4WMRF.js +32 -0
- package/dist/chunk-XEKD23J5.js +9 -0
- package/dist/chunk-XK5CIRMQ.js +112 -0
- package/dist/chunk-XSVNC2UD.cjs +18 -0
- package/dist/chunk-XTEI64OU.js +145 -0
- package/dist/chunk-XX6NQJMA.js +92 -0
- package/dist/{chunk-745DV5FL.js → chunk-Y7FO65VT.js} +3 -3
- package/dist/{chunk-PXUJF5HS.js → chunk-ZY6KU6P3.js} +4 -2
- package/dist/completion/CompletionService.cjs +30 -17
- package/dist/completion/CompletionService.js +29 -16
- package/dist/completion/NativeSearchInjector.cjs +3 -3
- package/dist/completion/NativeSearchInjector.js +2 -2
- package/dist/completion/openrouter-headers.cjs +3 -6
- package/dist/completion/openrouter-headers.d.cts +9 -12
- package/dist/completion/openrouter-headers.d.ts +9 -12
- package/dist/completion/openrouter-headers.js +2 -5
- package/dist/completion/openrouter-models.cjs +4 -3
- package/dist/completion/openrouter-models.js +2 -1
- package/dist/completion.cjs +30 -17
- package/dist/completion.js +29 -16
- package/dist/index.cjs +151 -20
- package/dist/index.d.cts +44 -10
- package/dist/index.d.ts +44 -10
- package/dist/index.js +160 -29
- package/dist/keyPolicy-Cp0FntZl.d.cts +90 -0
- package/dist/keyPolicy-Cp0FntZl.d.ts +90 -0
- package/dist/outbound-api/auditCapture.cjs +9 -0
- package/dist/outbound-api/auditCapture.d.cts +52 -0
- package/dist/outbound-api/auditCapture.d.ts +52 -0
- package/dist/outbound-api/auditCapture.js +9 -0
- package/dist/outbound-api/auditRedact.cjs +8 -0
- package/dist/outbound-api/auditRedact.d.cts +30 -0
- package/dist/outbound-api/auditRedact.d.ts +30 -0
- package/dist/outbound-api/auditRedact.js +8 -0
- package/dist/outbound-api/billingCapture.cjs +8 -0
- package/dist/outbound-api/billingCapture.d.cts +61 -0
- package/dist/outbound-api/billingCapture.d.ts +61 -0
- package/dist/outbound-api/billingCapture.js +8 -0
- package/dist/outbound-api/quotaWarn.cjs +12 -0
- package/dist/outbound-api/quotaWarn.d.cts +43 -0
- package/dist/outbound-api/quotaWarn.d.ts +43 -0
- package/dist/outbound-api/quotaWarn.js +12 -0
- package/dist/outbound-api/routeResolver.cjs +5 -2
- package/dist/outbound-api/routeResolver.d.cts +9 -2
- package/dist/outbound-api/routeResolver.d.ts +9 -2
- package/dist/outbound-api/routeResolver.js +4 -1
- package/dist/outbound-api/types.cjs +6 -1
- package/dist/outbound-api/types.d.cts +12 -144
- package/dist/outbound-api/types.d.ts +12 -144
- package/dist/outbound-api/types.js +6 -0
- package/dist/outbound-api.cjs +144 -17
- package/dist/outbound-api.d.cts +655 -25
- package/dist/outbound-api.d.ts +655 -25
- package/dist/outbound-api.js +148 -21
- package/dist/pipeline/AuthSource.d.cts +29 -1
- package/dist/pipeline/AuthSource.d.ts +29 -1
- package/dist/pipeline/LlmConfigProviderAuth.cjs +3 -3
- package/dist/pipeline/LlmConfigProviderAuth.js +2 -2
- package/dist/pipeline/SubscriptionAccountHealth.cjs +22 -0
- package/dist/pipeline/SubscriptionAccountHealth.d.cts +235 -0
- package/dist/pipeline/SubscriptionAccountHealth.d.ts +235 -0
- package/dist/pipeline/SubscriptionAccountHealth.js +22 -0
- package/dist/pipeline/SubscriptionAuthSource.cjs +2 -2
- package/dist/pipeline/SubscriptionAuthSource.d.cts +3 -2
- package/dist/pipeline/SubscriptionAuthSource.d.ts +3 -2
- package/dist/pipeline/SubscriptionAuthSource.js +1 -1
- package/dist/pipeline/SubscriptionAuthStrategy.d.cts +30 -1
- package/dist/pipeline/SubscriptionAuthStrategy.d.ts +30 -1
- package/dist/pipeline/auditSink.cjs +14 -0
- package/dist/pipeline/auditSink.d.cts +48 -0
- package/dist/pipeline/auditSink.d.ts +48 -0
- package/dist/pipeline/auditSink.js +14 -0
- package/dist/pipeline/auditUsageStash.cjs +10 -0
- package/dist/pipeline/auditUsageStash.d.cts +37 -0
- package/dist/pipeline/auditUsageStash.d.ts +37 -0
- package/dist/pipeline/auditUsageStash.js +10 -0
- package/dist/pipeline/billingEmit.cjs +14 -0
- package/dist/pipeline/billingEmit.d.cts +49 -0
- package/dist/pipeline/billingEmit.d.ts +49 -0
- package/dist/pipeline/billingEmit.js +14 -0
- package/dist/pipeline/executeProviderCall.cjs +2 -2
- package/dist/pipeline/executeProviderCall.js +1 -1
- package/dist/pipeline/upstreamFetch.cjs +14 -0
- package/dist/pipeline/upstreamFetch.d.cts +83 -0
- package/dist/pipeline/upstreamFetch.d.ts +83 -0
- package/dist/pipeline/upstreamFetch.js +14 -0
- package/dist/pipeline/webhookEmit.cjs +10 -0
- package/dist/pipeline/webhookEmit.d.cts +36 -0
- package/dist/pipeline/webhookEmit.d.ts +36 -0
- package/dist/pipeline/webhookEmit.js +10 -0
- package/dist/ports/usage-event-store.d.cts +26 -1
- package/dist/ports/usage-event-store.d.ts +26 -1
- package/dist/ports.d.cts +8 -1
- package/dist/ports.d.ts +8 -1
- package/dist/provider-proxy/ProviderProxy.cjs +30 -17
- package/dist/provider-proxy/ProviderProxy.js +29 -16
- package/dist/provider-proxy/identity/SubscriptionIdentityStore.cjs +15 -0
- package/dist/provider-proxy/identity/SubscriptionIdentityStore.d.cts +121 -0
- package/dist/provider-proxy/identity/SubscriptionIdentityStore.d.ts +121 -0
- package/dist/provider-proxy/identity/SubscriptionIdentityStore.js +15 -0
- package/dist/provider-proxy/identity/fingerprintHeaders.cjs +20 -0
- package/dist/provider-proxy/identity/fingerprintHeaders.d.cts +89 -0
- package/dist/provider-proxy/identity/fingerprintHeaders.d.ts +89 -0
- package/dist/provider-proxy/identity/fingerprintHeaders.js +20 -0
- package/dist/provider-proxy/ingress/providerProxyShared.cjs +30 -17
- package/dist/provider-proxy/ingress/providerProxyShared.d.cts +11 -2
- package/dist/provider-proxy/ingress/providerProxyShared.d.ts +11 -2
- package/dist/provider-proxy/ingress/providerProxyShared.js +29 -16
- package/dist/provider-proxy/matchText.cjs +4 -2
- package/dist/provider-proxy/matchText.d.cts +14 -1
- package/dist/provider-proxy/matchText.d.ts +14 -1
- package/dist/provider-proxy/matchText.js +3 -1
- package/dist/provider-proxy/types.d.cts +30 -0
- package/dist/provider-proxy/types.d.ts +30 -0
- package/dist/provider-proxy.cjs +30 -17
- package/dist/provider-proxy.js +29 -16
- package/dist/{routeResolver-HE-ZO0fO.d.ts → routeResolver-C4T7yClx.d.ts} +36 -10
- package/dist/{routeResolver-BrbK6ja9.d.cts → routeResolver-CB3tvJKy.d.cts} +36 -10
- package/dist/transformer/transformers/OpenAIResponseTransformer.cjs +2 -2
- package/dist/transformer/transformers/OpenAIResponseTransformer.js +1 -1
- package/dist/transformer/transformers.cjs +7 -7
- package/dist/transformer/transformers.js +8 -8
- package/dist/transformer.cjs +9 -9
- package/dist/transformer.js +8 -8
- package/dist/types-B8Arpxn0.d.cts +754 -0
- package/dist/types-lgPYQuZS.d.ts +754 -0
- package/dist/usage/usage-recorder.cjs +3 -2
- package/dist/usage/usage-recorder.d.cts +19 -1
- package/dist/usage/usage-recorder.d.ts +19 -1
- package/dist/usage/usage-recorder.js +2 -1
- package/dist/usage.cjs +3 -2
- package/dist/usage.js +2 -1
- package/package.json +64 -62
- package/dist/chunk-3MEACFK3.js +0 -193
- package/dist/chunk-6VIXXLMX.cjs +0 -14
- package/dist/chunk-E3WHL7CO.js +0 -14
- package/dist/chunk-G2FUJNA2.cjs +0 -193
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } var _class;// src/pipeline/SubscriptionAccountHealth.ts
|
|
2
|
+
var OVERLOAD_TTL_MS = 10 * 6e4;
|
|
3
|
+
var OVERLOAD_ENABLED_DEFAULT = true;
|
|
4
|
+
var AUTH_ERROR_TTL_MS = 30 * 6e4;
|
|
5
|
+
var SERVER_ERROR_TTL_MS = 5 * 6e4;
|
|
6
|
+
var BAN_BODY_MARKERS = [
|
|
7
|
+
"this organization has been disabled",
|
|
8
|
+
"oauth authentication is currently not allowed"
|
|
9
|
+
];
|
|
10
|
+
function readHeader(headers, name) {
|
|
11
|
+
if (typeof headers.get === "function") {
|
|
12
|
+
return headers.get(name);
|
|
13
|
+
}
|
|
14
|
+
const rec = headers;
|
|
15
|
+
return _nullishCoalesce(_nullishCoalesce(rec[name], () => ( rec[name.toLowerCase()])), () => ( null));
|
|
16
|
+
}
|
|
17
|
+
function parseIntHeader(value) {
|
|
18
|
+
if (value == null) return null;
|
|
19
|
+
const n = Number.parseInt(value.trim(), 10);
|
|
20
|
+
return Number.isFinite(n) ? n : null;
|
|
21
|
+
}
|
|
22
|
+
function resolveResetSeconds(providerId, headers) {
|
|
23
|
+
const unified = parseIntHeader(readHeader(headers, "anthropic-ratelimit-unified-reset"));
|
|
24
|
+
if (providerId === "claude") {
|
|
25
|
+
return { resetHeaderSeconds: unified, retryAfterSeconds: null };
|
|
26
|
+
}
|
|
27
|
+
const providerReset = _nullishCoalesce(parseIntHeader(readHeader(headers, "x-ratelimit-reset")), () => ( parseIntHeader(readHeader(headers, "ratelimit-reset"))));
|
|
28
|
+
const retryAfter = parseIntHeader(readHeader(headers, "retry-after"));
|
|
29
|
+
return {
|
|
30
|
+
resetHeaderSeconds: _nullishCoalesce(unified, () => ( providerReset)),
|
|
31
|
+
retryAfterSeconds: retryAfter
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function isBanBody(bodyText) {
|
|
35
|
+
if (!bodyText) return false;
|
|
36
|
+
const lower = bodyText.toLowerCase();
|
|
37
|
+
return BAN_BODY_MARKERS.some((marker) => lower.includes(marker));
|
|
38
|
+
}
|
|
39
|
+
function isRecordEmpty(record) {
|
|
40
|
+
return record.rateLimitEndAt === void 0 && record.overloadUntil === void 0 && record.tempUnavailableUntil === void 0 && !record.blocked;
|
|
41
|
+
}
|
|
42
|
+
var KEY_SEP = "\0";
|
|
43
|
+
var SubscriptionAccountHealth = (_class = class {
|
|
44
|
+
__init() {this.records = /* @__PURE__ */ new Map()}
|
|
45
|
+
__init2() {this.listeners = /* @__PURE__ */ new Set()}
|
|
46
|
+
__init3() {this.anomalyListeners = /* @__PURE__ */ new Set()}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
constructor(opts = {}) {;_class.prototype.__init.call(this);_class.prototype.__init2.call(this);_class.prototype.__init3.call(this);
|
|
53
|
+
this.now = _nullishCoalesce(opts.now, () => ( Date.now));
|
|
54
|
+
this.overloadEnabled = _nullishCoalesce(opts.overloadEnabled, () => ( OVERLOAD_ENABLED_DEFAULT));
|
|
55
|
+
this.overloadTtlMs = _nullishCoalesce(opts.overloadTtlMs, () => ( OVERLOAD_TTL_MS));
|
|
56
|
+
this.authErrorTtlMs = _nullishCoalesce(opts.authErrorTtlMs, () => ( AUTH_ERROR_TTL_MS));
|
|
57
|
+
this.serverErrorTtlMs = _nullishCoalesce(opts.serverErrorTtlMs, () => ( SERVER_ERROR_TTL_MS));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Re-apply cooldown config to the LIVE shared instance (subscription-account-
|
|
61
|
+
* health, LEAD OQ1). `buildDaemon` is synchronous so it cannot await the
|
|
62
|
+
* persisted server config; the async `start.ts` path loads it and applies the
|
|
63
|
+
* `accountHealth` segment here — every strategy/relay/sweeper that already
|
|
64
|
+
* captured this instance picks the new values up. Only defined fields override.
|
|
65
|
+
*/
|
|
66
|
+
configure(opts) {
|
|
67
|
+
if (opts.overloadEnabled !== void 0) this.overloadEnabled = opts.overloadEnabled;
|
|
68
|
+
if (opts.overloadTtlMs !== void 0) this.overloadTtlMs = opts.overloadTtlMs;
|
|
69
|
+
if (opts.authErrorTtlMs !== void 0) this.authErrorTtlMs = opts.authErrorTtlMs;
|
|
70
|
+
if (opts.serverErrorTtlMs !== void 0) this.serverErrorTtlMs = opts.serverErrorTtlMs;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Whether an account may be scheduled RIGHT NOW. `blocked` → false; any timer
|
|
74
|
+
* still in the future → false. Expired timers are lazily deleted on read (CRS
|
|
75
|
+
* lazy-clear parity) so an elapsed cooldown restores the account WITHOUT a
|
|
76
|
+
* timer — but the recovery SIGNAL is the sweeper's job (`sweepRecoveries`), so
|
|
77
|
+
* this read never emits (correctness is independent of the tick).
|
|
78
|
+
*/
|
|
79
|
+
isSchedulable(providerId, accountId, now = this.now()) {
|
|
80
|
+
const key = this.key(providerId, accountId);
|
|
81
|
+
const record = this.records.get(key);
|
|
82
|
+
if (!record) return true;
|
|
83
|
+
if (record.blocked) return false;
|
|
84
|
+
const active = this.clearExpired(record, now);
|
|
85
|
+
if (!active && isRecordEmpty(record)) this.records.delete(key);
|
|
86
|
+
return !active;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The single marking entry point (design D3). Faithful to CRS status semantics:
|
|
90
|
+
* - 429 + authoritative reset → `rateLimitEndAt`; a non-claude 429 may use
|
|
91
|
+
* `retryAfterSeconds`; a bare 429 (no resolvable reset) is NOT marked;
|
|
92
|
+
* - 529 → `overloadUntil` (gated by `overloadEnabled`);
|
|
93
|
+
* - 403-ban body → `blocked` (permanent); plain 403 → transient;
|
|
94
|
+
* - final-401 → transient; 5xx / thrown(null) → transient;
|
|
95
|
+
* - non-429 4xx → NEUTRAL (never marked);
|
|
96
|
+
* - 2xx → clear rate-limit + transient state (keep `blocked`).
|
|
97
|
+
*/
|
|
98
|
+
recordUpstreamOutcome(providerId, accountId, input) {
|
|
99
|
+
const now = _nullishCoalesce(input.now, () => ( this.now()));
|
|
100
|
+
const key = this.key(providerId, accountId);
|
|
101
|
+
const preExisting = this.records.get(key);
|
|
102
|
+
const record = _nullishCoalesce(preExisting, () => ( {}));
|
|
103
|
+
const status = input.status;
|
|
104
|
+
const wasUnhealthyBefore = preExisting ? this.isUnhealthyAt(preExisting, now) : false;
|
|
105
|
+
let anomalyState;
|
|
106
|
+
let recovered = false;
|
|
107
|
+
if (status === null) {
|
|
108
|
+
record.tempUnavailableUntil = now + this.serverErrorTtlMs;
|
|
109
|
+
} else if (status >= 200 && status < 300) {
|
|
110
|
+
const wasUnhealthy = record.rateLimitEndAt !== void 0 || record.overloadUntil !== void 0 || record.tempUnavailableUntil !== void 0;
|
|
111
|
+
delete record.rateLimitEndAt;
|
|
112
|
+
delete record.overloadUntil;
|
|
113
|
+
delete record.tempUnavailableUntil;
|
|
114
|
+
recovered = wasUnhealthy && !record.blocked;
|
|
115
|
+
} else if (status === 429) {
|
|
116
|
+
if (input.resetHeaderSeconds != null) {
|
|
117
|
+
record.rateLimitEndAt = input.resetHeaderSeconds * 1e3;
|
|
118
|
+
anomalyState = "rate_limited";
|
|
119
|
+
} else if (input.retryAfterSeconds != null) {
|
|
120
|
+
record.rateLimitEndAt = now + input.retryAfterSeconds * 1e3;
|
|
121
|
+
anomalyState = "rate_limited";
|
|
122
|
+
}
|
|
123
|
+
} else if (status === 529) {
|
|
124
|
+
if (this.overloadEnabled) {
|
|
125
|
+
record.overloadUntil = now + this.overloadTtlMs;
|
|
126
|
+
anomalyState = "overloaded";
|
|
127
|
+
}
|
|
128
|
+
} else if (status === 403) {
|
|
129
|
+
if (isBanBody(input.bodyText)) {
|
|
130
|
+
record.blocked = true;
|
|
131
|
+
anomalyState = "blocked";
|
|
132
|
+
} else {
|
|
133
|
+
record.tempUnavailableUntil = now + this.authErrorTtlMs;
|
|
134
|
+
anomalyState = "unauthorized";
|
|
135
|
+
}
|
|
136
|
+
} else if (status === 401) {
|
|
137
|
+
record.tempUnavailableUntil = now + this.authErrorTtlMs;
|
|
138
|
+
anomalyState = "unauthorized";
|
|
139
|
+
} else if (status >= 500) {
|
|
140
|
+
record.tempUnavailableUntil = now + this.serverErrorTtlMs;
|
|
141
|
+
}
|
|
142
|
+
if (isRecordEmpty(record)) this.records.delete(key);
|
|
143
|
+
else this.records.set(key, record);
|
|
144
|
+
if (recovered) this.emit({ providerId, accountId, at: now, kind: "rateLimitRecovery" });
|
|
145
|
+
if (anomalyState && !wasUnhealthyBefore) {
|
|
146
|
+
this.emitAnomaly({ providerId, accountId, at: now, state: anomalyState });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/** Whether a record is unhealthy at `now`: blocked, or any timer still future. */
|
|
150
|
+
isUnhealthyAt(record, now) {
|
|
151
|
+
if (record.blocked) return true;
|
|
152
|
+
if (record.rateLimitEndAt !== void 0 && now < record.rateLimitEndAt) return true;
|
|
153
|
+
if (record.overloadUntil !== void 0 && now < record.overloadUntil) return true;
|
|
154
|
+
if (record.tempUnavailableUntil !== void 0 && now < record.tempUnavailableUntil) return true;
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Clear ONLY the auth/transient unavailability mark (`tempUnavailableUntil`) —
|
|
159
|
+
* the narrow healing a background PROBE is allowed to do (subscription-account-
|
|
160
|
+
* probe #8, review M1). A probe 2xx from the lightweight `GET /v1/models` attests
|
|
161
|
+
* the TOKEN works (so a final-401 / plain-403 / 5xx transient may be healed), but
|
|
162
|
+
* it does NOT prove the traffic endpoint's rate-limit / overload has recovered —
|
|
163
|
+
* so it MUST NOT touch `rateLimitEndAt` / `overloadUntil` (those clear only on the
|
|
164
|
+
* upstream's authoritative reset, or a REAL-traffic 2xx). `blocked` (a ban) is
|
|
165
|
+
* never cleared here. Distinct from `recordUpstreamOutcome`'s 2xx path, which is
|
|
166
|
+
* UNCHANGED and still clears rate + transient together because it IS traffic.
|
|
167
|
+
* Returns whether a transient mark was actually cleared. Does NOT emit a recovery
|
|
168
|
+
* signal (a probe is internal maintenance; the account is schedulable-on-read).
|
|
169
|
+
*/
|
|
170
|
+
clearTransientMark(providerId, accountId) {
|
|
171
|
+
const key = this.key(providerId, accountId);
|
|
172
|
+
const record = this.records.get(key);
|
|
173
|
+
if (!record || record.tempUnavailableUntil === void 0) return false;
|
|
174
|
+
delete record.tempUnavailableUntil;
|
|
175
|
+
if (isRecordEmpty(record)) this.records.delete(key);
|
|
176
|
+
else this.records.set(key, record);
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Proactively surface accounts that transitioned unhealthy → schedulable since
|
|
181
|
+
* the last sweep (design D6). Clears their expired timers, emits a recovery
|
|
182
|
+
* signal for each through `onRecovered`, and returns the list. A `blocked`
|
|
183
|
+
* account never recovers here. Correctness does NOT depend on this running —
|
|
184
|
+
* `isSchedulable` already restores an elapsed cooldown lazily; this adds the
|
|
185
|
+
* proactive signal (#5/#8) + lets the daemon nudge a token refresh.
|
|
186
|
+
*/
|
|
187
|
+
sweepRecoveries(now = this.now()) {
|
|
188
|
+
const recovered = [];
|
|
189
|
+
for (const [key, record] of this.records) {
|
|
190
|
+
if (record.blocked) continue;
|
|
191
|
+
const hadTimer = record.rateLimitEndAt !== void 0 || record.overloadUntil !== void 0 || record.tempUnavailableUntil !== void 0;
|
|
192
|
+
const active = this.clearExpired(record, now);
|
|
193
|
+
if (hadTimer && !active && isRecordEmpty(record)) {
|
|
194
|
+
const [pid, aid] = this.parseKey(key);
|
|
195
|
+
recovered.push({ providerId: pid, accountId: aid, at: now, kind: "rateLimitRecovery" });
|
|
196
|
+
this.records.delete(key);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
for (const event of recovered) this.emit(event);
|
|
200
|
+
return recovered;
|
|
201
|
+
}
|
|
202
|
+
/** Admin-facing coarse status + cooldown-until for one account (secret-free). */
|
|
203
|
+
getStatus(providerId, accountId, now = this.now()) {
|
|
204
|
+
const record = this.records.get(this.key(providerId, accountId));
|
|
205
|
+
if (!record) return { state: "healthy" };
|
|
206
|
+
if (record.blocked) return { state: "blocked" };
|
|
207
|
+
if (record.rateLimitEndAt !== void 0 && now < record.rateLimitEndAt) {
|
|
208
|
+
return { state: "rate_limited", cooldownUntil: record.rateLimitEndAt };
|
|
209
|
+
}
|
|
210
|
+
if (record.overloadUntil !== void 0 && now < record.overloadUntil) {
|
|
211
|
+
return { state: "overloaded", cooldownUntil: record.overloadUntil };
|
|
212
|
+
}
|
|
213
|
+
if (record.tempUnavailableUntil !== void 0 && now < record.tempUnavailableUntil) {
|
|
214
|
+
return { state: "transient", cooldownUntil: record.tempUnavailableUntil };
|
|
215
|
+
}
|
|
216
|
+
return { state: "healthy" };
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Register a recovery listener (the OQ3 seam #5 webhooks + #8 health-cron
|
|
220
|
+
* consume). Returns an unsubscribe function. Two edges fire it:
|
|
221
|
+
* - TRAFFIC-DRIVEN: a `recordUpstreamOutcome` 2xx that clears an unhealthy
|
|
222
|
+
* account (guaranteed — the sweep would miss it, as the 2xx deletes the
|
|
223
|
+
* record first); and
|
|
224
|
+
* - PURE-IDLE: `sweepRecoveries` when an idle account's cooldown elapsed with
|
|
225
|
+
* no traffic (best-effort, on the 60s tick).
|
|
226
|
+
* A lazy `isSchedulable` read that heals an account does NOT emit (a request is
|
|
227
|
+
* already using it) — so consumers never double-count a recovery.
|
|
228
|
+
*/
|
|
229
|
+
onRecovered(listener) {
|
|
230
|
+
this.listeners.add(listener);
|
|
231
|
+
return () => this.listeners.delete(listener);
|
|
232
|
+
}
|
|
233
|
+
emit(event) {
|
|
234
|
+
for (const listener of this.listeners) {
|
|
235
|
+
try {
|
|
236
|
+
listener(event);
|
|
237
|
+
} catch (e) {
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Register an anomaly listener (the ADDITIVE seam #5 webhooks consumes; the
|
|
243
|
+
* frozen `kind` discriminator on {@link AccountRecoveryEvent} anticipated it).
|
|
244
|
+
* Fires ONLY on the healthy→unhealthy EDGE inside `recordUpstreamOutcome`
|
|
245
|
+
* (de-duped: an already-unhealthy account escalating does NOT re-fire). Returns
|
|
246
|
+
* an unsubscribe function. Existing marking/recovery behavior is UNCHANGED.
|
|
247
|
+
*/
|
|
248
|
+
onAnomaly(listener) {
|
|
249
|
+
this.anomalyListeners.add(listener);
|
|
250
|
+
return () => this.anomalyListeners.delete(listener);
|
|
251
|
+
}
|
|
252
|
+
emitAnomaly(event) {
|
|
253
|
+
for (const listener of this.anomalyListeners) {
|
|
254
|
+
try {
|
|
255
|
+
listener(event);
|
|
256
|
+
} catch (e2) {
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Delete every EXPIRED timer field on the record in place; return whether any
|
|
262
|
+
* timer is STILL active (in the future). `blocked` is not a timer and is left
|
|
263
|
+
* untouched here (only an explicit reset / restart clears a ban).
|
|
264
|
+
*/
|
|
265
|
+
clearExpired(record, now) {
|
|
266
|
+
let active = false;
|
|
267
|
+
if (record.rateLimitEndAt !== void 0) {
|
|
268
|
+
if (now >= record.rateLimitEndAt) delete record.rateLimitEndAt;
|
|
269
|
+
else active = true;
|
|
270
|
+
}
|
|
271
|
+
if (record.overloadUntil !== void 0) {
|
|
272
|
+
if (now >= record.overloadUntil) delete record.overloadUntil;
|
|
273
|
+
else active = true;
|
|
274
|
+
}
|
|
275
|
+
if (record.tempUnavailableUntil !== void 0) {
|
|
276
|
+
if (now >= record.tempUnavailableUntil) delete record.tempUnavailableUntil;
|
|
277
|
+
else active = true;
|
|
278
|
+
}
|
|
279
|
+
return active;
|
|
280
|
+
}
|
|
281
|
+
key(providerId, accountId) {
|
|
282
|
+
return `${providerId}${KEY_SEP}${accountId}`;
|
|
283
|
+
}
|
|
284
|
+
parseKey(key) {
|
|
285
|
+
const idx = key.indexOf(KEY_SEP);
|
|
286
|
+
return [key.slice(0, idx), key.slice(idx + 1)];
|
|
287
|
+
}
|
|
288
|
+
}, _class);
|
|
289
|
+
var _shared = null;
|
|
290
|
+
function getSharedAccountHealth() {
|
|
291
|
+
if (!_shared) _shared = new SubscriptionAccountHealth();
|
|
292
|
+
return _shared;
|
|
293
|
+
}
|
|
294
|
+
function setSharedAccountHealth(instance) {
|
|
295
|
+
_shared = instance;
|
|
296
|
+
}
|
|
297
|
+
function __resetSharedAccountHealthForTests() {
|
|
298
|
+
_shared = null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
exports.OVERLOAD_TTL_MS = OVERLOAD_TTL_MS; exports.OVERLOAD_ENABLED_DEFAULT = OVERLOAD_ENABLED_DEFAULT; exports.AUTH_ERROR_TTL_MS = AUTH_ERROR_TTL_MS; exports.SERVER_ERROR_TTL_MS = SERVER_ERROR_TTL_MS; exports.resolveResetSeconds = resolveResetSeconds; exports.SubscriptionAccountHealth = SubscriptionAccountHealth; exports.getSharedAccountHealth = getSharedAccountHealth; exports.setSharedAccountHealth = setSharedAccountHealth; exports.__resetSharedAccountHealthForTests = __resetSharedAccountHealthForTests;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getBillingCaptureConfig,
|
|
3
|
+
publishBillingEvent
|
|
4
|
+
} from "./chunk-TKJMZBAH.js";
|
|
5
|
+
import {
|
|
6
|
+
readAuditUsage
|
|
7
|
+
} from "./chunk-OZFM4X3S.js";
|
|
8
|
+
|
|
9
|
+
// src/outbound-api/billingCapture.ts
|
|
10
|
+
import { randomUUID } from "crypto";
|
|
11
|
+
var defaultDefer = (fn) => {
|
|
12
|
+
setTimeout(fn, 0);
|
|
13
|
+
};
|
|
14
|
+
function beginBillingCapture(_req, res, now, defer = defaultDefer) {
|
|
15
|
+
if (!getBillingCaptureConfig()) return null;
|
|
16
|
+
const ctx = {};
|
|
17
|
+
let finished = false;
|
|
18
|
+
const finalize = () => {
|
|
19
|
+
if (finished) return;
|
|
20
|
+
finished = true;
|
|
21
|
+
const status = res.statusCode;
|
|
22
|
+
defer(() => {
|
|
23
|
+
try {
|
|
24
|
+
const usage = readAuditUsage(res);
|
|
25
|
+
if (!usage) return;
|
|
26
|
+
const event = {
|
|
27
|
+
id: randomUUID(),
|
|
28
|
+
ts: now,
|
|
29
|
+
model: ctx.model ?? usage.model ?? "",
|
|
30
|
+
authMode: ctx.authMode ?? "byo",
|
|
31
|
+
inputTokens: usage.inputTokens ?? 0,
|
|
32
|
+
outputTokens: usage.outputTokens ?? 0,
|
|
33
|
+
costUsd: usage.costUsd ?? 0,
|
|
34
|
+
status
|
|
35
|
+
};
|
|
36
|
+
if (ctx.keyId != null) event.keyId = ctx.keyId;
|
|
37
|
+
const provider = ctx.provider ?? usage.provider;
|
|
38
|
+
if (provider) event.provider = provider;
|
|
39
|
+
publishBillingEvent(event);
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
res.once("close", finalize);
|
|
45
|
+
return ctx;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export {
|
|
49
|
+
beginBillingCapture
|
|
50
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// src/pipeline/auditSink.ts
|
|
2
|
+
var sink = null;
|
|
3
|
+
var captureConfig = null;
|
|
4
|
+
function setAuditSink(fn) {
|
|
5
|
+
sink = fn;
|
|
6
|
+
}
|
|
7
|
+
function setAuditCaptureConfig(config) {
|
|
8
|
+
captureConfig = config && config.enabled ? config : null;
|
|
9
|
+
}
|
|
10
|
+
function getAuditCaptureConfig() {
|
|
11
|
+
return captureConfig;
|
|
12
|
+
}
|
|
13
|
+
function recordAudit(record) {
|
|
14
|
+
const active = sink;
|
|
15
|
+
if (!active) return;
|
|
16
|
+
try {
|
|
17
|
+
active(record);
|
|
18
|
+
} catch {
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function __resetAuditSinkForTests() {
|
|
22
|
+
sink = null;
|
|
23
|
+
captureConfig = null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
setAuditSink,
|
|
28
|
+
setAuditCaptureConfig,
|
|
29
|
+
getAuditCaptureConfig,
|
|
30
|
+
recordAudit,
|
|
31
|
+
__resetAuditSinkForTests
|
|
32
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// src/pipeline/upstreamFetch.ts
|
|
2
|
+
import { Agent, buildConnector, ProxyAgent } from "undici";
|
|
3
|
+
import { SocksClient } from "socks";
|
|
4
|
+
var resolver = null;
|
|
5
|
+
var generation = 0;
|
|
6
|
+
var cache = /* @__PURE__ */ new Map();
|
|
7
|
+
function setUpstreamProxyResolver(fn) {
|
|
8
|
+
resolver = fn;
|
|
9
|
+
bumpUpstreamProxyGeneration();
|
|
10
|
+
}
|
|
11
|
+
function bumpUpstreamProxyGeneration() {
|
|
12
|
+
generation += 1;
|
|
13
|
+
const old = cache;
|
|
14
|
+
cache = /* @__PURE__ */ new Map();
|
|
15
|
+
for (const dispatcher of old.values()) {
|
|
16
|
+
try {
|
|
17
|
+
void dispatcher.close?.();
|
|
18
|
+
} catch {
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function normalizeProxy(cfg) {
|
|
23
|
+
if ("url" in cfg) {
|
|
24
|
+
let u;
|
|
25
|
+
try {
|
|
26
|
+
u = new URL(cfg.url);
|
|
27
|
+
} catch {
|
|
28
|
+
return void 0;
|
|
29
|
+
}
|
|
30
|
+
const proto = u.protocol.replace(/:$/, "").toLowerCase();
|
|
31
|
+
const kind2 = proto === "socks5" || proto === "socks" ? "socks5" : proto === "https" ? "https" : "http";
|
|
32
|
+
const port = u.port ? Number(u.port) : kind2 === "https" ? 443 : kind2 === "socks5" ? 1080 : 80;
|
|
33
|
+
if (!u.hostname || !Number.isFinite(port)) return void 0;
|
|
34
|
+
return {
|
|
35
|
+
kind: kind2,
|
|
36
|
+
host: u.hostname,
|
|
37
|
+
port,
|
|
38
|
+
username: u.username ? decodeURIComponent(u.username) : void 0,
|
|
39
|
+
password: u.password ? decodeURIComponent(u.password) : void 0
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const kind = cfg.type === "socks5" ? "socks5" : cfg.type === "https" ? "https" : "http";
|
|
43
|
+
if (!cfg.host || !Number.isFinite(cfg.port)) return void 0;
|
|
44
|
+
return {
|
|
45
|
+
kind,
|
|
46
|
+
host: cfg.host,
|
|
47
|
+
port: cfg.port,
|
|
48
|
+
username: cfg.username || void 0,
|
|
49
|
+
password: cfg.password || void 0
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function proxyKey(p) {
|
|
53
|
+
return `${p.kind}://${p.username ?? ""}:${p.password ?? ""}@${p.host}:${p.port}`;
|
|
54
|
+
}
|
|
55
|
+
function socksConnector(p) {
|
|
56
|
+
const base = buildConnector({});
|
|
57
|
+
return ((options, callback) => {
|
|
58
|
+
const destPort = Number(options.port) || (options.protocol === "https:" ? 443 : 80);
|
|
59
|
+
SocksClient.createConnection({
|
|
60
|
+
proxy: { host: p.host, port: p.port, type: 5, userId: p.username, password: p.password },
|
|
61
|
+
command: "connect",
|
|
62
|
+
destination: { host: options.hostname, port: destPort }
|
|
63
|
+
}).then(({ socket }) => {
|
|
64
|
+
base({ ...options, httpSocket: socket }, callback);
|
|
65
|
+
}).catch((err) => callback(err, null));
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function buildDispatcher(p) {
|
|
69
|
+
if (p.kind === "socks5") {
|
|
70
|
+
return new Agent({ connect: socksConnector(p) });
|
|
71
|
+
}
|
|
72
|
+
const auth = p.username != null ? `${encodeURIComponent(p.username)}:${encodeURIComponent(p.password ?? "")}@` : "";
|
|
73
|
+
const uri = `${p.kind}://${auth}${p.host}:${p.port}`;
|
|
74
|
+
return new ProxyAgent({ uri });
|
|
75
|
+
}
|
|
76
|
+
function resolveUpstreamDispatcher(ctx) {
|
|
77
|
+
const active = resolver;
|
|
78
|
+
if (!active) return void 0;
|
|
79
|
+
let cfg;
|
|
80
|
+
try {
|
|
81
|
+
cfg = active(ctx);
|
|
82
|
+
} catch {
|
|
83
|
+
return void 0;
|
|
84
|
+
}
|
|
85
|
+
if (!cfg) return void 0;
|
|
86
|
+
const norm = normalizeProxy(cfg);
|
|
87
|
+
if (!norm) return void 0;
|
|
88
|
+
const key = `${generation}\0${proxyKey(norm)}`;
|
|
89
|
+
let dispatcher = cache.get(key);
|
|
90
|
+
if (!dispatcher) {
|
|
91
|
+
dispatcher = buildDispatcher(norm);
|
|
92
|
+
cache.set(key, dispatcher);
|
|
93
|
+
}
|
|
94
|
+
return dispatcher;
|
|
95
|
+
}
|
|
96
|
+
function fetchUpstream(url, init, ctx) {
|
|
97
|
+
const dispatcher = resolveUpstreamDispatcher({ ...ctx ?? {}, url });
|
|
98
|
+
if (!dispatcher) return fetch(url, init);
|
|
99
|
+
return fetch(url, { ...init, dispatcher });
|
|
100
|
+
}
|
|
101
|
+
function __resetUpstreamProxyForTests() {
|
|
102
|
+
resolver = null;
|
|
103
|
+
bumpUpstreamProxyGeneration();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export {
|
|
107
|
+
setUpstreamProxyResolver,
|
|
108
|
+
bumpUpstreamProxyGeneration,
|
|
109
|
+
resolveUpstreamDispatcher,
|
|
110
|
+
fetchUpstream,
|
|
111
|
+
__resetUpstreamProxyForTests
|
|
112
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true});// src/pipeline/auditUsageStash.ts
|
|
2
|
+
var stash = /* @__PURE__ */ new WeakMap();
|
|
3
|
+
function stashAuditUsage(key, detail) {
|
|
4
|
+
const existing = stash.get(key);
|
|
5
|
+
stash.set(key, existing ? { ...existing, ...detail } : { ...detail });
|
|
6
|
+
}
|
|
7
|
+
function readAuditUsage(key) {
|
|
8
|
+
return stash.get(key);
|
|
9
|
+
}
|
|
10
|
+
function __clearAuditUsageForTests(key) {
|
|
11
|
+
stash.delete(key);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
exports.stashAuditUsage = stashAuditUsage; exports.readAuditUsage = readAuditUsage; exports.__clearAuditUsageForTests = __clearAuditUsageForTests;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import {
|
|
2
|
+
refreshNonStainless,
|
|
3
|
+
sanitizeFrozenHeaders
|
|
4
|
+
} from "./chunk-XX6NQJMA.js";
|
|
5
|
+
|
|
6
|
+
// src/provider-proxy/identity/SubscriptionIdentityStore.ts
|
|
7
|
+
var CC_HEADER_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
8
|
+
var KEY_SEP = "\0";
|
|
9
|
+
function normalizeUa(ua) {
|
|
10
|
+
if (typeof ua !== "string") return void 0;
|
|
11
|
+
const trimmed = ua.trim();
|
|
12
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
13
|
+
}
|
|
14
|
+
var SubscriptionIdentityStore = class {
|
|
15
|
+
identities = /* @__PURE__ */ new Map();
|
|
16
|
+
now;
|
|
17
|
+
enabled;
|
|
18
|
+
ua;
|
|
19
|
+
ccHeaderTtlMs;
|
|
20
|
+
persistence = null;
|
|
21
|
+
constructor(opts = {}) {
|
|
22
|
+
this.now = opts.now ?? Date.now;
|
|
23
|
+
this.enabled = opts.enabled ?? false;
|
|
24
|
+
this.ua = normalizeUa(opts.ua);
|
|
25
|
+
this.ccHeaderTtlMs = opts.ccHeaderTtlMs ?? CC_HEADER_TTL_MS;
|
|
26
|
+
}
|
|
27
|
+
/** Whether capture/replay is engaged (config flag). */
|
|
28
|
+
isEnabled() {
|
|
29
|
+
return this.enabled;
|
|
30
|
+
}
|
|
31
|
+
/** The operator UA baseline (piece 4), or undefined when unset. */
|
|
32
|
+
uaBaseline() {
|
|
33
|
+
return this.ua;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Re-apply config to the LIVE shared instance (bootstrap, mirroring the health
|
|
37
|
+
* tracker's `configure`). Only defined fields override; `ua: null` clears the
|
|
38
|
+
* baseline.
|
|
39
|
+
*/
|
|
40
|
+
configure(opts) {
|
|
41
|
+
if (opts.enabled !== void 0) this.enabled = opts.enabled;
|
|
42
|
+
if (opts.ua !== void 0) this.ua = normalizeUa(opts.ua ?? void 0);
|
|
43
|
+
}
|
|
44
|
+
/** Install (or clear) the write-through persistence port (P2). */
|
|
45
|
+
setPersistence(port) {
|
|
46
|
+
this.persistence = port;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Boot seed (P2, D5): install a PERSISTED identity for an account, but NEVER
|
|
50
|
+
* over a live in-memory capture (a re-captured identity from this session wins).
|
|
51
|
+
* A no-op for an empty/absent identity. Does NOT write back through the port.
|
|
52
|
+
*/
|
|
53
|
+
seed(providerId, accountId, identity) {
|
|
54
|
+
if (!accountId || !identity) return;
|
|
55
|
+
const headers = sanitizeFrozenHeaders(identity.headers ?? {});
|
|
56
|
+
if (Object.keys(headers).length === 0) return;
|
|
57
|
+
const key = this.key(providerId, accountId);
|
|
58
|
+
if (this.identities.has(key)) return;
|
|
59
|
+
this.identities.set(key, {
|
|
60
|
+
headers,
|
|
61
|
+
capturedAt: typeof identity.capturedAt === "number" ? identity.capturedAt : this.now()
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Capture a real client's fingerprint headers for an account. First-seen ⇒
|
|
66
|
+
* FREEZE the whole (sanitized) bag. Already-frozen ⇒ within the TTL, no change
|
|
67
|
+
* (stable identity); past the TTL, refresh the NON-stainless headers from the
|
|
68
|
+
* new observation while keeping stainless frozen-stable (P2). An empty/absent
|
|
69
|
+
* account id or an empty sanitized bag is a no-op (so an account with no
|
|
70
|
+
* fingerprint headers never freezes an empty identity → the caller falls back
|
|
71
|
+
* to the UA baseline). Fires the persistence port on a freeze/refresh.
|
|
72
|
+
*/
|
|
73
|
+
capture(providerId, accountId, incoming, now = this.now()) {
|
|
74
|
+
if (!accountId) return;
|
|
75
|
+
const clean = sanitizeFrozenHeaders(incoming);
|
|
76
|
+
if (Object.keys(clean).length === 0) return;
|
|
77
|
+
const key = this.key(providerId, accountId);
|
|
78
|
+
const existing = this.identities.get(key);
|
|
79
|
+
if (!existing) {
|
|
80
|
+
const frozen = { headers: clean, capturedAt: now };
|
|
81
|
+
this.identities.set(key, frozen);
|
|
82
|
+
this.emitPersist(providerId, accountId, frozen);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (now - existing.capturedAt < this.ccHeaderTtlMs) return;
|
|
86
|
+
const refreshed = {
|
|
87
|
+
headers: refreshNonStainless(existing.headers, clean),
|
|
88
|
+
capturedAt: now
|
|
89
|
+
};
|
|
90
|
+
this.identities.set(key, refreshed);
|
|
91
|
+
this.emitPersist(providerId, accountId, refreshed);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The frozen identity headers for an account, or `undefined` when none is
|
|
95
|
+
* frozen (or the frozen set is empty). Returns a COPY (the caller merges it into
|
|
96
|
+
* a mutable outbound header bag).
|
|
97
|
+
*/
|
|
98
|
+
replay(providerId, accountId) {
|
|
99
|
+
if (!accountId) return void 0;
|
|
100
|
+
const frozen = this.identities.get(this.key(providerId, accountId));
|
|
101
|
+
if (!frozen || Object.keys(frozen.headers).length === 0) return void 0;
|
|
102
|
+
return { ...frozen.headers };
|
|
103
|
+
}
|
|
104
|
+
/** Whether an identity is frozen for an account (admin coarse status — D7). */
|
|
105
|
+
hasIdentity(providerId, accountId) {
|
|
106
|
+
const frozen = this.identities.get(this.key(providerId, accountId));
|
|
107
|
+
return !!frozen && Object.keys(frozen.headers).length > 0;
|
|
108
|
+
}
|
|
109
|
+
/** The freeze/refresh time for an account's identity (admin coarse status). */
|
|
110
|
+
capturedAt(providerId, accountId) {
|
|
111
|
+
return this.identities.get(this.key(providerId, accountId))?.capturedAt;
|
|
112
|
+
}
|
|
113
|
+
emitPersist(providerId, accountId, identity) {
|
|
114
|
+
if (!this.persistence) return;
|
|
115
|
+
try {
|
|
116
|
+
this.persistence.persist(providerId, accountId, {
|
|
117
|
+
headers: { ...identity.headers },
|
|
118
|
+
capturedAt: identity.capturedAt
|
|
119
|
+
});
|
|
120
|
+
} catch {
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
key(providerId, accountId) {
|
|
124
|
+
return `${providerId}${KEY_SEP}${accountId}`;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
var _shared = null;
|
|
128
|
+
function getSharedIdentityStore() {
|
|
129
|
+
if (!_shared) _shared = new SubscriptionIdentityStore();
|
|
130
|
+
return _shared;
|
|
131
|
+
}
|
|
132
|
+
function setSharedIdentityStore(instance) {
|
|
133
|
+
_shared = instance;
|
|
134
|
+
}
|
|
135
|
+
function __resetSharedIdentityStoreForTests() {
|
|
136
|
+
_shared = null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export {
|
|
140
|
+
CC_HEADER_TTL_MS,
|
|
141
|
+
SubscriptionIdentityStore,
|
|
142
|
+
getSharedIdentityStore,
|
|
143
|
+
setSharedIdentityStore,
|
|
144
|
+
__resetSharedIdentityStoreForTests
|
|
145
|
+
};
|