@cortexkit/anthropic-auth-core 1.22.0 → 2.0.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/README.md +2 -0
- package/dist/accounts.d.ts +52 -8
- package/dist/cachekeep.d.ts +27 -1
- package/dist/cch.d.ts +11 -1
- package/dist/claude-code.d.ts +23 -1
- package/dist/claustrum-enrollment.d.ts +72 -0
- package/dist/claustrum-scoped-roster.d.ts +27 -0
- package/dist/claustrum-scoped-runtime.d.ts +31 -0
- package/dist/claustrum-scoped.d.ts +53 -0
- package/dist/claustrum.d.ts +16 -82
- package/dist/commands/account.d.ts +38 -11
- package/dist/constants.d.ts +3 -2
- package/dist/custom-headers.d.ts +3 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js +29700 -31
- package/dist/internal-types/LICENSE +21 -0
- package/dist/internal-types/commands/builtins.d.ts +71 -0
- package/dist/internal-types/commands/index.d.ts +10 -0
- package/dist/internal-types/commands/menu.d.ts +61 -0
- package/dist/internal-types/commands/model.d.ts +238 -0
- package/dist/internal-types/commands/pi.d.ts +23 -0
- package/dist/internal-types/commands/seam.d.ts +72 -0
- package/dist/internal-types/fs/refresh-file-lock.d.ts +33 -0
- package/dist/internal-types/logger/capture-sink.d.ts +14 -0
- package/dist/internal-types/logger/engine.d.ts +100 -0
- package/dist/internal-types/logger/index.d.ts +6 -0
- package/dist/internal-types/logger/redact.d.ts +11 -0
- package/dist/internal-types/pi-slot/index.d.ts +3 -0
- package/dist/internal-types/pi-slot/slot.d.ts +49 -0
- package/dist/internal-types/quota/codec.d.ts +11 -0
- package/dist/internal-types/quota/format.d.ts +46 -0
- package/dist/internal-types/quota/index.d.ts +10 -0
- package/dist/internal-types/quota/map.d.ts +63 -0
- package/dist/internal-types/quota/merge.d.ts +48 -0
- package/dist/internal-types/quota/projection.d.ts +64 -0
- package/dist/internal-types/store/attribution.d.ts +28 -0
- package/dist/internal-types/store/errors.d.ts +55 -0
- package/dist/internal-types/store/hooks.d.ts +12 -0
- package/dist/internal-types/store/identity.d.ts +39 -0
- package/dist/internal-types/store/index.d.ts +21 -0
- package/dist/internal-types/store/mutate.d.ts +189 -0
- package/dist/internal-types/store/pool.d.ts +196 -0
- package/dist/internal-types/store/provider-state.d.ts +131 -0
- package/dist/internal-types/store/publication.d.ts +47 -0
- package/dist/internal-types/store/pull.d.ts +36 -0
- package/dist/internal-types/store/refresh-lock.d.ts +63 -0
- package/dist/internal-types/store/refresh.d.ts +72 -0
- package/dist/internal-types/store/rows.d.ts +313 -0
- package/dist/internal-types/store/runtime.d.ts +36 -0
- package/dist/internal-types/store/schema.d.ts +443 -0
- package/dist/internal-types/store/settings.d.ts +63 -0
- package/dist/internal-types/store/torn.d.ts +78 -0
- package/dist/migration-source.d.ts +33 -0
- package/dist/model-remap.d.ts +27 -0
- package/dist/models.d.ts +60 -0
- package/dist/native-account-runtime.d.ts +121 -0
- package/dist/native-account-view.d.ts +126 -0
- package/dist/native-command-effects.d.ts +59 -0
- package/dist/native-credential-validation.d.ts +42 -0
- package/dist/native-custody-activation.d.ts +45 -0
- package/dist/native-custody.d.ts +114 -0
- package/dist/native-enrollment-authority.d.ts +2 -0
- package/dist/native-host-auth-read.d.ts +3 -0
- package/dist/native-host-auth-write.d.ts +51 -0
- package/dist/native-host-auth.d.ts +12 -0
- package/dist/native-local-credential-service.d.ts +76 -0
- package/dist/native-local-runtime-readers.d.ts +22 -0
- package/dist/native-lock-observer.d.ts +12 -0
- package/dist/native-menu-executor.d.ts +243 -0
- package/dist/native-menu-model.d.ts +142 -0
- package/dist/native-migration-proof.d.ts +28 -0
- package/dist/native-migration-source.d.ts +47 -0
- package/dist/native-migration.d.ts +66 -0
- package/dist/native-quota-codec.d.ts +32 -0
- package/dist/native-refresh-coordinator.d.ts +240 -0
- package/dist/native-roster-seed.d.ts +115 -0
- package/dist/native-runtime-registry.d.ts +19 -0
- package/dist/native-runtime.d.ts +204 -0
- package/dist/native-transient-account.d.ts +38 -0
- package/dist/native-ui.d.ts +45 -0
- package/dist/native-usage-publication.d.ts +11 -0
- package/dist/native-vault-admission.d.ts +8 -0
- package/dist/native-vault-connection.d.ts +33 -0
- package/dist/native-vault-paths.d.ts +13 -0
- package/dist/native-vault-runtime.d.ts +102 -0
- package/dist/native-vault-seed.d.ts +16 -0
- package/dist/oauth-profile.d.ts +2 -1
- package/dist/pool-authority.d.ts +159 -0
- package/dist/pool-binding.d.ts +32 -0
- package/dist/pool-paths.d.ts +21 -0
- package/dist/pool-store.d.ts +32 -0
- package/dist/prime.d.ts +11 -3
- package/dist/provider-error.d.ts +139 -0
- package/dist/quota-header-feed.d.ts +13 -0
- package/dist/quota-manager.d.ts +30 -5
- package/dist/relay.d.ts +22 -1
- package/dist/request-history.d.ts +34 -0
- package/dist/sse.d.ts +81 -0
- package/dist/sticky-routing.d.ts +29 -0
- package/package.json +9 -3
- package/dist/accounts.js +0 -3330
- package/dist/auth.js +0 -182
- package/dist/cache1h.js +0 -97
- package/dist/cachekeep-registry.js +0 -124
- package/dist/cachekeep.js +0 -578
- package/dist/cch.js +0 -127
- package/dist/claude-code.js +0 -388
- package/dist/claustrum.js +0 -529
- package/dist/commands/account.js +0 -239
- package/dist/constants.js +0 -82
- package/dist/dump.js +0 -616
- package/dist/fast.js +0 -66
- package/dist/json.js +0 -10
- package/dist/killswitch.js +0 -132
- package/dist/logger.js +0 -286
- package/dist/logging.js +0 -59
- package/dist/mid-conversation-output-config.js +0 -89
- package/dist/models.js +0 -141
- package/dist/network-errors.js +0 -27
- package/dist/oauth-profile.js +0 -54
- package/dist/pkce.js +0 -17
- package/dist/prime.js +0 -823
- package/dist/provider.js +0 -0
- package/dist/quota-header-feed.js +0 -279
- package/dist/quota-headers.js +0 -121
- package/dist/quota-manager.js +0 -805
- package/dist/quotas.js +0 -141
- package/dist/relay.js +0 -1243
- package/dist/routing.js +0 -94
- package/dist/start.js +0 -29
- package/dist/sticky-routing.js +0 -523
- package/dist/thinking-binding.js +0 -54
- package/dist/token-fingerprint.js +0 -8
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
export interface ProviderErrorRedactor {
|
|
2
|
+
/** Scrub credentials from error text. */
|
|
3
|
+
redactText(text: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* Scrub credentials from any thrown value without running caller code.
|
|
6
|
+
* Strings come back scrubbed and other primitives unchanged. Objects come
|
|
7
|
+
* back as a fresh projection that keeps only the fields hosts classify on
|
|
8
|
+
* (name, message, status, code, type, retry hints, headers, the nested
|
|
9
|
+
* `error` envelope, `cause` and aggregate `errors`), each read as own data
|
|
10
|
+
* and scrubbed. The original object, its prototype, getters, private state
|
|
11
|
+
* and any response body, stream or URL are never returned. Never throws.
|
|
12
|
+
*/
|
|
13
|
+
redactError(error: unknown): unknown;
|
|
14
|
+
/**
|
|
15
|
+
* Scrub a provider response. Successful non-stream responses are returned
|
|
16
|
+
* as-is. Successful event streams keep every successful frame byte-for-byte
|
|
17
|
+
* and rewrite only error envelopes. Non-2xx responses get scrubbed status
|
|
18
|
+
* text, headers, URL and body; the body is read lazily when the caller
|
|
19
|
+
* reads it. After disposal a non-2xx response keeps only its status and
|
|
20
|
+
* verifiable retry/rate-limit headers.
|
|
21
|
+
*/
|
|
22
|
+
redactResponse(response: Response): Response;
|
|
23
|
+
/** Forget the credentials. Later error reads fail closed to a generic message. */
|
|
24
|
+
dispose(): void;
|
|
25
|
+
}
|
|
26
|
+
/** Replacement for every scrubbed credential. */
|
|
27
|
+
export declare const PROVIDER_ERROR_REDACTION = "[REDACTED]";
|
|
28
|
+
/** Message used when error material is read after the redactor was disposed. */
|
|
29
|
+
export declare const PROVIDER_ERROR_WITHHELD_MESSAGE = "Provider error details were withheld because the request attempt has ended";
|
|
30
|
+
/** Message used when a non-2xx body is too large to scrub safely. */
|
|
31
|
+
export declare const PROVIDER_ERROR_OVERSIZED_MESSAGE = "Provider error body exceeded the redaction size limit and was withheld";
|
|
32
|
+
/** Largest non-2xx body read into memory for scrubbing. */
|
|
33
|
+
export declare const PROVIDER_ERROR_MAX_BODY_BYTES: number;
|
|
34
|
+
/** Largest SSE frame carried while looking for its end. */
|
|
35
|
+
export declare const PROVIDER_ERROR_MAX_FRAME_BYTES: number;
|
|
36
|
+
/**
|
|
37
|
+
* For producers of a non-2xx body, such as the relay, that stop buffering it
|
|
38
|
+
* at PROVIDER_ERROR_MAX_BODY_BYTES: error the raw body stream with a new
|
|
39
|
+
* instance, and the body handed out by `redactResponse` ends with the fixed
|
|
40
|
+
* PROVIDER_ERROR_OVERSIZED_MESSAGE JSON instead of an error, also when the
|
|
41
|
+
* redactor or scope was already disposed. Bytes the source had queued are
|
|
42
|
+
* dropped. Only objects this constructor created count: membership is
|
|
43
|
+
* looked up by identity in a private set, so nothing on the value (its
|
|
44
|
+
* message, code, prototype or any getter) is read, and a look-alike, a
|
|
45
|
+
* proxy around an instance or an object with the same prototype is treated
|
|
46
|
+
* as an ordinary failure. Not exported from the Core package entry point.
|
|
47
|
+
*/
|
|
48
|
+
export declare class ProviderErrorSourceOversizedError extends Error {
|
|
49
|
+
constructor();
|
|
50
|
+
}
|
|
51
|
+
/** Message of the error thrown when a closed stream scope is fed again. */
|
|
52
|
+
export declare const PROVIDER_ERROR_SCOPE_CLOSED_MESSAGE = "Provider error stream scope is closed";
|
|
53
|
+
/**
|
|
54
|
+
* Internal scope for a producer that already holds the raw event-stream
|
|
55
|
+
* bytes of one physical attempt (the relay): it feeds them through the same
|
|
56
|
+
* pipeline as `redactResponse` without wrapping a Response.
|
|
57
|
+
*/
|
|
58
|
+
export interface ProviderErrorStreamScope {
|
|
59
|
+
/**
|
|
60
|
+
* Bytes to forward for this chunk. Unchanged frames come back as the same
|
|
61
|
+
* bytes; only error envelopes are rewritten; an incomplete frame is held
|
|
62
|
+
* until its blank line. A frame over the bound throws
|
|
63
|
+
* `SseFrameTooLargeError`, and any failure disposes the scope.
|
|
64
|
+
*/
|
|
65
|
+
push(chunk: Uint8Array): Uint8Array[];
|
|
66
|
+
/** End of input: bytes for the unterminated tail, then the scope disposes. */
|
|
67
|
+
finish(): Uint8Array[];
|
|
68
|
+
/** Scrub a thrown error; generic once the scope is disposed. */
|
|
69
|
+
redactError(error: unknown): unknown;
|
|
70
|
+
/** Drop the credentials and any held partial frame. Idempotent. */
|
|
71
|
+
dispose(): void;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Create a stream scope for one physical attempt or reconnect, holding the
|
|
75
|
+
* exact credentials that attempt sent. Create a fresh scope after every
|
|
76
|
+
* fresh authorization. After `finish()` or `dispose()`, `push` and `finish`
|
|
77
|
+
* throw an error carrying no content and `redactError` fails closed.
|
|
78
|
+
*/
|
|
79
|
+
export declare function createProviderErrorStreamScope(secrets: readonly string[]): ProviderErrorStreamScope;
|
|
80
|
+
/**
|
|
81
|
+
* Create a redactor for one request attempt. `secrets` are the exact
|
|
82
|
+
* credentials that attempt sent; pass a new redactor for every attempt so a
|
|
83
|
+
* rotated or fallback credential is scrubbed by the attempt that used it, and
|
|
84
|
+
* call `dispose()` when the attempt ends.
|
|
85
|
+
*/
|
|
86
|
+
export declare function createProviderErrorRedactor(secrets: readonly string[]): ProviderErrorRedactor;
|
|
87
|
+
/**
|
|
88
|
+
* What the request asked for, which decides how a successful body is
|
|
89
|
+
* treated: `event-stream` for streaming model requests (the body always goes
|
|
90
|
+
* through the error-only SSE rewrite, whatever content type the response
|
|
91
|
+
* claims), `http` for every other request (successful bodies are returned
|
|
92
|
+
* untouched and never read).
|
|
93
|
+
*/
|
|
94
|
+
export type ProviderResponseMode = 'event-stream' | 'http';
|
|
95
|
+
/** How a response scope retired its credentials. */
|
|
96
|
+
export type ProviderResponseSettlement = 'eof' | 'cancel' | 'error' | 'untouched' | 'disposed';
|
|
97
|
+
/**
|
|
98
|
+
* Lifetime of one direct provider response. The credentials are retired at
|
|
99
|
+
* the response's terminal point rather than when the host remembers to
|
|
100
|
+
* dispose.
|
|
101
|
+
*/
|
|
102
|
+
export interface ProviderErrorResponseScope {
|
|
103
|
+
/**
|
|
104
|
+
* Scrub exactly one response. In `http` mode a successful response is
|
|
105
|
+
* returned as-is without reading its body; in `event-stream` mode a
|
|
106
|
+
* successful body is wrapped so only error envelopes are rewritten,
|
|
107
|
+
* whatever its content type. In both modes a non-2xx response gets
|
|
108
|
+
* scrubbed status text, headers, URL and a lazily scrubbed body. A second
|
|
109
|
+
* call, or any call after the scope has settled, throws a TypeError.
|
|
110
|
+
*/
|
|
111
|
+
redactResponse(response: Response): Response;
|
|
112
|
+
/** Scrub a thrown error while live; generic once the scope has settled. */
|
|
113
|
+
redactError(error: unknown): unknown;
|
|
114
|
+
/** Retire the credentials now. Idempotent. */
|
|
115
|
+
dispose(): void;
|
|
116
|
+
/**
|
|
117
|
+
* Resolves once, at the first terminal point, and never rejects:
|
|
118
|
+
* - `eof`, `cancel` or `error` when a wrapped body (a successful event
|
|
119
|
+
* stream or a non-2xx body) ends, is cancelled by its consumer, or fails;
|
|
120
|
+
* - `untouched` when `redactResponse` returned without wrapping a body,
|
|
121
|
+
* such as a successful non-event-stream response, whose body is never
|
|
122
|
+
* read;
|
|
123
|
+
* - `disposed` when `dispose()` came first.
|
|
124
|
+
* It marks the credentials' retirement, not completed I/O cleanup: a
|
|
125
|
+
* source cancellation may still be pending.
|
|
126
|
+
*/
|
|
127
|
+
readonly settled: Promise<ProviderResponseSettlement>;
|
|
128
|
+
}
|
|
129
|
+
/** Message of the TypeError thrown when a response scope is reused. */
|
|
130
|
+
export declare const PROVIDER_ERROR_RESPONSE_SCOPE_USED_MESSAGE = "Provider error response scope accepts one response while live";
|
|
131
|
+
/**
|
|
132
|
+
* Create a response scope for one physical attempt, holding the exact
|
|
133
|
+
* credentials that attempt sent, and the mode of the request that attempt
|
|
134
|
+
* made. Host code that classifies the outcome after
|
|
135
|
+
* the scope settles (retry, rerouting or quota decisions) must use the
|
|
136
|
+
* status, headers and already-scrubbed frames it read during the attempt:
|
|
137
|
+
* once settled, `redactError` only returns a generic error.
|
|
138
|
+
*/
|
|
139
|
+
export declare function createProviderErrorResponseScope(secrets: readonly string[], mode: ProviderResponseMode): ProviderErrorResponseScope;
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { type AccountStorage, type OAuthQuotaSnapshot, type QuotaFieldName, type QuotaFieldSource, type QuotaFieldSources } from './accounts.ts';
|
|
2
2
|
export declare const QUOTA_HEADER_FEED_SCHEMA_VERSION = 3;
|
|
3
3
|
export declare const QUOTA_HEADER_FEED_LEASE_MS = 180000;
|
|
4
|
+
/**
|
|
5
|
+
* Lease files are per process; each carries only the accounts whose response
|
|
6
|
+
* headers THAT process harvested. Consumers MUST union entries across all files
|
|
7
|
+
* inside `lease_horizon_ms`, deduplicating by account; "newest file wins" is wrong.
|
|
8
|
+
* `anthropic_account_uuid` is always present: null is unresolvable, while absence
|
|
9
|
+
* identifies an old producer. A fallback `account_ref` is store-local.
|
|
10
|
+
*/
|
|
4
11
|
export type QuotaHeaderFeedIdentity = {
|
|
5
12
|
identity_source: 'credential_id';
|
|
6
13
|
credential_id: string;
|
|
@@ -22,9 +29,12 @@ type QuotaHeaderFeedMetadata = {
|
|
|
22
29
|
observed_at_ms: number;
|
|
23
30
|
};
|
|
24
31
|
export type QuotaHeaderFeedEntry = QuotaHeaderFeedIdentity & QuotaHeaderFeedMetadata & {
|
|
32
|
+
/** Always present: null means UUID resolution failed; absence denotes an old producer. */
|
|
33
|
+
anthropic_account_uuid: string | null;
|
|
25
34
|
quota: QuotaHeaderFeedQuota;
|
|
26
35
|
};
|
|
27
36
|
export type QuotaHeaderFeedPublishEntry = QuotaHeaderFeedIdentity & QuotaHeaderFeedMetadata & {
|
|
37
|
+
anthropic_account_uuid: string | null;
|
|
28
38
|
quota: Omit<QuotaHeaderFeedQuota, 'provenance'> & {
|
|
29
39
|
fieldSources?: QuotaFieldSources;
|
|
30
40
|
};
|
|
@@ -44,8 +54,11 @@ export declare class QuotaHeaderFeedRegistry {
|
|
|
44
54
|
now?: () => number;
|
|
45
55
|
leaseMs?: number;
|
|
46
56
|
instanceId?: string;
|
|
57
|
+
removeFile?: (path: string) => Promise<void>;
|
|
58
|
+
beforeRemoveFile?: (path: string) => Promise<void>;
|
|
47
59
|
});
|
|
48
60
|
publish(entry: QuotaHeaderFeedPublishEntry): Promise<void>;
|
|
61
|
+
private reapStaleSiblingLeases;
|
|
49
62
|
list(): Promise<QuotaHeaderFeedEntry[]>;
|
|
50
63
|
dispose(): Promise<void>;
|
|
51
64
|
}
|
package/dist/quota-manager.d.ts
CHANGED
|
@@ -17,9 +17,24 @@ export type QuotaRefreshResult = {
|
|
|
17
17
|
/** False when backoff or a cross-process lock served the existing cache. */
|
|
18
18
|
fetched: boolean;
|
|
19
19
|
};
|
|
20
|
+
/** Pace a physical usage GET and verify the selected credential is still eligible before sending. */
|
|
21
|
+
export type QuotaRequestDispatch = (send: () => Promise<Response>, revalidate: () => Promise<void>) => Promise<Response>;
|
|
20
22
|
export type QuotaManagerOptions = {
|
|
21
23
|
storage: AccountStorage | null;
|
|
22
24
|
fetchImpl?: typeof fetch;
|
|
25
|
+
/**
|
|
26
|
+
* This callback checks or obtains credentials before its own HTTP request.
|
|
27
|
+
* accessToken may be empty when the host uses a native pool or vault instead
|
|
28
|
+
* of keeping a token in its quota settings snapshot. Pass dispatch into the
|
|
29
|
+
* native quota operation so every physical GET, including a retry, is paced
|
|
30
|
+
* only after admission and rechecks host custody and credential eligibility
|
|
31
|
+
* after the spacing wait.
|
|
32
|
+
*/
|
|
33
|
+
fetchQuotaSnapshot?: (request: {
|
|
34
|
+
kind: 'main' | 'fallback';
|
|
35
|
+
accountId: string | undefined;
|
|
36
|
+
accessToken: string;
|
|
37
|
+
}, dispatch: QuotaRequestDispatch) => Promise<OAuthQuotaSnapshot>;
|
|
23
38
|
now?: () => number;
|
|
24
39
|
onMainQuotaFetched?: (quota: OAuthQuotaSnapshot, checkedAt: number, tokenFingerprint: string, fetchStartedAt: number, quotaErrorGeneration: number, quotaErrorClearedAt: number | undefined) => void;
|
|
25
40
|
onApiError?: (error: AccountOperationError, quotaErrorGeneration: number) => void;
|
|
@@ -38,7 +53,10 @@ export declare class QuotaManager {
|
|
|
38
53
|
private fallbackAuthLineages;
|
|
39
54
|
private fallbackGenerations;
|
|
40
55
|
private inflightMain;
|
|
56
|
+
private lastMainPollAttempt;
|
|
41
57
|
private inflightMainAccountId;
|
|
58
|
+
private inflightMainAccessToken;
|
|
59
|
+
private inflightMainToken;
|
|
42
60
|
private inflightFallbacks;
|
|
43
61
|
private mainLastApiError;
|
|
44
62
|
private fallbackApiErrors;
|
|
@@ -46,6 +64,7 @@ export declare class QuotaManager {
|
|
|
46
64
|
private lastApiCallAt;
|
|
47
65
|
private storage;
|
|
48
66
|
private readonly fetchImpl;
|
|
67
|
+
private readonly fetchQuotaSnapshot;
|
|
49
68
|
private readonly now;
|
|
50
69
|
private readonly onMainQuotaFetched;
|
|
51
70
|
private readonly onApiError;
|
|
@@ -61,10 +80,13 @@ export declare class QuotaManager {
|
|
|
61
80
|
* Cached fallback quota entry, scoped to the configured account id.
|
|
62
81
|
*/
|
|
63
82
|
getFallback(accountId: string, account?: Pick<OAuthAccount, 'authLineageId'>): QuotaEntry | null;
|
|
83
|
+
/** A configured transport obtains its own credentials; policy snapshots need no tokens. */
|
|
84
|
+
canFetchWithoutAccessToken(): boolean;
|
|
64
85
|
getAllFallbacks(): Map<string, QuotaEntry>;
|
|
65
86
|
setMain(mainAccountId: string | undefined, entry: QuotaEntry): void;
|
|
87
|
+
clearMain(): void;
|
|
66
88
|
/** Bind quota state to the account behind the current main credential. */
|
|
67
|
-
setMainQuotaAccountIdentity(accountIdentity: string | undefined, strict?: boolean): number | undefined;
|
|
89
|
+
setMainQuotaAccountIdentity(accountIdentity: string | undefined, strict?: boolean, force?: boolean): number | undefined;
|
|
68
90
|
setFallback(accountId: string, entry: QuotaEntry, account: Pick<OAuthAccount, 'authLineageId'> | undefined): void;
|
|
69
91
|
clearFallback(accountId: string): void;
|
|
70
92
|
pushMainFromHeaders(mainAccountId: string | undefined, incoming: OAuthQuotaSnapshot): QuotaEntry;
|
|
@@ -122,13 +144,16 @@ export declare class QuotaManager {
|
|
|
122
144
|
private static quotaLockName;
|
|
123
145
|
private bindFallbackLineage;
|
|
124
146
|
private acceptFallbackQuotaObservation;
|
|
147
|
+
private isClosed;
|
|
148
|
+
close(): void;
|
|
125
149
|
/**
|
|
126
|
-
* Serialize
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* from triggering Anthropic's rate limits.
|
|
150
|
+
* Serialize quota refresh work, including credential admission and locks.
|
|
151
|
+
* The transport separately paces physical GETs. A refresh refused before
|
|
152
|
+
* HTTP dispatch must not postpone the next usable account's poll.
|
|
130
153
|
*/
|
|
131
154
|
private _enqueueApiFetch;
|
|
155
|
+
private _quotaDispatch;
|
|
156
|
+
private _quotaFetch;
|
|
132
157
|
private _fetchMain;
|
|
133
158
|
private _fetchFallback;
|
|
134
159
|
private static isAuthError;
|
package/dist/relay.d.ts
CHANGED
|
@@ -11,6 +11,19 @@ export type RelayPatch = {
|
|
|
11
11
|
deleteCount: number;
|
|
12
12
|
insert: string;
|
|
13
13
|
};
|
|
14
|
+
export type RelayAttemptAuthorization = {
|
|
15
|
+
headers: Headers;
|
|
16
|
+
/** Called only for a genuine upstream response, never optimistic/transport status. */
|
|
17
|
+
onUpstreamStatus?: (status: number) => void;
|
|
18
|
+
/**
|
|
19
|
+
* Additional exact credentials this physical attempt sends upstream, used
|
|
20
|
+
* only to scrub provider error material echoed back for this attempt. The
|
|
21
|
+
* credentials in the outgoing `authorization` and `x-api-key` headers are
|
|
22
|
+
* always scrubbed as well; this list can only add to them.
|
|
23
|
+
*/
|
|
24
|
+
errorSecrets?: readonly string[];
|
|
25
|
+
};
|
|
26
|
+
export type AuthorizeRelayAttempt = () => Promise<RelayAttemptAuthorization>;
|
|
14
27
|
export declare function getRelayConfig(storage: AccountStorage | null): RelayConfig | null;
|
|
15
28
|
export declare function generateRelayToken(): string;
|
|
16
29
|
export declare function hashBody(body: string): string;
|
|
@@ -23,7 +36,15 @@ export declare function sendViaRelay(options: {
|
|
|
23
36
|
body: RequestInit['body'] | null | undefined;
|
|
24
37
|
fallback: () => Promise<Response>;
|
|
25
38
|
affinity?: string | null;
|
|
39
|
+
/**
|
|
40
|
+
* Resolve a WebSocket send with a synthetic 200 event stream before the
|
|
41
|
+
* upstream answers. Honoured only for streaming requests (body `stream` is
|
|
42
|
+
* true); any other request waits for the real response on the same
|
|
43
|
+
* WebSocket transport.
|
|
44
|
+
*/
|
|
26
45
|
optimisticResponse?: boolean;
|
|
46
|
+
/** Fresh authorization for every physical relay attempt, including internal retries. */
|
|
47
|
+
authorizeAttempt?: AuthorizeRelayAttempt;
|
|
27
48
|
/**
|
|
28
49
|
* Observes genuine upstream headers once per delivered response. WebSocket
|
|
29
50
|
* retries before downstream bytes replace staged headers, so the callback
|
|
@@ -36,4 +57,4 @@ export declare function sendViaRelay(options: {
|
|
|
36
57
|
setTimeoutImpl?: typeof globalThis.setTimeout;
|
|
37
58
|
clearTimeoutImpl?: typeof globalThis.clearTimeout;
|
|
38
59
|
}): Promise<Response>;
|
|
39
|
-
export declare const WORKER_SCRIPT = "\nasync function hashBody(body) {\n const bytes = new TextEncoder().encode(body)\n const digest = await crypto.subtle.digest('SHA-256', bytes)\n const hex = [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('')\n return 'sha256:' + hex\n}\n\nfunction applyPatch(base, patch) {\n return base.slice(0, patch.start) + patch.insert + base.slice(patch.start + patch.deleteCount)\n}\n\nfunction applyPatchSet(base, patch) {\n if (!Array.isArray(patch)) return applyPatch(base, patch)\n if (patch.length === 0) return base\n\n let result = ''\n let cursor = 0\n for (const item of patch) {\n if (item.start < cursor) return { error: 'overlapping patch', status: 400 }\n result += base.slice(cursor, item.start)\n result += item.insert\n cursor = item.start + item.deleteCount\n }\n return result + base.slice(cursor)\n}\n\nasync function readState(env, affinity) {\n const raw = await env.RELAY_STATE.get('session:' + affinity)\n return raw ? JSON.parse(raw) : null\n}\n\nasync function writeState(env, affinity, state) {\n await env.RELAY_STATE.put('session:' + affinity, JSON.stringify(state), { expirationTtl: 86400 })\n}\n\nasync function resolveBody(env, payload) {\n if (payload.mode === 'full_sync') return payload.body\n if (payload.mode === 'patch') {\n const state = await readState(env, payload.affinity)\n if (!state || state.hash !== payload.base_hash) {\n return { error: 'state mismatch', status: 409 }\n }\n return applyPatchSet(state.body, payload.patch)\n }\n return { error: 'unknown mode', status: 400 }\n}\n\nasync function prepareUpstream(env, payload) {\n if ((payload.protocol !== 1 && payload.protocol !== 2) || payload.type !== 'request' || !payload.affinity || !payload.upstream?.url || !payload.next_hash) {\n return { error: 'invalid payload', status: 400 }\n }\n\n const body = await resolveBody(env, payload)\n if (body && typeof body === 'object' && 'error' in body) return body\n\n if (typeof body !== 'string' || (await hashBody(body)) !== payload.next_hash) {\n return { error: 'hash mismatch', status: 409 }\n }\n\n const stateWrite = writeState(env, payload.affinity, { body, hash: payload.next_hash, revision: payload.revision }).catch(() => {})\n console.log(JSON.stringify({\n relay: 'opencode-anthropic-auth',\n transport: 'relay',\n mode: payload.mode,\n revision: payload.revision,\n affinity: String(payload.affinity).slice(0, 12),\n bodyBytes: body.length,\n }))\n\n return { body, stateWrite }\n}\n\nfunction resolveBodyFromState(state, payload) {\n if (payload.mode === 'full_sync') return payload.body\n if (payload.mode === 'patch') {\n if (!state || state.hash !== payload.base_hash) {\n return { error: 'state mismatch', status: 409 }\n }\n return applyPatchSet(state.body, payload.patch)\n }\n return { error: 'unknown mode', status: 400 }\n}\n\nfunction deferWorkerTask(task) {\n return new Promise((resolve) => setTimeout(resolve, 0)).then(task)\n}\n\nconst WEBSOCKET_STREAM_FLUSH_BYTES = 1024\nconst WEBSOCKET_STREAM_FLUSH_MS = 20\n\nfunction createWebSocketStreamCoalescer(socket, options = {}) {\n const flushBytes = options.flushBytes || WEBSOCKET_STREAM_FLUSH_BYTES\n const flushMs = options.flushMs ?? WEBSOCKET_STREAM_FLUSH_MS\n let chunks = []\n let byteLength = 0\n let flushTimer = null\n const stats = {\n upstreamChunks: 0,\n upstreamBytes: 0,\n frames: 0,\n frameBytes: 0,\n }\n\n const clearFlushTimer = () => {\n if (flushTimer === null) return\n clearTimeout(flushTimer)\n flushTimer = null\n }\n\n const mergeChunks = () => {\n if (chunks.length === 1) return chunks[0]\n const frame = new Uint8Array(byteLength)\n let offset = 0\n for (const chunk of chunks) {\n frame.set(chunk, offset)\n offset += chunk.byteLength\n }\n return frame\n }\n\n const flush = () => {\n clearFlushTimer()\n if (byteLength === 0) return\n const frame = mergeChunks()\n chunks = []\n byteLength = 0\n socket.send(frame)\n stats.frames += 1\n stats.frameBytes += frame.byteLength\n }\n\n const scheduleFlush = () => {\n if (flushTimer !== null) return\n flushTimer = setTimeout(flush, flushMs)\n }\n\n return {\n push(value) {\n const chunk = value instanceof Uint8Array ? value : new Uint8Array(value)\n if (chunk.byteLength === 0) return\n stats.upstreamChunks += 1\n stats.upstreamBytes += chunk.byteLength\n chunks.push(chunk)\n byteLength += chunk.byteLength\n if (byteLength >= flushBytes) flush()\n else scheduleFlush()\n },\n flush,\n stats() {\n return { ...stats, pendingBytes: byteLength }\n },\n }\n}\n\nfunction checkpointWebSocketState(env, payload, body, nextState) {\n return deferWorkerTask(async () => {\n try {\n if ((await hashBody(body)) !== payload.next_hash) {\n throw new Error('hash mismatch')\n }\n await writeState(env, payload.affinity, nextState)\n } catch (error) {\n console.log(JSON.stringify({\n relay: 'opencode-anthropic-auth',\n transport: 'websocket',\n event: 'checkpoint_write_failed',\n affinity: String(payload.affinity).slice(0, 12),\n message: error instanceof Error ? error.message : String(error),\n }))\n }\n })\n}\n\nasync function prepareWebSocketUpstream(env, state, payload) {\n if (payload.protocol !== 2 || payload.type !== 'request' || !payload.id || !payload.affinity || !payload.upstream?.url || !payload.next_hash) {\n return { error: 'invalid payload', status: 400 }\n }\n\n const body = resolveBodyFromState(state, payload)\n if (body && typeof body === 'object' && 'error' in body) return body\n if (typeof body !== 'string') return { error: 'invalid body', status: 400 }\n if ((await hashBody(body)) !== payload.next_hash) {\n return { error: 'hash mismatch', status: 409 }\n }\n\n const nextState = { body, hash: payload.next_hash, revision: payload.revision }\n const checkpoint = checkpointWebSocketState(env, payload, body, nextState)\n const logAccepted = () => console.log(JSON.stringify({\n relay: 'opencode-anthropic-auth',\n transport: 'websocket',\n mode: payload.mode,\n revision: payload.revision,\n affinity: String(payload.affinity).slice(0, 12),\n bodyBytes: body.length,\n }))\n\n return { body, state: nextState, checkpoint, logAccepted }\n}\n\nfunction headersToObject(headers) {\n const result = {}\n for (const [key, value] of headers.entries()) result[key] = value\n return result\n}\n\nasync function handleRelayPayload(env, payload) {\n const prepared = await prepareUpstream(env, payload)\n if (prepared.error) return prepared\n const upstream = await fetch(payload.upstream.url, {\n method: payload.upstream.method || 'POST',\n headers: payload.upstream.headers,\n body: prepared.body,\n })\n return { upstream, stateWrite: prepared.stateWrite }\n}\n\nasync function handleWebSocket(socket, env, ctx, payload, getState, setState) {\n const heartbeat = setInterval(() => {\n try {\n socket.send(JSON.stringify({ protocol: 2, type: 'keepalive' }))\n } catch {\n clearInterval(heartbeat)\n }\n }, 15000)\n let streamCoalescer = null\n try {\n const result = await prepareWebSocketUpstream(env, getState(), payload)\n if (result.error) {\n socket.send(JSON.stringify({ protocol: 2, type: 'error', id: payload.id, status: result.status, message: result.error }))\n return\n }\n\n setState(result.state)\n socket.send(JSON.stringify({ protocol: 2, type: 'accepted', id: payload.id, hash: result.state.hash, revision: result.state.revision }))\n ctx?.waitUntil?.(deferWorkerTask(result.logAccepted))\n\n const upstreamPromise = fetch(payload.upstream.url, {\n method: payload.upstream.method || 'POST',\n headers: payload.upstream.headers,\n body: result.body,\n })\n ctx?.waitUntil?.(result.checkpoint)\n const upstream = await upstreamPromise\n socket.send(JSON.stringify({\n protocol: 2,\n type: 'response_start',\n id: payload.id,\n status: upstream.status,\n statusText: upstream.statusText,\n headers: headersToObject(upstream.headers),\n }))\n\n const reader = upstream.body?.getReader()\n if (reader) {\n streamCoalescer = createWebSocketStreamCoalescer(socket)\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n streamCoalescer.push(value)\n }\n streamCoalescer.flush()\n }\n const stats = streamCoalescer?.stats?.() || {}\n socket.send(JSON.stringify({ protocol: 2, type: 'done', id: payload.id, ...stats }))\n } catch (error) {\n try {\n streamCoalescer?.flush?.()\n } catch {}\n socket.send(JSON.stringify({ protocol: 2, type: 'error', id: payload.id, status: 500, message: error instanceof Error ? error.message : String(error) }))\n } finally {\n clearInterval(heartbeat)\n }\n}\n\nexport default {\n async fetch(request, env, ctx) {\n if (request.headers.get('Upgrade') === 'websocket') {\n const url = new URL(request.url)\n const token = url.searchParams.get('token')\n const affinity = url.searchParams.get('affinity')\n if (token !== env.RELAY_TOKEN) return new Response('unauthorized', { status: 401 })\n if (!affinity) return new Response('missing affinity', { status: 400 })\n const pair = new WebSocketPair()\n const client = pair[0]\n const server = pair[1]\n server.binaryType = 'arraybuffer'\n let state = null\n let ready = false\n server.accept()\n const loadState = readState(env, affinity).then((loadedState) => {\n state = loadedState\n ready = true\n server.send(JSON.stringify({\n protocol: 2,\n type: 'ready',\n state: state ? { hash: state.hash, revision: state.revision } : null,\n }))\n }).catch((error) => {\n server.send(JSON.stringify({ protocol: 2, type: 'error', status: 500, message: error instanceof Error ? error.message : String(error) }))\n server.close(1011, 'state load failed')\n })\n ctx?.waitUntil?.(loadState)\n if (!ctx?.waitUntil) void loadState\n let busy = false\n server.addEventListener('message', (event) => {\n if (!ready) {\n server.send(JSON.stringify({ protocol: 2, type: 'error', status: 425, message: 'relay state is not ready' }))\n return\n }\n if (busy) {\n server.send(JSON.stringify({ protocol: 2, type: 'error', status: 429, message: 'request already in flight' }))\n return\n }\n let payload\n try {\n payload = JSON.parse(event.data)\n } catch {\n server.send(JSON.stringify({ protocol: 2, type: 'error', status: 400, message: 'invalid JSON payload' }))\n return\n }\n payload.affinity = affinity\n busy = true\n const run = handleWebSocket(server, env, ctx, payload, () => state, (nextState) => { state = nextState }).finally(() => { busy = false })\n ctx?.waitUntil?.(run)\n if (!ctx?.waitUntil) void run\n })\n return new Response(null, { status: 101, webSocket: client })\n }\n\n if (request.method === 'GET') {\n return Response.json({ status: 'ok', transports: ['http', 'websocket'] })\n }\n if (request.method !== 'POST') return new Response('method not allowed', { status: 405 })\n if (request.headers.get('x-relay-token') !== env.RELAY_TOKEN) {\n return new Response('unauthorized', { status: 401 })\n }\n\n try {\n const payload = await request.json()\n const result = await handleRelayPayload(env, payload)\n if (result.error) return Response.json({ error: result.error }, { status: result.status })\n\n if (result.stateWrite) ctx.waitUntil(result.stateWrite)\n\n const upstream = result.upstream\n return new Response(upstream.body, {\n status: upstream.status,\n statusText: upstream.statusText,\n headers: upstream.headers,\n })\n } catch (error) {\n return Response.json(\n { error: error instanceof Error ? error.message : 'internal relay error' },\n { status: 502 },\n )\n }\n },\n}\n";
|
|
60
|
+
export declare const WORKER_SCRIPT = "\nasync function hashBody(body) {\n const bytes = new TextEncoder().encode(body)\n const digest = await crypto.subtle.digest('SHA-256', bytes)\n const hex = [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('')\n return 'sha256:' + hex\n}\n\nfunction applyPatch(base, patch) {\n return base.slice(0, patch.start) + patch.insert + base.slice(patch.start + patch.deleteCount)\n}\n\nfunction applyPatchSet(base, patch) {\n if (!Array.isArray(patch)) return applyPatch(base, patch)\n if (patch.length === 0) return base\n\n let result = ''\n let cursor = 0\n for (const item of patch) {\n if (item.start < cursor) return { error: 'overlapping patch', status: 400 }\n result += base.slice(cursor, item.start)\n result += item.insert\n cursor = item.start + item.deleteCount\n }\n return result + base.slice(cursor)\n}\n\nasync function readState(env, affinity) {\n const raw = await env.RELAY_STATE.get('session:' + affinity)\n return raw ? JSON.parse(raw) : null\n}\n\nasync function writeState(env, affinity, state) {\n await env.RELAY_STATE.put('session:' + affinity, JSON.stringify(state), { expirationTtl: 86400 })\n}\n\nasync function resolveBody(env, payload) {\n if (payload.mode === 'full_sync') return payload.body\n if (payload.mode === 'patch') {\n const state = await readState(env, payload.affinity)\n if (!state || state.hash !== payload.base_hash) {\n return { error: 'state mismatch', status: 409 }\n }\n return applyPatchSet(state.body, payload.patch)\n }\n return { error: 'unknown mode', status: 400 }\n}\n\nasync function prepareUpstream(env, payload) {\n if ((payload.protocol !== 1 && payload.protocol !== 2) || payload.type !== 'request' || !payload.affinity || !payload.upstream?.url || !payload.next_hash) {\n return { error: 'invalid payload', status: 400 }\n }\n\n const body = await resolveBody(env, payload)\n if (body && typeof body === 'object' && 'error' in body) return body\n\n if (typeof body !== 'string' || (await hashBody(body)) !== payload.next_hash) {\n return { error: 'hash mismatch', status: 409 }\n }\n\n const stateWrite = writeState(env, payload.affinity, { body, hash: payload.next_hash, revision: payload.revision }).catch(() => {})\n console.log(JSON.stringify({\n relay: 'opencode-anthropic-auth',\n transport: 'relay',\n mode: payload.mode,\n revision: payload.revision,\n affinity: String(payload.affinity).slice(0, 12),\n bodyBytes: body.length,\n }))\n\n return { body, stateWrite }\n}\n\nfunction resolveBodyFromState(state, payload) {\n if (payload.mode === 'full_sync') return payload.body\n if (payload.mode === 'patch') {\n if (!state || state.hash !== payload.base_hash) {\n return { error: 'state mismatch', status: 409 }\n }\n return applyPatchSet(state.body, payload.patch)\n }\n return { error: 'unknown mode', status: 400 }\n}\n\nfunction deferWorkerTask(task) {\n return new Promise((resolve) => setTimeout(resolve, 0)).then(task)\n}\n\nconst WEBSOCKET_STREAM_FLUSH_BYTES = 1024\nconst WEBSOCKET_STREAM_FLUSH_MS = 20\n\nfunction createWebSocketStreamCoalescer(socket, options = {}) {\n const flushBytes = options.flushBytes || WEBSOCKET_STREAM_FLUSH_BYTES\n const flushMs = options.flushMs ?? WEBSOCKET_STREAM_FLUSH_MS\n let chunks = []\n let byteLength = 0\n let flushTimer = null\n const stats = {\n upstreamChunks: 0,\n upstreamBytes: 0,\n frames: 0,\n frameBytes: 0,\n }\n\n const clearFlushTimer = () => {\n if (flushTimer === null) return\n clearTimeout(flushTimer)\n flushTimer = null\n }\n\n const mergeChunks = () => {\n if (chunks.length === 1) return chunks[0]\n const frame = new Uint8Array(byteLength)\n let offset = 0\n for (const chunk of chunks) {\n frame.set(chunk, offset)\n offset += chunk.byteLength\n }\n return frame\n }\n\n const flush = () => {\n clearFlushTimer()\n if (byteLength === 0) return\n const frame = mergeChunks()\n chunks = []\n byteLength = 0\n socket.send(frame)\n stats.frames += 1\n stats.frameBytes += frame.byteLength\n }\n\n const scheduleFlush = () => {\n if (flushTimer !== null) return\n flushTimer = setTimeout(flush, flushMs)\n }\n\n return {\n push(value) {\n const chunk = value instanceof Uint8Array ? value : new Uint8Array(value)\n if (chunk.byteLength === 0) return\n stats.upstreamChunks += 1\n stats.upstreamBytes += chunk.byteLength\n chunks.push(chunk)\n byteLength += chunk.byteLength\n if (byteLength >= flushBytes) flush()\n else scheduleFlush()\n },\n flush,\n stats() {\n return { ...stats, pendingBytes: byteLength }\n },\n }\n}\n\nfunction checkpointWebSocketState(env, payload, body, nextState) {\n return deferWorkerTask(async () => {\n try {\n if ((await hashBody(body)) !== payload.next_hash) {\n throw new Error('hash mismatch')\n }\n await writeState(env, payload.affinity, nextState)\n } catch {\n console.log(JSON.stringify({\n relay: 'opencode-anthropic-auth',\n transport: 'websocket',\n event: 'checkpoint_write_failed',\n affinity: String(payload.affinity).slice(0, 12),\n code: 'checkpoint_write_error',\n }))\n }\n })\n}\n\nasync function prepareWebSocketUpstream(env, state, payload) {\n if (payload.protocol !== 2 || payload.type !== 'request' || !payload.id || !payload.affinity || !payload.upstream?.url || !payload.next_hash) {\n return { error: 'invalid payload', status: 400 }\n }\n\n const body = resolveBodyFromState(state, payload)\n if (body && typeof body === 'object' && 'error' in body) return body\n if (typeof body !== 'string') return { error: 'invalid body', status: 400 }\n if ((await hashBody(body)) !== payload.next_hash) {\n return { error: 'hash mismatch', status: 409 }\n }\n\n const nextState = { body, hash: payload.next_hash, revision: payload.revision }\n const checkpoint = checkpointWebSocketState(env, payload, body, nextState)\n const logAccepted = () => console.log(JSON.stringify({\n relay: 'opencode-anthropic-auth',\n transport: 'websocket',\n mode: payload.mode,\n revision: payload.revision,\n affinity: String(payload.affinity).slice(0, 12),\n bodyBytes: body.length,\n }))\n\n return { body, state: nextState, checkpoint, logAccepted }\n}\n\nfunction headersToObject(headers) {\n const result = {}\n for (const [key, value] of headers.entries()) result[key] = value\n return result\n}\n\nasync function handleRelayPayload(env, payload) {\n const prepared = await prepareUpstream(env, payload)\n if (prepared.error) return prepared\n const upstream = await fetch(payload.upstream.url, {\n method: payload.upstream.method || 'POST',\n headers: payload.upstream.headers,\n body: prepared.body,\n })\n return { upstream, stateWrite: prepared.stateWrite }\n}\n\nasync function handleWebSocket(socket, env, ctx, payload, getState, setState) {\n const heartbeat = setInterval(() => {\n try {\n socket.send(JSON.stringify({ protocol: 2, type: 'keepalive' }))\n } catch {\n clearInterval(heartbeat)\n }\n }, 15000)\n let streamCoalescer = null\n try {\n const result = await prepareWebSocketUpstream(env, getState(), payload)\n if (result.error) {\n socket.send(JSON.stringify({ protocol: 2, type: 'error', id: payload.id, status: result.status, message: result.error }))\n return\n }\n\n setState(result.state)\n socket.send(JSON.stringify({ protocol: 2, type: 'accepted', id: payload.id, hash: result.state.hash, revision: result.state.revision }))\n ctx?.waitUntil?.(deferWorkerTask(result.logAccepted))\n\n const upstreamPromise = fetch(payload.upstream.url, {\n method: payload.upstream.method || 'POST',\n headers: payload.upstream.headers,\n body: result.body,\n })\n ctx?.waitUntil?.(result.checkpoint)\n const upstream = await upstreamPromise\n socket.send(JSON.stringify({\n protocol: 2,\n type: 'response_start',\n id: payload.id,\n status: upstream.status,\n statusText: upstream.statusText,\n headers: headersToObject(upstream.headers),\n }))\n\n const reader = upstream.body?.getReader()\n if (reader) {\n streamCoalescer = createWebSocketStreamCoalescer(socket)\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n streamCoalescer.push(value)\n }\n streamCoalescer.flush()\n }\n const stats = streamCoalescer?.stats?.() || {}\n socket.send(JSON.stringify({ protocol: 2, type: 'done', id: payload.id, ...stats }))\n } catch (error) {\n try {\n streamCoalescer?.flush?.()\n } catch {}\n socket.send(JSON.stringify({ protocol: 2, type: 'error', id: payload.id, status: 500, message: error instanceof Error ? error.message : String(error) }))\n } finally {\n clearInterval(heartbeat)\n }\n}\n\nexport default {\n async fetch(request, env, ctx) {\n if (request.headers.get('Upgrade') === 'websocket') {\n const url = new URL(request.url)\n const token = url.searchParams.get('token')\n const affinity = url.searchParams.get('affinity')\n if (token !== env.RELAY_TOKEN) return new Response('unauthorized', { status: 401 })\n if (!affinity) return new Response('missing affinity', { status: 400 })\n const pair = new WebSocketPair()\n const client = pair[0]\n const server = pair[1]\n server.binaryType = 'arraybuffer'\n let state = null\n let ready = false\n server.accept()\n const loadState = readState(env, affinity).then((loadedState) => {\n state = loadedState\n ready = true\n server.send(JSON.stringify({\n protocol: 2,\n type: 'ready',\n state: state ? { hash: state.hash, revision: state.revision } : null,\n }))\n }).catch((error) => {\n server.send(JSON.stringify({ protocol: 2, type: 'error', status: 500, message: error instanceof Error ? error.message : String(error) }))\n server.close(1011, 'state load failed')\n })\n ctx?.waitUntil?.(loadState)\n if (!ctx?.waitUntil) void loadState\n let busy = false\n server.addEventListener('message', (event) => {\n if (!ready) {\n server.send(JSON.stringify({ protocol: 2, type: 'error', status: 425, message: 'relay state is not ready' }))\n return\n }\n if (busy) {\n server.send(JSON.stringify({ protocol: 2, type: 'error', status: 429, message: 'request already in flight' }))\n return\n }\n let payload\n try {\n payload = JSON.parse(event.data)\n } catch {\n server.send(JSON.stringify({ protocol: 2, type: 'error', status: 400, message: 'invalid JSON payload' }))\n return\n }\n payload.affinity = affinity\n busy = true\n const run = handleWebSocket(server, env, ctx, payload, () => state, (nextState) => { state = nextState }).finally(() => { busy = false })\n ctx?.waitUntil?.(run)\n if (!ctx?.waitUntil) void run\n })\n return new Response(null, { status: 101, webSocket: client })\n }\n\n if (request.method === 'GET') {\n return Response.json({ status: 'ok', transports: ['http', 'websocket'] })\n }\n if (request.method !== 'POST') return new Response('method not allowed', { status: 405 })\n if (request.headers.get('x-relay-token') !== env.RELAY_TOKEN) {\n return new Response('unauthorized', { status: 401 })\n }\n\n try {\n const payload = await request.json()\n const result = await handleRelayPayload(env, payload)\n if (result.error) return Response.json({ error: result.error }, { status: result.status })\n\n if (result.stateWrite) ctx.waitUntil(result.stateWrite)\n\n const upstream = result.upstream\n return new Response(upstream.body, {\n status: upstream.status,\n statusText: upstream.statusText,\n headers: upstream.headers,\n })\n } catch (error) {\n return Response.json(\n { error: error instanceof Error ? error.message : 'internal relay error' },\n { status: 502 },\n )\n }\n },\n}\n";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export declare const TRAILING_ASSISTANT_HISTORY_MESSAGE: string;
|
|
2
|
+
/** Shape-only details keep conversation content out of errors and logs. */
|
|
3
|
+
export declare class TrailingAssistantHistoryError extends Error {
|
|
4
|
+
readonly details: {
|
|
5
|
+
messageCount: number;
|
|
6
|
+
trailingMessageIndex: number;
|
|
7
|
+
emptyMessagesAfter: number;
|
|
8
|
+
contentShape: 'string' | 'array' | 'missing' | 'other';
|
|
9
|
+
contentBlockCount?: number;
|
|
10
|
+
contentBlockTypes?: string[];
|
|
11
|
+
};
|
|
12
|
+
readonly check = "meaningful_trailing_assistant";
|
|
13
|
+
constructor(details: {
|
|
14
|
+
messageCount: number;
|
|
15
|
+
trailingMessageIndex: number;
|
|
16
|
+
emptyMessagesAfter: number;
|
|
17
|
+
contentShape: 'string' | 'array' | 'missing' | 'other';
|
|
18
|
+
contentBlockCount?: number;
|
|
19
|
+
contentBlockTypes?: string[];
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Refuse meaningful or unknown assistant trailers without mutating history.
|
|
24
|
+
* Converters can use this before lowering so dropped opaque or incomplete
|
|
25
|
+
* blocks cannot hide a missing user boundary. Return the empty-trailer count.
|
|
26
|
+
* Ordinary user-ended requests inspect only the final element.
|
|
27
|
+
*/
|
|
28
|
+
export declare function assertNoMeaningfulTrailingAssistant(messages: readonly unknown[]): number;
|
|
29
|
+
/**
|
|
30
|
+
* Remove only provably empty trailers from an owned wire-message array. The
|
|
31
|
+
* wire does not distinguish a completed answer from a partial prefill, so
|
|
32
|
+
* deleting meaningful content could replay the previous question.
|
|
33
|
+
*/
|
|
34
|
+
export declare function stripEmptyTrailingAssistantMessages(messages: unknown): number;
|
package/dist/sse.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** Largest single frame a framer accepts before it refuses the stream. */
|
|
2
|
+
export declare const DEFAULT_SSE_MAX_FRAME_BYTES: number;
|
|
3
|
+
/**
|
|
4
|
+
* Raised when a single frame, terminated or not, grows past the framer's
|
|
5
|
+
* byte bound. The framer cannot continue after this error. The message
|
|
6
|
+
* carries only sizes, never frame content.
|
|
7
|
+
*/
|
|
8
|
+
export declare class SseFrameTooLargeError extends Error {
|
|
9
|
+
readonly code = "SSE_FRAME_TOO_LARGE";
|
|
10
|
+
readonly maxFrameBytes: number;
|
|
11
|
+
constructor(maxFrameBytes: number);
|
|
12
|
+
}
|
|
13
|
+
export interface SseFramerOptions {
|
|
14
|
+
/** Upper bound for one frame, complete or partial, in bytes. */
|
|
15
|
+
maxFrameBytes?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface SseFramer {
|
|
18
|
+
/**
|
|
19
|
+
* Feed the next chunk of the stream. Returns every frame completed by this
|
|
20
|
+
* chunk, in order. Each frame includes its own line terminators and the
|
|
21
|
+
* blank line that ends it. Returned arrays may be views of `chunk`, so
|
|
22
|
+
* callers must not mutate them. Throws `SseFrameTooLargeError` when any
|
|
23
|
+
* single frame, complete or still partial, exceeds the bound; frames
|
|
24
|
+
* completed earlier in the same chunk are not returned in that case, and
|
|
25
|
+
* the framer refuses all later input.
|
|
26
|
+
*/
|
|
27
|
+
push(chunk: Uint8Array): Uint8Array[];
|
|
28
|
+
/**
|
|
29
|
+
* Signal end of input and return the unterminated tail, if any. A stream
|
|
30
|
+
* that ends mid-frame yields that partial frame here instead of losing it.
|
|
31
|
+
*/
|
|
32
|
+
finish(): Uint8Array | undefined;
|
|
33
|
+
/** Bytes of the current partial frame held while waiting for its end. */
|
|
34
|
+
readonly bufferedBytes: number;
|
|
35
|
+
}
|
|
36
|
+
export declare function createSseFramer(options?: SseFramerOptions): SseFramer;
|
|
37
|
+
/** One parsed frame. Field values follow the WHATWG event-stream rules. */
|
|
38
|
+
export interface SseEvent {
|
|
39
|
+
/** Last `event:` value, or undefined when the frame sets none. */
|
|
40
|
+
event?: string;
|
|
41
|
+
/**
|
|
42
|
+
* All `data:` lines joined with LF, or undefined when the frame has no data
|
|
43
|
+
* line (a comment or heartbeat that dispatches no event).
|
|
44
|
+
*/
|
|
45
|
+
data?: string;
|
|
46
|
+
/** Last `id:` value, ignoring values that contain NUL. */
|
|
47
|
+
id?: string;
|
|
48
|
+
/** Last `retry:` value made only of ASCII digits. */
|
|
49
|
+
retry?: number;
|
|
50
|
+
}
|
|
51
|
+
export interface SseDecodeOptions {
|
|
52
|
+
/**
|
|
53
|
+
* The frame is the first one of its stream, so one leading byte-order mark
|
|
54
|
+
* is dropped as the event-stream rules require. Elsewhere U+FEFF is an
|
|
55
|
+
* ordinary character (for example part of a field name) and is kept.
|
|
56
|
+
*/
|
|
57
|
+
streamStart?: boolean;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Decode a frame as UTF-8, replacing invalid sequences like EventSource. A
|
|
61
|
+
* leading byte-order mark is dropped only for the first frame of a stream.
|
|
62
|
+
* The one-argument form stays usable as an `Array.prototype.map` callback.
|
|
63
|
+
*/
|
|
64
|
+
export declare function decodeSseFrame(frame: Uint8Array): string;
|
|
65
|
+
export declare function decodeSseFrame(frame: Uint8Array, options: SseDecodeOptions): string;
|
|
66
|
+
/**
|
|
67
|
+
* Parse one frame produced by `createSseFramer`. Accepts CRLF, LF and CR line
|
|
68
|
+
* endings in any mix, comment lines, fields without a colon and multi-line
|
|
69
|
+
* data. Unknown fields are ignored.
|
|
70
|
+
*/
|
|
71
|
+
export declare function parseSseFrame(frame: Uint8Array | string): SseEvent;
|
|
72
|
+
/** True when the frame ends with a blank line rather than being an EOF tail. */
|
|
73
|
+
export declare function isTerminatedSseFrame(frame: Uint8Array): boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Cheap raw-byte gate for frames that might be error envelopes: the literal
|
|
76
|
+
* `error`, or any JSON `\u` escape, which could spell an `error` key or type
|
|
77
|
+
* without the literal appearing. Frames that fail this gate cannot be error
|
|
78
|
+
* envelopes and are never decoded. Frames that pass still need parsing; a
|
|
79
|
+
* successful text delta that mentions "error" passes the gate too.
|
|
80
|
+
*/
|
|
81
|
+
export declare function isSseCandidateFrame(frame: Uint8Array): boolean;
|
package/dist/sticky-routing.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { type AccountOperationError, type AccountStorage, type OAuthQuotaSnapshot } from './accounts.ts';
|
|
2
|
+
import type { NativeVaultRosterDocument } from './native-roster-seed.ts';
|
|
3
|
+
import { type NativeVaultRoutingSeed } from './native-vault-seed.ts';
|
|
2
4
|
export declare const STICKY_ROUTING_MAIN_ACCOUNT_ID = "main";
|
|
3
5
|
export declare const STICKY_ROUTING_SHORT_RESET_GRACE_MS: number;
|
|
4
6
|
export declare const STICKY_ROUTING_ASSIGNMENT_TTL_MS: number;
|
|
@@ -22,7 +24,10 @@ export type StickyRouteCandidate = {
|
|
|
22
24
|
};
|
|
23
25
|
export type StickyRouteAssignment = {
|
|
24
26
|
accountId: string;
|
|
27
|
+
/** Verified account UUID for a credential held by the vault. Pins using credentials stored by this host omit the field. */
|
|
28
|
+
accountIdentity?: string;
|
|
25
29
|
family: StickyRouteFamily;
|
|
30
|
+
affinityModelId?: string;
|
|
26
31
|
assignedAt: number;
|
|
27
32
|
lastSeenAt: number;
|
|
28
33
|
initialInputBytes: number;
|
|
@@ -57,6 +62,21 @@ export declare function createStickyNoRouteResponse(input: {
|
|
|
57
62
|
modelId?: string;
|
|
58
63
|
now?: number;
|
|
59
64
|
}): Response;
|
|
65
|
+
/**
|
|
66
|
+
* Produce deterministic journal bytes from captured, parsed legacy routing JSON.
|
|
67
|
+
* The offline caller must prove each old-to-concrete row binding, including main;
|
|
68
|
+
* a map entry is not credential or identity proof. Unmapped assignments and their
|
|
69
|
+
* implicit prompt-byte deficits are dropped. Quota observations must separately
|
|
70
|
+
* retain their proven checkedAt values for those deficits to remain attributable.
|
|
71
|
+
*
|
|
72
|
+
* The caller owns source/path revalidation, the digest, journal authority and the
|
|
73
|
+
* locked temp-plus-rename write with an ownership check before rename. An absent
|
|
74
|
+
* source is handled by the caller, not by substituting an empty state here. This
|
|
75
|
+
* helper neither authorizes migration nor reads clocks, prunes or writes state.
|
|
76
|
+
* Invalid source data or unusable map values throw Error('invalid-source') without
|
|
77
|
+
* including any captured data. Input must be parsed JSON, not executable objects.
|
|
78
|
+
*/
|
|
79
|
+
export declare function migrateStickyRoutingState(source: unknown, rowIdMap: ReadonlyMap<string, string>): string;
|
|
60
80
|
export declare function stickyQuotaSnapshotIsFresh(quota: OAuthQuotaSnapshot | undefined, storage: AccountStorage | null, now?: number, modelId?: string): boolean;
|
|
61
81
|
export declare function stickyRouteFamilyForModel(model: unknown): StickyRouteFamily;
|
|
62
82
|
export declare function stickyRouteCandidateWeight(input: {
|
|
@@ -80,10 +100,12 @@ export declare class StickySessionRouter {
|
|
|
80
100
|
private state;
|
|
81
101
|
private stateMtimeNs;
|
|
82
102
|
private touchChain;
|
|
103
|
+
private seedFinalized;
|
|
83
104
|
constructor(options: {
|
|
84
105
|
path: string;
|
|
85
106
|
now?: () => number;
|
|
86
107
|
assignmentTtlMs?: number;
|
|
108
|
+
vaultSeed?: NativeVaultRoutingSeed;
|
|
87
109
|
});
|
|
88
110
|
private now;
|
|
89
111
|
private readState;
|
|
@@ -93,14 +115,21 @@ export declare class StickySessionRouter {
|
|
|
93
115
|
private assignmentIsActive;
|
|
94
116
|
private prune;
|
|
95
117
|
private writeState;
|
|
118
|
+
/** Import legacy session pins only after vault account discovery produces a complete verified roster. No empty routing file is published while that roster is incomplete. */
|
|
119
|
+
prepareLegacySeed(knownRoster?: NativeVaultRosterDocument | null): Promise<void>;
|
|
120
|
+
private logicalAccountId;
|
|
121
|
+
setVaultSeed(seed: NativeVaultRoutingSeed | undefined): void;
|
|
96
122
|
private scheduleTouch;
|
|
97
123
|
private selectCandidate;
|
|
98
124
|
resolve(input: {
|
|
99
125
|
sessionId: string;
|
|
100
126
|
family: StickyRouteFamily;
|
|
101
127
|
modelId?: string;
|
|
128
|
+
affinityModelId?: string;
|
|
102
129
|
candidates: readonly StickyRouteCandidate[];
|
|
103
130
|
retainAccountIds: ReadonlySet<string>;
|
|
131
|
+
/** Verified UUIDs for every vault account the host retains, even when quota prevents selection. Omit when credentials are stored by the host instead of the vault. */
|
|
132
|
+
accountIdentities?: ReadonlyMap<string, string>;
|
|
104
133
|
storage: AccountStorage | null;
|
|
105
134
|
inputBytes: number;
|
|
106
135
|
preferredAccountId?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cortexkit/anthropic-auth-core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -21,14 +21,20 @@
|
|
|
21
21
|
"LICENSE"
|
|
22
22
|
],
|
|
23
23
|
"scripts": {
|
|
24
|
-
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
24
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json --emitDeclarationOnly && bun scripts/build.ts",
|
|
25
25
|
"test": "bun run build && bun test src/tests",
|
|
26
|
+
"test:scripts": "bun test scripts",
|
|
27
|
+
"check:native-type-closure": "bun scripts/check-native-type-closure.ts",
|
|
26
28
|
"typecheck": "tsc",
|
|
27
29
|
"types": "tsc",
|
|
28
30
|
"prepublishOnly": "bun run build"
|
|
29
31
|
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@cortexkit/common-auth": "0.14.2"
|
|
34
|
+
},
|
|
30
35
|
"dependencies": {
|
|
31
|
-
"@cortexkit/
|
|
36
|
+
"@cortexkit/claustrum-client": "0.6.2",
|
|
37
|
+
"jsonc-parser": "^3.3.1",
|
|
32
38
|
"xxhash-wasm": "^1.1.0"
|
|
33
39
|
}
|
|
34
40
|
}
|