@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.
Files changed (133) hide show
  1. package/README.md +2 -0
  2. package/dist/accounts.d.ts +52 -8
  3. package/dist/cachekeep.d.ts +27 -1
  4. package/dist/cch.d.ts +11 -1
  5. package/dist/claude-code.d.ts +23 -1
  6. package/dist/claustrum-enrollment.d.ts +72 -0
  7. package/dist/claustrum-scoped-roster.d.ts +27 -0
  8. package/dist/claustrum-scoped-runtime.d.ts +31 -0
  9. package/dist/claustrum-scoped.d.ts +53 -0
  10. package/dist/claustrum.d.ts +16 -82
  11. package/dist/commands/account.d.ts +38 -11
  12. package/dist/constants.d.ts +3 -2
  13. package/dist/custom-headers.d.ts +3 -0
  14. package/dist/index.d.ts +50 -0
  15. package/dist/index.js +29700 -31
  16. package/dist/internal-types/LICENSE +21 -0
  17. package/dist/internal-types/commands/builtins.d.ts +71 -0
  18. package/dist/internal-types/commands/index.d.ts +10 -0
  19. package/dist/internal-types/commands/menu.d.ts +61 -0
  20. package/dist/internal-types/commands/model.d.ts +238 -0
  21. package/dist/internal-types/commands/pi.d.ts +23 -0
  22. package/dist/internal-types/commands/seam.d.ts +72 -0
  23. package/dist/internal-types/fs/refresh-file-lock.d.ts +33 -0
  24. package/dist/internal-types/logger/capture-sink.d.ts +14 -0
  25. package/dist/internal-types/logger/engine.d.ts +100 -0
  26. package/dist/internal-types/logger/index.d.ts +6 -0
  27. package/dist/internal-types/logger/redact.d.ts +11 -0
  28. package/dist/internal-types/pi-slot/index.d.ts +3 -0
  29. package/dist/internal-types/pi-slot/slot.d.ts +49 -0
  30. package/dist/internal-types/quota/codec.d.ts +11 -0
  31. package/dist/internal-types/quota/format.d.ts +46 -0
  32. package/dist/internal-types/quota/index.d.ts +10 -0
  33. package/dist/internal-types/quota/map.d.ts +63 -0
  34. package/dist/internal-types/quota/merge.d.ts +48 -0
  35. package/dist/internal-types/quota/projection.d.ts +64 -0
  36. package/dist/internal-types/store/attribution.d.ts +28 -0
  37. package/dist/internal-types/store/errors.d.ts +55 -0
  38. package/dist/internal-types/store/hooks.d.ts +12 -0
  39. package/dist/internal-types/store/identity.d.ts +39 -0
  40. package/dist/internal-types/store/index.d.ts +21 -0
  41. package/dist/internal-types/store/mutate.d.ts +189 -0
  42. package/dist/internal-types/store/pool.d.ts +196 -0
  43. package/dist/internal-types/store/provider-state.d.ts +131 -0
  44. package/dist/internal-types/store/publication.d.ts +47 -0
  45. package/dist/internal-types/store/pull.d.ts +36 -0
  46. package/dist/internal-types/store/refresh-lock.d.ts +63 -0
  47. package/dist/internal-types/store/refresh.d.ts +72 -0
  48. package/dist/internal-types/store/rows.d.ts +313 -0
  49. package/dist/internal-types/store/runtime.d.ts +36 -0
  50. package/dist/internal-types/store/schema.d.ts +443 -0
  51. package/dist/internal-types/store/settings.d.ts +63 -0
  52. package/dist/internal-types/store/torn.d.ts +78 -0
  53. package/dist/migration-source.d.ts +33 -0
  54. package/dist/model-remap.d.ts +27 -0
  55. package/dist/models.d.ts +60 -0
  56. package/dist/native-account-runtime.d.ts +121 -0
  57. package/dist/native-account-view.d.ts +126 -0
  58. package/dist/native-command-effects.d.ts +59 -0
  59. package/dist/native-credential-validation.d.ts +42 -0
  60. package/dist/native-custody-activation.d.ts +45 -0
  61. package/dist/native-custody.d.ts +114 -0
  62. package/dist/native-enrollment-authority.d.ts +2 -0
  63. package/dist/native-host-auth-read.d.ts +3 -0
  64. package/dist/native-host-auth-write.d.ts +51 -0
  65. package/dist/native-host-auth.d.ts +12 -0
  66. package/dist/native-local-credential-service.d.ts +76 -0
  67. package/dist/native-local-runtime-readers.d.ts +22 -0
  68. package/dist/native-lock-observer.d.ts +12 -0
  69. package/dist/native-menu-executor.d.ts +243 -0
  70. package/dist/native-menu-model.d.ts +142 -0
  71. package/dist/native-migration-proof.d.ts +28 -0
  72. package/dist/native-migration-source.d.ts +47 -0
  73. package/dist/native-migration.d.ts +66 -0
  74. package/dist/native-quota-codec.d.ts +32 -0
  75. package/dist/native-refresh-coordinator.d.ts +240 -0
  76. package/dist/native-roster-seed.d.ts +115 -0
  77. package/dist/native-runtime-registry.d.ts +19 -0
  78. package/dist/native-runtime.d.ts +204 -0
  79. package/dist/native-transient-account.d.ts +38 -0
  80. package/dist/native-ui.d.ts +45 -0
  81. package/dist/native-usage-publication.d.ts +11 -0
  82. package/dist/native-vault-admission.d.ts +8 -0
  83. package/dist/native-vault-connection.d.ts +33 -0
  84. package/dist/native-vault-paths.d.ts +13 -0
  85. package/dist/native-vault-runtime.d.ts +102 -0
  86. package/dist/native-vault-seed.d.ts +16 -0
  87. package/dist/oauth-profile.d.ts +2 -1
  88. package/dist/pool-authority.d.ts +159 -0
  89. package/dist/pool-binding.d.ts +32 -0
  90. package/dist/pool-paths.d.ts +21 -0
  91. package/dist/pool-store.d.ts +32 -0
  92. package/dist/prime.d.ts +11 -3
  93. package/dist/provider-error.d.ts +139 -0
  94. package/dist/quota-header-feed.d.ts +13 -0
  95. package/dist/quota-manager.d.ts +30 -5
  96. package/dist/relay.d.ts +22 -1
  97. package/dist/request-history.d.ts +34 -0
  98. package/dist/sse.d.ts +81 -0
  99. package/dist/sticky-routing.d.ts +29 -0
  100. package/package.json +9 -3
  101. package/dist/accounts.js +0 -3330
  102. package/dist/auth.js +0 -182
  103. package/dist/cache1h.js +0 -97
  104. package/dist/cachekeep-registry.js +0 -124
  105. package/dist/cachekeep.js +0 -578
  106. package/dist/cch.js +0 -127
  107. package/dist/claude-code.js +0 -388
  108. package/dist/claustrum.js +0 -529
  109. package/dist/commands/account.js +0 -239
  110. package/dist/constants.js +0 -82
  111. package/dist/dump.js +0 -616
  112. package/dist/fast.js +0 -66
  113. package/dist/json.js +0 -10
  114. package/dist/killswitch.js +0 -132
  115. package/dist/logger.js +0 -286
  116. package/dist/logging.js +0 -59
  117. package/dist/mid-conversation-output-config.js +0 -89
  118. package/dist/models.js +0 -141
  119. package/dist/network-errors.js +0 -27
  120. package/dist/oauth-profile.js +0 -54
  121. package/dist/pkce.js +0 -17
  122. package/dist/prime.js +0 -823
  123. package/dist/provider.js +0 -0
  124. package/dist/quota-header-feed.js +0 -279
  125. package/dist/quota-headers.js +0 -121
  126. package/dist/quota-manager.js +0 -805
  127. package/dist/quotas.js +0 -141
  128. package/dist/relay.js +0 -1243
  129. package/dist/routing.js +0 -94
  130. package/dist/start.js +0 -29
  131. package/dist/sticky-routing.js +0 -523
  132. package/dist/thinking-binding.js +0 -54
  133. 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
  }
@@ -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 API calls through a shared gate so only one
127
- * quota API request runs at a time, with a minimum gap
128
- * between calls. Prevents concurrent and rapid-fire calls
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;
@@ -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": "1.22.0",
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/subc-client": "^0.8.1",
36
+ "@cortexkit/claustrum-client": "0.6.2",
37
+ "jsonc-parser": "^3.3.1",
32
38
  "xxhash-wasm": "^1.1.0"
33
39
  }
34
40
  }