@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.
- 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 +2284 -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-request-metadata.d.ts.map +1 -1
- package/dist/agy-request-metadata.js +8 -21
- package/dist/agy-request-metadata.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/fingerprint.d.ts +3 -3
- package/dist/fingerprint.d.ts.map +1 -1
- package/dist/fingerprint.js +4 -4
- package/dist/fingerprint.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/model-registry.d.ts +16 -0
- package/dist/model-registry.d.ts.map +1 -1
- package/dist/model-registry.js +242 -30
- package/dist/model-registry.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/agy-generation.d.ts +3 -0
- package/dist/transform/agy-generation.d.ts.map +1 -0
- package/dist/transform/agy-generation.js +28 -0
- package/dist/transform/agy-generation.js.map +1 -0
- package/dist/transform/claude.d.ts +7 -3
- package/dist/transform/claude.d.ts.map +1 -1
- package/dist/transform/claude.js +8 -2
- package/dist/transform/claude.js.map +1 -1
- package/dist/transform/index.d.ts +1 -0
- package/dist/transform/index.d.ts.map +1 -1
- package/dist/transform/index.js +1 -0
- package/dist/transform/index.js.map +1 -1
- package/dist/transform/model-resolver.d.ts +2 -1
- package/dist/transform/model-resolver.d.ts.map +1 -1
- package/dist/transform/model-resolver.js +46 -5
- package/dist/transform/model-resolver.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,49 @@
|
|
|
1
|
+
export type PiSlotErrorCode = 'invalid-options' | 'invalid-auth' | 'invalid-stash' | 'unsafe-path' | 'missing-stash' | 'conflict' | 'lock-timeout' | 'lock-compromised' | 'io';
|
|
2
|
+
/** Errors deliberately contain neither paths nor credentials nor underlying error messages. */
|
|
3
|
+
export declare class PiSlotError extends Error {
|
|
4
|
+
readonly code: PiSlotErrorCode;
|
|
5
|
+
constructor(code: PiSlotErrorCode);
|
|
6
|
+
}
|
|
7
|
+
export interface PiSlotOptions {
|
|
8
|
+
authPath: string;
|
|
9
|
+
provider: string;
|
|
10
|
+
/**
|
|
11
|
+
* Where the provider's original Pi login is kept while the vault serves.
|
|
12
|
+
* Put it in a private directory, and give each provider its own file: one
|
|
13
|
+
* stash holds one provider's original login, and a second provider writing
|
|
14
|
+
* to it would overwrite the first one's backup.
|
|
15
|
+
*/
|
|
16
|
+
stashPath: string;
|
|
17
|
+
/**
|
|
18
|
+
* The api_key Pi stores in the slot while the vault serves. It is a fixed,
|
|
19
|
+
* non-secret string that no provider accepts. Pi runs a key starting with
|
|
20
|
+
* `!` as a shell command and expands `$NAME` from the environment, so the
|
|
21
|
+
* placeholder may contain neither.
|
|
22
|
+
*/
|
|
23
|
+
placeholderKey: string;
|
|
24
|
+
lockTimeoutMs?: number;
|
|
25
|
+
}
|
|
26
|
+
export interface PiSlotInspection {
|
|
27
|
+
slot: 'original' | 'placeholder' | 'foreign' | 'empty';
|
|
28
|
+
stash: 'valid' | 'missing';
|
|
29
|
+
}
|
|
30
|
+
export interface PiSlot {
|
|
31
|
+
inspect(): Promise<PiSlotInspection>;
|
|
32
|
+
enterVault(): Promise<void>;
|
|
33
|
+
exitVault(): Promise<'restored' | 'conflict' | 'nothing-to-do'>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Production entry point. Pi caches which models are usable; after a
|
|
37
|
+
* successful transition the extension calls `ctx.modelRegistry.refresh()` so
|
|
38
|
+
* Pi re-reads the slot it just changed.
|
|
39
|
+
*/
|
|
40
|
+
export declare function openPiSlot(options: PiSlotOptions): PiSlot;
|
|
41
|
+
/**
|
|
42
|
+
* Internal test entry point: pause reads, renewals, or renames to simulate races,
|
|
43
|
+
* and observe durable writes/deletion to simulate crashes. Not a public export.
|
|
44
|
+
*/
|
|
45
|
+
export declare function createPiSlot(options: PiSlotOptions, onStep?: (step: 'stash-written' | 'auth-written' | 'stash-deleted') => void, hooks?: {
|
|
46
|
+
afterReadCheck?: (path: string) => Promise<void>;
|
|
47
|
+
onRenew?: (renew: () => Promise<void>) => void;
|
|
48
|
+
beforeRename?: (path: string) => Promise<void>;
|
|
49
|
+
}): PiSlot;
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { lstat, mkdir, open, unlink } from 'node:fs/promises';
|
|
4
|
+
import { dirname, resolve } from 'node:path';
|
|
5
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
6
|
+
import { writeJsonAtomic } from '../fs/atomic-write.js';
|
|
7
|
+
import { acquirePiLock } from './lock.js';
|
|
8
|
+
/** Errors deliberately contain neither paths nor credentials nor underlying error messages. */
|
|
9
|
+
export class PiSlotError extends Error {
|
|
10
|
+
code;
|
|
11
|
+
constructor(code) {
|
|
12
|
+
super(`Pi slot: ${code}`);
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.name = 'PiSlotError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// JSON.parse validates grammar first. This scan only locates top-level value text ranges.
|
|
18
|
+
function rootSpans(text) {
|
|
19
|
+
let i = text.indexOf('{') + 1;
|
|
20
|
+
const root = { offset: i - 1, children: [] };
|
|
21
|
+
const whitespace = () => {
|
|
22
|
+
while (/\s/.test(text[i] ?? '') && i < text.length)
|
|
23
|
+
i++;
|
|
24
|
+
};
|
|
25
|
+
const stringEnd = () => {
|
|
26
|
+
i++;
|
|
27
|
+
while (i < text.length) {
|
|
28
|
+
if (text[i++] === '"')
|
|
29
|
+
return;
|
|
30
|
+
if (text[i - 1] === '\\')
|
|
31
|
+
i++;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
whitespace();
|
|
35
|
+
while (text[i] !== '}') {
|
|
36
|
+
const start = i;
|
|
37
|
+
stringEnd();
|
|
38
|
+
const key = JSON.parse(text.slice(start, i));
|
|
39
|
+
whitespace();
|
|
40
|
+
i++; // colon
|
|
41
|
+
whitespace();
|
|
42
|
+
const offset = i;
|
|
43
|
+
let depth = 0;
|
|
44
|
+
while (i < text.length) {
|
|
45
|
+
const char = text[i];
|
|
46
|
+
if (char === '"') {
|
|
47
|
+
stringEnd();
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (char === '{' || char === '[')
|
|
51
|
+
depth++;
|
|
52
|
+
if (char === '}' || char === ']') {
|
|
53
|
+
if (depth === 0)
|
|
54
|
+
break;
|
|
55
|
+
depth--;
|
|
56
|
+
}
|
|
57
|
+
if (char === ',' && depth === 0)
|
|
58
|
+
break;
|
|
59
|
+
i++;
|
|
60
|
+
}
|
|
61
|
+
let end = i;
|
|
62
|
+
while (/\s/.test(text[end - 1] ?? ''))
|
|
63
|
+
end--;
|
|
64
|
+
root.children.push({
|
|
65
|
+
key,
|
|
66
|
+
offset: start,
|
|
67
|
+
length: end - start,
|
|
68
|
+
value: { offset, length: end - offset },
|
|
69
|
+
});
|
|
70
|
+
whitespace();
|
|
71
|
+
if (text[i] === '}')
|
|
72
|
+
break;
|
|
73
|
+
i++; // comma
|
|
74
|
+
whitespace();
|
|
75
|
+
}
|
|
76
|
+
return root;
|
|
77
|
+
}
|
|
78
|
+
function object(value) {
|
|
79
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
80
|
+
}
|
|
81
|
+
function canonical(value) {
|
|
82
|
+
if (Array.isArray(value))
|
|
83
|
+
return `[${value.map(canonical).join(',')}]`;
|
|
84
|
+
if (object(value)) {
|
|
85
|
+
return `{${Object.keys(value)
|
|
86
|
+
.sort()
|
|
87
|
+
.map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`)
|
|
88
|
+
.join(',')}}`;
|
|
89
|
+
}
|
|
90
|
+
return JSON.stringify(value);
|
|
91
|
+
}
|
|
92
|
+
function digest(value) {
|
|
93
|
+
return createHash('sha256').update(canonical(value)).digest('hex');
|
|
94
|
+
}
|
|
95
|
+
function code(error) {
|
|
96
|
+
return object(error) ? error.code : undefined;
|
|
97
|
+
}
|
|
98
|
+
async function safeRead(path, privateFile = false, afterReadCheck) {
|
|
99
|
+
let file;
|
|
100
|
+
try {
|
|
101
|
+
file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
102
|
+
const stat = await file.stat();
|
|
103
|
+
if (!stat.isFile() || (privateFile && (stat.mode & 0o777) !== 0o600)) {
|
|
104
|
+
throw new PiSlotError('unsafe-path');
|
|
105
|
+
}
|
|
106
|
+
await afterReadCheck?.(path);
|
|
107
|
+
// Refuse a replaced pathname even though the descriptor still pins the checked file.
|
|
108
|
+
const current = await lstat(path);
|
|
109
|
+
if (!current.isFile() ||
|
|
110
|
+
current.dev !== stat.dev ||
|
|
111
|
+
current.ino !== stat.ino) {
|
|
112
|
+
throw new PiSlotError('unsafe-path');
|
|
113
|
+
}
|
|
114
|
+
const bytes = await file.readFile();
|
|
115
|
+
const text = bytes.toString('utf8');
|
|
116
|
+
if (!bytes.equals(Buffer.from(text))) {
|
|
117
|
+
throw new PiSlotError(privateFile ? 'invalid-stash' : 'invalid-auth');
|
|
118
|
+
}
|
|
119
|
+
return text;
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (code(error) === 'ENOENT' && !file)
|
|
123
|
+
return undefined;
|
|
124
|
+
if (code(error) === 'ELOOP' || code(error) === 'ENOENT') {
|
|
125
|
+
throw new PiSlotError('unsafe-path');
|
|
126
|
+
}
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
await file?.close();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function parseAuth(text, provider) {
|
|
134
|
+
try {
|
|
135
|
+
const data = JSON.parse(text);
|
|
136
|
+
if (!object(data))
|
|
137
|
+
throw new Error();
|
|
138
|
+
const root = rootSpans(text);
|
|
139
|
+
const properties = root.children;
|
|
140
|
+
// Duplicate provider keys have ambiguous byte ownership, even though JSON.parse accepts them.
|
|
141
|
+
const names = properties.map((node) => node.key);
|
|
142
|
+
if (new Set(names).size !== names.length)
|
|
143
|
+
throw new Error();
|
|
144
|
+
const property = properties.find((node) => node.key === provider);
|
|
145
|
+
const entry = property ? data[provider] : null;
|
|
146
|
+
if (entry !== null && !object(entry))
|
|
147
|
+
throw new Error();
|
|
148
|
+
if (property && entry === null)
|
|
149
|
+
throw new Error();
|
|
150
|
+
return { text, root, property, entry: entry };
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
throw new PiSlotError('invalid-auth');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function entryText(auth) {
|
|
157
|
+
const value = auth.property?.value;
|
|
158
|
+
return value
|
|
159
|
+
? auth.text.slice(value.offset, value.offset + value.length)
|
|
160
|
+
: null;
|
|
161
|
+
}
|
|
162
|
+
function replaceEntry(auth, provider, next) {
|
|
163
|
+
const { text, root, property } = auth;
|
|
164
|
+
const properties = root.children ?? [];
|
|
165
|
+
if (property) {
|
|
166
|
+
const value = property.value;
|
|
167
|
+
if (!value)
|
|
168
|
+
throw new PiSlotError('invalid-auth');
|
|
169
|
+
if (next !== null) {
|
|
170
|
+
return (text.slice(0, value.offset) +
|
|
171
|
+
next +
|
|
172
|
+
text.slice(value.offset + value.length));
|
|
173
|
+
}
|
|
174
|
+
const index = properties.indexOf(property);
|
|
175
|
+
const previous = properties[index - 1];
|
|
176
|
+
const following = properties[index + 1];
|
|
177
|
+
const start = previous ? previous.offset + previous.length : property.offset;
|
|
178
|
+
const end = !previous && following
|
|
179
|
+
? following.offset
|
|
180
|
+
: property.offset + property.length;
|
|
181
|
+
return text.slice(0, start) + text.slice(end);
|
|
182
|
+
}
|
|
183
|
+
if (next === null)
|
|
184
|
+
return text;
|
|
185
|
+
const last = properties.at(-1);
|
|
186
|
+
// Insert next to the last value, not after its whitespace, so deletion reverses an empty-slot swap.
|
|
187
|
+
const offset = last ? last.offset + last.length : root.offset + 1;
|
|
188
|
+
return `${text.slice(0, offset)}${last ? ',' : ''}${JSON.stringify(provider)}:${next}${text.slice(offset)}`;
|
|
189
|
+
}
|
|
190
|
+
async function syncDir(path) {
|
|
191
|
+
const dir = await open(path, 'r');
|
|
192
|
+
try {
|
|
193
|
+
await dir.sync();
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
await dir.close();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async function atomicWrite(path, text, assertLock, beforeRename) {
|
|
200
|
+
await writeJsonAtomic(path, null, {
|
|
201
|
+
serialize: () => text,
|
|
202
|
+
durable: true,
|
|
203
|
+
beforeRename: async () => {
|
|
204
|
+
await beforeRename?.(path);
|
|
205
|
+
assertLock();
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Production entry point. Pi caches which models are usable; after a
|
|
211
|
+
* successful transition the extension calls `ctx.modelRegistry.refresh()` so
|
|
212
|
+
* Pi re-reads the slot it just changed.
|
|
213
|
+
*/
|
|
214
|
+
export function openPiSlot(options) {
|
|
215
|
+
return createPiSlot(options);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Internal test entry point: pause reads, renewals, or renames to simulate races,
|
|
219
|
+
* and observe durable writes/deletion to simulate crashes. Not a public export.
|
|
220
|
+
*/
|
|
221
|
+
export function createPiSlot(options, onStep, hooks) {
|
|
222
|
+
const { provider, placeholderKey } = options;
|
|
223
|
+
const timeout = options.lockTimeoutMs ?? 30_000;
|
|
224
|
+
if (!provider ||
|
|
225
|
+
!options.authPath ||
|
|
226
|
+
!options.stashPath ||
|
|
227
|
+
!placeholderKey ||
|
|
228
|
+
placeholderKey.startsWith('!') ||
|
|
229
|
+
placeholderKey.includes('$') ||
|
|
230
|
+
!Number.isFinite(timeout) ||
|
|
231
|
+
timeout < 0) {
|
|
232
|
+
throw new PiSlotError('invalid-options');
|
|
233
|
+
}
|
|
234
|
+
const authPath = resolve(options.authPath);
|
|
235
|
+
const stashPath = resolve(options.stashPath);
|
|
236
|
+
if (authPath === stashPath || stashPath === `${authPath}.lock`) {
|
|
237
|
+
throw new PiSlotError('invalid-options');
|
|
238
|
+
}
|
|
239
|
+
const authIdentity = digest(authPath);
|
|
240
|
+
const placeholderIdentity = digest(placeholderKey);
|
|
241
|
+
const placeholder = { type: 'api_key', key: placeholderKey };
|
|
242
|
+
const isPlaceholder = (entry) => digest(entry) === digest(placeholder);
|
|
243
|
+
async function loadStash() {
|
|
244
|
+
const text = await safeRead(stashPath, true, hooks?.afterReadCheck);
|
|
245
|
+
if (text === undefined)
|
|
246
|
+
return undefined;
|
|
247
|
+
try {
|
|
248
|
+
const stash = JSON.parse(text);
|
|
249
|
+
if (stash.version !== 1 ||
|
|
250
|
+
stash.provider !== provider ||
|
|
251
|
+
stash.authIdentity !== authIdentity ||
|
|
252
|
+
stash.placeholderIdentity !== placeholderIdentity ||
|
|
253
|
+
(stash.entry !== null && !object(stash.entry)) ||
|
|
254
|
+
stash.sha256 !== digest(stash.entry) ||
|
|
255
|
+
(stash.entry === null
|
|
256
|
+
? stash.entryJson !== null
|
|
257
|
+
: typeof stash.entryJson !== 'string' ||
|
|
258
|
+
digest(JSON.parse(stash.entryJson)) !== stash.sha256)) {
|
|
259
|
+
throw new Error();
|
|
260
|
+
}
|
|
261
|
+
return stash;
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
throw new PiSlotError('invalid-stash');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
async function locked(fn) {
|
|
268
|
+
let release;
|
|
269
|
+
let assertFresh;
|
|
270
|
+
let compromised = false;
|
|
271
|
+
const assertLock = () => {
|
|
272
|
+
assertFresh?.();
|
|
273
|
+
if (compromised)
|
|
274
|
+
throw new PiSlotError('lock-compromised');
|
|
275
|
+
};
|
|
276
|
+
try {
|
|
277
|
+
await mkdir(dirname(authPath), { recursive: true, mode: 0o700 });
|
|
278
|
+
const deadline = Date.now() + timeout;
|
|
279
|
+
let retry = 0;
|
|
280
|
+
while (!release) {
|
|
281
|
+
try {
|
|
282
|
+
const lease = await acquirePiLock(authPath, () => {
|
|
283
|
+
compromised = true;
|
|
284
|
+
}, hooks?.onRenew);
|
|
285
|
+
release = lease.release;
|
|
286
|
+
assertFresh = lease.assertFresh;
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
if (code(error) !== 'ELOCKED')
|
|
290
|
+
throw error;
|
|
291
|
+
const remaining = deadline - Date.now();
|
|
292
|
+
if (remaining <= 0)
|
|
293
|
+
throw new PiSlotError('lock-timeout');
|
|
294
|
+
await sleep(Math.min(Math.round(Math.min(10 * 2 ** retry++, 1_000) * (1 + Math.random())), remaining));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
assertLock();
|
|
298
|
+
const result = await fn(assertLock);
|
|
299
|
+
assertLock();
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
if (error instanceof PiSlotError)
|
|
304
|
+
throw error;
|
|
305
|
+
throw new PiSlotError('io');
|
|
306
|
+
}
|
|
307
|
+
finally {
|
|
308
|
+
await release?.().catch(() => { });
|
|
309
|
+
assertLock();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
async function load() {
|
|
313
|
+
const auth = parseAuth((await safeRead(authPath, false, hooks?.afterReadCheck)) ?? '{}', provider);
|
|
314
|
+
const stash = await loadStash();
|
|
315
|
+
return { auth, stash };
|
|
316
|
+
}
|
|
317
|
+
async function deleteStash(assertLock) {
|
|
318
|
+
assertLock();
|
|
319
|
+
await unlink(stashPath);
|
|
320
|
+
await syncDir(dirname(stashPath));
|
|
321
|
+
onStep?.('stash-deleted');
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
inspect: () => locked(async () => {
|
|
325
|
+
const { auth, stash } = await load();
|
|
326
|
+
const slot = isPlaceholder(auth.entry)
|
|
327
|
+
? 'placeholder'
|
|
328
|
+
: auth.entry === null
|
|
329
|
+
? 'empty'
|
|
330
|
+
: !stash || digest(auth.entry) === stash.sha256
|
|
331
|
+
? 'original'
|
|
332
|
+
: 'foreign';
|
|
333
|
+
return { slot, stash: stash ? 'valid' : 'missing' };
|
|
334
|
+
}),
|
|
335
|
+
enterVault: () => locked(async (assertLock) => {
|
|
336
|
+
const { auth, stash } = await load();
|
|
337
|
+
if (isPlaceholder(auth.entry)) {
|
|
338
|
+
if (!stash)
|
|
339
|
+
throw new PiSlotError('missing-stash');
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (stash && digest(auth.entry) !== stash.sha256)
|
|
343
|
+
throw new PiSlotError('conflict');
|
|
344
|
+
if (!stash) {
|
|
345
|
+
await mkdir(dirname(stashPath), { recursive: true, mode: 0o700 });
|
|
346
|
+
const dir = await lstat(dirname(stashPath));
|
|
347
|
+
if (!dir.isDirectory() || (dir.mode & 0o777) !== 0o700)
|
|
348
|
+
throw new PiSlotError('unsafe-path');
|
|
349
|
+
const saved = {
|
|
350
|
+
version: 1,
|
|
351
|
+
provider,
|
|
352
|
+
authIdentity,
|
|
353
|
+
placeholderIdentity,
|
|
354
|
+
entry: auth.entry,
|
|
355
|
+
entryJson: entryText(auth),
|
|
356
|
+
sha256: digest(auth.entry),
|
|
357
|
+
};
|
|
358
|
+
await atomicWrite(stashPath, JSON.stringify(saved), assertLock, hooks?.beforeRename);
|
|
359
|
+
onStep?.('stash-written');
|
|
360
|
+
}
|
|
361
|
+
await atomicWrite(authPath, replaceEntry(auth, provider, JSON.stringify(placeholder)), assertLock, hooks?.beforeRename);
|
|
362
|
+
onStep?.('auth-written');
|
|
363
|
+
}),
|
|
364
|
+
exitVault: () => locked(async (assertLock) => {
|
|
365
|
+
const { auth, stash } = await load();
|
|
366
|
+
if (!stash) {
|
|
367
|
+
if (isPlaceholder(auth.entry))
|
|
368
|
+
throw new PiSlotError('missing-stash');
|
|
369
|
+
return 'nothing-to-do';
|
|
370
|
+
}
|
|
371
|
+
if (isPlaceholder(auth.entry)) {
|
|
372
|
+
await atomicWrite(authPath, replaceEntry(auth, provider, stash.entryJson), assertLock, hooks?.beforeRename);
|
|
373
|
+
onStep?.('auth-written');
|
|
374
|
+
}
|
|
375
|
+
else if (digest(auth.entry) !== stash.sha256) {
|
|
376
|
+
return 'conflict';
|
|
377
|
+
}
|
|
378
|
+
// Also completes a restore interrupted between the auth rename and stash deletion.
|
|
379
|
+
await deleteStash(assertLock);
|
|
380
|
+
return 'restored';
|
|
381
|
+
}),
|
|
382
|
+
};
|
|
383
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface QuotaCodec {
|
|
2
|
+
readonly validate: (value: unknown) => boolean;
|
|
3
|
+
readonly merge: (stored: unknown, observation: unknown) => unknown;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* The quota codec the account-pool store (the `/store` subpath) takes when it
|
|
7
|
+
* is opened: `validate` checks a stored per-row quota map and `merge` applies
|
|
8
|
+
* an observation to one, so the store persists the map without interpreting
|
|
9
|
+
* it.
|
|
10
|
+
*/
|
|
11
|
+
export declare const quotaCodec: QuotaCodec;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { isQuotaMap } from './map.js';
|
|
2
|
+
import { mergeQuotaObservation } from './merge.js';
|
|
3
|
+
/**
|
|
4
|
+
* The quota codec the account-pool store (the `/store` subpath) takes when it
|
|
5
|
+
* is opened: `validate` checks a stored per-row quota map and `merge` applies
|
|
6
|
+
* an observation to one, so the store persists the map without interpreting
|
|
7
|
+
* it.
|
|
8
|
+
*/
|
|
9
|
+
export const quotaCodec = Object.freeze({
|
|
10
|
+
validate: isQuotaMap,
|
|
11
|
+
merge: mergeQuotaObservation,
|
|
12
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ProjectedLimit, ProjectedQuota } from './projection.js';
|
|
2
|
+
/** A reading older than this is marked with its age. */
|
|
3
|
+
export declare const QUOTA_STALE_AFTER_MS: number;
|
|
4
|
+
/** What `formatQuota` returns when there is no reading at all. */
|
|
5
|
+
export declare const NO_QUOTA_READING = "no quota reading yet";
|
|
6
|
+
/**
|
|
7
|
+
* What `formatQuota` returns when the provider answered but reported no
|
|
8
|
+
* limit for any window (every window is an absence record or a tombstone).
|
|
9
|
+
*/
|
|
10
|
+
export declare const NO_LIMITS_REPORTED = "no limits reported";
|
|
11
|
+
/**
|
|
12
|
+
* `compact` fits a footer or a sidebar row: each window's figure, the credit
|
|
13
|
+
* budget and the stale marker, without reset times. `full` adds each reset.
|
|
14
|
+
*/
|
|
15
|
+
export type QuotaTextForm = 'compact' | 'full';
|
|
16
|
+
export interface QuotaTextOptions {
|
|
17
|
+
/** The current time in ms, for reset countdowns and the stale marker. */
|
|
18
|
+
now: number;
|
|
19
|
+
/** Defaults to `full`. */
|
|
20
|
+
form?: QuotaTextForm;
|
|
21
|
+
/** Defaults to `QUOTA_STALE_AFTER_MS`. */
|
|
22
|
+
staleAfterMs?: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The name a window is shown under. A general window (scope `all`) is named
|
|
26
|
+
* by its stored length; a model-scoped window keeps the label the plugin
|
|
27
|
+
* stored it under, which already names the model (`seven_day_opus`). A
|
|
28
|
+
* general window stored without a length (a snapshot from before lengths
|
|
29
|
+
* were recorded) falls back to its label.
|
|
30
|
+
*/
|
|
31
|
+
export declare function quotaWindowName(limit: Pick<ProjectedLimit, 'scope' | 'label' | 'windowMinutes'>): string;
|
|
32
|
+
/**
|
|
33
|
+
* The pieces of a quota text, in order: one per reported window
|
|
34
|
+
* (`5h 58% left, resets 2h`), then the credit budget when there is one, then
|
|
35
|
+
* `checked 3h ago` when the oldest piece shown is stale. Empty when nothing
|
|
36
|
+
* was reported; `formatQuota` turns that into a sentence. A caller printing
|
|
37
|
+
* one line per window (the terminal auth menu) uses these directly.
|
|
38
|
+
*/
|
|
39
|
+
export declare function quotaTextParts(quota: ProjectedQuota | undefined, options: QuotaTextOptions): string[];
|
|
40
|
+
/**
|
|
41
|
+
* A row's quota as one line: `5h 58% left · 7d 90% left` in the compact
|
|
42
|
+
* form, with `, resets 2h` after each window in the full form. With nothing
|
|
43
|
+
* reported it is `NO_QUOTA_READING`, or `NO_LIMITS_REPORTED` when the
|
|
44
|
+
* provider answered without a limit for any window.
|
|
45
|
+
*/
|
|
46
|
+
export declare function formatQuota(quota: ProjectedQuota | undefined, options: QuotaTextOptions): string;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// The one quota wording every surface shows: the command menu's dialogs on
|
|
2
|
+
// OpenCode and Pi, the terminal auth menu, and the plugins' sidebars. Each of
|
|
3
|
+
// them calls `formatQuota` (or `quotaTextParts` for one line per window), so
|
|
4
|
+
// the wording cannot drift between plugins or between a dialog and a sidebar.
|
|
5
|
+
//
|
|
6
|
+
// The rules: a window is named by its length (`5h`, `7d`), or, for a window
|
|
7
|
+
// that binds only one model family, by the label the plugin stored it under.
|
|
8
|
+
// Every figure is "% left". A window the provider did not report (an absence
|
|
9
|
+
// record or a retirement tombstone) is left out rather than written as "not
|
|
10
|
+
// reported". A reset is a short time from now (`resets 2h`). A reading older
|
|
11
|
+
// than fifteen minutes says how old it is (`checked 3h ago`).
|
|
12
|
+
//
|
|
13
|
+
// Like the rest of this module it reads no clock: `now` is a parameter.
|
|
14
|
+
import { ALL_SCOPE } from './map.js';
|
|
15
|
+
/** A reading older than this is marked with its age. */
|
|
16
|
+
export const QUOTA_STALE_AFTER_MS = 15 * 60_000;
|
|
17
|
+
/** What `formatQuota` returns when there is no reading at all. */
|
|
18
|
+
export const NO_QUOTA_READING = 'no quota reading yet';
|
|
19
|
+
/**
|
|
20
|
+
* What `formatQuota` returns when the provider answered but reported no
|
|
21
|
+
* limit for any window (every window is an absence record or a tombstone).
|
|
22
|
+
*/
|
|
23
|
+
export const NO_LIMITS_REPORTED = 'no limits reported';
|
|
24
|
+
/**
|
|
25
|
+
* A length or a time span in its largest whole-ish unit: `45m`, `5h`, `3d`.
|
|
26
|
+
* Spans under an hour are minutes, under a day hours, otherwise days, each
|
|
27
|
+
* rounded to the nearest unit and never shown as zero.
|
|
28
|
+
*/
|
|
29
|
+
function shortSpan(ms) {
|
|
30
|
+
const minutes = Math.max(1, Math.round(ms / 60_000));
|
|
31
|
+
if (minutes < 60)
|
|
32
|
+
return `${minutes}m`;
|
|
33
|
+
const hours = Math.round(minutes / 60);
|
|
34
|
+
if (hours < 24)
|
|
35
|
+
return `${hours}h`;
|
|
36
|
+
return `${Math.max(1, Math.round(minutes / 1440))}d`;
|
|
37
|
+
}
|
|
38
|
+
/** A stored window length in minutes as its exact name: `5h`, `7d`, `90m`. */
|
|
39
|
+
function lengthName(minutes) {
|
|
40
|
+
if (minutes % 1440 === 0)
|
|
41
|
+
return `${minutes / 1440}d`;
|
|
42
|
+
if (minutes % 60 === 0)
|
|
43
|
+
return `${minutes / 60}h`;
|
|
44
|
+
return `${minutes}m`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The name a window is shown under. A general window (scope `all`) is named
|
|
48
|
+
* by its stored length; a model-scoped window keeps the label the plugin
|
|
49
|
+
* stored it under, which already names the model (`seven_day_opus`). A
|
|
50
|
+
* general window stored without a length (a snapshot from before lengths
|
|
51
|
+
* were recorded) falls back to its label.
|
|
52
|
+
*/
|
|
53
|
+
export function quotaWindowName(limit) {
|
|
54
|
+
if (limit.scope !== ALL_SCOPE || limit.windowMinutes === undefined)
|
|
55
|
+
return limit.label;
|
|
56
|
+
return lengthName(limit.windowMinutes);
|
|
57
|
+
}
|
|
58
|
+
function percentLeft(remaining) {
|
|
59
|
+
return `${Math.round(Math.min(100, Math.max(0, remaining)))}% left`;
|
|
60
|
+
}
|
|
61
|
+
function resetText(resetsAt, now) {
|
|
62
|
+
if (resetsAt === undefined)
|
|
63
|
+
return '';
|
|
64
|
+
const at = Date.parse(resetsAt);
|
|
65
|
+
// A reset already passed says nothing about the future; leave it out.
|
|
66
|
+
if (!Number.isFinite(at) || at <= now)
|
|
67
|
+
return '';
|
|
68
|
+
return `, resets ${shortSpan(at - now)}`;
|
|
69
|
+
}
|
|
70
|
+
/** Shortest window first, so `5h` reads before `7d`; unknown lengths last. */
|
|
71
|
+
function displayOrder(left, right) {
|
|
72
|
+
const l = left.windowMinutes;
|
|
73
|
+
const r = right.windowMinutes;
|
|
74
|
+
if (l !== undefined && r !== undefined && l !== r)
|
|
75
|
+
return l - r;
|
|
76
|
+
if ((l === undefined) !== (r === undefined))
|
|
77
|
+
return l === undefined ? 1 : -1;
|
|
78
|
+
if (left.scope !== right.scope) {
|
|
79
|
+
if (left.scope === ALL_SCOPE)
|
|
80
|
+
return -1;
|
|
81
|
+
if (right.scope === ALL_SCOPE)
|
|
82
|
+
return 1;
|
|
83
|
+
return left.scope < right.scope ? -1 : 1;
|
|
84
|
+
}
|
|
85
|
+
return left.label < right.label ? -1 : left.label > right.label ? 1 : 0;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The pieces of a quota text, in order: one per reported window
|
|
89
|
+
* (`5h 58% left, resets 2h`), then the credit budget when there is one, then
|
|
90
|
+
* `checked 3h ago` when the oldest piece shown is stale. Empty when nothing
|
|
91
|
+
* was reported; `formatQuota` turns that into a sentence. A caller printing
|
|
92
|
+
* one line per window (the terminal auth menu) uses these directly.
|
|
93
|
+
*/
|
|
94
|
+
export function quotaTextParts(quota, options) {
|
|
95
|
+
if (!quota)
|
|
96
|
+
return [];
|
|
97
|
+
const { now } = options;
|
|
98
|
+
const full = (options.form ?? 'full') === 'full';
|
|
99
|
+
const parts = [];
|
|
100
|
+
const times = [];
|
|
101
|
+
const readings = quota.limits
|
|
102
|
+
.filter((limit) => limit.kind === 'reading' && limit.remainingPercent !== undefined)
|
|
103
|
+
.sort(displayOrder);
|
|
104
|
+
for (const limit of readings) {
|
|
105
|
+
times.push(limit.checkedAt);
|
|
106
|
+
parts.push(`${quotaWindowName(limit)} ${percentLeft(limit.remainingPercent ?? 0)}${full ? resetText(limit.resetsAt, now) : ''}`);
|
|
107
|
+
}
|
|
108
|
+
const budget = quota.budget;
|
|
109
|
+
if (budget) {
|
|
110
|
+
times.push(budget.checkedAt);
|
|
111
|
+
const figure = budget.reached
|
|
112
|
+
? 'credits spent'
|
|
113
|
+
: budget.remainingPercent !== undefined
|
|
114
|
+
? `credits ${percentLeft(budget.remainingPercent)}`
|
|
115
|
+
: 'credits available';
|
|
116
|
+
parts.push(`${figure}${full ? resetText(budget.resetsAt, now) : ''}`);
|
|
117
|
+
}
|
|
118
|
+
if (times.length > 0) {
|
|
119
|
+
const age = now - Math.min(...times);
|
|
120
|
+
if (age > (options.staleAfterMs ?? QUOTA_STALE_AFTER_MS))
|
|
121
|
+
parts.push(`checked ${shortSpan(age)} ago`);
|
|
122
|
+
}
|
|
123
|
+
return parts;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* A row's quota as one line: `5h 58% left · 7d 90% left` in the compact
|
|
127
|
+
* form, with `, resets 2h` after each window in the full form. With nothing
|
|
128
|
+
* reported it is `NO_QUOTA_READING`, or `NO_LIMITS_REPORTED` when the
|
|
129
|
+
* provider answered without a limit for any window.
|
|
130
|
+
*/
|
|
131
|
+
export function formatQuota(quota, options) {
|
|
132
|
+
const parts = quotaTextParts(quota, options);
|
|
133
|
+
if (parts.length > 0)
|
|
134
|
+
return parts.join(' · ');
|
|
135
|
+
return quota && quota.limits.length > 0
|
|
136
|
+
? NO_LIMITS_REPORTED
|
|
137
|
+
: NO_QUOTA_READING;
|
|
138
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type { QuotaCodec } from './codec.js';
|
|
2
|
+
export { quotaCodec } from './codec.js';
|
|
3
|
+
export type { QuotaTextForm, QuotaTextOptions } from './format.js';
|
|
4
|
+
export { formatQuota, NO_LIMITS_REPORTED, NO_QUOTA_READING, QUOTA_STALE_AFTER_MS, quotaTextParts, quotaWindowName, } from './format.js';
|
|
5
|
+
export type { CreditBudgetCleared, CreditBudgetEntry, CreditBudgetReading, QuotaAbsentEntry, QuotaEntry, QuotaMap, QuotaReadingEntry, QuotaRetiredEntry, } from './map.js';
|
|
6
|
+
export { ALL_SCOPE, DEFAULT_REQUIRED_LABELS, emptyQuotaMap, isQuotaMap, } from './map.js';
|
|
7
|
+
export type { ObservedBudget, ObservedPair, ObservedReading, QuotaObservation, } from './merge.js';
|
|
8
|
+
export { isQuotaObservation, mergeQuotaObservation, QuotaCodecError, } from './merge.js';
|
|
9
|
+
export type { ExhaustionReset, ProjectedBudget, ProjectedLimit, ProjectedQuota, } from './projection.js';
|
|
10
|
+
export { budgetExhaustedResetAt, futureResetAt, projectQuota, readsExhausted, } from './projection.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { quotaCodec } from './codec.js';
|
|
2
|
+
export { formatQuota, NO_LIMITS_REPORTED, NO_QUOTA_READING, QUOTA_STALE_AFTER_MS, quotaTextParts, quotaWindowName, } from './format.js';
|
|
3
|
+
export { ALL_SCOPE, DEFAULT_REQUIRED_LABELS, emptyQuotaMap, isQuotaMap, } from './map.js';
|
|
4
|
+
export { isQuotaObservation, mergeQuotaObservation, QuotaCodecError, } from './merge.js';
|
|
5
|
+
export { budgetExhaustedResetAt, futureResetAt, projectQuota, readsExhausted, } from './projection.js';
|