@cortexkit/anthropic-auth-core 1.23.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 (134) hide show
  1. package/README.md +2 -0
  2. package/dist/accounts.d.ts +3 -16
  3. package/dist/cachekeep.d.ts +9 -0
  4. package/dist/claude-code.d.ts +16 -0
  5. package/dist/claustrum-enrollment.d.ts +2 -0
  6. package/dist/claustrum-scoped-roster.d.ts +4 -0
  7. package/dist/claustrum-scoped-runtime.d.ts +3 -0
  8. package/dist/claustrum-scoped.d.ts +14 -0
  9. package/dist/claustrum.d.ts +0 -209
  10. package/dist/commands/account.d.ts +2 -3
  11. package/dist/index.d.ts +44 -1
  12. package/dist/index.js +29700 -38
  13. package/dist/internal-types/LICENSE +21 -0
  14. package/dist/internal-types/commands/builtins.d.ts +71 -0
  15. package/dist/internal-types/commands/index.d.ts +10 -0
  16. package/dist/internal-types/commands/menu.d.ts +61 -0
  17. package/dist/internal-types/commands/model.d.ts +238 -0
  18. package/dist/internal-types/commands/pi.d.ts +23 -0
  19. package/dist/internal-types/commands/seam.d.ts +72 -0
  20. package/dist/internal-types/fs/refresh-file-lock.d.ts +33 -0
  21. package/dist/internal-types/logger/capture-sink.d.ts +14 -0
  22. package/dist/internal-types/logger/engine.d.ts +100 -0
  23. package/dist/internal-types/logger/index.d.ts +6 -0
  24. package/dist/internal-types/logger/redact.d.ts +11 -0
  25. package/dist/internal-types/pi-slot/index.d.ts +3 -0
  26. package/dist/internal-types/pi-slot/slot.d.ts +49 -0
  27. package/dist/internal-types/quota/codec.d.ts +11 -0
  28. package/dist/internal-types/quota/format.d.ts +46 -0
  29. package/dist/internal-types/quota/index.d.ts +10 -0
  30. package/dist/internal-types/quota/map.d.ts +63 -0
  31. package/dist/internal-types/quota/merge.d.ts +48 -0
  32. package/dist/internal-types/quota/projection.d.ts +64 -0
  33. package/dist/internal-types/store/attribution.d.ts +28 -0
  34. package/dist/internal-types/store/errors.d.ts +55 -0
  35. package/dist/internal-types/store/hooks.d.ts +12 -0
  36. package/dist/internal-types/store/identity.d.ts +39 -0
  37. package/dist/internal-types/store/index.d.ts +21 -0
  38. package/dist/internal-types/store/mutate.d.ts +189 -0
  39. package/dist/internal-types/store/pool.d.ts +196 -0
  40. package/dist/internal-types/store/provider-state.d.ts +131 -0
  41. package/dist/internal-types/store/publication.d.ts +47 -0
  42. package/dist/internal-types/store/pull.d.ts +36 -0
  43. package/dist/internal-types/store/refresh-lock.d.ts +63 -0
  44. package/dist/internal-types/store/refresh.d.ts +72 -0
  45. package/dist/internal-types/store/rows.d.ts +313 -0
  46. package/dist/internal-types/store/runtime.d.ts +36 -0
  47. package/dist/internal-types/store/schema.d.ts +443 -0
  48. package/dist/internal-types/store/settings.d.ts +63 -0
  49. package/dist/internal-types/store/torn.d.ts +78 -0
  50. package/dist/migration-source.d.ts +33 -0
  51. package/dist/models.d.ts +41 -0
  52. package/dist/native-account-runtime.d.ts +121 -0
  53. package/dist/native-account-view.d.ts +126 -0
  54. package/dist/native-command-effects.d.ts +59 -0
  55. package/dist/native-credential-validation.d.ts +42 -0
  56. package/dist/native-custody-activation.d.ts +45 -0
  57. package/dist/native-custody.d.ts +114 -0
  58. package/dist/native-enrollment-authority.d.ts +2 -0
  59. package/dist/native-host-auth-read.d.ts +3 -0
  60. package/dist/native-host-auth-write.d.ts +51 -0
  61. package/dist/native-host-auth.d.ts +12 -0
  62. package/dist/native-local-credential-service.d.ts +76 -0
  63. package/dist/native-local-runtime-readers.d.ts +22 -0
  64. package/dist/native-lock-observer.d.ts +12 -0
  65. package/dist/native-menu-executor.d.ts +243 -0
  66. package/dist/native-menu-model.d.ts +142 -0
  67. package/dist/native-migration-proof.d.ts +28 -0
  68. package/dist/native-migration-source.d.ts +47 -0
  69. package/dist/native-migration.d.ts +66 -0
  70. package/dist/native-quota-codec.d.ts +32 -0
  71. package/dist/native-refresh-coordinator.d.ts +240 -0
  72. package/dist/native-roster-seed.d.ts +115 -0
  73. package/dist/native-runtime-registry.d.ts +19 -0
  74. package/dist/native-runtime.d.ts +204 -0
  75. package/dist/native-transient-account.d.ts +38 -0
  76. package/dist/native-ui.d.ts +45 -0
  77. package/dist/native-usage-publication.d.ts +11 -0
  78. package/dist/native-vault-admission.d.ts +8 -0
  79. package/dist/native-vault-connection.d.ts +33 -0
  80. package/dist/native-vault-paths.d.ts +13 -0
  81. package/dist/native-vault-runtime.d.ts +102 -0
  82. package/dist/native-vault-seed.d.ts +16 -0
  83. package/dist/pool-authority.d.ts +159 -0
  84. package/dist/pool-binding.d.ts +32 -0
  85. package/dist/pool-paths.d.ts +21 -0
  86. package/dist/pool-store.d.ts +32 -0
  87. package/dist/provider-error.d.ts +139 -0
  88. package/dist/quota-manager.d.ts +18 -7
  89. package/dist/relay.d.ts +14 -1
  90. package/dist/request-history.d.ts +34 -0
  91. package/dist/sse.d.ts +81 -0
  92. package/dist/sticky-routing.d.ts +27 -0
  93. package/package.json +9 -4
  94. package/dist/accounts.js +0 -3881
  95. package/dist/auth.js +0 -185
  96. package/dist/cache1h.js +0 -97
  97. package/dist/cachekeep-registry.js +0 -124
  98. package/dist/cachekeep.js +0 -604
  99. package/dist/cch.js +0 -172
  100. package/dist/claude-code.js +0 -403
  101. package/dist/claustrum-enrollment-registry.d.ts +0 -17
  102. package/dist/claustrum-enrollment-registry.js +0 -194
  103. package/dist/claustrum-enrollment.js +0 -485
  104. package/dist/claustrum-scoped-roster.js +0 -194
  105. package/dist/claustrum-scoped-runtime.js +0 -230
  106. package/dist/claustrum-scoped.js +0 -193
  107. package/dist/claustrum.js +0 -1471
  108. package/dist/commands/account.js +0 -368
  109. package/dist/constants.js +0 -83
  110. package/dist/custom-headers.js +0 -102
  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 -288
  116. package/dist/logging.js +0 -59
  117. package/dist/mid-conversation-output-config.js +0 -89
  118. package/dist/model-remap.js +0 -72
  119. package/dist/models.js +0 -177
  120. package/dist/network-errors.js +0 -27
  121. package/dist/oauth-profile.js +0 -60
  122. package/dist/pkce.js +0 -17
  123. package/dist/prime.js +0 -841
  124. package/dist/provider.js +0 -0
  125. package/dist/quota-header-feed.js +0 -331
  126. package/dist/quota-headers.js +0 -121
  127. package/dist/quota-manager.js +0 -856
  128. package/dist/quotas.js +0 -141
  129. package/dist/relay.js +0 -1303
  130. package/dist/routing.js +0 -94
  131. package/dist/start.js +0 -29
  132. package/dist/sticky-routing.js +0 -541
  133. package/dist/thinking-binding.js +0 -54
  134. package/dist/token-fingerprint.js +0 -8
@@ -17,15 +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;
23
- /** Resolve custody credentials at the actual HTTP boundary, after quota gates. */
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
+ */
24
33
  fetchQuotaSnapshot?: (request: {
25
34
  kind: 'main' | 'fallback';
26
35
  accountId: string | undefined;
27
36
  accessToken: string;
28
- }) => Promise<OAuthQuotaSnapshot>;
37
+ }, dispatch: QuotaRequestDispatch) => Promise<OAuthQuotaSnapshot>;
29
38
  now?: () => number;
30
39
  onMainQuotaFetched?: (quota: OAuthQuotaSnapshot, checkedAt: number, tokenFingerprint: string, fetchStartedAt: number, quotaErrorGeneration: number, quotaErrorClearedAt: number | undefined) => void;
31
40
  onApiError?: (error: AccountOperationError, quotaErrorGeneration: number) => void;
@@ -44,6 +53,7 @@ export declare class QuotaManager {
44
53
  private fallbackAuthLineages;
45
54
  private fallbackGenerations;
46
55
  private inflightMain;
56
+ private lastMainPollAttempt;
47
57
  private inflightMainAccountId;
48
58
  private inflightMainAccessToken;
49
59
  private inflightMainToken;
@@ -70,7 +80,7 @@ export declare class QuotaManager {
70
80
  * Cached fallback quota entry, scoped to the configured account id.
71
81
  */
72
82
  getFallback(accountId: string, account?: Pick<OAuthAccount, 'authLineageId'>): QuotaEntry | null;
73
- /** A host transport resolves scoped credentials at dispatch time. */
83
+ /** A configured transport obtains its own credentials; policy snapshots need no tokens. */
74
84
  canFetchWithoutAccessToken(): boolean;
75
85
  getAllFallbacks(): Map<string, QuotaEntry>;
76
86
  setMain(mainAccountId: string | undefined, entry: QuotaEntry): void;
@@ -137,12 +147,13 @@ export declare class QuotaManager {
137
147
  private isClosed;
138
148
  close(): void;
139
149
  /**
140
- * Serialize API calls through a shared gate so only one
141
- * quota API request runs at a time, with a minimum gap
142
- * between calls. Prevents concurrent and rapid-fire calls
143
- * 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.
144
153
  */
145
154
  private _enqueueApiFetch;
155
+ private _quotaDispatch;
156
+ private _quotaFetch;
146
157
  private _fetchMain;
147
158
  private _fetchFallback;
148
159
  private static isAuthError;
package/dist/relay.d.ts CHANGED
@@ -15,6 +15,13 @@ export type RelayAttemptAuthorization = {
15
15
  headers: Headers;
16
16
  /** Called only for a genuine upstream response, never optimistic/transport status. */
17
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[];
18
25
  };
19
26
  export type AuthorizeRelayAttempt = () => Promise<RelayAttemptAuthorization>;
20
27
  export declare function getRelayConfig(storage: AccountStorage | null): RelayConfig | null;
@@ -29,6 +36,12 @@ export declare function sendViaRelay(options: {
29
36
  body: RequestInit['body'] | null | undefined;
30
37
  fallback: () => Promise<Response>;
31
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
+ */
32
45
  optimisticResponse?: boolean;
33
46
  /** Fresh authorization for every physical relay attempt, including internal retries. */
34
47
  authorizeAttempt?: AuthorizeRelayAttempt;
@@ -44,4 +57,4 @@ export declare function sendViaRelay(options: {
44
57
  setTimeoutImpl?: typeof globalThis.setTimeout;
45
58
  clearTimeoutImpl?: typeof globalThis.clearTimeout;
46
59
  }): Promise<Response>;
47
- 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,6 +24,8 @@ 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;
26
30
  affinityModelId?: string;
27
31
  assignedAt: number;
@@ -58,6 +62,21 @@ export declare function createStickyNoRouteResponse(input: {
58
62
  modelId?: string;
59
63
  now?: number;
60
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;
61
80
  export declare function stickyQuotaSnapshotIsFresh(quota: OAuthQuotaSnapshot | undefined, storage: AccountStorage | null, now?: number, modelId?: string): boolean;
62
81
  export declare function stickyRouteFamilyForModel(model: unknown): StickyRouteFamily;
63
82
  export declare function stickyRouteCandidateWeight(input: {
@@ -81,10 +100,12 @@ export declare class StickySessionRouter {
81
100
  private state;
82
101
  private stateMtimeNs;
83
102
  private touchChain;
103
+ private seedFinalized;
84
104
  constructor(options: {
85
105
  path: string;
86
106
  now?: () => number;
87
107
  assignmentTtlMs?: number;
108
+ vaultSeed?: NativeVaultRoutingSeed;
88
109
  });
89
110
  private now;
90
111
  private readState;
@@ -94,6 +115,10 @@ export declare class StickySessionRouter {
94
115
  private assignmentIsActive;
95
116
  private prune;
96
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;
97
122
  private scheduleTouch;
98
123
  private selectCandidate;
99
124
  resolve(input: {
@@ -103,6 +128,8 @@ export declare class StickySessionRouter {
103
128
  affinityModelId?: string;
104
129
  candidates: readonly StickyRouteCandidate[];
105
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>;
106
133
  storage: AccountStorage | null;
107
134
  inputBytes: number;
108
135
  preferredAccountId?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/anthropic-auth-core",
3
- "version": "1.23.0",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -21,15 +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/claustrum-client": "0.3.0",
32
- "@cortexkit/subc-client": "^0.11.1",
36
+ "@cortexkit/claustrum-client": "0.6.2",
37
+ "jsonc-parser": "^3.3.1",
33
38
  "xxhash-wasm": "^1.1.0"
34
39
  }
35
40
  }