openlimiter 1.2.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 (55) hide show
  1. package/README.md +45 -16
  2. package/dist/bin.js +53 -4
  3. package/dist/bin.js.map +1 -1
  4. package/dist/cli.d.ts +110 -11
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +1064 -64
  7. package/dist/cli.js.map +1 -1
  8. package/dist/codex-device-login.d.ts +87 -0
  9. package/dist/codex-device-login.d.ts.map +1 -0
  10. package/dist/codex-device-login.js +291 -0
  11. package/dist/codex-device-login.js.map +1 -0
  12. package/dist/config.d.ts +44 -1
  13. package/dist/config.d.ts.map +1 -1
  14. package/dist/config.js +129 -7
  15. package/dist/config.js.map +1 -1
  16. package/dist/hub-auth.d.ts +137 -0
  17. package/dist/hub-auth.d.ts.map +1 -0
  18. package/dist/hub-auth.js +280 -0
  19. package/dist/hub-auth.js.map +1 -0
  20. package/dist/hub-sync.d.ts +144 -0
  21. package/dist/hub-sync.d.ts.map +1 -0
  22. package/dist/hub-sync.js +370 -0
  23. package/dist/hub-sync.js.map +1 -0
  24. package/dist/hub.d.ts +60 -0
  25. package/dist/hub.d.ts.map +1 -0
  26. package/dist/hub.js +178 -0
  27. package/dist/hub.js.map +1 -0
  28. package/dist/index.d.ts +6 -2
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +6 -2
  31. package/dist/index.js.map +1 -1
  32. package/dist/ingest.d.ts +26 -0
  33. package/dist/ingest.d.ts.map +1 -1
  34. package/dist/ingest.js +116 -1
  35. package/dist/ingest.js.map +1 -1
  36. package/dist/qr.d.ts.map +1 -1
  37. package/dist/qr.js +1 -0
  38. package/dist/qr.js.map +1 -1
  39. package/dist/session.d.ts +46 -0
  40. package/dist/session.d.ts.map +1 -0
  41. package/dist/session.js +121 -0
  42. package/dist/session.js.map +1 -0
  43. package/dist/statusline.d.ts +14 -0
  44. package/dist/statusline.d.ts.map +1 -1
  45. package/dist/statusline.js +258 -19
  46. package/dist/statusline.js.map +1 -1
  47. package/dist/terminal.d.ts +109 -0
  48. package/dist/terminal.d.ts.map +1 -0
  49. package/dist/terminal.js +974 -0
  50. package/dist/terminal.js.map +1 -0
  51. package/package.json +4 -4
  52. package/dist/serve.d.ts +0 -170
  53. package/dist/serve.d.ts.map +0 -1
  54. package/dist/serve.js +0 -533
  55. package/dist/serve.js.map +0 -1
@@ -0,0 +1,280 @@
1
+ /**
2
+ * Signing in to the hub, staying signed in, and reading who is signed in.
3
+ *
4
+ * The device flow itself is nothing this build invented: a code and an
5
+ * address are shown, somebody types the code somewhere else, and this process
6
+ * polls until the hub says approved, denied or expired. The hub is the only
7
+ * clock that matters for expiry: it is the one thing that can say the device
8
+ * code has lapsed, so this build never guesses at a deadline of its own. The
9
+ * poll loop still carries a safety ceiling, derived from the hub's own stated
10
+ * lifetime, so a stubbed or misbehaving transport in a test cannot spin it
11
+ * forever.
12
+ */
13
+ import { cliLoginPollRequest, cliLoginStartRequest, grantRenewRequest, hubConfigured, parseHubJson } from "./hub.js";
14
+ import { sessionIsFresh } from "./session.js";
15
+ /** The sentence a hub side revocation prints, word for word. */
16
+ export const REVOKED_SENTENCE = "Signed out on the hub, run openlimiter login";
17
+ /** The sentence a login code the hub already consumed prints, word for word. */
18
+ export const CODE_CONSUMED_SENTENCE = "That code was already used, run openlimiter login again";
19
+ /** Extra polls past the hub's own stated lifetime, before this build gives up. */
20
+ export const LOGIN_SAFETY_MARGIN_POLLS = 5;
21
+ function isFiniteInRange(value, minimum, maximum) {
22
+ return typeof value === "number" && Number.isFinite(value) && value >= minimum && value <= maximum;
23
+ }
24
+ function isBoundedString(value, minimum, maximum) {
25
+ return typeof value === "string" && value.length >= minimum && value.length <= maximum;
26
+ }
27
+ function isInstant(value) {
28
+ return isBoundedString(value, 1, 64) && Number.isFinite(Date.parse(value));
29
+ }
30
+ /**
31
+ * Whether Ctrl C asked this poll loop to stop.
32
+ *
33
+ * A plain `signal?.aborted === true` reads fine once, but TypeScript narrows
34
+ * that exact expression across the loop body, so a second identical check
35
+ * after an `await` is seen as comparing an already narrowed `false` against
36
+ * `true` and refused as unreachable. A function call has no such memory.
37
+ */
38
+ export function isAborted(signal) {
39
+ return signal !== undefined && signal.aborted;
40
+ }
41
+ /** Parse the device flow's opening answer, trusting nothing about its shape. */
42
+ export function parseLoginStart(body) {
43
+ const parsed = parseHubJson(body);
44
+ if (parsed === null)
45
+ return null;
46
+ const userCode = parsed["user_code"];
47
+ const deviceCode = parsed["device_code"];
48
+ const verificationUrl = parsed["verification_url"];
49
+ const interval = parsed["interval"];
50
+ const expiresIn = parsed["expires_in"];
51
+ if (!isBoundedString(userCode, 1, 64) ||
52
+ !isBoundedString(deviceCode, 1, 256) ||
53
+ !isBoundedString(verificationUrl, 8, 512) ||
54
+ !verificationUrl.startsWith("https://") ||
55
+ !isFiniteInRange(interval, 1, 300) ||
56
+ !isFiniteInRange(expiresIn, 1, 86_400)) {
57
+ return null;
58
+ }
59
+ return {
60
+ userCode,
61
+ deviceCode,
62
+ verificationUrl,
63
+ intervalSeconds: interval,
64
+ expiresInSeconds: expiresIn
65
+ };
66
+ }
67
+ /** Parse one poll answer, in the five shapes the contract allows. */
68
+ export function parseLoginPoll(body) {
69
+ const parsed = parseHubJson(body);
70
+ if (parsed === null)
71
+ return null;
72
+ const status = parsed["status"];
73
+ if (status === "pending" ||
74
+ status === "slow_down" ||
75
+ status === "denied" ||
76
+ status === "expired" ||
77
+ status === "consumed") {
78
+ return { status };
79
+ }
80
+ if (status !== "approved")
81
+ return null;
82
+ const token = parsed["token"];
83
+ const expiresAt = parsed["expires_at"];
84
+ const refreshCredential = parsed["refresh_credential"];
85
+ const refreshExpiresAt = parsed["refresh_expires_at"];
86
+ const deviceId = parsed["device_id"];
87
+ if (!isBoundedString(token, 16, 32_768) ||
88
+ !isInstant(expiresAt) ||
89
+ !isBoundedString(refreshCredential, 16, 4_096) ||
90
+ !isInstant(refreshExpiresAt) ||
91
+ !isBoundedString(deviceId, 1, 128)) {
92
+ return null;
93
+ }
94
+ return { status: "approved", token, expiresAt, refreshCredential, refreshExpiresAt, deviceId };
95
+ }
96
+ /** Parse a renewal answer: a fresh token and a rotated refresh credential. */
97
+ export function parseGrantRenewal(body) {
98
+ const parsed = parseHubJson(body);
99
+ if (parsed === null)
100
+ return null;
101
+ const token = parsed["token"];
102
+ const expiresAt = parsed["expires_at"];
103
+ const refreshCredential = parsed["refresh_credential"];
104
+ const refreshExpiresAt = parsed["refresh_expires_at"];
105
+ if (!isBoundedString(token, 16, 32_768) ||
106
+ !isInstant(expiresAt) ||
107
+ !isBoundedString(refreshCredential, 16, 4_096) ||
108
+ !isInstant(refreshExpiresAt)) {
109
+ return null;
110
+ }
111
+ return { token, expiresAt, refreshCredential, refreshExpiresAt };
112
+ }
113
+ function base64UrlDecode(segment) {
114
+ if (segment.length === 0 || segment.length > 16_384 || !/^[A-Za-z0-9_-]+$/u.test(segment)) {
115
+ return null;
116
+ }
117
+ try {
118
+ return Buffer.from(segment, "base64url").toString("utf8");
119
+ }
120
+ catch {
121
+ return null;
122
+ }
123
+ }
124
+ /**
125
+ * A human name for the account, read from the token if it carries one.
126
+ *
127
+ * The hub contract names no account label field at all, and a token is the
128
+ * one thing the approved answer definitely carries that could hold one: a
129
+ * JSON Web Token's own payload, unsigned and unverified here because nothing
130
+ * downstream trusts this label for anything but display. A token that is not
131
+ * a JWT, or carries no email claim, falls back to the device id, which is
132
+ * always there and is at least a stable name to look at with `whoami`.
133
+ */
134
+ export function accountLabelFromToken(token, fallback) {
135
+ const segments = token.split(".");
136
+ if (segments.length !== 3)
137
+ return fallback;
138
+ const payload = segments[1];
139
+ if (payload === undefined)
140
+ return fallback;
141
+ const decoded = base64UrlDecode(payload);
142
+ if (decoded === null)
143
+ return fallback;
144
+ try {
145
+ const parsed = JSON.parse(decoded);
146
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
147
+ const email = parsed["email"];
148
+ if (typeof email === "string" && email.length > 0 && email.length <= 256)
149
+ return email;
150
+ }
151
+ }
152
+ catch {
153
+ /* Not a JWT this build can read the claims of. The device id still names it. */
154
+ }
155
+ return fallback;
156
+ }
157
+ /**
158
+ * Run the whole device flow: start, show the code, poll until an ending.
159
+ *
160
+ * `slow_down` widens the interval rather than being treated as a failure,
161
+ * which is the device flow's own documented way of saying "you are polling
162
+ * this endpoint too often". A poll that could not be sent or read is not
163
+ * treated as `denied` either: a network blip inside a three minute window is
164
+ * routine, and the loop tries again next interval rather than ending the
165
+ * whole sign in over it.
166
+ */
167
+ export async function runDeviceLogin(options) {
168
+ if (!hubConfigured(options.environment))
169
+ return { kind: "not_configured" };
170
+ const startRequest = cliLoginStartRequest(options.environment);
171
+ if (startRequest === null)
172
+ return { kind: "not_configured" };
173
+ let startReply;
174
+ try {
175
+ startReply = await options.transport(startRequest);
176
+ }
177
+ catch {
178
+ return { kind: "error", message: "could not reach the hub" };
179
+ }
180
+ if (startReply.status < 200 || startReply.status >= 300) {
181
+ return { kind: "error", message: "the hub refused the sign in request" };
182
+ }
183
+ const start = parseLoginStart(startReply.body);
184
+ if (start === null) {
185
+ return { kind: "error", message: "the hub answered with something this build could not read" };
186
+ }
187
+ options.emit("Enter this code: " + start.userCode);
188
+ options.emit("At: " + start.verificationUrl);
189
+ if (options.open)
190
+ options.openBrowser?.(start.verificationUrl);
191
+ let intervalSeconds = start.intervalSeconds;
192
+ const maxAttempts = Math.ceil(start.expiresInSeconds / intervalSeconds) + LOGIN_SAFETY_MARGIN_POLLS;
193
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
194
+ if (isAborted(options.interruptSignal))
195
+ return { kind: "cancelled" };
196
+ await options.sleep(intervalSeconds * 1_000);
197
+ if (isAborted(options.interruptSignal))
198
+ return { kind: "cancelled" };
199
+ const pollRequest = cliLoginPollRequest(options.environment, start.deviceCode);
200
+ if (pollRequest === null)
201
+ return { kind: "error", message: "the device code could not be sent" };
202
+ let pollReply;
203
+ try {
204
+ pollReply = await options.transport(pollRequest);
205
+ }
206
+ catch {
207
+ continue;
208
+ }
209
+ if (pollReply.status < 200 || pollReply.status >= 300)
210
+ continue;
211
+ const poll = parseLoginPoll(pollReply.body);
212
+ if (poll === null)
213
+ continue;
214
+ if (poll.status === "pending")
215
+ continue;
216
+ if (poll.status === "slow_down") {
217
+ intervalSeconds += 5;
218
+ continue;
219
+ }
220
+ if (poll.status === "denied")
221
+ return { kind: "denied" };
222
+ if (poll.status === "expired")
223
+ return { kind: "expired" };
224
+ if (poll.status === "consumed")
225
+ return { kind: "expired", message: CODE_CONSUMED_SENTENCE };
226
+ const session = {
227
+ version: 1,
228
+ token: poll.token,
229
+ expiresAt: poll.expiresAt,
230
+ refreshCredential: poll.refreshCredential,
231
+ refreshExpiresAt: poll.refreshExpiresAt,
232
+ deviceId: poll.deviceId,
233
+ accountLabel: accountLabelFromToken(poll.token, poll.deviceId)
234
+ };
235
+ return { kind: "signed_in", session };
236
+ }
237
+ return { kind: "expired" };
238
+ }
239
+ /**
240
+ * Make sure a session is good for one more upload, renewing if it is not.
241
+ *
242
+ * A session inside the renewal window is used exactly as it stands: renewing
243
+ * early for no reason spends the one time use refresh credential for nothing.
244
+ * A 401 from the renewal call is read as the hub having revoked this device,
245
+ * never as an ordinary network failure, because the two need different
246
+ * endings: one clears the session and asks for a fresh sign in, the other
247
+ * leaves the session in place and lets the next attempt try again.
248
+ */
249
+ export async function ensureFreshSession(session, now, environment, transport) {
250
+ if (sessionIsFresh(session, now))
251
+ return { kind: "fresh", session };
252
+ const request = grantRenewRequest(environment, session.refreshCredential);
253
+ if (request === null)
254
+ return { kind: "error" };
255
+ let reply;
256
+ try {
257
+ reply = await transport(request);
258
+ }
259
+ catch {
260
+ return { kind: "error" };
261
+ }
262
+ if (reply.status === 401)
263
+ return { kind: "revoked" };
264
+ if (reply.status < 200 || reply.status >= 300)
265
+ return { kind: "error" };
266
+ const renewal = parseGrantRenewal(reply.body);
267
+ if (renewal === null)
268
+ return { kind: "error" };
269
+ return {
270
+ kind: "renewed",
271
+ session: {
272
+ ...session,
273
+ token: renewal.token,
274
+ expiresAt: renewal.expiresAt,
275
+ refreshCredential: renewal.refreshCredential,
276
+ refreshExpiresAt: renewal.refreshExpiresAt
277
+ }
278
+ };
279
+ }
280
+ //# sourceMappingURL=hub-auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hub-auth.js","sourceRoot":"","sources":["../src/hub-auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,144 @@
1
+ import { type Snapshot } from "@openlimiter/core";
2
+ import { type HubTransport } from "./hub.js";
3
+ export declare const SYNC_CURSOR_FILE_NAME = "openlimiter-sync-cursor.json";
4
+ /** The contract version this build speaks, and nothing else. */
5
+ export declare const SYNC_SCHEMA_VERSION = 2;
6
+ /**
7
+ * What this build calls itself on the wire.
8
+ *
9
+ * A constant, not a read of `package.json`: this module ships inside a
10
+ * published package, and a runtime file read is one more thing that can fail
11
+ * inside a command a status line depends on. See `acquire/identity.ts` in the
12
+ * core package for the same choice, made for the same reason.
13
+ */
14
+ export declare const SYNC_CLIENT_VERSION = "1.3.1";
15
+ /** Most rows one envelope may carry, usage and spend counted together. */
16
+ export declare const SYNC_MAX_ROWS = 128;
17
+ /** How long an unresolved upload may still be retried as itself. */
18
+ export declare const SYNC_RESUME_WINDOW_HOURS = 23;
19
+ export interface UsageSample {
20
+ readonly account_id: string;
21
+ readonly provider: string;
22
+ readonly meter: string;
23
+ readonly window_id: string;
24
+ readonly usage_percent: number;
25
+ readonly reset_at: string | null;
26
+ readonly observed_at: string;
27
+ readonly stale: boolean;
28
+ }
29
+ export interface ForecastInput {
30
+ readonly first_observed_at: string;
31
+ readonly last_observed_at: string;
32
+ readonly sample_count: number;
33
+ readonly burn_usd_per_day: number;
34
+ }
35
+ export interface ApiSpendSample {
36
+ readonly source_id: string;
37
+ readonly account_id: string;
38
+ readonly provider: string;
39
+ readonly key_label: string;
40
+ readonly month: string;
41
+ readonly spend_usd: number;
42
+ readonly budget_usd: number | null;
43
+ readonly source_period: readonly [string, string];
44
+ readonly currency_source: string;
45
+ readonly raw_unit_scale: number;
46
+ readonly forecast_date: string | null;
47
+ readonly forecast_input: ForecastInput | null;
48
+ readonly period_complete: boolean;
49
+ }
50
+ export interface SyncEnvelope {
51
+ readonly event_id: string;
52
+ readonly device_id: string;
53
+ readonly previous_sequence: number;
54
+ readonly sequence: number;
55
+ readonly observed_at: string;
56
+ readonly client_version: string;
57
+ readonly schema_version: number;
58
+ readonly usage_samples: readonly UsageSample[];
59
+ readonly api_spend_samples: readonly ApiSpendSample[];
60
+ }
61
+ /**
62
+ * Every usage row this device would upload, from the cache it already writes
63
+ * for the terminal and the tray.
64
+ *
65
+ * Only `PERCENT` rows are usage: a credit or token count is not a usage
66
+ * fraction and does not belong beside one. `API_BUDGET_PERCENT` is excluded
67
+ * too, porting the same rule `usage_samples_from_cache` in account.rs reads
68
+ * its own cache by: it is a budget reading at PERCENT unit, not a usage
69
+ * window, and sending it is exactly what gets a whole envelope rejected
70
+ * rather than the one row dropped, since the hub does not recognise it as
71
+ * usage at all. The Rust reader excludes the Moonshot provider by name too,
72
+ * as a second, belt and suspenders check; nothing here reproduces that one,
73
+ * because it cannot fire on this side of the boundary. Moonshot's own budget
74
+ * reading already carries provider `KIMI` in this build's own vocabulary
75
+ * (`PROVIDER_CODES` in types.ts has no `MOONSHOT` at all, and
76
+ * `normalizeMeter` refuses the whole reading for any provider string outside
77
+ * that set before a `Snapshot` is ever built), so the meter check above is
78
+ * this side's whole answer, not half of one. A row that fails a bound,
79
+ * including an unreadable account id, is dropped rather than repaired,
80
+ * matching the same rule the desktop reads its own cache by.
81
+ */
82
+ export declare function usageSamplesFromSnapshots(snapshots: readonly Snapshot[], now: string): UsageSample[];
83
+ /**
84
+ * Every spend row the cache has enough to build.
85
+ *
86
+ * This build tracks a point in time reading, not a month to date total with a
87
+ * forecast: those live in the desktop's dedicated spend engine, which this
88
+ * package does not have. So every row this function builds states its period
89
+ * honestly as "the start of this month through the moment of this upload"
90
+ * and never claims a forecast or a completed period it did not compute.
91
+ * `usedAmount`, `limitAmount` and `currency` travel together or not at all
92
+ * (see `Snapshot` in the core package), which is what this reads as "the
93
+ * cache has one".
94
+ */
95
+ /** The providers the hub accepts an API spend sample for (mirrors API_PROVIDERS in the server contract); every other provider's spend stays local, otherwise one row would void the whole envelope. */
96
+ export declare const SYNC_API_SPEND_PROVIDERS: ReadonlySet<string>;
97
+ export declare function apiSpendSamplesFromSnapshots(snapshots: readonly Snapshot[], envelopeObservedAt: string): ApiSpendSample[];
98
+ export interface EnvelopeIdentity {
99
+ readonly deviceId: string;
100
+ readonly eventId: string;
101
+ readonly previousSequence: number;
102
+ readonly observedAt: string;
103
+ }
104
+ /**
105
+ * One envelope from one set of readings.
106
+ *
107
+ * A reading observed after the envelope itself, which a clock stepping
108
+ * backwards between the cache write and this call would produce, is pulled
109
+ * back to the envelope's own timestamp rather than dropped. See
110
+ * `build_envelope` in `account.rs` for the same rule.
111
+ */
112
+ export declare function buildSyncEnvelope(identity: EnvelopeIdentity, usageSamples: readonly UsageSample[], apiSpendSamples: readonly ApiSpendSample[]): SyncEnvelope;
113
+ export interface RunSyncOptions {
114
+ readonly directory: string;
115
+ readonly environment: Readonly<Record<string, string | undefined>>;
116
+ readonly transport: HubTransport;
117
+ readonly now: string;
118
+ readonly token: string;
119
+ readonly deviceId: string;
120
+ readonly snapshots: readonly Snapshot[];
121
+ }
122
+ export type SyncResult = {
123
+ readonly kind: "nothing_to_sync";
124
+ } | {
125
+ readonly kind: "accepted";
126
+ readonly rows: number;
127
+ readonly tier: string | null;
128
+ } | {
129
+ readonly kind: "revoked";
130
+ } | {
131
+ readonly kind: "rejected";
132
+ } | {
133
+ readonly kind: "unavailable";
134
+ };
135
+ /**
136
+ * One upload, on the version 2 contract, with the same three way ending the
137
+ * hub's cursor rule requires: accepted advances the cursor, a reply that
138
+ * never arrived leaves the same upload pending for the next run, and a
139
+ * conflict names the hub's own sequence and is retried once.
140
+ */
141
+ export declare function runSync(options: RunSyncOptions): Promise<SyncResult>;
142
+ /** The state directory's default, exported so a caller need not repeat it. */
143
+ export declare function defaultSyncDirectory(): string;
144
+ //# sourceMappingURL=hub-sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hub-sync.d.ts","sourceRoot":"","sources":["../src/hub-sync.ts"],"names":[],"mappings":"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"}