@cortexkit/antigravity-auth-core 2.2.1 → 3.0.0
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/README.md +24 -2
- package/dist/abort-forwarding.d.ts +3 -0
- package/dist/abort-forwarding.d.ts.map +1 -0
- package/dist/abort-forwarding.js +19 -0
- package/dist/abort-forwarding.js.map +1 -0
- package/dist/account-identity.d.ts +52 -0
- package/dist/account-identity.d.ts.map +1 -0
- package/dist/account-identity.js +66 -0
- package/dist/account-identity.js.map +1 -0
- package/dist/account-manager.d.ts +228 -43
- package/dist/account-manager.d.ts.map +1 -1
- package/dist/account-manager.js +942 -903
- package/dist/account-manager.js.map +1 -1
- package/dist/account-migration.d.ts +309 -0
- package/dist/account-migration.d.ts.map +1 -0
- package/dist/account-migration.js +2282 -0
- package/dist/account-migration.js.map +1 -0
- package/dist/account-repository-codecs.d.ts +82 -0
- package/dist/account-repository-codecs.d.ts.map +1 -0
- package/dist/account-repository-codecs.js +609 -0
- package/dist/account-repository-codecs.js.map +1 -0
- package/dist/account-repository-types.d.ts +831 -0
- package/dist/account-repository-types.d.ts.map +1 -0
- package/dist/account-repository-types.js +137 -0
- package/dist/account-repository-types.js.map +1 -0
- package/dist/account-repository.d.ts +442 -0
- package/dist/account-repository.d.ts.map +1 -0
- package/dist/account-repository.js +2253 -0
- package/dist/account-repository.js.map +1 -0
- package/dist/account-selector.d.ts +470 -0
- package/dist/account-selector.d.ts.map +1 -0
- package/dist/account-selector.js +1383 -0
- package/dist/account-selector.js.map +1 -0
- package/dist/account-storage.d.ts.map +1 -1
- package/dist/account-storage.js +5 -0
- package/dist/account-storage.js.map +1 -1
- package/dist/agy-transport.d.ts.map +1 -1
- package/dist/agy-transport.js +21 -35
- package/dist/agy-transport.js.map +1 -1
- package/dist/antigravity-command-menu.d.ts +171 -0
- package/dist/antigravity-command-menu.d.ts.map +1 -0
- package/dist/antigravity-command-menu.js +523 -0
- package/dist/antigravity-command-menu.js.map +1 -0
- package/dist/atomic-write.d.ts +18 -1
- package/dist/atomic-write.d.ts.map +1 -1
- package/dist/atomic-write.js +15 -3
- package/dist/atomic-write.js.map +1 -1
- package/dist/common-auth-embedded/NOTICE.txt +21 -0
- package/dist/common-auth-embedded/auth-menu/accounts.d.ts +97 -0
- package/dist/common-auth-embedded/auth-menu/accounts.js +387 -0
- package/dist/common-auth-embedded/auth-menu/ansi.d.ts +26 -0
- package/dist/common-auth-embedded/auth-menu/ansi.js +98 -0
- package/dist/common-auth-embedded/auth-menu/confirm.d.ts +7 -0
- package/dist/common-auth-embedded/auth-menu/confirm.js +21 -0
- package/dist/common-auth-embedded/auth-menu/doctor.d.ts +52 -0
- package/dist/common-auth-embedded/auth-menu/doctor.js +81 -0
- package/dist/common-auth-embedded/auth-menu/index.d.ts +17 -0
- package/dist/common-auth-embedded/auth-menu/index.js +12 -0
- package/dist/common-auth-embedded/auth-menu/login.d.ts +55 -0
- package/dist/common-auth-embedded/auth-menu/login.js +83 -0
- package/dist/common-auth-embedded/auth-menu/menu.d.ts +59 -0
- package/dist/common-auth-embedded/auth-menu/menu.js +73 -0
- package/dist/common-auth-embedded/auth-menu/opencode-v1.d.ts +49 -0
- package/dist/common-auth-embedded/auth-menu/opencode-v1.js +47 -0
- package/dist/common-auth-embedded/auth-menu/select.d.ts +21 -0
- package/dist/common-auth-embedded/auth-menu/select.js +159 -0
- package/dist/common-auth-embedded/auth-menu/terminal.d.ts +43 -0
- package/dist/common-auth-embedded/auth-menu/terminal.js +19 -0
- package/dist/common-auth-embedded/claustrum/consumer.d.ts +102 -0
- package/dist/common-auth-embedded/claustrum/consumer.js +298 -0
- package/dist/common-auth-embedded/claustrum/custody.d.ts +175 -0
- package/dist/common-auth-embedded/claustrum/custody.js +354 -0
- package/dist/common-auth-embedded/claustrum/enrollment.d.ts +158 -0
- package/dist/common-auth-embedded/claustrum/enrollment.js +759 -0
- package/dist/common-auth-embedded/claustrum/errors.d.ts +16 -0
- package/dist/common-auth-embedded/claustrum/errors.js +8 -0
- package/dist/common-auth-embedded/claustrum/host-slot.d.ts +39 -0
- package/dist/common-auth-embedded/claustrum/host-slot.js +72 -0
- package/dist/common-auth-embedded/claustrum/index.d.ts +18 -0
- package/dist/common-auth-embedded/claustrum/index.js +22 -0
- package/dist/common-auth-embedded/claustrum/interlock.d.ts +27 -0
- package/dist/common-auth-embedded/claustrum/interlock.js +29 -0
- package/dist/common-auth-embedded/claustrum/roster.d.ts +208 -0
- package/dist/common-auth-embedded/claustrum/roster.js +536 -0
- package/dist/common-auth-embedded/commands/builtins.d.ts +71 -0
- package/dist/common-auth-embedded/commands/builtins.js +534 -0
- package/dist/common-auth-embedded/commands/index.d.ts +10 -0
- package/dist/common-auth-embedded/commands/index.js +5 -0
- package/dist/common-auth-embedded/commands/menu.d.ts +61 -0
- package/dist/common-auth-embedded/commands/menu.js +300 -0
- package/dist/common-auth-embedded/commands/model.d.ts +238 -0
- package/dist/common-auth-embedded/commands/model.js +16 -0
- package/dist/common-auth-embedded/commands/pi.d.ts +23 -0
- package/dist/common-auth-embedded/commands/pi.js +206 -0
- package/dist/common-auth-embedded/commands/seam.d.ts +72 -0
- package/dist/common-auth-embedded/commands/seam.js +301 -0
- package/dist/common-auth-embedded/fs/atomic-write.d.ts +20 -0
- package/dist/common-auth-embedded/fs/atomic-write.js +80 -0
- package/dist/common-auth-embedded/fs/index.d.ts +7 -0
- package/dist/common-auth-embedded/fs/index.js +6 -0
- package/dist/common-auth-embedded/fs/lock-constants.d.ts +14 -0
- package/dist/common-auth-embedded/fs/lock-constants.js +14 -0
- package/dist/common-auth-embedded/fs/refresh-file-lock.d.ts +33 -0
- package/dist/common-auth-embedded/fs/refresh-file-lock.js +501 -0
- package/dist/common-auth-embedded/fs/with-lock.d.ts +33 -0
- package/dist/common-auth-embedded/fs/with-lock.js +48 -0
- package/dist/common-auth-embedded/logger/capture-sink.d.ts +14 -0
- package/dist/common-auth-embedded/logger/capture-sink.js +13 -0
- package/dist/common-auth-embedded/logger/engine.d.ts +100 -0
- package/dist/common-auth-embedded/logger/engine.js +246 -0
- package/dist/common-auth-embedded/logger/index.d.ts +6 -0
- package/dist/common-auth-embedded/logger/index.js +3 -0
- package/dist/common-auth-embedded/logger/redact.d.ts +11 -0
- package/dist/common-auth-embedded/logger/redact.js +83 -0
- package/dist/common-auth-embedded/pi-slot/index.d.ts +3 -0
- package/dist/common-auth-embedded/pi-slot/index.js +2 -0
- package/dist/common-auth-embedded/pi-slot/lock.js +212 -0
- package/dist/common-auth-embedded/pi-slot/slot.d.ts +49 -0
- package/dist/common-auth-embedded/pi-slot/slot.js +383 -0
- package/dist/common-auth-embedded/quota/codec.d.ts +11 -0
- package/dist/common-auth-embedded/quota/codec.js +12 -0
- package/dist/common-auth-embedded/quota/format.d.ts +46 -0
- package/dist/common-auth-embedded/quota/format.js +138 -0
- package/dist/common-auth-embedded/quota/index.d.ts +10 -0
- package/dist/common-auth-embedded/quota/index.js +5 -0
- package/dist/common-auth-embedded/quota/map.d.ts +63 -0
- package/dist/common-auth-embedded/quota/map.js +105 -0
- package/dist/common-auth-embedded/quota/merge.d.ts +48 -0
- package/dist/common-auth-embedded/quota/merge.js +162 -0
- package/dist/common-auth-embedded/quota/projection.d.ts +64 -0
- package/dist/common-auth-embedded/quota/projection.js +134 -0
- package/dist/common-auth-embedded/routing/admission.d.ts +93 -0
- package/dist/common-auth-embedded/routing/admission.js +142 -0
- package/dist/common-auth-embedded/routing/index.d.ts +8 -0
- package/dist/common-auth-embedded/routing/index.js +4 -0
- package/dist/common-auth-embedded/routing/ordered.d.ts +47 -0
- package/dist/common-auth-embedded/routing/ordered.js +58 -0
- package/dist/common-auth-embedded/routing/pins.d.ts +22 -0
- package/dist/common-auth-embedded/routing/pins.js +34 -0
- package/dist/common-auth-embedded/routing/sticky.d.ts +158 -0
- package/dist/common-auth-embedded/routing/sticky.js +339 -0
- package/dist/common-auth-embedded/source-output.json +1090 -0
- package/dist/common-auth-embedded/store/attribution.d.ts +28 -0
- package/dist/common-auth-embedded/store/attribution.js +60 -0
- package/dist/common-auth-embedded/store/errors.d.ts +55 -0
- package/dist/common-auth-embedded/store/errors.js +38 -0
- package/dist/common-auth-embedded/store/hooks.d.ts +12 -0
- package/dist/common-auth-embedded/store/hooks.js +34 -0
- package/dist/common-auth-embedded/store/identity.d.ts +39 -0
- package/dist/common-auth-embedded/store/identity.js +79 -0
- package/dist/common-auth-embedded/store/index.d.ts +21 -0
- package/dist/common-auth-embedded/store/index.js +7 -0
- package/dist/common-auth-embedded/store/mutate.d.ts +189 -0
- package/dist/common-auth-embedded/store/mutate.js +500 -0
- package/dist/common-auth-embedded/store/pool.d.ts +196 -0
- package/dist/common-auth-embedded/store/pool.js +120 -0
- package/dist/common-auth-embedded/store/provider-state.d.ts +131 -0
- package/dist/common-auth-embedded/store/provider-state.js +226 -0
- package/dist/common-auth-embedded/store/publication.d.ts +47 -0
- package/dist/common-auth-embedded/store/publication.js +304 -0
- package/dist/common-auth-embedded/store/pull.d.ts +36 -0
- package/dist/common-auth-embedded/store/pull.js +143 -0
- package/dist/common-auth-embedded/store/refresh-lock.d.ts +63 -0
- package/dist/common-auth-embedded/store/refresh-lock.js +136 -0
- package/dist/common-auth-embedded/store/refresh.d.ts +72 -0
- package/dist/common-auth-embedded/store/refresh.js +225 -0
- package/dist/common-auth-embedded/store/reserved.js +13 -0
- package/dist/common-auth-embedded/store/rows.d.ts +313 -0
- package/dist/common-auth-embedded/store/rows.js +1258 -0
- package/dist/common-auth-embedded/store/runtime.d.ts +36 -0
- package/dist/common-auth-embedded/store/runtime.js +68 -0
- package/dist/common-auth-embedded/store/schema.d.ts +443 -0
- package/dist/common-auth-embedded/store/schema.js +754 -0
- package/dist/common-auth-embedded/store/settings.d.ts +63 -0
- package/dist/common-auth-embedded/store/settings.js +120 -0
- package/dist/common-auth-embedded/store/torn.d.ts +78 -0
- package/dist/common-auth-embedded/store/torn.js +294 -0
- package/dist/common-auth-embedded/util/any-signal.js +42 -0
- package/dist/common-auth-runtime.d.ts +73 -0
- package/dist/common-auth-runtime.d.ts.map +1 -0
- package/dist/common-auth-runtime.js +65 -0
- package/dist/common-auth-runtime.js.map +1 -0
- package/dist/credential-redaction.d.ts +13 -0
- package/dist/credential-redaction.d.ts.map +1 -0
- package/dist/credential-redaction.js +230 -0
- package/dist/credential-redaction.js.map +1 -0
- package/dist/fetch-timeout.d.ts +4 -2
- package/dist/fetch-timeout.d.ts.map +1 -1
- package/dist/fetch-timeout.js +121 -14
- package/dist/fetch-timeout.js.map +1 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -1
- package/dist/quota-manager.d.ts.map +1 -1
- package/dist/quota-manager.js +35 -10
- package/dist/quota-manager.js.map +1 -1
- package/dist/quota-presentation.d.ts +10 -0
- package/dist/quota-presentation.d.ts.map +1 -0
- package/dist/quota-presentation.js +60 -0
- package/dist/quota-presentation.js.map +1 -0
- package/dist/quota-types.d.ts +2 -0
- package/dist/quota-types.d.ts.map +1 -1
- package/dist/rotation.d.ts +8 -0
- package/dist/rotation.d.ts.map +1 -1
- package/dist/rotation.js +24 -0
- package/dist/rotation.js.map +1 -1
- package/dist/transform/claude.d.ts +7 -3
- package/dist/transform/claude.d.ts.map +1 -1
- package/dist/transform/claude.js +6 -2
- package/dist/transform/claude.js.map +1 -1
- package/dist/transform/types.d.ts +1 -1
- package/dist/transform/types.d.ts.map +1 -1
- package/dist/vault-account-source.d.ts +183 -0
- package/dist/vault-account-source.d.ts.map +1 -0
- package/dist/vault-account-source.js +296 -0
- package/dist/vault-account-source.js.map +1 -0
- package/dist/vault-enrollment-authority.d.ts +16 -0
- package/dist/vault-enrollment-authority.d.ts.map +1 -0
- package/dist/vault-enrollment-authority.js +19 -0
- package/dist/vault-enrollment-authority.js.map +1 -0
- package/dist/vault-family.d.ts +11 -0
- package/dist/vault-family.d.ts.map +1 -0
- package/dist/vault-family.js +24 -0
- package/dist/vault-family.js.map +1 -0
- package/dist/vault-provider-state.d.ts +162 -0
- package/dist/vault-provider-state.d.ts.map +1 -0
- package/dist/vault-provider-state.js +422 -0
- package/dist/vault-provider-state.js.map +1 -0
- package/package.json +39 -2
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
// `sticky-balanced` routing: openai-auth's quota-weighted session placement
|
|
2
|
+
// and pin-break classification, judged over the quota projection rather than
|
|
3
|
+
// openai-auth's fixed primary/secondary snapshot.
|
|
4
|
+
import { budgetExhaustedResetAt, projectQuota, } from '../quota/projection.js';
|
|
5
|
+
import { admit, } from './admission.js';
|
|
6
|
+
import { isPinValid } from './pins.js';
|
|
7
|
+
export const QUOTA_STALENESS_MS = 15 * 60_000;
|
|
8
|
+
export const MIN_RESET_HOURS = 1 / 60;
|
|
9
|
+
export const MIN_WEIGHT = 1e-6;
|
|
10
|
+
/** The projection's time, else the caller's cache-entry time. */
|
|
11
|
+
export function snapshotCheckedAt(quota, entryCheckedAt) {
|
|
12
|
+
for (const checkedAt of [quota?.checkedAt, entryCheckedAt]) {
|
|
13
|
+
if (typeof checkedAt === 'number' && Number.isFinite(checkedAt)) {
|
|
14
|
+
return checkedAt;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The status rule used when the adapter supplies none, carried from
|
|
21
|
+
* openai-auth: 401 and 403 are permanent; no response, 0, a non-finite
|
|
22
|
+
* status, 429 and 5xx are transient; anything else leaves the row healthy.
|
|
23
|
+
* A provider whose 403 can mean an organisation or model policy rather than
|
|
24
|
+
* a dead account supplies its own classifier and can fall back to this one
|
|
25
|
+
* for the statuses it does not single out.
|
|
26
|
+
*/
|
|
27
|
+
export function defaultStickyStatusClass(status) {
|
|
28
|
+
if (status === 401 || status === 403)
|
|
29
|
+
return 'permanent';
|
|
30
|
+
if (status === undefined ||
|
|
31
|
+
status === 0 ||
|
|
32
|
+
!Number.isFinite(status) ||
|
|
33
|
+
(status >= 500 && status <= 599) ||
|
|
34
|
+
status === 429) {
|
|
35
|
+
return 'transient';
|
|
36
|
+
}
|
|
37
|
+
return 'healthy';
|
|
38
|
+
}
|
|
39
|
+
function windowReadings(quota) {
|
|
40
|
+
// Every reading is an independent constraint, so all of them are judged:
|
|
41
|
+
// a third window (for example a short window beside two weekly ones) can
|
|
42
|
+
// be the one that is nearly spent. Longest known window first, unknown
|
|
43
|
+
// lengths last, as openai-auth sorts, so a break decision with several
|
|
44
|
+
// spent windows names the longest. Tombstones and absence records carry no
|
|
45
|
+
// capacity figure and are left out.
|
|
46
|
+
return quota.limits
|
|
47
|
+
.filter((limit) => limit.kind === 'reading')
|
|
48
|
+
.sort((left, right) => {
|
|
49
|
+
const leftKnown = left.windowMinutes !== undefined;
|
|
50
|
+
const rightKnown = right.windowMinutes !== undefined;
|
|
51
|
+
if (leftKnown !== rightKnown)
|
|
52
|
+
return leftKnown ? -1 : 1;
|
|
53
|
+
if (leftKnown && rightKnown) {
|
|
54
|
+
return (right.windowMinutes ?? 0) - (left.windowMinutes ?? 0);
|
|
55
|
+
}
|
|
56
|
+
return 0;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/** Classifies whether a pinned session should leave its row after a failure. */
|
|
60
|
+
export function decideStickyBreak(input) {
|
|
61
|
+
const statusClass = (input.classifyStatus ?? defaultStickyStatusClass)(input.status);
|
|
62
|
+
if (statusClass === 'permanent') {
|
|
63
|
+
return { action: 'migrate', reason: 'permanent' };
|
|
64
|
+
}
|
|
65
|
+
if (!input.quota)
|
|
66
|
+
return { action: 'retain', reason: 'unknown' };
|
|
67
|
+
const checkedAt = snapshotCheckedAt(input.quota, input.quotaCheckedAt);
|
|
68
|
+
if (checkedAt === undefined ||
|
|
69
|
+
!Number.isFinite(checkedAt) ||
|
|
70
|
+
input.now - checkedAt > QUOTA_STALENESS_MS) {
|
|
71
|
+
return { action: 'retain', reason: 'stale' };
|
|
72
|
+
}
|
|
73
|
+
// After the stale check, so a stale snapshot never judges the account on a
|
|
74
|
+
// reading the killswitch would consider below its floor.
|
|
75
|
+
if (input.killswitchPasses === false) {
|
|
76
|
+
return { action: 'migrate', reason: 'killswitch' };
|
|
77
|
+
}
|
|
78
|
+
for (const limit of windowReadings(input.quota)) {
|
|
79
|
+
const remaining = limit.remainingPercent;
|
|
80
|
+
if (typeof remaining === 'number' &&
|
|
81
|
+
Number.isFinite(remaining) &&
|
|
82
|
+
remaining <= 0) {
|
|
83
|
+
return {
|
|
84
|
+
action: 'migrate',
|
|
85
|
+
reason: 'exhausted',
|
|
86
|
+
window: { scope: limit.scope, label: limit.label },
|
|
87
|
+
...(typeof limit.resetsAt === 'string'
|
|
88
|
+
? { resetsAt: limit.resetsAt }
|
|
89
|
+
: {}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// A reached credit budget is exhaustion on its own axis, judged by the same
|
|
94
|
+
// signal admission uses so the two never disagree on what "spent" means.
|
|
95
|
+
const budgetReset = budgetExhaustedResetAt(input.quota, input.now);
|
|
96
|
+
if (budgetReset) {
|
|
97
|
+
return {
|
|
98
|
+
action: 'migrate',
|
|
99
|
+
reason: 'exhausted',
|
|
100
|
+
resetsAt: budgetReset.resetsAt,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return { action: 'retain', reason: statusClass };
|
|
104
|
+
}
|
|
105
|
+
export function sustainableWindowWeight(window, reservePercent, now) {
|
|
106
|
+
const spendable = Math.max(0, window.remainingPercent - reservePercent);
|
|
107
|
+
if (spendable <= 0)
|
|
108
|
+
return 0;
|
|
109
|
+
if (!window.resetsAt)
|
|
110
|
+
return spendable;
|
|
111
|
+
const resetMs = Date.parse(window.resetsAt);
|
|
112
|
+
// A lapsed reset cannot yield a spend rate: the divisor would clamp to
|
|
113
|
+
// MIN_RESET_HOURS and inflate the weight about sixty-fold on stale
|
|
114
|
+
// information, so the un-rate-adjusted spendable capacity is used instead.
|
|
115
|
+
if (!Number.isFinite(resetMs) || resetMs <= now)
|
|
116
|
+
return spendable;
|
|
117
|
+
const hours = Math.max((resetMs - now) / 3_600_000, MIN_RESET_HOURS);
|
|
118
|
+
return spendable / hours;
|
|
119
|
+
}
|
|
120
|
+
function compareAccountIds(left, right) {
|
|
121
|
+
if (left < right)
|
|
122
|
+
return -1;
|
|
123
|
+
if (left > right)
|
|
124
|
+
return 1;
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
function candidateWeight(candidate, now) {
|
|
128
|
+
if (!candidate.quota)
|
|
129
|
+
return undefined;
|
|
130
|
+
const quotaCheckedAt = snapshotCheckedAt(candidate.quota, candidate.quotaCheckedAt);
|
|
131
|
+
if (quotaCheckedAt === undefined ||
|
|
132
|
+
now - quotaCheckedAt > QUOTA_STALENESS_MS) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
// Missing reserve data must leave a window usable rather than silently
|
|
136
|
+
// excluding its account.
|
|
137
|
+
// The account is as constrained as its tightest window, whichever it is.
|
|
138
|
+
const weights = windowReadings(candidate.quota).map((limit) => sustainableWindowWeight({
|
|
139
|
+
remainingPercent: limit.remainingPercent ?? Number.NaN,
|
|
140
|
+
...(limit.resetsAt === undefined ? {} : { resetsAt: limit.resetsAt }),
|
|
141
|
+
}, candidate.reservePercent[limit.label] ?? 0, now));
|
|
142
|
+
// The credit budget is a third pressure axis on its own reset clock. It has
|
|
143
|
+
// no configured reserve, and a malformed reading is ignored rather than
|
|
144
|
+
// allowed to zero the account's weight.
|
|
145
|
+
const budget = candidate.quota.budget;
|
|
146
|
+
if (budget &&
|
|
147
|
+
typeof budget.remainingPercent === 'number' &&
|
|
148
|
+
Number.isFinite(budget.remainingPercent)) {
|
|
149
|
+
weights.push(sustainableWindowWeight({
|
|
150
|
+
remainingPercent: budget.remainingPercent,
|
|
151
|
+
...(budget.resetsAt === undefined
|
|
152
|
+
? {}
|
|
153
|
+
: { resetsAt: budget.resetsAt }),
|
|
154
|
+
}, 0, now));
|
|
155
|
+
}
|
|
156
|
+
const weight = weights.length > 0 ? Math.min(...weights) : 0;
|
|
157
|
+
return weight > 0 ? { candidate, quotaCheckedAt, weight } : undefined;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Places a session: the lowest projected pressure among candidates with a
|
|
161
|
+
* fresh positive weight, else (`mode-fallback`) the first candidate in
|
|
162
|
+
* configured order, preferring one with an applicable reset credit.
|
|
163
|
+
*/
|
|
164
|
+
export function selectStickyCandidate(input) {
|
|
165
|
+
// A candidate killed by the killswitch is excluded from BOTH weighted
|
|
166
|
+
// placement and the fallback branch, which must never become a way to
|
|
167
|
+
// spend on a killed account.
|
|
168
|
+
const eligibleCandidates = input.candidates.filter((candidate) => candidate.killswitchPasses !== false);
|
|
169
|
+
if (input.candidates.length === 0) {
|
|
170
|
+
throw new Error('Cannot select a sticky candidate: input.candidates is empty');
|
|
171
|
+
}
|
|
172
|
+
if (eligibleCandidates.length === 0)
|
|
173
|
+
return undefined;
|
|
174
|
+
const weighted = eligibleCandidates
|
|
175
|
+
.map((candidate) => candidateWeight(candidate, input.now))
|
|
176
|
+
.filter((candidate) => candidate !== undefined);
|
|
177
|
+
if (weighted.length > 0) {
|
|
178
|
+
weighted.sort((left, right) => {
|
|
179
|
+
// MIN_WEIGHT only guards the division; every weight here is positive.
|
|
180
|
+
const leftScore = ((input.pendingBytes.get(left.candidate.accountId) ?? 0) +
|
|
181
|
+
input.requestBytes) /
|
|
182
|
+
Math.max(left.weight, MIN_WEIGHT);
|
|
183
|
+
const rightScore = ((input.pendingBytes.get(right.candidate.accountId) ?? 0) +
|
|
184
|
+
input.requestBytes) /
|
|
185
|
+
Math.max(right.weight, MIN_WEIGHT);
|
|
186
|
+
return (leftScore - rightScore ||
|
|
187
|
+
left.candidate.configuredOrder - right.candidate.configuredOrder ||
|
|
188
|
+
compareAccountIds(left.candidate.accountId, right.candidate.accountId));
|
|
189
|
+
});
|
|
190
|
+
const selected = weighted[0];
|
|
191
|
+
if (selected) {
|
|
192
|
+
return {
|
|
193
|
+
accountId: selected.candidate.accountId,
|
|
194
|
+
quotaCheckedAt: selected.quotaCheckedAt,
|
|
195
|
+
source: 'weighted',
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
input.onEmptyWeightedSet?.();
|
|
200
|
+
const fallback = [...eligibleCandidates].sort((left, right) => {
|
|
201
|
+
const leftHasCredits = (left.resetCreditsApplicable ?? 0) > 0 ? 1 : 0;
|
|
202
|
+
const rightHasCredits = (right.resetCreditsApplicable ?? 0) > 0 ? 1 : 0;
|
|
203
|
+
return (rightHasCredits - leftHasCredits ||
|
|
204
|
+
left.configuredOrder - right.configuredOrder ||
|
|
205
|
+
compareAccountIds(left.accountId, right.accountId));
|
|
206
|
+
})[0];
|
|
207
|
+
if (!fallback) {
|
|
208
|
+
throw new Error('Cannot select a sticky candidate: input.candidates is empty');
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
accountId: fallback.accountId,
|
|
212
|
+
quotaCheckedAt: snapshotCheckedAt(fallback.quota, fallback.quotaCheckedAt),
|
|
213
|
+
source: 'mode-fallback',
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Routes one request in `sticky-balanced` mode. A valid pin whose row is
|
|
218
|
+
* admitted, not excluded and not killed is dispatched as is. Otherwise
|
|
219
|
+
* selection runs over the non-excluded rows; each selected row admission
|
|
220
|
+
* refused is removed and selection re-runs, so the loop ends either on an
|
|
221
|
+
* admitted row or with no admissible account.
|
|
222
|
+
*/
|
|
223
|
+
export function routeSticky(input) {
|
|
224
|
+
const admission = admit(input);
|
|
225
|
+
const admitted = new Map(admission.admitted.map((row) => [row.id, row]));
|
|
226
|
+
const refusals = new Map(admission.refused.map((r) => [r.id, r]));
|
|
227
|
+
const excluded = new Set(admission.excluded.map((row) => row.id));
|
|
228
|
+
const validIds = new Set(input.rows.map((row) => row.id));
|
|
229
|
+
const pinValid = input.pin !== undefined &&
|
|
230
|
+
isPinValid(input.pin, validIds, input.identities?.get(input.pin.accountId));
|
|
231
|
+
if (pinValid && input.pin) {
|
|
232
|
+
const pinned = admitted.get(input.pin.accountId);
|
|
233
|
+
if (pinned && input.killswitch?.get(pinned.id) !== false) {
|
|
234
|
+
return {
|
|
235
|
+
outcome: 'dispatch',
|
|
236
|
+
accountId: pinned.id,
|
|
237
|
+
source: 'pin',
|
|
238
|
+
...(pinned.projection?.checkedAt === undefined
|
|
239
|
+
? {}
|
|
240
|
+
: { quotaCheckedAt: pinned.projection.checkedAt }),
|
|
241
|
+
pin: { action: 'retain' },
|
|
242
|
+
refusedSelections: [],
|
|
243
|
+
admission,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// A spent window with a future reset, a spent credit budget and a killswitch
|
|
248
|
+
// verdict all say the pinned row will not serve until some known later time,
|
|
249
|
+
// so the pin may move. A row refused for want of a usable reading, or
|
|
250
|
+
// excluded by a short rate-limit mark or refresh backoff, may serve again on
|
|
251
|
+
// the next reading, so its pin stays.
|
|
252
|
+
const pinRefusal = input.pin ? refusals.get(input.pin.accountId) : undefined;
|
|
253
|
+
const pinMoves = pinValid &&
|
|
254
|
+
input.pin !== undefined &&
|
|
255
|
+
input.refusedPinPolicy === 'move-on-confirmed-exhaustion' &&
|
|
256
|
+
(input.killswitch?.get(input.pin.accountId) === false ||
|
|
257
|
+
pinRefusal?.reason === 'exhausted' ||
|
|
258
|
+
pinRefusal?.reason === 'budget-spent');
|
|
259
|
+
const reserveFor = (row) => {
|
|
260
|
+
const perRow = typeof input.rowReservePercent === 'function'
|
|
261
|
+
? input.rowReservePercent(row)
|
|
262
|
+
: input.rowReservePercent?.get(row.id);
|
|
263
|
+
return perRow ?? input.reservePercent ?? {};
|
|
264
|
+
};
|
|
265
|
+
const scope = input.scope;
|
|
266
|
+
let candidates = input.rows
|
|
267
|
+
.map((row, configuredOrder) => ({ row, configuredOrder }))
|
|
268
|
+
.filter(({ row }) => !excluded.has(row.id))
|
|
269
|
+
.map(({ row, configuredOrder }) => {
|
|
270
|
+
const killswitchPasses = input.killswitch?.get(row.id);
|
|
271
|
+
const credits = input.resetCreditsApplicable?.get(row.id);
|
|
272
|
+
return {
|
|
273
|
+
accountId: row.id,
|
|
274
|
+
quota: row.kind === 'api-key'
|
|
275
|
+
? undefined
|
|
276
|
+
: (admitted.get(row.id)?.projection ??
|
|
277
|
+
projectQuota(row.quota, scope)),
|
|
278
|
+
reservePercent: reserveFor(row),
|
|
279
|
+
configuredOrder,
|
|
280
|
+
...(credits === undefined ? {} : { resetCreditsApplicable: credits }),
|
|
281
|
+
...(killswitchPasses === undefined ? {} : { killswitchPasses }),
|
|
282
|
+
};
|
|
283
|
+
});
|
|
284
|
+
const refusedSelections = [];
|
|
285
|
+
const unplaced = pinValid
|
|
286
|
+
? { action: 'retain' }
|
|
287
|
+
: input.pin
|
|
288
|
+
? { action: 'clear' }
|
|
289
|
+
: { action: 'none' };
|
|
290
|
+
while (candidates.length > 0) {
|
|
291
|
+
const selection = selectStickyCandidate({
|
|
292
|
+
candidates,
|
|
293
|
+
pendingBytes: input.pendingBytes ?? new Map(),
|
|
294
|
+
requestBytes: input.requestBytes,
|
|
295
|
+
now: input.now,
|
|
296
|
+
...(input.onEmptyWeightedSet
|
|
297
|
+
? { onEmptyWeightedSet: input.onEmptyWeightedSet }
|
|
298
|
+
: {}),
|
|
299
|
+
});
|
|
300
|
+
if (!selection)
|
|
301
|
+
break;
|
|
302
|
+
const refusal = refusals.get(selection.accountId);
|
|
303
|
+
if (refusal) {
|
|
304
|
+
refusedSelections.push(refusal);
|
|
305
|
+
candidates = candidates.filter((candidate) => candidate.accountId !== selection.accountId);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const identity = input.identities?.get(selection.accountId);
|
|
309
|
+
return {
|
|
310
|
+
outcome: 'dispatch',
|
|
311
|
+
accountId: selection.accountId,
|
|
312
|
+
source: selection.source,
|
|
313
|
+
...(selection.quotaCheckedAt === undefined
|
|
314
|
+
? {}
|
|
315
|
+
: { quotaCheckedAt: selection.quotaCheckedAt }),
|
|
316
|
+
pin: pinValid && !pinMoves
|
|
317
|
+
? { action: 'retain' }
|
|
318
|
+
: {
|
|
319
|
+
action: 'assign',
|
|
320
|
+
pin: {
|
|
321
|
+
accountId: selection.accountId,
|
|
322
|
+
inputBytes: input.requestBytes,
|
|
323
|
+
...(identity === undefined ? {} : { wireIdentity: identity }),
|
|
324
|
+
...(selection.quotaCheckedAt === undefined
|
|
325
|
+
? {}
|
|
326
|
+
: { quotaCheckedAt: selection.quotaCheckedAt }),
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
refusedSelections,
|
|
330
|
+
admission,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
outcome: 'no-admissible-account',
|
|
335
|
+
pin: unplaced,
|
|
336
|
+
refusedSelections,
|
|
337
|
+
admission,
|
|
338
|
+
};
|
|
339
|
+
}
|