@openlimiter/core 0.4.0 → 1.3.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 (68) hide show
  1. package/dist/acquire/antigravity-probe.d.ts +139 -0
  2. package/dist/acquire/antigravity-probe.d.ts.map +1 -0
  3. package/dist/acquire/antigravity-probe.js +599 -0
  4. package/dist/acquire/antigravity-probe.js.map +1 -0
  5. package/dist/acquire/cadence.d.ts +74 -0
  6. package/dist/acquire/cadence.d.ts.map +1 -0
  7. package/dist/acquire/cadence.js +171 -0
  8. package/dist/acquire/cadence.js.map +1 -0
  9. package/dist/acquire/coordination.d.ts +157 -0
  10. package/dist/acquire/coordination.d.ts.map +1 -0
  11. package/dist/acquire/coordination.js +292 -0
  12. package/dist/acquire/coordination.js.map +1 -0
  13. package/dist/acquire/credentials.d.ts +113 -0
  14. package/dist/acquire/credentials.d.ts.map +1 -0
  15. package/dist/acquire/credentials.js +316 -0
  16. package/dist/acquire/credentials.js.map +1 -0
  17. package/dist/acquire/identity.d.ts +20 -0
  18. package/dist/acquire/identity.d.ts.map +1 -0
  19. package/dist/acquire/identity.js +20 -0
  20. package/dist/acquire/identity.js.map +1 -0
  21. package/dist/acquire/index.d.ts +10 -0
  22. package/dist/acquire/index.d.ts.map +1 -0
  23. package/dist/acquire/index.js +10 -0
  24. package/dist/acquire/index.js.map +1 -0
  25. package/dist/acquire/providers.d.ts +86 -0
  26. package/dist/acquire/providers.d.ts.map +1 -0
  27. package/dist/acquire/providers.js +175 -0
  28. package/dist/acquire/providers.js.map +1 -0
  29. package/dist/acquire/runner.d.ts +146 -0
  30. package/dist/acquire/runner.d.ts.map +1 -0
  31. package/dist/acquire/runner.js +345 -0
  32. package/dist/acquire/runner.js.map +1 -0
  33. package/dist/acquire/transport.d.ts +194 -0
  34. package/dist/acquire/transport.d.ts.map +1 -0
  35. package/dist/acquire/transport.js +434 -0
  36. package/dist/acquire/transport.js.map +1 -0
  37. package/dist/acquire/windows-credential.d.ts +74 -0
  38. package/dist/acquire/windows-credential.d.ts.map +1 -0
  39. package/dist/acquire/windows-credential.js +124 -0
  40. package/dist/acquire/windows-credential.js.map +1 -0
  41. package/dist/cache.d.ts +28 -0
  42. package/dist/cache.d.ts.map +1 -1
  43. package/dist/cache.js +179 -25
  44. package/dist/cache.js.map +1 -1
  45. package/dist/connection-state.d.ts +53 -2
  46. package/dist/connection-state.d.ts.map +1 -1
  47. package/dist/connection-state.js +98 -9
  48. package/dist/connection-state.js.map +1 -1
  49. package/dist/failures.d.ts +2 -1
  50. package/dist/failures.d.ts.map +1 -1
  51. package/dist/failures.js +7 -3
  52. package/dist/failures.js.map +1 -1
  53. package/dist/index.d.ts +1 -0
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +1 -0
  56. package/dist/index.js.map +1 -1
  57. package/dist/merge.d.ts.map +1 -1
  58. package/dist/merge.js +17 -1
  59. package/dist/merge.js.map +1 -1
  60. package/dist/normalizer.d.ts +2 -0
  61. package/dist/normalizer.d.ts.map +1 -1
  62. package/dist/normalizer.js +39 -4
  63. package/dist/normalizer.js.map +1 -1
  64. package/dist/types.d.ts +63 -1
  65. package/dist/types.d.ts.map +1 -1
  66. package/dist/types.js +22 -0
  67. package/dist/types.js.map +1 -1
  68. package/package.json +3 -2
@@ -0,0 +1,292 @@
1
+ /**
2
+ * Who refreshes the cache, and how a terminal gets fresh bars with no daemon.
3
+ *
4
+ * Two questions live here, and they are different questions.
5
+ *
6
+ * The first is ownership. A machine running the desktop tray already has a
7
+ * refresher, and a status line that polls anyway doubles the traffic the
8
+ * provider sees for no new information. So a cache carrying a desktop write
9
+ * inside the last interval is a cache the command line tool leaves alone.
10
+ *
11
+ * The second is freshness without a background service. A status line render
12
+ * reads the cache and nothing else, and when the cache is older than a minute
13
+ * it starts a detached refresh under a lock and returns immediately. The render
14
+ * never waits for the network; the next render shows what the refresh found.
15
+ */
16
+ import { randomUUID } from "node:crypto";
17
+ import { lstat, open, readFile, unlink, utimes } from "node:fs/promises";
18
+ import path from "node:path";
19
+ import { readJsonFileSafely, resolveStateDirectory, writeFileAtomically } from "../cache.js";
20
+ import { canonicalJson } from "../normalizer.js";
21
+ import { DESKTOP_OWNERSHIP_SECONDS } from "./cadence.js";
22
+ /**
23
+ * How old the cache may be before a render starts a refresh behind itself.
24
+ *
25
+ * A minute, which is the number Lucas's own status line has used for months. It
26
+ * is short enough that a person who opens a terminal sees current bars and long
27
+ * enough that a status line redrawing on every keystroke starts nothing.
28
+ */
29
+ export const CACHE_REFRESH_STALE_SECONDS = 60;
30
+ /** The lock one detached refresh holds, beside the cache lock and separate. */
31
+ export const REFRESH_LOCK_NAME = "openlimiter-refresh.lock";
32
+ /**
33
+ * How often a running refresh says it is still alive.
34
+ *
35
+ * The lock's modification time is the signal, so the heartbeat only touches the
36
+ * file and never rewrites it: the token inside has to stay byte for byte the
37
+ * same, because that token is what proves ownership at every write below.
38
+ */
39
+ export const REFRESH_LOCK_HEARTBEAT_MILLISECONDS = 10_000;
40
+ /**
41
+ * How long a silent lock is honoured before it is treated as abandoned.
42
+ *
43
+ * Six missed heartbeats. The bound used to be a flat two minutes measured from
44
+ * when the lock was TAKEN, which quietly assumed a whole round fits in two
45
+ * minutes; seven providers, two of them two hop, each with a fifteen second
46
+ * budget, does not fit, so a legitimate refresh could be declared dead and
47
+ * raced by the next status line render. Measuring from the last heartbeat
48
+ * instead means the bound is about liveness rather than about duration, and a
49
+ * round may take as long as it takes.
50
+ */
51
+ export const REFRESH_LOCK_STALE_MILLISECONDS = 60_000;
52
+ function errorCode(error) {
53
+ return error.code;
54
+ }
55
+ /** The newest observation in a set of rows, in epoch milliseconds, or null. */
56
+ export function newestObservedMilliseconds(snapshots) {
57
+ let newest = null;
58
+ for (const snapshot of snapshots) {
59
+ const observed = Date.parse(snapshot.observedAt);
60
+ if (!Number.isFinite(observed))
61
+ continue;
62
+ if (newest === null || observed > newest)
63
+ newest = observed;
64
+ }
65
+ return newest;
66
+ }
67
+ /**
68
+ * How old the freshest row is, in seconds.
69
+ *
70
+ * An empty cache has no age, which the caller reads as "refresh": there is
71
+ * nothing to be stale, and nothing to show either.
72
+ */
73
+ export function cacheAgeSeconds(snapshots, now) {
74
+ const current = Date.parse(now);
75
+ const newest = newestObservedMilliseconds(snapshots);
76
+ if (!Number.isFinite(current) || newest === null)
77
+ return null;
78
+ return (current - newest) / 1_000;
79
+ }
80
+ /**
81
+ * Whether a writer has kept this cache fresh inside the ownership window.
82
+ *
83
+ * Absent markers answer false, which is the backward compatible reading: a
84
+ * cache written before this field existed says nothing about who wrote it, and
85
+ * a reader that assumed a desktop was running would stop refreshing on a
86
+ * machine that has none.
87
+ */
88
+ export function writerHoldsCache(snapshots, writer, now, withinSeconds = DESKTOP_OWNERSHIP_SECONDS) {
89
+ const current = Date.parse(now);
90
+ if (!Number.isFinite(current))
91
+ return false;
92
+ return snapshots.some((snapshot) => {
93
+ if (snapshot.writer !== writer)
94
+ return false;
95
+ const observed = Date.parse(snapshot.observedAt);
96
+ if (!Number.isFinite(observed))
97
+ return false;
98
+ /* A row stamped in the future is a clock disagreement, not a fresh write,
99
+ and treating it as ownership would silence this machine indefinitely. */
100
+ if (observed > current)
101
+ return false;
102
+ return current - observed <= withinSeconds * 1_000;
103
+ });
104
+ }
105
+ /** Whether a running desktop is already keeping this cache fresh. */
106
+ export function desktopHoldsCache(snapshots, now, withinSeconds = DESKTOP_OWNERSHIP_SECONDS) {
107
+ return writerHoldsCache(snapshots, "desktop", now, withinSeconds);
108
+ }
109
+ /** Stamp rows with the process that is about to write them. */
110
+ export function withWriter(snapshots, writer) {
111
+ return snapshots.map((snapshot) => ({ ...snapshot, writer }));
112
+ }
113
+ /**
114
+ * Whether this render should start a refresh behind itself.
115
+ *
116
+ * Three inputs and no side effects, so the whole policy is one testable
117
+ * function rather than a condition spread across two commands.
118
+ */
119
+ export function shouldStartRefresh(snapshots, now, staleSeconds = CACHE_REFRESH_STALE_SECONDS) {
120
+ if (desktopHoldsCache(snapshots, now)) {
121
+ return { refresh: false, reason: "desktop_running" };
122
+ }
123
+ const age = cacheAgeSeconds(snapshots, now);
124
+ if (age !== null && age <= staleSeconds)
125
+ return { refresh: false, reason: "fresh" };
126
+ return { refresh: true };
127
+ }
128
+ /* ------------------------------------------------------------------ lock */
129
+ function lockPath(directory) {
130
+ return path.join(directory, REFRESH_LOCK_NAME);
131
+ }
132
+ /**
133
+ * Whether a refresh is running right now.
134
+ *
135
+ * A lock file older than the stale bound is not a running refresh, it is a
136
+ * process that died, and it is reported as free so the next render can take
137
+ * over. Nothing is deleted here: reclaiming belongs to the acquisition below,
138
+ * which does it under the same create attempt that would have failed.
139
+ */
140
+ export async function refreshLockHeld(directory = resolveStateDirectory(), nowMilliseconds = Date.now()) {
141
+ try {
142
+ const observed = await lstat(lockPath(directory));
143
+ if (!Number.isFinite(observed.mtimeMs))
144
+ return true;
145
+ return nowMilliseconds - observed.mtimeMs < REFRESH_LOCK_STALE_MILLISECONDS;
146
+ }
147
+ catch (error) {
148
+ if (errorCode(error) === "ENOENT")
149
+ return false;
150
+ /* A lock we cannot even inspect is treated as held. Refusing to refresh is
151
+ recoverable; two refreshes racing on one credential is not. */
152
+ return true;
153
+ }
154
+ }
155
+ /**
156
+ * Take the refresh lock, or say why not.
157
+ *
158
+ * Exclusive creation is the whole mechanism: the operating system decides the
159
+ * winner, so two terminals starting a refresh in the same millisecond produce
160
+ * exactly one round of requests. The token inside the file is checked before
161
+ * the release, so a process that lost its lock to the stale reclaim cannot
162
+ * delete the lock its successor is holding.
163
+ */
164
+ export async function acquireRefreshLock(directory = resolveStateDirectory(), nowMilliseconds = Date.now()) {
165
+ const target = lockPath(directory);
166
+ const token = JSON.stringify({ at: nowMilliseconds, id: randomUUID(), pid: process.pid });
167
+ for (let attempt = 0; attempt < 2; attempt += 1) {
168
+ try {
169
+ const handle = await open(target, "wx", 0o600);
170
+ try {
171
+ await handle.writeFile(token, "utf8");
172
+ }
173
+ finally {
174
+ await handle.close().catch(() => undefined);
175
+ }
176
+ const owns = async () => {
177
+ try {
178
+ return (await readFile(target, "utf8")) === token;
179
+ }
180
+ catch {
181
+ return false;
182
+ }
183
+ };
184
+ /*
185
+ * The heartbeat is unreferenced, so it can never hold the process open
186
+ * past the work it was watching, and it stops touching the file the
187
+ * moment this holder stops owning it.
188
+ */
189
+ const beat = setInterval(() => {
190
+ void (async () => {
191
+ if (!(await owns()))
192
+ return;
193
+ const stamp = new Date();
194
+ await utimes(target, stamp, stamp).catch(() => undefined);
195
+ })();
196
+ }, REFRESH_LOCK_HEARTBEAT_MILLISECONDS);
197
+ beat.unref();
198
+ return {
199
+ ok: true,
200
+ stillOwned: owns,
201
+ release: async () => {
202
+ clearInterval(beat);
203
+ if (!(await owns()))
204
+ return;
205
+ await unlink(target).catch(() => undefined);
206
+ }
207
+ };
208
+ }
209
+ catch (error) {
210
+ const code = errorCode(error);
211
+ if (code !== "EEXIST" && code !== "EPERM" && code !== "EACCES") {
212
+ return { ok: false, reason: "unavailable" };
213
+ }
214
+ if (attempt > 0)
215
+ return { ok: false, reason: "held" };
216
+ if (await refreshLockHeld(directory, nowMilliseconds)) {
217
+ return { ok: false, reason: "held" };
218
+ }
219
+ await unlink(target).catch(() => undefined);
220
+ }
221
+ }
222
+ return { ok: false, reason: "held" };
223
+ }
224
+ /* ------------------------------------------------------------ detachment */
225
+ /** The file that remembers a refresh this machine could not start. */
226
+ export const REFRESH_SPAWN_FAILURE_NAME = "openlimiter-refresh-spawn.json";
227
+ /**
228
+ * Remember that a background refresh could not be started.
229
+ *
230
+ * A detached spawn fails asynchronously: the executable is missing, or the
231
+ * platform refuses, and the error arrives after the status line has already
232
+ * drawn and returned. There is nobody left to tell at that point, so the fact
233
+ * is written down instead and doctor reads it back. Best effort throughout,
234
+ * because a machine that cannot start a refresh may well not be able to write
235
+ * this either, and neither failure may break a render.
236
+ */
237
+ export async function recordRefreshSpawnFailure(directory, now) {
238
+ await writeFileAtomically(path.join(directory, REFRESH_SPAWN_FAILURE_NAME), canonicalJson({ at: now, version: 1 })).catch(() => undefined);
239
+ }
240
+ /** When the last background refresh failed to start, if one did. */
241
+ export async function readRefreshSpawnFailure(directory = resolveStateDirectory()) {
242
+ const document = await readJsonFileSafely(path.join(directory, REFRESH_SPAWN_FAILURE_NAME), 4_096);
243
+ if (!document.ok)
244
+ return null;
245
+ const value = document.value;
246
+ if (typeof value !== "object" || value === null || Array.isArray(value))
247
+ return null;
248
+ const at = value["at"];
249
+ return typeof at === "string" && Number.isFinite(Date.parse(at)) ? at : null;
250
+ }
251
+ /** Forget it, which a refresh that actually ran is the proof of. */
252
+ export async function clearRefreshSpawnFailure(directory = resolveStateDirectory()) {
253
+ await unlink(path.join(directory, REFRESH_SPAWN_FAILURE_NAME)).catch(() => undefined);
254
+ }
255
+ /**
256
+ * Start a refresh behind the caller, and never wait for it.
257
+ *
258
+ * Everything expensive happens in the child. This function creates one lock
259
+ * file and hands the operating system a detached process, so the render that
260
+ * called it returns in the time a file create takes. A failure to spawn is
261
+ * reported and never thrown: a status line that cannot start a refresh still
262
+ * has to draw the bars it already has.
263
+ */
264
+ export async function spawnDetachedRefresh(options) {
265
+ const decision = shouldStartRefresh(options.snapshots, options.now, options.staleSeconds ?? CACHE_REFRESH_STALE_SECONDS);
266
+ if (!decision.refresh)
267
+ return { spawned: false, reason: decision.reason };
268
+ if (options.openLimiterScript === "")
269
+ return { spawned: false, reason: "no_script" };
270
+ const directory = options.stateDirectory ?? resolveStateDirectory();
271
+ /*
272
+ * The parent only checks the lock; the child takes it. Taking it here would
273
+ * mean releasing it here, and the parent exits long before the child has
274
+ * finished, which would leave every render starting one more refresh.
275
+ */
276
+ if (await refreshLockHeld(directory)) {
277
+ return { spawned: false, reason: "already_running" };
278
+ }
279
+ try {
280
+ options.spawn(options.nodeExecutable, [options.openLimiterScript, "refresh", "--detached"], {
281
+ onError: () => {
282
+ void recordRefreshSpawnFailure(directory, options.now);
283
+ }
284
+ });
285
+ return { spawned: true };
286
+ }
287
+ catch {
288
+ await recordRefreshSpawnFailure(directory, options.now);
289
+ return { spawned: false, reason: "failed" };
290
+ }
291
+ }
292
+ //# sourceMappingURL=coordination.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coordination.js","sourceRoot":"","sources":["../../src/acquire/coordination.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,113 @@
1
+ /** The providers this path can read a credential for. */
2
+ export declare const ACQUISITION_PROVIDERS: readonly ["CLAUDE", "CODEX", "GEMINI_CLI", "ANTIGRAVITY", "GROK", "KIMI", "OPENROUTER"];
3
+ export type AcquisitionProvider = (typeof ACQUISITION_PROVIDERS)[number];
4
+ /** Largest credential document this path will read into memory. */
5
+ export declare const MAX_CREDENTIAL_FILE_BYTES = 65536;
6
+ /**
7
+ * Why no usable credential came back.
8
+ *
9
+ * `absent` is the ordinary answer on a machine that does not run that client.
10
+ * `expired` is separated from `invalid` because only one of them has an action
11
+ * a person can take, and the row says so.
12
+ */
13
+ export type CredentialFailureReason = "absent" | "unreadable" | "invalid" | "expired" | "keychain_not_read";
14
+ /**
15
+ * Whose login this actually is.
16
+ *
17
+ * The one that matters is `shared_code_assist`. Antigravity keeps its token in
18
+ * the Windows credential store, and when that store holds nothing this reader
19
+ * falls back to the Gemini CLI's file, which is what the Antigravity client's
20
+ * own quota shares a backend with. That fallback is legitimate and it is what
21
+ * every other reader does, but the resulting row is NOT Antigravity's own
22
+ * login, and presenting it as one would tell a person they had connected
23
+ * something they never connected. So the origin travels with the credential and
24
+ * the row says which it is.
25
+ */
26
+ export declare const CREDENTIAL_ORIGINS: readonly ["vendor_store", "vendor_file", "shared_code_assist", "user_key"];
27
+ export type CredentialOrigin = (typeof CREDENTIAL_ORIGINS)[number];
28
+ export interface AcquiredCredential {
29
+ /** The bearer secret. Never printed, never logged, never persisted by us. */
30
+ readonly secret: string;
31
+ /** The account the provider knows this credential by, when it states one. */
32
+ readonly accountId: string | null;
33
+ /** Expiry in epoch milliseconds, when the document states one. */
34
+ readonly expiresAtMilliseconds: number | null;
35
+ /** Where this credential came from, so a row can say whose login it is. */
36
+ readonly origin: CredentialOrigin;
37
+ }
38
+ export type CredentialResult = {
39
+ ok: true;
40
+ credential: AcquiredCredential;
41
+ } | {
42
+ ok: false;
43
+ reason: CredentialFailureReason;
44
+ };
45
+ export interface CredentialLookupOptions {
46
+ readonly platform?: NodeJS.Platform;
47
+ readonly environment?: Readonly<Record<string, string | undefined>>;
48
+ readonly homeDirectory?: string;
49
+ /** Injected clock, so an expiry test does not depend on the wall clock. */
50
+ readonly now?: string;
51
+ /**
52
+ * How a Windows Credential Manager entry is read.
53
+ *
54
+ * Injected so the Antigravity path can be proved without a real credential
55
+ * on the machine, and so no test ever touches the live credential store.
56
+ */
57
+ readonly readWindowsCredential?: WindowsCredentialReader;
58
+ }
59
+ export type WindowsCredentialReader = (target: string) => Promise<{
60
+ ok: true;
61
+ value: string;
62
+ } | {
63
+ ok: false;
64
+ reason: CredentialFailureReason;
65
+ }>;
66
+ /**
67
+ * Every place a provider's own client is known to keep its credential, in the
68
+ * order the vendor's own environment variable would win.
69
+ *
70
+ * The first readable document that yields a usable secret wins. Order matters
71
+ * only for a machine carrying two of them, and in that case the explicit
72
+ * environment variable is the one the person set on purpose.
73
+ */
74
+ export declare function credentialCandidatePaths(provider: AcquisitionProvider, options?: CredentialLookupOptions): string[];
75
+ /** The Windows Credential Manager target the Antigravity client writes to. */
76
+ export declare const ANTIGRAVITY_CREDENTIAL_TARGET = "gemini:antigravity";
77
+ /** The macOS keychain service Claude Code writes to, which this path skips. */
78
+ export declare const CLAUDE_KEYCHAIN_SERVICE = "Claude Code-credentials";
79
+ /** The issuer a Grok credential file should be read under when it names one. */
80
+ export declare const GROK_PREFERRED_ISSUER = "https://auth.x.ai";
81
+ /**
82
+ * An expiry, whatever unit the document happened to state it in.
83
+ *
84
+ * Seconds, milliseconds and RFC3339 all appear across these six clients. A
85
+ * value below the seconds ceiling is read as seconds, which is the same rule
86
+ * every reset parser in this product already uses.
87
+ */
88
+ export declare function expiryMilliseconds(value: unknown): number | null;
89
+ /**
90
+ * Read one credential document that has already been parsed.
91
+ *
92
+ * Exported so a test can prove every shape without writing a file, and so the
93
+ * Windows Credential Manager path can reuse exactly the same reader for the
94
+ * envelope it gets back from the credential store.
95
+ */
96
+ export declare function readCredentialDocument(provider: AcquisitionProvider, document: unknown, nowMilliseconds: number, origin?: CredentialOrigin): CredentialResult;
97
+ /**
98
+ * Read the credential a provider's own client stored, from disk.
99
+ *
100
+ * The result is one of four things and never a mixture: a usable credential, a
101
+ * provider that is simply not installed here, a document that could not be
102
+ * believed, or a login that has run out. Nothing is refreshed and nothing is
103
+ * written back.
104
+ */
105
+ export declare function readAcquisitionCredential(provider: AcquisitionProvider, options?: CredentialLookupOptions): Promise<CredentialResult>;
106
+ /**
107
+ * One sentence per failure, for the row a person actually reads.
108
+ *
109
+ * No dashes anywhere, and every sentence names the next step rather than the
110
+ * internal reason, because a person cannot act on the word "invalid".
111
+ */
112
+ export declare const CREDENTIAL_FAILURE_SENTENCE: Readonly<Record<CredentialFailureReason, string>>;
113
+ //# sourceMappingURL=credentials.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../../src/acquire/credentials.ts"],"names":[],"mappings":"AAuBA,yDAAyD;AACzD,eAAO,MAAM,qBAAqB,yFAQxB,CAAC;AAEX,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzE,mEAAmE;AACnE,eAAO,MAAM,yBAAyB,QAAS,CAAC;AAEhD;;;;;;GAMG;AACH,MAAM,MAAM,uBAAuB,GAC/B,QAAQ,GACR,YAAY,GACZ,SAAS,GACT,SAAS,GACT,mBAAmB,CAAC;AAExB;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,kBAAkB,4EAKrB,CAAC;AAEX,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEnE,MAAM,WAAW,kBAAkB;IACjC,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,kEAAkE;IAClE,QAAQ,CAAC,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9C,2EAA2E;IAC3E,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;CACnC;AAED,MAAM,MAAM,gBAAgB,GACxB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,UAAU,EAAE,kBAAkB,CAAA;CAAE,GAC5C;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,uBAAuB,CAAA;CAAE,CAAC;AAEnD,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IACpC,QAAQ,CAAC,WAAW,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IACpE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,2EAA2E;IAC3E,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,QAAQ,CAAC,qBAAqB,CAAC,EAAE,uBAAuB,CAAC;CAC1D;AAED,MAAM,MAAM,uBAAuB,GAAG,CACpC,MAAM,EAAE,MAAM,KACX,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,uBAAuB,CAAA;CAAE,CAAC,CAAC;AAoC3F;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,GAAE,uBAA4B,GACpC,MAAM,EAAE,CAqDV;AAED,8EAA8E;AAC9E,eAAO,MAAM,6BAA6B,uBAAuB,CAAC;AAElE,+EAA+E;AAC/E,eAAO,MAAM,uBAAuB,4BAA4B,CAAC;AA6DjE,gFAAgF;AAChF,eAAO,MAAM,qBAAqB,sBAAsB,CAAC;AAEzD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAWhE;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,mBAAmB,EAC7B,QAAQ,EAAE,OAAO,EACjB,eAAe,EAAE,MAAM,EACvB,MAAM,GAAE,gBAAgC,GACvC,gBAAgB,CA4ClB;AAED;;;;;;;GAOG;AACH,wBAAsB,yBAAyB,CAC7C,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,GAAE,uBAA4B,GACpC,OAAO,CAAC,gBAAgB,CAAC,CAsD3B;AAED;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,EACtC,QAAQ,CAAC,MAAM,CAAC,uBAAuB,EAAE,MAAM,CAAC,CAOjD,CAAC"}