@rynfar/meridian 1.72.0 → 1.73.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-1jstkdj9.js +30 -0
- package/dist/{cli-9e5cxp89.js → cli-2h3w5n6g.js} +163 -65
- package/dist/cli-3zwfvexc.js +56 -0
- package/dist/{cli-hxxy0m1z.js → cli-82e2m44k.js} +2 -1
- package/dist/cli-8sw5wr9j.js +136 -0
- package/dist/{cli-khhjyk04.js → cli-chsp880k.js} +214 -21
- package/dist/cli-dh0a1bw4.js +619 -0
- package/dist/cli-eehk3052.js +57 -0
- package/dist/cli-grfp0s1j.js +330 -0
- package/dist/{cli-ab7zkp1a.js → cli-p5zbk952.js} +2027 -315
- package/dist/{cli-hj61zpt7.js → cli-p9ca4p13.js} +17 -2
- package/dist/cli-qpfky8wy.js +133 -0
- package/dist/cli.js +47 -12
- package/dist/cliDashboard-c0dvkj4y.js +325 -0
- package/dist/configDir.d.ts +30 -0
- package/dist/configDir.d.ts.map +1 -0
- package/dist/organizationName-90f0atkn.js +29 -0
- package/dist/{pluginPage-f5gdmbxc.js → pluginPage-3kxsrdpj.js} +1 -1
- package/dist/{profileCli-1eecbmgk.js → profileCli-1bfgc05h.js} +13 -3
- package/dist/{profilePage-mamej7kp.js → profilePage-etxqg3vn.js} +252 -60
- package/dist/profileRename-4a4km6g8.js +22 -0
- package/dist/{profiles-wch9h234.js → profiles-0mp32ek9.js} +16 -3
- package/dist/proxy/adapterInstances.d.ts +1 -1
- package/dist/proxy/adapterInstances.d.ts.map +1 -1
- package/dist/proxy/adapters/claudecode.d.ts.map +1 -1
- package/dist/proxy/adapters/droid.d.ts.map +1 -1
- package/dist/proxy/authDiscovery.d.ts +52 -0
- package/dist/proxy/authDiscovery.d.ts.map +1 -0
- package/dist/proxy/credentialsMode.d.ts +77 -0
- package/dist/proxy/credentialsMode.d.ts.map +1 -0
- package/dist/proxy/design.d.ts.map +1 -1
- package/dist/proxy/followActive.d.ts +262 -0
- package/dist/proxy/followActive.d.ts.map +1 -0
- package/dist/proxy/limitDetection.d.ts +110 -0
- package/dist/proxy/limitDetection.d.ts.map +1 -0
- package/dist/proxy/models.d.ts.map +1 -1
- package/dist/proxy/oauthPlan.d.ts +63 -0
- package/dist/proxy/oauthPlan.d.ts.map +1 -0
- package/dist/proxy/oauthUsage.d.ts +65 -2
- package/dist/proxy/oauthUsage.d.ts.map +1 -1
- package/dist/proxy/openai.d.ts +4 -2
- package/dist/proxy/openai.d.ts.map +1 -1
- package/dist/proxy/organizationName.d.ts +71 -0
- package/dist/proxy/organizationName.d.ts.map +1 -0
- package/dist/proxy/passthroughEarlyStop.d.ts +10 -0
- package/dist/proxy/passthroughEarlyStop.d.ts.map +1 -1
- package/dist/proxy/planAllowance.d.ts +75 -0
- package/dist/proxy/planAllowance.d.ts.map +1 -0
- package/dist/proxy/probes.d.ts +85 -0
- package/dist/proxy/probes.d.ts.map +1 -0
- package/dist/proxy/profileCli.d.ts +53 -1
- package/dist/proxy/profileCli.d.ts.map +1 -1
- package/dist/proxy/profileHealth.d.ts +127 -0
- package/dist/proxy/profileHealth.d.ts.map +1 -0
- package/dist/proxy/profileRename.d.ts +83 -0
- package/dist/proxy/profileRename.d.ts.map +1 -0
- package/dist/proxy/profiles.d.ts +72 -6
- package/dist/proxy/profiles.d.ts.map +1 -1
- package/dist/proxy/query.d.ts +7 -0
- package/dist/proxy/query.d.ts.map +1 -1
- package/dist/proxy/routing.d.ts +59 -1
- package/dist/proxy/routing.d.ts.map +1 -1
- package/dist/proxy/sdkFeatures.d.ts +1 -1
- package/dist/proxy/sdkFeatures.d.ts.map +1 -1
- package/dist/proxy/server.d.ts.map +1 -1
- package/dist/proxy/session/fingerprint.d.ts +5 -0
- package/dist/proxy/session/fingerprint.d.ts.map +1 -1
- package/dist/proxy/session/lineage.d.ts.map +1 -1
- package/dist/proxy/setup.d.ts.map +1 -1
- package/dist/proxy/supervision.d.ts +41 -0
- package/dist/proxy/supervision.d.ts.map +1 -0
- package/dist/proxy/tokenRefresh.d.ts +38 -4
- package/dist/proxy/tokenRefresh.d.ts.map +1 -1
- package/dist/server.js +11 -7
- package/dist/settings.d.ts +83 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/{setup-ndmjpy23.js → setup-7fr49z92.js} +1 -1
- package/dist/statusProbe-9x9q261e.js +151 -0
- package/dist/telemetry/dashboard.d.ts.map +1 -1
- package/dist/telemetry/index.d.ts +20 -2
- package/dist/telemetry/index.d.ts.map +1 -1
- package/dist/telemetry/landing.d.ts +1 -1
- package/dist/telemetry/landing.d.ts.map +1 -1
- package/dist/telemetry/logStore.d.ts +11 -0
- package/dist/telemetry/logStore.d.ts.map +1 -1
- package/dist/telemetry/pricingStore.d.ts +2 -2
- package/dist/telemetry/pricingStore.d.ts.map +1 -1
- package/dist/telemetry/profileBar.d.ts +2 -2
- package/dist/telemetry/profileBar.d.ts.map +1 -1
- package/dist/telemetry/profileFacts.d.ts +17 -0
- package/dist/telemetry/profileFacts.d.ts.map +1 -0
- package/dist/telemetry/profileOrder.d.ts +68 -0
- package/dist/telemetry/profileOrder.d.ts.map +1 -0
- package/dist/telemetry/profilePage.d.ts.map +1 -1
- package/dist/telemetry/profileSort.d.ts +35 -0
- package/dist/telemetry/profileSort.d.ts.map +1 -0
- package/dist/telemetry/profileSpent.d.ts +68 -0
- package/dist/telemetry/profileSpent.d.ts.map +1 -0
- package/dist/telemetry/routeChain.d.ts +32 -0
- package/dist/telemetry/routeChain.d.ts.map +1 -0
- package/dist/telemetry/routes.d.ts.map +1 -1
- package/dist/telemetry/settingsPage.d.ts +1 -1
- package/dist/telemetry/settingsPage.d.ts.map +1 -1
- package/dist/telemetry/sqlite.d.ts.map +1 -1
- package/dist/telemetry/store.d.ts +6 -1
- package/dist/telemetry/store.d.ts.map +1 -1
- package/dist/telemetry/types.d.ts +132 -2
- package/dist/telemetry/types.d.ts.map +1 -1
- package/dist/{tokenRefresh-kzz08kea.js → tokenRefresh-eg9h83wd.js} +6 -1
- package/package.json +1 -1
- package/dist/cli-5jxyma6z.js +0 -277
- package/dist/cli-vj9cv18n.js +0 -38
- package/dist/proxy/settings.d.ts +0 -30
- package/dist/proxy/settings.d.ts.map +0 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What Anthropic told us during authentication, rendered safe to log.
|
|
3
|
+
*
|
|
4
|
+
* Three payloads decide what Meridian knows about an account — the token
|
|
5
|
+
* endpoint's response, `GET /api/oauth/profile`, and `claude auth status` —
|
|
6
|
+
* and none of them is currently observable. When a field is absent, or renamed,
|
|
7
|
+
* or Anthropic starts returning a plan tier nobody has seen before, the only
|
|
8
|
+
* symptom downstream is a profile that reads `unknown`, with nothing in any log
|
|
9
|
+
* to say whether the field was missing from the response or dropped on the way
|
|
10
|
+
* to disk. That distinction is the whole of the diagnosis.
|
|
11
|
+
*
|
|
12
|
+
* The obstacle is that two of those payloads carry credentials, so they cannot
|
|
13
|
+
* simply be logged. `logger.ts`'s `sanitize()` redacts by key name, which is
|
|
14
|
+
* the wrong polarity here: it protects the names somebody already thought of,
|
|
15
|
+
* and a field discovered tomorrow is by definition not one of them.
|
|
16
|
+
*
|
|
17
|
+
* So this inverts it. Every string is redacted to its length unless its key is
|
|
18
|
+
* on an allow-list of values known to carry no secret. A new field is therefore
|
|
19
|
+
* reported by NAME and TYPE — enough to see it arrived and to decide what it is
|
|
20
|
+
* — while its contents stay out of the log until somebody deliberately adds it
|
|
21
|
+
* to the list.
|
|
22
|
+
*
|
|
23
|
+
* This is a leaf module — pure, no imports.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* String-valued keys whose contents are safe to print.
|
|
27
|
+
*
|
|
28
|
+
* Every one is either a duration, a timestamp, an enum Anthropic publishes, or
|
|
29
|
+
* the scope list that already appears in plaintext in the authorize URL. A key
|
|
30
|
+
* that identifies a person or an organization is deliberately absent: `email`,
|
|
31
|
+
* `email_address`, `name`, `display_name` and `uuid` are all reported by length
|
|
32
|
+
* only, because a debug log is routinely pasted into an issue.
|
|
33
|
+
*/
|
|
34
|
+
export declare const SAFE_AUTH_STRING_KEYS: ReadonlySet<string>;
|
|
35
|
+
/**
|
|
36
|
+
* Render a parsed auth response as a loggable field map.
|
|
37
|
+
*
|
|
38
|
+
* The shape mirrors the payload rather than flattening it, so a nested
|
|
39
|
+
* `organization.rate_limit_tier` reads the same way in the log as it does in
|
|
40
|
+
* Anthropic's documentation and in the code that consumes it.
|
|
41
|
+
*/
|
|
42
|
+
export declare function describeAuthFields(payload: unknown): unknown;
|
|
43
|
+
/**
|
|
44
|
+
* The keys a payload actually carried, nested ones included, as dotted paths.
|
|
45
|
+
*
|
|
46
|
+
* Logged alongside the described payload because it is the line worth grepping:
|
|
47
|
+
* comparing two logins reduces to comparing two key lists, which answers "did
|
|
48
|
+
* this account's response even contain the field" without reading the values at
|
|
49
|
+
* all.
|
|
50
|
+
*/
|
|
51
|
+
export declare function authFieldPaths(payload: unknown, prefix?: string): string[];
|
|
52
|
+
//# sourceMappingURL=authDiscovery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authDiscovery.d.ts","sourceRoot":"","sources":["../../src/proxy/authDiscovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,EAAE,WAAW,CAAC,MAAM,CAepD,CAAA;AA0BF;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAE5D;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAK,GAAG,MAAM,EAAE,CAStE"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only credential mode — `MERIDIAN_CREDENTIALS_READONLY`.
|
|
3
|
+
*
|
|
4
|
+
* Lets a second Meridian instance run beside a production one against the
|
|
5
|
+
* SAME credential files without being able to corrupt them. When set, the
|
|
6
|
+
* instance never initiates a token refresh and never writes a credential; it
|
|
7
|
+
* re-reads credentials from disk so refreshes performed by the OTHER instance
|
|
8
|
+
* are picked up.
|
|
9
|
+
*
|
|
10
|
+
* The hazard this exists for: two instances holding the same OAuth refresh
|
|
11
|
+
* token. Meridian refreshes proactively on boot and on a 45s cadence. If
|
|
12
|
+
* Anthropic rotates the refresh token on use, whichever instance refreshes
|
|
13
|
+
* first invalidates the other's copy, and recovery is an interactive
|
|
14
|
+
* `claude login` per account.
|
|
15
|
+
*
|
|
16
|
+
* Sharing is not a choice the operator makes, which is why this is the only
|
|
17
|
+
* defence rather than one layer of several. `MERIDIAN_CONFIG_DIR` relocates
|
|
18
|
+
* `settings.json` and nothing else: `profiles.json` is a module-level constant
|
|
19
|
+
* in `profiles.ts`, so every instance on a machine reads the same profile
|
|
20
|
+
* list, pointing at the same `claudeConfigDir` credential directories. There
|
|
21
|
+
* is no supported way to give a second instance a different profile set, so
|
|
22
|
+
* any second instance holds the first one's real accounts from the moment it
|
|
23
|
+
* boots. This flag is what stops it writing to them.
|
|
24
|
+
*
|
|
25
|
+
* Env var rather than a settings key on purpose: settings are editable from
|
|
26
|
+
* the web UI, and switching this on for the production instance by accident
|
|
27
|
+
* would silently stop it refreshing its own tokens. The systemd unit is
|
|
28
|
+
* already where the two instances differ.
|
|
29
|
+
*
|
|
30
|
+
* Leaf module — no imports from server.ts or session/.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* Whether this instance is forbidden from refreshing or writing credentials.
|
|
34
|
+
*
|
|
35
|
+
* Resolved per call rather than frozen at import time, matching
|
|
36
|
+
* `settings.ts`'s treatment of MERIDIAN_CONFIG_DIR: module-level credential
|
|
37
|
+
* stores are constructed at import, so a value captured then would be fixed
|
|
38
|
+
* before any test (or embedding host) could set it.
|
|
39
|
+
*/
|
|
40
|
+
export declare function isCredentialsReadOnly(): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Report a refused credential write, loudly, and return false so the caller
|
|
43
|
+
* takes its existing write-failed path.
|
|
44
|
+
*
|
|
45
|
+
* Deliberately `console.error` and not `claudeLog`: the latter is gated behind
|
|
46
|
+
* OPENCODE_CLAUDE_PROVIDER_DEBUG, so in a normal deployment it prints nothing.
|
|
47
|
+
*
|
|
48
|
+
* Why failing LOUDLY beats failing safe here: this refusal is the backstop for
|
|
49
|
+
* a write path nobody thought of. Silently swallowing it would leave an
|
|
50
|
+
* operator believing the guarantee held while some future call site quietly
|
|
51
|
+
* did nothing — and the failure it is protecting against (a corrupted token
|
|
52
|
+
* file that a production instance depends on, costing an interactive
|
|
53
|
+
* `claude login` across every account, including borrowed ones) is expensive
|
|
54
|
+
* and manual to recover from. A noisy log that turns out to be benign costs a
|
|
55
|
+
* grep; a silent one costs the fleet. If this line ever appears, the correct
|
|
56
|
+
* response is to find the call site and gate it upstream of the store.
|
|
57
|
+
*
|
|
58
|
+
* @param operation short label for the call site (e.g. "oauth-refresh")
|
|
59
|
+
* @param target store identity — a path or keychain service, NEVER a value
|
|
60
|
+
*/
|
|
61
|
+
export declare function refuseCredentialWrite(operation: string, target: string): false;
|
|
62
|
+
/**
|
|
63
|
+
* Announce the mode once at startup, unmistakably.
|
|
64
|
+
*
|
|
65
|
+
* An instance silently in read-only mode when you thought it was normal is a
|
|
66
|
+
* confusing bug — tokens quietly stop being refreshed. An instance loudly in
|
|
67
|
+
* it is self-documenting.
|
|
68
|
+
*
|
|
69
|
+
* Not gated on `silent`, unlike the rest of the startup output: MERIDIAN_SILENT
|
|
70
|
+
* suppresses routine chatter, and this is not routine — it is a non-default
|
|
71
|
+
* operating mode that changes what the process is allowed to do. One line,
|
|
72
|
+
* once, and only when the flag is actually set.
|
|
73
|
+
*/
|
|
74
|
+
export declare function logCredentialsModeBanner(): void;
|
|
75
|
+
/** Reset the once-per-process banner — for testing only. */
|
|
76
|
+
export declare function resetCredentialsModeBanner(): void;
|
|
77
|
+
//# sourceMappingURL=credentialsMode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentialsMode.d.ts","sourceRoot":"","sources":["../../src/proxy/credentialsMode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAIH;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,IAAI,OAAO,CAE/C;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,KAAK,CAO9E;AAKD;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,IAAI,IAAI,CAQ/C;AAED,4DAA4D;AAC5D,wBAAgB,0BAA0B,IAAI,IAAI,CAEjD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"design.d.ts","sourceRoot":"","sources":["../../src/proxy/design.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"design.d.ts","sourceRoot":"","sources":["../../src/proxy/design.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Follow-the-active-profile mode — `MERIDIAN_FOLLOW_ACTIVE`.
|
|
3
|
+
*
|
|
4
|
+
* Makes a development instance take its active profile from ANOTHER Meridian
|
|
5
|
+
* instance instead of from its own `settings.json`, so the two serve from the
|
|
6
|
+
* same account and a side-by-side comparison is like-for-like.
|
|
7
|
+
*
|
|
8
|
+
* The problem it solves: `MERIDIAN_CONFIG_DIR` relocates `settings.json` and
|
|
9
|
+
* nothing else, so a second instance keeps its own `activeProfile`. Whatever
|
|
10
|
+
* moves the primary's active profile — a UI click, the CLI, an external fleet
|
|
11
|
+
* scheduler — has no effect on the second instance, which quietly serves from
|
|
12
|
+
* a different account. Measured on one box: the primary was on one profile
|
|
13
|
+
* while the dev instance served the same instant from another.
|
|
14
|
+
*
|
|
15
|
+
* What this changes: exactly ONE input to `resolveProfile` — the active
|
|
16
|
+
* profile. The precedence chain around it is untouched, so an explicit
|
|
17
|
+
* `x-meridian-profile` header still wins, and sticky/priority routing behave
|
|
18
|
+
* as they always did (priority mode does not consult the active profile at
|
|
19
|
+
* all for unpinned requests, so follow mode is a no-op there).
|
|
20
|
+
*
|
|
21
|
+
* Degradation is the design constraint, not an afterthought. The followed
|
|
22
|
+
* instance is polled in the background and the last good value is cached; the
|
|
23
|
+
* request path never waits on the network. If the followed instance is down,
|
|
24
|
+
* slow, or answers rubbish, the last known value keeps being served — and if
|
|
25
|
+
* there has never been a good value, the local active profile is used. A dev
|
|
26
|
+
* convenience must not be able to take an instance offline.
|
|
27
|
+
*
|
|
28
|
+
* Leaf module — no imports from server.ts, session/ or profiles.ts.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* Poll cadence. The followed value changes on the order of minutes (a human
|
|
32
|
+
* clicking, or a scheduler rotating accounts), so a fetch per request would be
|
|
33
|
+
* absurd; 10s bounds the divergence window without making the followed
|
|
34
|
+
* instance do meaningful work — `/profiles/list` serves its auth status from a
|
|
35
|
+
* 60s cache that the instance's own 45s keepalive already keeps warm.
|
|
36
|
+
*/
|
|
37
|
+
export declare const FOLLOW_POLL_INTERVAL_MS = 10000;
|
|
38
|
+
/** Per-poll timeout. Short: a slow followed instance must not accumulate polls. */
|
|
39
|
+
export declare const FOLLOW_FETCH_TIMEOUT_MS = 2000;
|
|
40
|
+
/**
|
|
41
|
+
* How long a value may go unconfirmed before it is REPORTED stale.
|
|
42
|
+
*
|
|
43
|
+
* Staleness deliberately does not change routing. Falling back to the local
|
|
44
|
+
* value after a timeout would silently split the comparison exactly when you
|
|
45
|
+
* are least likely to notice, and the local value is not more correct — it is
|
|
46
|
+
* just different. A stale-but-known followed value remains the best available
|
|
47
|
+
* answer to "what is the primary on?"; the dev instance's own `activeProfile`
|
|
48
|
+
* is arbitrary. So a stale value is still followed, and is flagged as stale
|
|
49
|
+
* everywhere the mode is surfaced.
|
|
50
|
+
*/
|
|
51
|
+
export declare const FOLLOW_STALE_AFTER_MS = 120000;
|
|
52
|
+
export type FollowTarget = {
|
|
53
|
+
kind: "off";
|
|
54
|
+
} | {
|
|
55
|
+
kind: "on";
|
|
56
|
+
url: string;
|
|
57
|
+
} | {
|
|
58
|
+
kind: "invalid";
|
|
59
|
+
raw: string;
|
|
60
|
+
message: string;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Parse the env var value into a normalized base URL.
|
|
64
|
+
*
|
|
65
|
+
* A bare `host:port` is accepted and assumed to be http, because that is what
|
|
66
|
+
* anyone types first and `new URL()` would otherwise read `127.0.0.1` as a
|
|
67
|
+
* scheme. Trailing slashes are stripped so callers can concatenate paths.
|
|
68
|
+
*
|
|
69
|
+
* A malformed value yields "invalid" rather than throwing: the caller warns
|
|
70
|
+
* and runs without follow mode. Refusing to start on a typo would take an
|
|
71
|
+
* instance offline, which is the one outcome this feature must never cause.
|
|
72
|
+
*/
|
|
73
|
+
export declare function parseFollowTarget(raw: string | undefined): FollowTarget;
|
|
74
|
+
/**
|
|
75
|
+
* Whether a follow target addresses the instance doing the following.
|
|
76
|
+
*
|
|
77
|
+
* Self-follow is a deadlock, not a harmless no-op: the instance would follow
|
|
78
|
+
* its own active profile (so the value never changes) while refusing local
|
|
79
|
+
* writes (so nothing can change it), leaving the profile frozen forever. It is
|
|
80
|
+
* also the realistic typo — copying the wrong port out of a unit file.
|
|
81
|
+
*/
|
|
82
|
+
export declare function isSelfTarget(url: string, selfHost: string, selfPort: number): boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Extract `activeProfile` from a `/profiles/list` body.
|
|
85
|
+
*
|
|
86
|
+
* Deliberately paranoid about the shape. The followed instance may be a
|
|
87
|
+
* different version, a different program listening on a recycled port, or an
|
|
88
|
+
* error page — "answers rubbish" is one of the failure modes this must
|
|
89
|
+
* survive, and a non-string or absurdly long value must be treated as no
|
|
90
|
+
* value at all rather than routed to.
|
|
91
|
+
*/
|
|
92
|
+
export declare function readActiveProfile(body: unknown): string | undefined;
|
|
93
|
+
/** A profile the followed instance has, and where its credentials live. */
|
|
94
|
+
export interface FollowedProfile {
|
|
95
|
+
id: string;
|
|
96
|
+
/** Absolute CLAUDE_CONFIG_DIR, on a filesystem both instances can read. */
|
|
97
|
+
credentialDir: string;
|
|
98
|
+
}
|
|
99
|
+
export interface FollowedRoster {
|
|
100
|
+
/** Profiles this instance can serve by pointing at the same directory. */
|
|
101
|
+
adoptable: FollowedProfile[];
|
|
102
|
+
/** Profiles it cannot, because their credentials are an inline secret. */
|
|
103
|
+
unadoptable: string[];
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Extract the profile roster from a `/profiles/list` body.
|
|
107
|
+
*
|
|
108
|
+
* Following one scalar is not enough. `MERIDIAN_CONFIG_DIR` gives a second
|
|
109
|
+
* instance its own `profiles.json` — that is the whole point of it — so the
|
|
110
|
+
* two rosters diverge the moment an account is added to either. The followed
|
|
111
|
+
* value then names a profile this instance does not have, `decideFollowedProfile`
|
|
112
|
+
* returns "unknown-profile", and the dev instance serves from an unrelated
|
|
113
|
+
* account: exactly the divergence follow mode exists to prevent. Measured on
|
|
114
|
+
* one box, a profile added to the primary was still absent from the follower
|
|
115
|
+
* minutes later, and its traffic 429'd against the wrong account.
|
|
116
|
+
*
|
|
117
|
+
* Adoptability is decided by the SENDER, which is the only side that can. A
|
|
118
|
+
* profile authenticated by a file both instances can read crosses as a path; a
|
|
119
|
+
* profile authenticated by an inline API key or OAuth token cannot cross at
|
|
120
|
+
* all, because the secret must not leave the process that holds it. The sender
|
|
121
|
+
* reports the second kind by id alone so this instance can say why an account
|
|
122
|
+
* it can see is one it cannot serve.
|
|
123
|
+
*
|
|
124
|
+
* Returns undefined — distinct from an empty roster — when the body carries no
|
|
125
|
+
* roster information at all. `credentialDir` is emitted for EVERY profile, null
|
|
126
|
+
* included, so its total absence means the followed instance predates this and
|
|
127
|
+
* cannot answer the question. Guessing "nothing is adoptable" there would warn
|
|
128
|
+
* about accounts that are merely unreported.
|
|
129
|
+
*/
|
|
130
|
+
export declare function readFollowedRoster(body: unknown): FollowedRoster | undefined;
|
|
131
|
+
export interface FollowState {
|
|
132
|
+
/** Last value successfully read, and when it was last CONFIRMED by a poll. */
|
|
133
|
+
lastGood?: {
|
|
134
|
+
profileId: string;
|
|
135
|
+
at: number;
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Last roster successfully read. Kept across a failed poll for the same
|
|
139
|
+
* reason `lastGood` is: a followed instance that has gone quiet has not
|
|
140
|
+
* withdrawn its accounts, and dropping them would strand every session
|
|
141
|
+
* pinned to one.
|
|
142
|
+
*/
|
|
143
|
+
lastRoster?: {
|
|
144
|
+
roster: FollowedRoster;
|
|
145
|
+
at: number;
|
|
146
|
+
};
|
|
147
|
+
/** When the most recent poll completed, successful or not. */
|
|
148
|
+
lastPollAt?: number;
|
|
149
|
+
/** Most recent poll failure. Cleared on the next success. */
|
|
150
|
+
lastError?: {
|
|
151
|
+
message: string;
|
|
152
|
+
at: number;
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
export type FollowOutcome = {
|
|
156
|
+
follow: true;
|
|
157
|
+
profileId: string;
|
|
158
|
+
stale: boolean;
|
|
159
|
+
} | {
|
|
160
|
+
follow: false;
|
|
161
|
+
reason: "no-value" | "unknown-profile";
|
|
162
|
+
followedValue?: string;
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* Decide whether the followed value should be used, given what this instance
|
|
166
|
+
* actually has. Pure — the whole point is that every branch is testable
|
|
167
|
+
* without a second instance running.
|
|
168
|
+
*
|
|
169
|
+
* `unknown-profile` is a fallback, not a resolution: routing to a profile this
|
|
170
|
+
* instance does not have would resolve to "first configured profile" deeper in
|
|
171
|
+
* `resolveProfile` and silently serve from an unrelated account. Using the
|
|
172
|
+
* local choice and saying so is the smaller surprise.
|
|
173
|
+
*/
|
|
174
|
+
export declare function decideFollowedProfile(state: FollowState, availableIds: readonly string[], now: number, staleAfterMs?: number): FollowOutcome;
|
|
175
|
+
/**
|
|
176
|
+
* The followed instance's base URL, or undefined when not following.
|
|
177
|
+
*
|
|
178
|
+
* Resolved per call rather than frozen at import time, matching `settings.ts`'s
|
|
179
|
+
* treatment of MERIDIAN_CONFIG_DIR — a value captured at import would be fixed
|
|
180
|
+
* before any test (or embedding host) could set it. Parsing is memoized on the
|
|
181
|
+
* raw string so the request path does not build a URL object per call.
|
|
182
|
+
*/
|
|
183
|
+
export declare function followTarget(): {
|
|
184
|
+
url: string;
|
|
185
|
+
} | undefined;
|
|
186
|
+
/** Whether this instance takes its active profile from another one. */
|
|
187
|
+
export declare function isFollowEnabled(): boolean;
|
|
188
|
+
/**
|
|
189
|
+
* The follow decision for a given profile list, or undefined when not
|
|
190
|
+
* following. Called on the request path — synchronous, cache-only, no I/O.
|
|
191
|
+
*/
|
|
192
|
+
export declare function followedActiveProfile(availableIds: readonly string[]): FollowOutcome | undefined;
|
|
193
|
+
/**
|
|
194
|
+
* Profiles contributed by the followed instance. Called on the request path
|
|
195
|
+
* through `getEffectiveProfiles` — synchronous, cache-only, no I/O.
|
|
196
|
+
*
|
|
197
|
+
* Empty until the first successful poll, so an instance whose followed peer is
|
|
198
|
+
* down starts with its own profiles rather than none.
|
|
199
|
+
*/
|
|
200
|
+
export declare function adoptedProfiles(): readonly FollowedProfile[];
|
|
201
|
+
/** Follow state as surfaced by `/profiles/list`. Undefined when not following. */
|
|
202
|
+
export interface FollowStatus {
|
|
203
|
+
/** Base URL of the followed instance. */
|
|
204
|
+
url: string;
|
|
205
|
+
/** Value actually in effect, or null when falling back to the local one. */
|
|
206
|
+
activeProfile: string | null;
|
|
207
|
+
/** Last value read from the followed instance, even if unusable here. */
|
|
208
|
+
followedValue: string | null;
|
|
209
|
+
/** Why the followed value is not in effect, when it isn't. */
|
|
210
|
+
reason: "no-value" | "unknown-profile" | null;
|
|
211
|
+
/** Followed value has not been confirmed recently — still in effect. */
|
|
212
|
+
stale: boolean;
|
|
213
|
+
/** When the followed value was last confirmed. */
|
|
214
|
+
lastSyncedAt: number | null;
|
|
215
|
+
/** Most recent poll failure, if the last poll failed. */
|
|
216
|
+
lastError: string | null;
|
|
217
|
+
/** Profile ids taken from the followed instance and served here. */
|
|
218
|
+
adoptedProfiles: string[];
|
|
219
|
+
/** Profile ids it has that cannot be adopted, so a UI can say why. */
|
|
220
|
+
unadoptableProfiles: string[];
|
|
221
|
+
/** When the roster was last confirmed, or null if never read. */
|
|
222
|
+
rosterSyncedAt: number | null;
|
|
223
|
+
}
|
|
224
|
+
export declare function followStatus(availableIds: readonly string[]): FollowStatus | undefined;
|
|
225
|
+
/**
|
|
226
|
+
* Read the followed instance's active profile once and update the cache.
|
|
227
|
+
*
|
|
228
|
+
* Never throws and never clears a good value on failure: a failed poll leaves
|
|
229
|
+
* the last known value in place, which is what "degrade, never fail" means
|
|
230
|
+
* here. Exported so tests can drive it without a timer.
|
|
231
|
+
*/
|
|
232
|
+
export declare function pollFollowedActiveProfile(): Promise<void>;
|
|
233
|
+
/**
|
|
234
|
+
* Arm the background poll. Idempotent.
|
|
235
|
+
*
|
|
236
|
+
* `self` is the address this instance actually bound (the configured port may
|
|
237
|
+
* be 0), used only for the self-follow guard.
|
|
238
|
+
*/
|
|
239
|
+
export declare function startFollowPolling(self?: {
|
|
240
|
+
host: string;
|
|
241
|
+
port: number;
|
|
242
|
+
}): void;
|
|
243
|
+
/** Stop the background poll. Idempotent. */
|
|
244
|
+
export declare function stopFollowPolling(): void;
|
|
245
|
+
/**
|
|
246
|
+
* Announce the mode once at startup.
|
|
247
|
+
*
|
|
248
|
+
* Not gated on `silent`, matching the treatment of other non-default operating
|
|
249
|
+
* modes: MERIDIAN_SILENT suppresses routine chatter, and this is not routine —
|
|
250
|
+
* it changes where a core piece of this instance's state comes from. An
|
|
251
|
+
* instance silently following another is exactly the hour-long confusion this
|
|
252
|
+
* line exists to prevent.
|
|
253
|
+
*
|
|
254
|
+
* @param routingMode current routing mode, so the one combination where follow
|
|
255
|
+
* mode does nothing useful says so instead of being discovered the hard way.
|
|
256
|
+
*/
|
|
257
|
+
export declare function logFollowBanner(routingMode?: string): void;
|
|
258
|
+
/** Reset all module state — for testing only. */
|
|
259
|
+
export declare function resetFollowActive(): void;
|
|
260
|
+
/** Seed the cache directly — for testing only. */
|
|
261
|
+
export declare function setFollowStateForTesting(next: FollowState): void;
|
|
262
|
+
//# sourceMappingURL=followActive.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"followActive.d.ts","sourceRoot":"","sources":["../../src/proxy/followActive.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which allowance did Anthropic just refuse?
|
|
3
|
+
*
|
|
4
|
+
* An account that has run out keeps reporting healthy percentages: measured on
|
|
5
|
+
* the live fleet, `corp4` rendered `5h 67% / 7d 7%` on /profiles at the same
|
|
6
|
+
* minute its requests were being refused. The cached numbers are a snapshot of
|
|
7
|
+
* the last successful read; a refusal is news that arrives later and never
|
|
8
|
+
* overwrites them. So the refusal has to be recorded and reported on its own
|
|
9
|
+
* terms, next to the cached numbers rather than instead of them.
|
|
10
|
+
*
|
|
11
|
+
* This module answers only the "which bucket, and when does it reset" part.
|
|
12
|
+
* It is a leaf module and every function here is pure - the stores that hold
|
|
13
|
+
* the answers live in profileHealth.ts.
|
|
14
|
+
*
|
|
15
|
+
* Evidence ladder, best source first. The distinction that matters to a reader
|
|
16
|
+
* of the UI is `reported` (Anthropic said so) versus inferred (we worked it
|
|
17
|
+
* out from stale numbers), so every diagnosis carries it explicitly:
|
|
18
|
+
*
|
|
19
|
+
* 1. `sdk_event` - the SDK's own `rate_limit_event` said `rejected` and
|
|
20
|
+
* named `rateLimitType`. Unambiguous.
|
|
21
|
+
* 2. `error_message` - the CLI's prose names the window. Measured live:
|
|
22
|
+
* "You've hit your session limit · resets 12:30am
|
|
23
|
+
* (America/Chicago)" - that is both the bucket (Claude
|
|
24
|
+
* Code calls the 5-hour window a "session") and a real
|
|
25
|
+
* reset clock, so it is worth parsing rather than
|
|
26
|
+
* discarding.
|
|
27
|
+
* 3. `cached_usage` - nothing said which, so deduce from the last-read
|
|
28
|
+
* windows: whichever is nearest its limit, or, when the
|
|
29
|
+
* weekly windows are nowhere near theirs, the 5-hour one
|
|
30
|
+
* by elimination.
|
|
31
|
+
* 4. `unknown` - say so rather than guessing a bucket.
|
|
32
|
+
*
|
|
33
|
+
* There are no `anthropic-ratelimit-*` response headers to read here: Meridian
|
|
34
|
+
* reaches Anthropic through the Claude Agent SDK subprocess, never over HTTP it
|
|
35
|
+
* can see, so `rate_limit_event` IS this proxy's structured form of those
|
|
36
|
+
* headers and tier 1 above is the header check.
|
|
37
|
+
*/
|
|
38
|
+
/** How the bucket was determined. See the ladder in the module doc. */
|
|
39
|
+
export type LimitSource = "sdk_event" | "error_message" | "cached_usage" | "unknown";
|
|
40
|
+
export interface LimitDiagnosis {
|
|
41
|
+
/** Anthropic's window key ("five_hour", "seven_day", "seven_day_opus", …),
|
|
42
|
+
* or null when nothing identified it. */
|
|
43
|
+
bucket: string | null;
|
|
44
|
+
/** True when Anthropic named the bucket; false when we deduced it. Drives
|
|
45
|
+
* the "(guess)" marking in the UI. */
|
|
46
|
+
reported: boolean;
|
|
47
|
+
source: LimitSource;
|
|
48
|
+
/** Epoch ms the window reopens, when known. */
|
|
49
|
+
resetsAt: number | null;
|
|
50
|
+
/** One line explaining the verdict, rendered as the badge's tooltip. */
|
|
51
|
+
rationale: string;
|
|
52
|
+
}
|
|
53
|
+
export interface ParsedLimitWording {
|
|
54
|
+
bucket: string | null;
|
|
55
|
+
/** The qualifier as written, kept for the rationale so an unrecognized one
|
|
56
|
+
* ("daily", "monthly") still tells the reader what the CLI said. */
|
|
57
|
+
qualifier: string | null;
|
|
58
|
+
clock: string | null;
|
|
59
|
+
zone: string | null;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Pull the bucket and reset clock out of an SDK error message. Returns nulls
|
|
63
|
+
* for anything it cannot read - this never throws on unfamiliar prose, because
|
|
64
|
+
* the CLI's wording has changed three times already (#764, #787).
|
|
65
|
+
*/
|
|
66
|
+
export declare function parseLimitWording(errMsg: string): ParsedLimitWording;
|
|
67
|
+
/**
|
|
68
|
+
* Resolve "resets 12:30am (America/Chicago)" to an instant: the next time that
|
|
69
|
+
* wall clock occurs in that zone.
|
|
70
|
+
*
|
|
71
|
+
* The offset is read twice - once at `now`, then again at the candidate - so a
|
|
72
|
+
* reset that lands on the far side of a DST boundary resolves to the right
|
|
73
|
+
* instant rather than being an hour out.
|
|
74
|
+
*
|
|
75
|
+
* Returns null for an unparseable clock or unknown zone. Only ever looks one
|
|
76
|
+
* day ahead, so the result is bounded to ~24h from `now`: the CLI prints a
|
|
77
|
+
* clock time with no date, and a value further out than that would be a
|
|
78
|
+
* misreading, not information.
|
|
79
|
+
*/
|
|
80
|
+
export declare function resolveResetClock(clock: string, zone: string | null | undefined, now: number): number | null;
|
|
81
|
+
export interface SdkLimitEvidence {
|
|
82
|
+
rateLimitType?: string;
|
|
83
|
+
status?: string;
|
|
84
|
+
utilization?: number | null;
|
|
85
|
+
resetsAt?: number | null;
|
|
86
|
+
observedAt: number;
|
|
87
|
+
}
|
|
88
|
+
export interface CachedWindow {
|
|
89
|
+
type: string;
|
|
90
|
+
utilization: number | null;
|
|
91
|
+
resetsAt: number | null;
|
|
92
|
+
}
|
|
93
|
+
export interface DiagnoseLimitInput {
|
|
94
|
+
/** Raw SDK error text, before `classifyError` replaces it with proxy prose. */
|
|
95
|
+
message: string;
|
|
96
|
+
now: number;
|
|
97
|
+
/** This profile's SDK rate-limit entries (rateLimitStore.getAll). */
|
|
98
|
+
sdkEntries?: readonly SdkLimitEvidence[];
|
|
99
|
+
/** This profile's last-read OAuth usage windows. */
|
|
100
|
+
windows?: readonly CachedWindow[];
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Decide which allowance was refused, and say how confident that is.
|
|
104
|
+
*
|
|
105
|
+
* Never throws and always returns a diagnosis - an "unknown" verdict is a
|
|
106
|
+
* useful thing to render ("refusing, cause unknown") and far better than
|
|
107
|
+
* silently attributing the refusal to whichever window happened to look busiest.
|
|
108
|
+
*/
|
|
109
|
+
export declare function diagnoseLimit(input: DiagnoseLimitInput): LimitDiagnosis;
|
|
110
|
+
//# sourceMappingURL=limitDetection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"limitDetection.d.ts","sourceRoot":"","sources":["../../src/proxy/limitDetection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,uEAAuE;AACvE,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,eAAe,GAAG,cAAc,GAAG,SAAS,CAAA;AAEpF,MAAM,WAAW,cAAc;IAC7B;8CAC0C;IAC1C,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB;2CACuC;IACvC,QAAQ,EAAE,OAAO,CAAA;IACjB,MAAM,EAAE,WAAW,CAAA;IACnB,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,wEAAwE;IACxE,SAAS,EAAE,MAAM,CAAA;CAClB;AAiCD,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB;yEACqE;IACrE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CACpB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,kBAAkB,CAiBpE;AAsDD;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAgB5G;AASD,MAAM,WAAW,gBAAgB;IAC/B,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,kBAAkB;IACjC,+EAA+E;IAC/E,OAAO,EAAE,MAAM,CAAA;IACf,GAAG,EAAE,MAAM,CAAA;IACX,qEAAqE;IACrE,UAAU,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAA;IACxC,oDAAoD;IACpD,OAAO,CAAC,EAAE,SAAS,YAAY,EAAE,CAAA;CAClC;AAQD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,cAAc,CAgEvE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../src/proxy/models.ts"],"names":[],"mappings":"AAAA;;GAEG;
|
|
1
|
+
{"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../src/proxy/models.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The account's plan, as Anthropic reports it for a given access token.
|
|
3
|
+
*
|
|
4
|
+
* Shared by the two moments a plan can be learned: an interactive login, which
|
|
5
|
+
* has just minted a token, and a token refresh, which has just minted another.
|
|
6
|
+
* It lives in its own module rather than in profileCli.ts because profileCli
|
|
7
|
+
* already imports tokenRefresh for the credential store, so the refresh path
|
|
8
|
+
* importing back would close a cycle.
|
|
9
|
+
*
|
|
10
|
+
* This is a leaf module — one authenticated GET, no imports.
|
|
11
|
+
*/
|
|
12
|
+
interface OAuthProfileResponse {
|
|
13
|
+
organization?: {
|
|
14
|
+
organization_type?: string | null;
|
|
15
|
+
rate_limit_tier?: string | null;
|
|
16
|
+
seat_tier?: string | null;
|
|
17
|
+
} | null;
|
|
18
|
+
}
|
|
19
|
+
export interface OAuthPlanFields {
|
|
20
|
+
subscriptionType?: string;
|
|
21
|
+
rateLimitTier?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Which seat a Team member holds, and the ONLY field that separates a
|
|
24
|
+
* Premium seat from a Standard one. Measured across twelve live accounts:
|
|
25
|
+
* every Premium seat reports `rate_limit_tier: "default_claude_max_5x"` —
|
|
26
|
+
* byte-identical to what a personal Max 5x reports — and a Standard seat
|
|
27
|
+
* reports `default_raven`, a codename naming no published allotment. So the
|
|
28
|
+
* rate-limit tier can size neither kind of Team seat: one because it lies
|
|
29
|
+
* and one because it says nothing. Null on every personal account.
|
|
30
|
+
*/
|
|
31
|
+
seatTier?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Translate Anthropic's wire `organization_type` into the vocabulary the Claude
|
|
35
|
+
* CLI writes on disk — the wire value is prefixed (`claude_max`), the stored one
|
|
36
|
+
* is not (`max`). Mirroring the CLI's own mapping is what keeps a credential
|
|
37
|
+
* file Meridian writes indistinguishable from one `claude login` wrote, which
|
|
38
|
+
* matters because `claude auth status` reads it back and is what ultimately
|
|
39
|
+
* feeds `/profiles/list`, `/health` and the `max`-only branch of `/v1/models`.
|
|
40
|
+
*
|
|
41
|
+
* An unrecognized or absent type yields undefined so the caller omits the key,
|
|
42
|
+
* rather than inventing a plan for an account it could not identify.
|
|
43
|
+
*/
|
|
44
|
+
export declare function subscriptionTypeFromOrganizationType(organizationType: string | null | undefined): string | undefined;
|
|
45
|
+
export declare function extractPlanFields(profile: OAuthProfileResponse | null | undefined): OAuthPlanFields;
|
|
46
|
+
/**
|
|
47
|
+
* Whether a stored credential is still missing plan information.
|
|
48
|
+
*
|
|
49
|
+
* Either field being absent counts, because they answer different questions
|
|
50
|
+
* and arrive from different writers: `claude login` records `subscriptionType`
|
|
51
|
+
* but not always `rateLimitTier`, and only `rateLimitTier` distinguishes Max 5x
|
|
52
|
+
* from Max 20x. Requiring both means a file half-filled by the CLI still gets
|
|
53
|
+
* completed.
|
|
54
|
+
*/
|
|
55
|
+
export declare function planFieldsMissing(fields: OAuthPlanFields | null | undefined): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Best-effort plan lookup for a valid access token. Never throws and never
|
|
58
|
+
* fails its caller: a profile whose plan is unknown is strictly better than no
|
|
59
|
+
* profile at all, and every consumer already treats it as optional.
|
|
60
|
+
*/
|
|
61
|
+
export declare function fetchOAuthPlanFields(accessToken: string): Promise<OAuthPlanFields>;
|
|
62
|
+
export {};
|
|
63
|
+
//# sourceMappingURL=oauthPlan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauthPlan.d.ts","sourceRoot":"","sources":["../../src/proxy/oauthPlan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAkBH,UAAU,oBAAoB;IAC5B,YAAY,CAAC,EAAE;QACb,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QACjC,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QAC/B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAC1B,GAAG,IAAI,CAAA;CACT;AAED,MAAM,WAAW,eAAe;IAC9B,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oCAAoC,CAClD,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC1C,MAAM,GAAG,SAAS,CAQpB;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,oBAAoB,GAAG,IAAI,GAAG,SAAS,GAAG,eAAe,CASnG;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAErF;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAuCxF"}
|