@cortexkit/antigravity-auth-core 2.2.1 → 3.0.1

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.
Files changed (253) hide show
  1. package/README.md +24 -2
  2. package/dist/abort-forwarding.d.ts +3 -0
  3. package/dist/abort-forwarding.d.ts.map +1 -0
  4. package/dist/abort-forwarding.js +19 -0
  5. package/dist/abort-forwarding.js.map +1 -0
  6. package/dist/account-identity.d.ts +52 -0
  7. package/dist/account-identity.d.ts.map +1 -0
  8. package/dist/account-identity.js +66 -0
  9. package/dist/account-identity.js.map +1 -0
  10. package/dist/account-manager.d.ts +228 -43
  11. package/dist/account-manager.d.ts.map +1 -1
  12. package/dist/account-manager.js +942 -903
  13. package/dist/account-manager.js.map +1 -1
  14. package/dist/account-migration.d.ts +309 -0
  15. package/dist/account-migration.d.ts.map +1 -0
  16. package/dist/account-migration.js +2284 -0
  17. package/dist/account-migration.js.map +1 -0
  18. package/dist/account-repository-codecs.d.ts +82 -0
  19. package/dist/account-repository-codecs.d.ts.map +1 -0
  20. package/dist/account-repository-codecs.js +609 -0
  21. package/dist/account-repository-codecs.js.map +1 -0
  22. package/dist/account-repository-types.d.ts +831 -0
  23. package/dist/account-repository-types.d.ts.map +1 -0
  24. package/dist/account-repository-types.js +137 -0
  25. package/dist/account-repository-types.js.map +1 -0
  26. package/dist/account-repository.d.ts +442 -0
  27. package/dist/account-repository.d.ts.map +1 -0
  28. package/dist/account-repository.js +2253 -0
  29. package/dist/account-repository.js.map +1 -0
  30. package/dist/account-selector.d.ts +470 -0
  31. package/dist/account-selector.d.ts.map +1 -0
  32. package/dist/account-selector.js +1383 -0
  33. package/dist/account-selector.js.map +1 -0
  34. package/dist/account-storage.d.ts.map +1 -1
  35. package/dist/account-storage.js +5 -0
  36. package/dist/account-storage.js.map +1 -1
  37. package/dist/agy-request-metadata.d.ts.map +1 -1
  38. package/dist/agy-request-metadata.js +8 -21
  39. package/dist/agy-request-metadata.js.map +1 -1
  40. package/dist/agy-transport.d.ts.map +1 -1
  41. package/dist/agy-transport.js +21 -35
  42. package/dist/agy-transport.js.map +1 -1
  43. package/dist/antigravity-command-menu.d.ts +171 -0
  44. package/dist/antigravity-command-menu.d.ts.map +1 -0
  45. package/dist/antigravity-command-menu.js +523 -0
  46. package/dist/antigravity-command-menu.js.map +1 -0
  47. package/dist/atomic-write.d.ts +18 -1
  48. package/dist/atomic-write.d.ts.map +1 -1
  49. package/dist/atomic-write.js +15 -3
  50. package/dist/atomic-write.js.map +1 -1
  51. package/dist/common-auth-embedded/NOTICE.txt +21 -0
  52. package/dist/common-auth-embedded/auth-menu/accounts.d.ts +97 -0
  53. package/dist/common-auth-embedded/auth-menu/accounts.js +387 -0
  54. package/dist/common-auth-embedded/auth-menu/ansi.d.ts +26 -0
  55. package/dist/common-auth-embedded/auth-menu/ansi.js +98 -0
  56. package/dist/common-auth-embedded/auth-menu/confirm.d.ts +7 -0
  57. package/dist/common-auth-embedded/auth-menu/confirm.js +21 -0
  58. package/dist/common-auth-embedded/auth-menu/doctor.d.ts +52 -0
  59. package/dist/common-auth-embedded/auth-menu/doctor.js +81 -0
  60. package/dist/common-auth-embedded/auth-menu/index.d.ts +17 -0
  61. package/dist/common-auth-embedded/auth-menu/index.js +12 -0
  62. package/dist/common-auth-embedded/auth-menu/login.d.ts +55 -0
  63. package/dist/common-auth-embedded/auth-menu/login.js +83 -0
  64. package/dist/common-auth-embedded/auth-menu/menu.d.ts +59 -0
  65. package/dist/common-auth-embedded/auth-menu/menu.js +73 -0
  66. package/dist/common-auth-embedded/auth-menu/opencode-v1.d.ts +49 -0
  67. package/dist/common-auth-embedded/auth-menu/opencode-v1.js +47 -0
  68. package/dist/common-auth-embedded/auth-menu/select.d.ts +21 -0
  69. package/dist/common-auth-embedded/auth-menu/select.js +159 -0
  70. package/dist/common-auth-embedded/auth-menu/terminal.d.ts +43 -0
  71. package/dist/common-auth-embedded/auth-menu/terminal.js +19 -0
  72. package/dist/common-auth-embedded/claustrum/consumer.d.ts +102 -0
  73. package/dist/common-auth-embedded/claustrum/consumer.js +298 -0
  74. package/dist/common-auth-embedded/claustrum/custody.d.ts +175 -0
  75. package/dist/common-auth-embedded/claustrum/custody.js +354 -0
  76. package/dist/common-auth-embedded/claustrum/enrollment.d.ts +158 -0
  77. package/dist/common-auth-embedded/claustrum/enrollment.js +759 -0
  78. package/dist/common-auth-embedded/claustrum/errors.d.ts +16 -0
  79. package/dist/common-auth-embedded/claustrum/errors.js +8 -0
  80. package/dist/common-auth-embedded/claustrum/host-slot.d.ts +39 -0
  81. package/dist/common-auth-embedded/claustrum/host-slot.js +72 -0
  82. package/dist/common-auth-embedded/claustrum/index.d.ts +18 -0
  83. package/dist/common-auth-embedded/claustrum/index.js +22 -0
  84. package/dist/common-auth-embedded/claustrum/interlock.d.ts +27 -0
  85. package/dist/common-auth-embedded/claustrum/interlock.js +29 -0
  86. package/dist/common-auth-embedded/claustrum/roster.d.ts +208 -0
  87. package/dist/common-auth-embedded/claustrum/roster.js +536 -0
  88. package/dist/common-auth-embedded/commands/builtins.d.ts +71 -0
  89. package/dist/common-auth-embedded/commands/builtins.js +534 -0
  90. package/dist/common-auth-embedded/commands/index.d.ts +10 -0
  91. package/dist/common-auth-embedded/commands/index.js +5 -0
  92. package/dist/common-auth-embedded/commands/menu.d.ts +61 -0
  93. package/dist/common-auth-embedded/commands/menu.js +300 -0
  94. package/dist/common-auth-embedded/commands/model.d.ts +238 -0
  95. package/dist/common-auth-embedded/commands/model.js +16 -0
  96. package/dist/common-auth-embedded/commands/pi.d.ts +23 -0
  97. package/dist/common-auth-embedded/commands/pi.js +206 -0
  98. package/dist/common-auth-embedded/commands/seam.d.ts +72 -0
  99. package/dist/common-auth-embedded/commands/seam.js +301 -0
  100. package/dist/common-auth-embedded/fs/atomic-write.d.ts +20 -0
  101. package/dist/common-auth-embedded/fs/atomic-write.js +80 -0
  102. package/dist/common-auth-embedded/fs/index.d.ts +7 -0
  103. package/dist/common-auth-embedded/fs/index.js +6 -0
  104. package/dist/common-auth-embedded/fs/lock-constants.d.ts +14 -0
  105. package/dist/common-auth-embedded/fs/lock-constants.js +14 -0
  106. package/dist/common-auth-embedded/fs/refresh-file-lock.d.ts +33 -0
  107. package/dist/common-auth-embedded/fs/refresh-file-lock.js +501 -0
  108. package/dist/common-auth-embedded/fs/with-lock.d.ts +33 -0
  109. package/dist/common-auth-embedded/fs/with-lock.js +48 -0
  110. package/dist/common-auth-embedded/logger/capture-sink.d.ts +14 -0
  111. package/dist/common-auth-embedded/logger/capture-sink.js +13 -0
  112. package/dist/common-auth-embedded/logger/engine.d.ts +100 -0
  113. package/dist/common-auth-embedded/logger/engine.js +246 -0
  114. package/dist/common-auth-embedded/logger/index.d.ts +6 -0
  115. package/dist/common-auth-embedded/logger/index.js +3 -0
  116. package/dist/common-auth-embedded/logger/redact.d.ts +11 -0
  117. package/dist/common-auth-embedded/logger/redact.js +83 -0
  118. package/dist/common-auth-embedded/pi-slot/index.d.ts +3 -0
  119. package/dist/common-auth-embedded/pi-slot/index.js +2 -0
  120. package/dist/common-auth-embedded/pi-slot/lock.js +212 -0
  121. package/dist/common-auth-embedded/pi-slot/slot.d.ts +49 -0
  122. package/dist/common-auth-embedded/pi-slot/slot.js +383 -0
  123. package/dist/common-auth-embedded/quota/codec.d.ts +11 -0
  124. package/dist/common-auth-embedded/quota/codec.js +12 -0
  125. package/dist/common-auth-embedded/quota/format.d.ts +46 -0
  126. package/dist/common-auth-embedded/quota/format.js +138 -0
  127. package/dist/common-auth-embedded/quota/index.d.ts +10 -0
  128. package/dist/common-auth-embedded/quota/index.js +5 -0
  129. package/dist/common-auth-embedded/quota/map.d.ts +63 -0
  130. package/dist/common-auth-embedded/quota/map.js +105 -0
  131. package/dist/common-auth-embedded/quota/merge.d.ts +48 -0
  132. package/dist/common-auth-embedded/quota/merge.js +162 -0
  133. package/dist/common-auth-embedded/quota/projection.d.ts +64 -0
  134. package/dist/common-auth-embedded/quota/projection.js +134 -0
  135. package/dist/common-auth-embedded/routing/admission.d.ts +93 -0
  136. package/dist/common-auth-embedded/routing/admission.js +142 -0
  137. package/dist/common-auth-embedded/routing/index.d.ts +8 -0
  138. package/dist/common-auth-embedded/routing/index.js +4 -0
  139. package/dist/common-auth-embedded/routing/ordered.d.ts +47 -0
  140. package/dist/common-auth-embedded/routing/ordered.js +58 -0
  141. package/dist/common-auth-embedded/routing/pins.d.ts +22 -0
  142. package/dist/common-auth-embedded/routing/pins.js +34 -0
  143. package/dist/common-auth-embedded/routing/sticky.d.ts +158 -0
  144. package/dist/common-auth-embedded/routing/sticky.js +339 -0
  145. package/dist/common-auth-embedded/source-output.json +1090 -0
  146. package/dist/common-auth-embedded/store/attribution.d.ts +28 -0
  147. package/dist/common-auth-embedded/store/attribution.js +60 -0
  148. package/dist/common-auth-embedded/store/errors.d.ts +55 -0
  149. package/dist/common-auth-embedded/store/errors.js +38 -0
  150. package/dist/common-auth-embedded/store/hooks.d.ts +12 -0
  151. package/dist/common-auth-embedded/store/hooks.js +34 -0
  152. package/dist/common-auth-embedded/store/identity.d.ts +39 -0
  153. package/dist/common-auth-embedded/store/identity.js +79 -0
  154. package/dist/common-auth-embedded/store/index.d.ts +21 -0
  155. package/dist/common-auth-embedded/store/index.js +7 -0
  156. package/dist/common-auth-embedded/store/mutate.d.ts +189 -0
  157. package/dist/common-auth-embedded/store/mutate.js +500 -0
  158. package/dist/common-auth-embedded/store/pool.d.ts +196 -0
  159. package/dist/common-auth-embedded/store/pool.js +120 -0
  160. package/dist/common-auth-embedded/store/provider-state.d.ts +131 -0
  161. package/dist/common-auth-embedded/store/provider-state.js +226 -0
  162. package/dist/common-auth-embedded/store/publication.d.ts +47 -0
  163. package/dist/common-auth-embedded/store/publication.js +304 -0
  164. package/dist/common-auth-embedded/store/pull.d.ts +36 -0
  165. package/dist/common-auth-embedded/store/pull.js +143 -0
  166. package/dist/common-auth-embedded/store/refresh-lock.d.ts +63 -0
  167. package/dist/common-auth-embedded/store/refresh-lock.js +136 -0
  168. package/dist/common-auth-embedded/store/refresh.d.ts +72 -0
  169. package/dist/common-auth-embedded/store/refresh.js +225 -0
  170. package/dist/common-auth-embedded/store/reserved.js +13 -0
  171. package/dist/common-auth-embedded/store/rows.d.ts +313 -0
  172. package/dist/common-auth-embedded/store/rows.js +1258 -0
  173. package/dist/common-auth-embedded/store/runtime.d.ts +36 -0
  174. package/dist/common-auth-embedded/store/runtime.js +68 -0
  175. package/dist/common-auth-embedded/store/schema.d.ts +443 -0
  176. package/dist/common-auth-embedded/store/schema.js +754 -0
  177. package/dist/common-auth-embedded/store/settings.d.ts +63 -0
  178. package/dist/common-auth-embedded/store/settings.js +120 -0
  179. package/dist/common-auth-embedded/store/torn.d.ts +78 -0
  180. package/dist/common-auth-embedded/store/torn.js +294 -0
  181. package/dist/common-auth-embedded/util/any-signal.js +42 -0
  182. package/dist/common-auth-runtime.d.ts +73 -0
  183. package/dist/common-auth-runtime.d.ts.map +1 -0
  184. package/dist/common-auth-runtime.js +65 -0
  185. package/dist/common-auth-runtime.js.map +1 -0
  186. package/dist/credential-redaction.d.ts +13 -0
  187. package/dist/credential-redaction.d.ts.map +1 -0
  188. package/dist/credential-redaction.js +230 -0
  189. package/dist/credential-redaction.js.map +1 -0
  190. package/dist/fetch-timeout.d.ts +4 -2
  191. package/dist/fetch-timeout.d.ts.map +1 -1
  192. package/dist/fetch-timeout.js +121 -14
  193. package/dist/fetch-timeout.js.map +1 -1
  194. package/dist/fingerprint.d.ts +3 -3
  195. package/dist/fingerprint.d.ts.map +1 -1
  196. package/dist/fingerprint.js +4 -4
  197. package/dist/fingerprint.js.map +1 -1
  198. package/dist/index.d.ts +16 -1
  199. package/dist/index.d.ts.map +1 -1
  200. package/dist/index.js +15 -0
  201. package/dist/index.js.map +1 -1
  202. package/dist/model-registry.d.ts +16 -0
  203. package/dist/model-registry.d.ts.map +1 -1
  204. package/dist/model-registry.js +242 -30
  205. package/dist/model-registry.js.map +1 -1
  206. package/dist/quota-manager.d.ts.map +1 -1
  207. package/dist/quota-manager.js +35 -10
  208. package/dist/quota-manager.js.map +1 -1
  209. package/dist/quota-presentation.d.ts +10 -0
  210. package/dist/quota-presentation.d.ts.map +1 -0
  211. package/dist/quota-presentation.js +60 -0
  212. package/dist/quota-presentation.js.map +1 -0
  213. package/dist/quota-types.d.ts +2 -0
  214. package/dist/quota-types.d.ts.map +1 -1
  215. package/dist/rotation.d.ts +8 -0
  216. package/dist/rotation.d.ts.map +1 -1
  217. package/dist/rotation.js +24 -0
  218. package/dist/rotation.js.map +1 -1
  219. package/dist/transform/agy-generation.d.ts +3 -0
  220. package/dist/transform/agy-generation.d.ts.map +1 -0
  221. package/dist/transform/agy-generation.js +28 -0
  222. package/dist/transform/agy-generation.js.map +1 -0
  223. package/dist/transform/claude.d.ts +7 -3
  224. package/dist/transform/claude.d.ts.map +1 -1
  225. package/dist/transform/claude.js +8 -2
  226. package/dist/transform/claude.js.map +1 -1
  227. package/dist/transform/index.d.ts +1 -0
  228. package/dist/transform/index.d.ts.map +1 -1
  229. package/dist/transform/index.js +1 -0
  230. package/dist/transform/index.js.map +1 -1
  231. package/dist/transform/model-resolver.d.ts +2 -1
  232. package/dist/transform/model-resolver.d.ts.map +1 -1
  233. package/dist/transform/model-resolver.js +46 -5
  234. package/dist/transform/model-resolver.js.map +1 -1
  235. package/dist/transform/types.d.ts +1 -1
  236. package/dist/transform/types.d.ts.map +1 -1
  237. package/dist/vault-account-source.d.ts +183 -0
  238. package/dist/vault-account-source.d.ts.map +1 -0
  239. package/dist/vault-account-source.js +296 -0
  240. package/dist/vault-account-source.js.map +1 -0
  241. package/dist/vault-enrollment-authority.d.ts +16 -0
  242. package/dist/vault-enrollment-authority.d.ts.map +1 -0
  243. package/dist/vault-enrollment-authority.js +19 -0
  244. package/dist/vault-enrollment-authority.js.map +1 -0
  245. package/dist/vault-family.d.ts +11 -0
  246. package/dist/vault-family.d.ts.map +1 -0
  247. package/dist/vault-family.js +24 -0
  248. package/dist/vault-family.js.map +1 -0
  249. package/dist/vault-provider-state.d.ts +162 -0
  250. package/dist/vault-provider-state.d.ts.map +1 -0
  251. package/dist/vault-provider-state.js +422 -0
  252. package/dist/vault-provider-state.js.map +1 -0
  253. 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
+ }