@athenaintel/react 0.24.1 → 0.25.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 (145) hide show
  1. package/README.md +18 -0
  2. package/dist/assets/AssetPanel.cjs +3 -2
  3. package/dist/assets/AssetPanel.cjs.map +1 -1
  4. package/dist/assets/AssetPanel.js +3 -2
  5. package/dist/assets/AssetPanel.js.map +1 -1
  6. package/dist/assets/AthenaAssetEmbed.cjs +1 -0
  7. package/dist/assets/AthenaAssetEmbed.cjs.map +1 -1
  8. package/dist/assets/AthenaAssetEmbed.js +1 -0
  9. package/dist/assets/AthenaAssetEmbed.js.map +1 -1
  10. package/dist/assets/use-asset-embed.cjs +22 -23
  11. package/dist/assets/use-asset-embed.cjs.map +1 -1
  12. package/dist/assets/use-asset-embed.d.ts +9 -0
  13. package/dist/assets/use-asset-embed.js +22 -23
  14. package/dist/assets/use-asset-embed.js.map +1 -1
  15. package/dist/auth/AthenaAuthProvider.cjs +32 -4
  16. package/dist/auth/AthenaAuthProvider.cjs.map +1 -1
  17. package/dist/auth/AthenaAuthProvider.d.ts +21 -0
  18. package/dist/auth/AthenaAuthProvider.js +33 -5
  19. package/dist/auth/AthenaAuthProvider.js.map +1 -1
  20. package/dist/auth/AthenaCredentialSourceContext.cjs +21 -0
  21. package/dist/auth/AthenaCredentialSourceContext.cjs.map +1 -0
  22. package/dist/auth/AthenaCredentialSourceContext.d.ts +14 -0
  23. package/dist/auth/AthenaCredentialSourceContext.js +21 -0
  24. package/dist/auth/AthenaCredentialSourceContext.js.map +1 -0
  25. package/dist/auth/AthenaSSOProvider.cjs +71 -47
  26. package/dist/auth/AthenaSSOProvider.cjs.map +1 -1
  27. package/dist/auth/AthenaSSOProvider.js +71 -47
  28. package/dist/auth/AthenaSSOProvider.js.map +1 -1
  29. package/dist/auth/credential-source.cjs +120 -0
  30. package/dist/auth/credential-source.cjs.map +1 -0
  31. package/dist/auth/credential-source.d.ts +68 -0
  32. package/dist/auth/credential-source.js +120 -0
  33. package/dist/auth/credential-source.js.map +1 -0
  34. package/dist/auth/types.d.ts +9 -0
  35. package/dist/chat/AthenaChat.cjs +8 -0
  36. package/dist/chat/AthenaChat.cjs.map +1 -1
  37. package/dist/chat/AthenaChat.js +8 -0
  38. package/dist/chat/AthenaChat.js.map +1 -1
  39. package/dist/chat-ui/mentions/suggestions/data/api-client.cjs +20 -11
  40. package/dist/chat-ui/mentions/suggestions/data/api-client.cjs.map +1 -1
  41. package/dist/chat-ui/mentions/suggestions/data/api-client.d.ts +11 -2
  42. package/dist/chat-ui/mentions/suggestions/data/api-client.js +20 -11
  43. package/dist/chat-ui/mentions/suggestions/data/api-client.js.map +1 -1
  44. package/dist/collab/mint.cjs +6 -15
  45. package/dist/collab/mint.cjs.map +1 -1
  46. package/dist/collab/mint.d.ts +8 -0
  47. package/dist/collab/mint.js +6 -15
  48. package/dist/collab/mint.js.map +1 -1
  49. package/dist/collab/presence-mint.cjs +20 -16
  50. package/dist/collab/presence-mint.cjs.map +1 -1
  51. package/dist/collab/presence-mint.js +21 -17
  52. package/dist/collab/presence-mint.js.map +1 -1
  53. package/dist/collab/presence-react.cjs +12 -5
  54. package/dist/collab/presence-react.cjs.map +1 -1
  55. package/dist/collab/presence-react.d.ts +3 -1
  56. package/dist/collab/presence-react.js +12 -5
  57. package/dist/collab/presence-react.js.map +1 -1
  58. package/dist/collab/react.cjs +5 -2
  59. package/dist/collab/react.cjs.map +1 -1
  60. package/dist/collab/react.js +5 -2
  61. package/dist/collab/react.js.map +1 -1
  62. package/dist/dictation/use-athena-dictation-engine.cjs +12 -11
  63. package/dist/dictation/use-athena-dictation-engine.cjs.map +1 -1
  64. package/dist/dictation/use-athena-dictation-engine.d.ts +4 -3
  65. package/dist/dictation/use-athena-dictation-engine.js +12 -11
  66. package/dist/dictation/use-athena-dictation-engine.js.map +1 -1
  67. package/dist/index.d.ts +1 -1
  68. package/dist/packages/react/package.json.cjs +1 -1
  69. package/dist/packages/react/package.json.js +1 -1
  70. package/dist/provider/AthenaContext.cjs.map +1 -1
  71. package/dist/provider/AthenaContext.d.ts +7 -1
  72. package/dist/provider/AthenaContext.js.map +1 -1
  73. package/dist/provider/AthenaProvider.cjs +23 -2
  74. package/dist/provider/AthenaProvider.cjs.map +1 -1
  75. package/dist/provider/AthenaProvider.js +23 -2
  76. package/dist/provider/AthenaProvider.js.map +1 -1
  77. package/dist/runtime/auth-refusal-restart.cjs +66 -0
  78. package/dist/runtime/auth-refusal-restart.cjs.map +1 -0
  79. package/dist/runtime/auth-refusal-restart.d.ts +35 -0
  80. package/dist/runtime/auth-refusal-restart.js +66 -0
  81. package/dist/runtime/auth-refusal-restart.js.map +1 -0
  82. package/dist/runtime/auth.cjs +58 -21
  83. package/dist/runtime/auth.cjs.map +1 -1
  84. package/dist/runtime/auth.d.ts +11 -1
  85. package/dist/runtime/auth.js +59 -22
  86. package/dist/runtime/auth.js.map +1 -1
  87. package/dist/runtime/credential-renewal.cjs +29 -0
  88. package/dist/runtime/credential-renewal.cjs.map +1 -0
  89. package/dist/runtime/credential-renewal.d.ts +35 -0
  90. package/dist/runtime/credential-renewal.js +29 -0
  91. package/dist/runtime/credential-renewal.js.map +1 -0
  92. package/dist/search/useAthenaRepoSearch.cjs +3 -5
  93. package/dist/search/useAthenaRepoSearch.cjs.map +1 -1
  94. package/dist/search/useAthenaRepoSearch.d.ts +7 -7
  95. package/dist/search/useAthenaRepoSearch.js +3 -5
  96. package/dist/search/useAthenaRepoSearch.js.map +1 -1
  97. package/dist/threads/api.cjs +5 -1
  98. package/dist/threads/api.cjs.map +1 -1
  99. package/dist/threads/api.js +5 -1
  100. package/dist/threads/api.js.map +1 -1
  101. package/dist/threads/conversation-list-fetcher.cjs +5 -5
  102. package/dist/threads/conversation-list-fetcher.cjs.map +1 -1
  103. package/dist/threads/conversation-list-fetcher.d.ts +6 -5
  104. package/dist/threads/conversation-list-fetcher.js +5 -5
  105. package/dist/threads/conversation-list-fetcher.js.map +1 -1
  106. package/dist/threads/statewire-thread-list.cjs +13 -14
  107. package/dist/threads/statewire-thread-list.cjs.map +1 -1
  108. package/dist/threads/statewire-thread-list.js +13 -14
  109. package/dist/threads/statewire-thread-list.js.map +1 -1
  110. package/dist/threads/use-rest-conversation-history.cjs +6 -2
  111. package/dist/threads/use-rest-conversation-history.cjs.map +1 -1
  112. package/dist/threads/use-rest-conversation-history.d.ts +3 -1
  113. package/dist/threads/use-rest-conversation-history.js +7 -3
  114. package/dist/threads/use-rest-conversation-history.js.map +1 -1
  115. package/dist/threads/use-statewire-conversation-history.cjs +9 -2
  116. package/dist/threads/use-statewire-conversation-history.cjs.map +1 -1
  117. package/dist/threads/use-statewire-conversation-history.d.ts +3 -1
  118. package/dist/threads/use-statewire-conversation-history.js +10 -3
  119. package/dist/threads/use-statewire-conversation-history.js.map +1 -1
  120. package/dist/threads/use-thread-archive.cjs +2 -1
  121. package/dist/threads/use-thread-archive.cjs.map +1 -1
  122. package/dist/threads/use-thread-archive.d.ts +3 -1
  123. package/dist/threads/use-thread-archive.js +2 -1
  124. package/dist/threads/use-thread-archive.js.map +1 -1
  125. package/dist/threads/use-thread-favorite.cjs.map +1 -1
  126. package/dist/threads/use-thread-favorite.d.ts +3 -1
  127. package/dist/threads/use-thread-favorite.js.map +1 -1
  128. package/dist/threads/use-thread-field-override.cjs +2 -1
  129. package/dist/threads/use-thread-field-override.cjs.map +1 -1
  130. package/dist/threads/use-thread-field-override.d.ts +3 -1
  131. package/dist/threads/use-thread-field-override.js +2 -1
  132. package/dist/threads/use-thread-field-override.js.map +1 -1
  133. package/dist/threads/use-thread-rename.cjs.map +1 -1
  134. package/dist/threads/use-thread-rename.d.ts +3 -1
  135. package/dist/threads/use-thread-rename.js.map +1 -1
  136. package/dist/tools/tool-uis.cjs +11 -15
  137. package/dist/tools/tool-uis.cjs.map +1 -1
  138. package/dist/tools/tool-uis.js +11 -15
  139. package/dist/tools/tool-uis.js.map +1 -1
  140. package/dist/voice/use-athena-voice-session.cjs +17 -5
  141. package/dist/voice/use-athena-voice-session.cjs.map +1 -1
  142. package/dist/voice/use-athena-voice-session.d.ts +2 -2
  143. package/dist/voice/use-athena-voice-session.js +17 -5
  144. package/dist/voice/use-athena-voice-session.js.map +1 -1
  145. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"auth.js","sources":["../../src/runtime/auth.ts"],"sourcesContent":["import { useEffect, useMemo, useRef, useState } from 'react';\nimport { athenaDiagnostics } from '../diagnostics/athena-diagnostics';\n\n/**\n * Check whether a PostMessage origin is trusted.\n * Accepts:\n * - explicit trusted origins passed by the host app\n * - the current iframe origin (for same-origin wrappers like Marathon)\n * - any *.athenaintel.com subdomain and localhost (legacy/default behavior)\n */\nfunction isTrustedOrigin({\n origin,\n trustedOrigins,\n}: {\n origin: string;\n trustedOrigins: string[];\n}): boolean {\n try {\n const normalizedOrigin = new URL(origin).origin;\n if (trustedOrigins.includes(normalizedOrigin)) {\n return true;\n }\n\n const { hostname } = new URL(normalizedOrigin);\n return (\n hostname === 'athenaintel.com' ||\n hostname.endsWith('.athenaintel.com') ||\n hostname === 'localhost'\n );\n } catch {\n return false;\n }\n}\n\nexport interface ParentBridgeState {\n /** PropelAuth access token from the parent window. */\n token: string | null;\n /**\n * Statewire sync mount (`…/v2/threads`) minted by the host. `null` when the\n * host minted none; the provider then derives it from the configured\n * `apiUrl`'s origin (or the environment preset).\n */\n statewireSyncUrl: string | null;\n /**\n * Bare Agora API base (no path) minted by the host. `null` when the host\n * minted none; the provider then uses the configured `backendUrl` (or the\n * environment preset).\n */\n backendBaseUrl: string | null;\n /** Athena frontend origin provided by the parent wrapper. */\n appUrl: string | null;\n /**\n * True once a host answered the bridge — a `/_athena/auth` mint or an\n * `athena-config` message — whether or not it minted the statewire fields.\n * Lets the provider tell an outdated host (answered, no mount) from no host\n * at all (the environment preset applies).\n */\n hostAnswered: boolean;\n /** True once config has been received from the parent (or timeout fired). */\n ready: boolean;\n}\n\nconst BRIDGE_URL_FIELDS = ['statewireSyncUrl', 'backendBaseUrl', 'appUrl'] as const;\n\ntype BridgeUrlField = (typeof BRIDGE_URL_FIELDS)[number];\n\n/** The URL half of {@link ParentBridgeState} — what a host can hand the SDK. */\nexport type ParentBridgeUrls = Pick<ParentBridgeState, BridgeUrlField>;\n\nconst EMPTY_BRIDGE_URLS: ParentBridgeUrls = {\n statewireSyncUrl: null,\n backendBaseUrl: null,\n appUrl: null,\n};\n\n/**\n * Fold an `athena-config` postMessage payload over the current URL set. A\n * field is taken only when it is a string; anything else keeps the previous\n * value. Keys outside {@link BridgeUrlField} (a legacy parent's `apiUrl` /\n * `backendUrl` among them) are dropped.\n */\nexport function mergeBridgeConfigMessage(\n previous: ParentBridgeUrls,\n payload: Partial<Record<BridgeUrlField, unknown>>,\n): ParentBridgeUrls {\n const next: ParentBridgeUrls = {\n statewireSyncUrl: previous.statewireSyncUrl,\n backendBaseUrl: previous.backendBaseUrl,\n appUrl: previous.appUrl,\n };\n for (const field of BRIDGE_URL_FIELDS) {\n const value = payload[field];\n if (typeof value === 'string') {\n next[field] = value;\n }\n }\n return next;\n}\n\n/**\n * Read the URL set out of a `/_athena/auth` mint body. Non-string fields are\n * `null`.\n */\nexport function readAuthMintUrls(data: Partial<Record<BridgeUrlField, unknown>>): ParentBridgeUrls {\n return mergeBridgeConfigMessage(EMPTY_BRIDGE_URLS, data);\n}\n\nexport interface ParentBridgeOptions {\n /**\n * Exact parent origins that are allowed to send auth/config via postMessage.\n * Useful for private-cloud or custom-domain deployments.\n */\n trustedOrigins?: string[];\n}\n\n/** How long to wait for the parent bridge before falling back to defaults. */\nconst BRIDGE_TIMEOUT_MS = 2000;\nconst TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1000;\nconst TOKEN_REFRESH_RETRY_BASE_MS = 60 * 1000;\nconst TOKEN_REFRESH_RETRY_MAX_MS = 5 * 60 * 1000;\nconst DEFAULT_TOKEN_REFRESH_MS = 55 * 60 * 1000;\nconst EMPTY_TRUSTED_ORIGINS: string[] = [];\n\n/** Untrusted shape of the `/_athena/auth` mint body; every field is type-guarded on read. */\ninterface AthenaAuthResponse {\n token?: unknown;\n statewireSyncUrl?: unknown;\n backendBaseUrl?: unknown;\n appUrl?: unknown;\n expires_at?: unknown;\n}\n\nconst normalizeOrigin = (value: string): string | null => {\n try {\n return new URL(value).origin;\n } catch {\n return null;\n }\n};\n\nexport const getTokenRefreshDelay = (expiresAt: unknown, nowMs: number): number => {\n if (typeof expiresAt !== 'string') {\n return DEFAULT_TOKEN_REFRESH_MS;\n }\n\n const expiresAtMs = Date.parse(expiresAt);\n if (!Number.isFinite(expiresAtMs)) {\n return DEFAULT_TOKEN_REFRESH_MS;\n }\n\n return Math.max(TOKEN_REFRESH_RETRY_BASE_MS, expiresAtMs - nowMs - TOKEN_REFRESH_BUFFER_MS);\n};\n\nexport const getAuthRetryDelay = (retryAttempt: number): number => {\n const safeAttempt = Number.isFinite(retryAttempt) ? Math.max(0, Math.floor(retryAttempt)) : 0;\n return Math.min(TOKEN_REFRESH_RETRY_MAX_MS, TOKEN_REFRESH_RETRY_BASE_MS * 2 ** safeAttempt);\n};\n\n/**\n * Listens for auth tokens AND environment config from the host environment.\n *\n * Two transport modes, picked automatically:\n *\n * 1. **Iframe (legacy / Olympus inline preview)**: a parent window posts\n * `athena-config` and `athena-auth` via PostMessage. The hook also posts\n * `athena-auth-ready` so the parent knows when to (re-)send.\n *\n * 2. **Standalone (Marathon-served deploy URL)**: top-level navigation, no\n * parent. The hook fetches `/_athena/auth` on its own origin. Marathon\n * validates its session cookie (sent by the browser automatically) and\n * returns `{ token, statewireSyncUrl, backendBaseUrl, appUrl }` — the\n * statewire mount, the bare Agora base and the app origin. A 404 means\n * we're not on a Marathon origin — the hook falls through with empty\n * state.\n *\n * In both modes a timeout safety net flips `ready: true` so the app never\n * blocks indefinitely.\n */\nexport function useParentBridge({\n trustedOrigins = EMPTY_TRUSTED_ORIGINS,\n}: ParentBridgeOptions = {}): ParentBridgeState {\n const isInIframe = typeof window !== 'undefined' && window.parent !== window;\n const runtimeTrustedOrigins = useMemo(() => {\n const origins = new Set<string>();\n\n if (typeof window !== 'undefined') {\n origins.add(window.location.origin);\n }\n\n for (const trustedOrigin of trustedOrigins) {\n const normalizedOrigin = normalizeOrigin(trustedOrigin);\n if (normalizedOrigin) {\n origins.add(normalizedOrigin);\n }\n }\n\n return [...origins];\n }, [trustedOrigins]);\n\n const [state, setState] = useState<ParentBridgeState>({\n token: null,\n ...EMPTY_BRIDGE_URLS,\n hostAnswered: false,\n ready: false,\n });\n const readySignalSent = useRef(false);\n const configReceived = useRef(false);\n\n useEffect(() => {\n if (!isInIframe) return;\n\n const endBridgeSpan = athenaDiagnostics.span('auth.bridge', 'auth.bridge.start', {\n mode: 'iframe',\n });\n let spanEnded = false;\n const endOnce: typeof endBridgeSpan = (name, options) => {\n if (spanEnded) return 0;\n spanEnded = true;\n return endBridgeSpan(name, options);\n };\n\n const handler = (event: MessageEvent) => {\n if (!isTrustedOrigin({ origin: event.origin, trustedOrigins: runtimeTrustedOrigins })) {\n return;\n }\n if (!event.data || typeof event.data !== 'object') return;\n\n if (event.data.type === 'athena-config') {\n configReceived.current = true;\n setState((prev) => ({\n ...prev,\n ...mergeBridgeConfigMessage(prev, event.data),\n hostAnswered: true,\n // Don't set ready here — wait for athena-auth (token) or the timeout.\n // Setting ready on config alone causes a race: the thread list fires\n // before the token arrives, falling back to X-API-KEY.\n }));\n }\n\n if (event.data.type === 'athena-auth' && typeof event.data.token === 'string') {\n endOnce('auth.bridge.ready', { data: { mode: 'iframe' } });\n athenaDiagnostics.emit('auth.token.acquired', {\n level: 'debug',\n data: { source: 'iframe-postmessage' },\n });\n setState((prev) => ({\n ...prev,\n token: event.data.token,\n ready: true,\n }));\n }\n };\n window.addEventListener('message', handler);\n\n if (!readySignalSent.current) {\n window.parent.postMessage({ type: 'athena-auth-ready' }, '*');\n readySignalSent.current = true;\n }\n\n const timer = setTimeout(() => {\n endOnce('auth.bridge.timeout', {\n data: {\n mode: 'iframe',\n timeout_ms: BRIDGE_TIMEOUT_MS,\n config_received: configReceived.current,\n },\n });\n setState((prev) => (prev.ready ? prev : { ...prev, ready: true }));\n }, BRIDGE_TIMEOUT_MS);\n\n return () => {\n window.removeEventListener('message', handler);\n clearTimeout(timer);\n };\n }, [isInIframe, runtimeTrustedOrigins]);\n\n useEffect(() => {\n if (isInIframe) return;\n if (typeof window === 'undefined') return;\n\n let cancelled = false;\n let requestTimer: ReturnType<typeof setTimeout> | null = null;\n let refreshTimer: ReturnType<typeof setTimeout> | null = null;\n let controller: AbortController | null = null;\n let retryAttempt = 0;\n\n const clearRequestTimer = () => {\n if (requestTimer) {\n clearTimeout(requestTimer);\n requestTimer = null;\n }\n };\n\n const clearRefreshTimer = () => {\n if (refreshTimer) {\n clearTimeout(refreshTimer);\n refreshTimer = null;\n }\n };\n\n const markReady = () => {\n if (cancelled) return;\n setState((prev) => (prev.ready ? prev : { ...prev, ready: true }));\n };\n\n const scheduleRefresh = (expiresAt: unknown) => {\n clearRefreshTimer();\n retryAttempt = 0;\n const delay = getTokenRefreshDelay(expiresAt, Date.now());\n refreshTimer = setTimeout(() => {\n void fetchAuth({ markReadyOnFailure: false });\n }, delay);\n };\n\n const scheduleRetry = () => {\n clearRefreshTimer();\n const delay = getAuthRetryDelay(retryAttempt);\n retryAttempt += 1;\n refreshTimer = setTimeout(() => {\n void fetchAuth({ markReadyOnFailure: false });\n }, delay);\n };\n\n const fetchAuth = async ({\n markReadyOnFailure,\n }: {\n markReadyOnFailure: boolean;\n }) => {\n controller?.abort();\n controller = new AbortController();\n\n // `markReadyOnFailure` distinguishes the initial acquisition (a span the\n // provider blocks on) from a background refresh. Idempotent: the abort\n // timer and the catch path can both reach an end call.\n const isInitial = markReadyOnFailure;\n const rawEndSpan = isInitial\n ? athenaDiagnostics.span('auth.bridge', 'auth.bridge.start', { mode: 'marathon' })\n : null;\n let spanEnded = false;\n const endBridgeSpan: typeof rawEndSpan = rawEndSpan\n ? (name, options) => {\n if (spanEnded) return 0;\n spanEnded = true;\n return rawEndSpan(name, options);\n }\n : null;\n\n clearRequestTimer();\n requestTimer = setTimeout(() => {\n controller?.abort();\n endBridgeSpan?.('auth.bridge.timeout', {\n data: { mode: 'marathon', timeout_ms: BRIDGE_TIMEOUT_MS },\n });\n if (markReadyOnFailure) {\n markReady();\n }\n }, BRIDGE_TIMEOUT_MS);\n\n try {\n const resp = await fetch('/_athena/auth', {\n credentials: 'include',\n headers: { Accept: 'application/json' },\n signal: controller.signal,\n });\n if (cancelled) return;\n\n if (!resp.ok) {\n if (resp.status === 404) {\n // Not a Marathon origin — expected for standalone/local dev.\n endBridgeSpan?.('auth.bridge.absent', {\n data: { mode: 'marathon', status: resp.status },\n });\n markReady();\n } else {\n const failure = {\n level: 'warn' as const,\n data: { mode: 'marathon', status: resp.status },\n };\n if (endBridgeSpan) {\n endBridgeSpan('auth.bridge.error', failure);\n } else {\n athenaDiagnostics.emit('auth.token.refresh_failed', failure);\n }\n if (markReadyOnFailure) {\n markReady();\n }\n scheduleRetry();\n }\n return;\n }\n\n const data = (await resp.json()) as AthenaAuthResponse;\n if (cancelled) return;\n\n if (endBridgeSpan) {\n endBridgeSpan('auth.bridge.ready', {\n data: { mode: 'marathon', has_token: typeof data.token === 'string' },\n });\n athenaDiagnostics.emit('auth.token.acquired', {\n level: 'debug',\n data: { source: 'marathon', expires_at: data.expires_at },\n });\n } else {\n athenaDiagnostics.emit('auth.token.refreshed', {\n level: 'debug',\n data: { source: 'marathon', expires_at: data.expires_at },\n });\n }\n\n setState({\n token: typeof data.token === 'string' ? data.token : null,\n ...readAuthMintUrls(data),\n hostAnswered: true,\n ready: true,\n });\n scheduleRefresh(data.expires_at);\n } catch (error) {\n if (!cancelled && !(error instanceof DOMException && error.name === 'AbortError')) {\n const failure = { level: 'warn' as const, data: { mode: 'marathon' }, error };\n if (endBridgeSpan) {\n endBridgeSpan('auth.bridge.error', failure);\n } else {\n athenaDiagnostics.emit('auth.token.refresh_failed', failure);\n }\n }\n if (markReadyOnFailure) {\n markReady();\n }\n if (!cancelled) {\n scheduleRetry();\n }\n } finally {\n clearRequestTimer();\n }\n };\n\n void fetchAuth({ markReadyOnFailure: true });\n\n return () => {\n cancelled = true;\n controller?.abort();\n clearRequestTimer();\n clearRefreshTimer();\n };\n }, [isInIframe]);\n\n return state;\n}\n\n/**\n * Legacy hook — returns just the auth token from the parent bridge.\n * Prefer useParentBridge() for new code.\n */\nexport function useParentAuth(): string | null {\n return useParentBridge().token;\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"auth.js","sources":["../../src/runtime/auth.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport type { AthenaTokenRenewer } from '../auth/credential-source';\nimport { athenaDiagnostics } from '../diagnostics/athena-diagnostics';\n\n/**\n * Check whether a PostMessage origin is trusted.\n * Accepts:\n * - explicit trusted origins passed by the host app\n * - the current iframe origin (for same-origin wrappers like Marathon)\n * - any *.athenaintel.com subdomain and localhost (legacy/default behavior)\n */\nfunction isTrustedOrigin({\n origin,\n trustedOrigins,\n}: {\n origin: string;\n trustedOrigins: string[];\n}): boolean {\n try {\n const normalizedOrigin = new URL(origin).origin;\n if (trustedOrigins.includes(normalizedOrigin)) {\n return true;\n }\n\n const { hostname } = new URL(normalizedOrigin);\n return (\n hostname === 'athenaintel.com' ||\n hostname.endsWith('.athenaintel.com') ||\n hostname === 'localhost'\n );\n } catch {\n return false;\n }\n}\n\nexport interface ParentBridgeState {\n /** PropelAuth access token from the parent window. */\n token: string | null;\n /**\n * Statewire sync mount (`…/v2/threads`) minted by the host. `null` when the\n * host minted none; the provider then derives it from the configured\n * `apiUrl`'s origin (or the environment preset).\n */\n statewireSyncUrl: string | null;\n /**\n * Bare Agora API base (no path) minted by the host. `null` when the host\n * minted none; the provider then uses the configured `backendUrl` (or the\n * environment preset).\n */\n backendBaseUrl: string | null;\n /** Athena frontend origin provided by the parent wrapper. */\n appUrl: string | null;\n /**\n * True once a host answered the bridge — a `/_athena/auth` mint or an\n * `athena-config` message — whether or not it minted the statewire fields.\n * Lets the provider tell an outdated host (answered, no mount) from no host\n * at all (the environment preset applies).\n */\n hostAnswered: boolean;\n /** True once config has been received from the parent (or timeout fired). */\n ready: boolean;\n}\n\n/**\n * {@link ParentBridgeState} plus the bridge's token renewal: `force` (the\n * token was refused) asks the host for a new one — a fresh `/_athena/auth`\n * mint on a Marathon origin, or an `athena-auth-ready` request answered by the\n * parent window's next `athena-auth` — and resolves the held token otherwise.\n */\nexport type ParentBridge = ParentBridgeState & { renewToken: AthenaTokenRenewer };\n\nconst BRIDGE_URL_FIELDS = ['statewireSyncUrl', 'backendBaseUrl', 'appUrl'] as const;\n\ntype BridgeUrlField = (typeof BRIDGE_URL_FIELDS)[number];\n\n/** The URL half of {@link ParentBridgeState} — what a host can hand the SDK. */\nexport type ParentBridgeUrls = Pick<ParentBridgeState, BridgeUrlField>;\n\nconst EMPTY_BRIDGE_URLS: ParentBridgeUrls = {\n statewireSyncUrl: null,\n backendBaseUrl: null,\n appUrl: null,\n};\n\n/**\n * Fold an `athena-config` postMessage payload over the current URL set. A\n * field is taken only when it is a string; anything else keeps the previous\n * value. Keys outside {@link BridgeUrlField} (a legacy parent's `apiUrl` /\n * `backendUrl` among them) are dropped.\n */\nexport function mergeBridgeConfigMessage(\n previous: ParentBridgeUrls,\n payload: Partial<Record<BridgeUrlField, unknown>>,\n): ParentBridgeUrls {\n const next: ParentBridgeUrls = {\n statewireSyncUrl: previous.statewireSyncUrl,\n backendBaseUrl: previous.backendBaseUrl,\n appUrl: previous.appUrl,\n };\n for (const field of BRIDGE_URL_FIELDS) {\n const value = payload[field];\n if (typeof value === 'string') {\n next[field] = value;\n }\n }\n return next;\n}\n\n/**\n * Read the URL set out of a `/_athena/auth` mint body. Non-string fields are\n * `null`.\n */\nexport function readAuthMintUrls(data: Partial<Record<BridgeUrlField, unknown>>): ParentBridgeUrls {\n return mergeBridgeConfigMessage(EMPTY_BRIDGE_URLS, data);\n}\n\nexport interface ParentBridgeOptions {\n /**\n * Exact parent origins that are allowed to send auth/config via postMessage.\n * Useful for private-cloud or custom-domain deployments.\n */\n trustedOrigins?: string[];\n}\n\n/** How long to wait for the parent bridge before falling back to defaults. */\nconst BRIDGE_TIMEOUT_MS = 2000;\n/** How long a renewal waits for the parent window to answer `athena-auth-ready`. */\nconst PARENT_TOKEN_RENEWAL_TIMEOUT_MS = 5000;\nconst TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1000;\nconst TOKEN_REFRESH_RETRY_BASE_MS = 60 * 1000;\nconst TOKEN_REFRESH_RETRY_MAX_MS = 5 * 60 * 1000;\nconst DEFAULT_TOKEN_REFRESH_MS = 55 * 60 * 1000;\nconst EMPTY_TRUSTED_ORIGINS: string[] = [];\n\n/** Untrusted shape of the `/_athena/auth` mint body; every field is type-guarded on read. */\ninterface AthenaAuthResponse {\n token?: unknown;\n statewireSyncUrl?: unknown;\n backendBaseUrl?: unknown;\n appUrl?: unknown;\n expires_at?: unknown;\n}\n\nconst normalizeOrigin = (value: string): string | null => {\n try {\n return new URL(value).origin;\n } catch {\n return null;\n }\n};\n\nexport const getTokenRefreshDelay = (expiresAt: unknown, nowMs: number): number => {\n if (typeof expiresAt !== 'string') {\n return DEFAULT_TOKEN_REFRESH_MS;\n }\n\n const expiresAtMs = Date.parse(expiresAt);\n if (!Number.isFinite(expiresAtMs)) {\n return DEFAULT_TOKEN_REFRESH_MS;\n }\n\n return Math.max(TOKEN_REFRESH_RETRY_BASE_MS, expiresAtMs - nowMs - TOKEN_REFRESH_BUFFER_MS);\n};\n\nexport const getAuthRetryDelay = (retryAttempt: number): number => {\n const safeAttempt = Number.isFinite(retryAttempt) ? Math.max(0, Math.floor(retryAttempt)) : 0;\n return Math.min(TOKEN_REFRESH_RETRY_MAX_MS, TOKEN_REFRESH_RETRY_BASE_MS * 2 ** safeAttempt);\n};\n\n/**\n * Listens for auth tokens AND environment config from the host environment.\n *\n * Two transport modes, picked automatically:\n *\n * 1. **Iframe (legacy / Olympus inline preview)**: a parent window posts\n * `athena-config` and `athena-auth` via PostMessage. The hook also posts\n * `athena-auth-ready` so the parent knows when to (re-)send.\n *\n * 2. **Standalone (Marathon-served deploy URL)**: top-level navigation, no\n * parent. The hook fetches `/_athena/auth` on its own origin. Marathon\n * validates its session cookie (sent by the browser automatically) and\n * returns `{ token, statewireSyncUrl, backendBaseUrl, appUrl }` — the\n * statewire mount, the bare Agora base and the app origin. A 404 means\n * we're not on a Marathon origin — the hook falls through with empty\n * state.\n *\n * In both modes a timeout safety net flips `ready: true` so the app never\n * blocks indefinitely.\n */\nexport function useParentBridge({\n trustedOrigins = EMPTY_TRUSTED_ORIGINS,\n}: ParentBridgeOptions = {}): ParentBridge {\n const isInIframe = typeof window !== 'undefined' && window.parent !== window;\n const runtimeTrustedOrigins = useMemo(() => {\n const origins = new Set<string>();\n\n if (typeof window !== 'undefined') {\n origins.add(window.location.origin);\n }\n\n for (const trustedOrigin of trustedOrigins) {\n const normalizedOrigin = normalizeOrigin(trustedOrigin);\n if (normalizedOrigin) {\n origins.add(normalizedOrigin);\n }\n }\n\n return [...origins];\n }, [trustedOrigins]);\n\n const [state, setState] = useState<ParentBridgeState>({\n token: null,\n ...EMPTY_BRIDGE_URLS,\n hostAnswered: false,\n ready: false,\n });\n const readySignalSent = useRef(false);\n const configReceived = useRef(false);\n const tokenRef = useRef<string | null>(state.token);\n tokenRef.current = state.token;\n // Installed by whichever transport mode is active (iframe or Marathon).\n const renewerRef = useRef<AthenaTokenRenewer | null>(null);\n\n useEffect(() => {\n if (!isInIframe) return;\n\n const endBridgeSpan = athenaDiagnostics.span('auth.bridge', 'auth.bridge.start', {\n mode: 'iframe',\n });\n let spanEnded = false;\n const endOnce: typeof endBridgeSpan = (name, options) => {\n if (spanEnded) return 0;\n spanEnded = true;\n return endBridgeSpan(name, options);\n };\n\n // Renewals waiting for the parent's next `athena-auth`.\n const tokenWaiters = new Set<(token: string | null) => void>();\n\n const handler = (event: MessageEvent) => {\n if (!isTrustedOrigin({ origin: event.origin, trustedOrigins: runtimeTrustedOrigins })) {\n return;\n }\n if (!event.data || typeof event.data !== 'object') return;\n\n if (event.data.type === 'athena-config') {\n configReceived.current = true;\n setState((prev) => ({\n ...prev,\n ...mergeBridgeConfigMessage(prev, event.data),\n hostAnswered: true,\n // Don't set ready here — wait for athena-auth (token) or the timeout.\n // Setting ready on config alone causes a race: the thread list fires\n // before the token arrives, falling back to X-API-KEY.\n }));\n }\n\n if (event.data.type === 'athena-auth' && typeof event.data.token === 'string') {\n endOnce('auth.bridge.ready', { data: { mode: 'iframe' } });\n athenaDiagnostics.emit('auth.token.acquired', {\n level: 'debug',\n data: { source: 'iframe-postmessage' },\n });\n setState((prev) => ({\n ...prev,\n token: event.data.token,\n ready: true,\n }));\n tokenRef.current = event.data.token;\n for (const resolve of [...tokenWaiters]) resolve(event.data.token);\n }\n };\n window.addEventListener('message', handler);\n\n // A renewal asks the parent to send its current token again — the same\n // `athena-auth-ready` handshake a parent already answers — and settles on\n // the next `athena-auth`, or on the held token when none arrives in time.\n renewerRef.current = ({ force }) => {\n if (!force) return Promise.resolve(tokenRef.current);\n return new Promise<string | null>((resolve) => {\n const settle = (token: string | null) => {\n clearTimeout(timer);\n tokenWaiters.delete(settle);\n resolve(token ?? tokenRef.current);\n };\n const timer = setTimeout(() => settle(null), PARENT_TOKEN_RENEWAL_TIMEOUT_MS);\n tokenWaiters.add(settle);\n window.parent.postMessage({ type: 'athena-auth-ready' }, '*');\n });\n };\n\n if (!readySignalSent.current) {\n window.parent.postMessage({ type: 'athena-auth-ready' }, '*');\n readySignalSent.current = true;\n }\n\n const timer = setTimeout(() => {\n endOnce('auth.bridge.timeout', {\n data: {\n mode: 'iframe',\n timeout_ms: BRIDGE_TIMEOUT_MS,\n config_received: configReceived.current,\n },\n });\n setState((prev) => (prev.ready ? prev : { ...prev, ready: true }));\n }, BRIDGE_TIMEOUT_MS);\n\n return () => {\n window.removeEventListener('message', handler);\n clearTimeout(timer);\n renewerRef.current = null;\n for (const resolve of [...tokenWaiters]) resolve(null);\n };\n }, [isInIframe, runtimeTrustedOrigins]);\n\n useEffect(() => {\n if (isInIframe) return;\n if (typeof window === 'undefined') return;\n\n let cancelled = false;\n let refreshTimer: ReturnType<typeof setTimeout> | null = null;\n let controller: AbortController | null = null;\n let inFlight: Promise<string | null> | null = null;\n let retryAttempt = 0;\n\n const clearRefreshTimer = () => {\n if (refreshTimer) {\n clearTimeout(refreshTimer);\n refreshTimer = null;\n }\n };\n\n const markReady = () => {\n if (cancelled) return;\n setState((prev) => (prev.ready ? prev : { ...prev, ready: true }));\n };\n\n const scheduleRefresh = (expiresAt: unknown) => {\n clearRefreshTimer();\n retryAttempt = 0;\n const delay = getTokenRefreshDelay(expiresAt, Date.now());\n refreshTimer = setTimeout(() => {\n void fetchAuth({ markReadyOnFailure: false });\n }, delay);\n };\n\n const scheduleRetry = () => {\n clearRefreshTimer();\n const delay = getAuthRetryDelay(retryAttempt);\n retryAttempt += 1;\n refreshTimer = setTimeout(() => {\n void fetchAuth({ markReadyOnFailure: false });\n }, delay);\n };\n\n const mintAuth = async ({\n markReadyOnFailure,\n }: {\n markReadyOnFailure: boolean;\n }): Promise<string | null> => {\n const requestController = new AbortController();\n controller = requestController;\n\n // `markReadyOnFailure` distinguishes the initial acquisition (a span the\n // provider blocks on) from a background refresh. Idempotent: the abort\n // timer and the catch path can both reach an end call.\n const isInitial = markReadyOnFailure;\n const rawEndSpan = isInitial\n ? athenaDiagnostics.span('auth.bridge', 'auth.bridge.start', { mode: 'marathon' })\n : null;\n let spanEnded = false;\n const endBridgeSpan: typeof rawEndSpan = rawEndSpan\n ? (name, options) => {\n if (spanEnded) return 0;\n spanEnded = true;\n return rawEndSpan(name, options);\n }\n : null;\n\n const requestTimer = setTimeout(() => {\n requestController.abort();\n endBridgeSpan?.('auth.bridge.timeout', {\n data: { mode: 'marathon', timeout_ms: BRIDGE_TIMEOUT_MS },\n });\n if (markReadyOnFailure) {\n markReady();\n }\n }, BRIDGE_TIMEOUT_MS);\n\n try {\n const resp = await fetch('/_athena/auth', {\n credentials: 'include',\n headers: { Accept: 'application/json' },\n signal: requestController.signal,\n });\n if (cancelled) return null;\n\n if (!resp.ok) {\n if (resp.status === 404) {\n // Not a Marathon origin — expected for standalone/local dev.\n endBridgeSpan?.('auth.bridge.absent', {\n data: { mode: 'marathon', status: resp.status },\n });\n markReady();\n } else {\n const failure = {\n level: 'warn' as const,\n data: { mode: 'marathon', status: resp.status },\n };\n if (endBridgeSpan) {\n endBridgeSpan('auth.bridge.error', failure);\n } else {\n athenaDiagnostics.emit('auth.token.refresh_failed', failure);\n }\n if (markReadyOnFailure) {\n markReady();\n }\n scheduleRetry();\n }\n return null;\n }\n\n const data = (await resp.json()) as AthenaAuthResponse;\n if (cancelled) return null;\n\n if (endBridgeSpan) {\n endBridgeSpan('auth.bridge.ready', {\n data: { mode: 'marathon', has_token: typeof data.token === 'string' },\n });\n athenaDiagnostics.emit('auth.token.acquired', {\n level: 'debug',\n data: { source: 'marathon', expires_at: data.expires_at },\n });\n } else {\n athenaDiagnostics.emit('auth.token.refreshed', {\n level: 'debug',\n data: { source: 'marathon', expires_at: data.expires_at },\n });\n }\n\n const token = typeof data.token === 'string' ? data.token : null;\n setState({\n token,\n ...readAuthMintUrls(data),\n hostAnswered: true,\n ready: true,\n });\n tokenRef.current = token;\n scheduleRefresh(data.expires_at);\n return token;\n } catch (error) {\n if (!cancelled && !(error instanceof DOMException && error.name === 'AbortError')) {\n const failure = { level: 'warn' as const, data: { mode: 'marathon' }, error };\n if (endBridgeSpan) {\n endBridgeSpan('auth.bridge.error', failure);\n } else {\n athenaDiagnostics.emit('auth.token.refresh_failed', failure);\n }\n }\n if (markReadyOnFailure) {\n markReady();\n }\n if (!cancelled) {\n scheduleRetry();\n }\n return null;\n } finally {\n clearTimeout(requestTimer);\n if (controller === requestController) controller = null;\n }\n };\n\n // One mint at a time. The initial acquisition, a scheduled refresh, a\n // retry and a forced renewal that overlap share the request in flight\n // instead of aborting it, so each request keeps its own timeout and the\n // schedule never cancels a renewal a connection is waiting on.\n const fetchAuth = (options: { markReadyOnFailure: boolean }): Promise<string | null> => {\n inFlight ??= mintAuth(options).finally(() => {\n inFlight = null;\n });\n return inFlight;\n };\n\n // A forced renewal mints now instead of waiting for the scheduled refresh\n // (joining a mint already in flight); the schedule restarts from the new\n // token's expiry.\n renewerRef.current = ({ force }) => {\n if (!force) return Promise.resolve(tokenRef.current);\n return fetchAuth({ markReadyOnFailure: false }).then((token) => token ?? tokenRef.current);\n };\n\n void fetchAuth({ markReadyOnFailure: true });\n\n return () => {\n cancelled = true;\n controller?.abort();\n clearRefreshTimer();\n renewerRef.current = null;\n };\n }, [isInIframe]);\n\n const renewToken = useCallback<AthenaTokenRenewer>(\n (options) => renewerRef.current?.(options) ?? Promise.resolve(tokenRef.current),\n [],\n );\n\n return useMemo(() => ({ ...state, renewToken }), [state, renewToken]);\n}\n\n/**\n * Legacy hook — returns just the auth token from the parent bridge.\n * Prefer useParentBridge() for new code.\n */\nexport function useParentAuth(): string | null {\n return useParentBridge().token;\n}\n"],"names":["timer"],"mappings":";;AAWA,SAAS,gBAAgB;AAAA,EACvB;AAAA,EACA;AACF,GAGY;AACV,MAAI;AACF,UAAM,mBAAmB,IAAI,IAAI,MAAM,EAAE;AACzC,QAAI,eAAe,SAAS,gBAAgB,GAAG;AAC7C,aAAO;AAAA,IACT;AAEA,UAAM,EAAE,SAAA,IAAa,IAAI,IAAI,gBAAgB;AAC7C,WACE,aAAa,qBACb,SAAS,SAAS,kBAAkB,KACpC,aAAa;AAAA,EAEjB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAsCA,MAAM,oBAAoB,CAAC,oBAAoB,kBAAkB,QAAQ;AAOzE,MAAM,oBAAsC;AAAA,EAC1C,kBAAkB;AAAA,EAClB,gBAAgB;AAAA,EAChB,QAAQ;AACV;AAQO,SAAS,yBACd,UACA,SACkB;AAClB,QAAM,OAAyB;AAAA,IAC7B,kBAAkB,SAAS;AAAA,IAC3B,gBAAgB,SAAS;AAAA,IACzB,QAAQ,SAAS;AAAA,EAAA;AAEnB,aAAW,SAAS,mBAAmB;AACrC,UAAM,QAAQ,QAAQ,KAAK;AAC3B,QAAI,OAAO,UAAU,UAAU;AAC7B,WAAK,KAAK,IAAI;AAAA,IAChB;AAAA,EACF;AACA,SAAO;AACT;AAMO,SAAS,iBAAiB,MAAkE;AACjG,SAAO,yBAAyB,mBAAmB,IAAI;AACzD;AAWA,MAAM,oBAAoB;AAE1B,MAAM,kCAAkC;AACxC,MAAM,0BAA0B,IAAI,KAAK;AACzC,MAAM,8BAA8B,KAAK;AACzC,MAAM,6BAA6B,IAAI,KAAK;AAC5C,MAAM,2BAA2B,KAAK,KAAK;AAC3C,MAAM,wBAAkC,CAAA;AAWxC,MAAM,kBAAkB,CAAC,UAAiC;AACxD,MAAI;AACF,WAAO,IAAI,IAAI,KAAK,EAAE;AAAA,EACxB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,MAAM,uBAAuB,CAAC,WAAoB,UAA0B;AACjF,MAAI,OAAO,cAAc,UAAU;AACjC,WAAO;AAAA,EACT;AAEA,QAAM,cAAc,KAAK,MAAM,SAAS;AACxC,MAAI,CAAC,OAAO,SAAS,WAAW,GAAG;AACjC,WAAO;AAAA,EACT;AAEA,SAAO,KAAK,IAAI,6BAA6B,cAAc,QAAQ,uBAAuB;AAC5F;AAEO,MAAM,oBAAoB,CAAC,iBAAiC;AACjE,QAAM,cAAc,OAAO,SAAS,YAAY,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,YAAY,CAAC,IAAI;AAC5F,SAAO,KAAK,IAAI,4BAA4B,8BAA8B,KAAK,WAAW;AAC5F;AAsBO,SAAS,gBAAgB;AAAA,EAC9B,iBAAiB;AACnB,IAAyB,IAAkB;AACzC,QAAM,aAAa,OAAO,WAAW,eAAe,OAAO,WAAW;AACtE,QAAM,wBAAwB,QAAQ,MAAM;AAC1C,UAAM,8BAAc,IAAA;AAEpB,QAAI,OAAO,WAAW,aAAa;AACjC,cAAQ,IAAI,OAAO,SAAS,MAAM;AAAA,IACpC;AAEA,eAAW,iBAAiB,gBAAgB;AAC1C,YAAM,mBAAmB,gBAAgB,aAAa;AACtD,UAAI,kBAAkB;AACpB,gBAAQ,IAAI,gBAAgB;AAAA,MAC9B;AAAA,IACF;AAEA,WAAO,CAAC,GAAG,OAAO;AAAA,EACpB,GAAG,CAAC,cAAc,CAAC;AAEnB,QAAM,CAAC,OAAO,QAAQ,IAAI,SAA4B;AAAA,IACpD,OAAO;AAAA,IACP,GAAG;AAAA,IACH,cAAc;AAAA,IACd,OAAO;AAAA,EAAA,CACR;AACD,QAAM,kBAAkB,OAAO,KAAK;AACpC,QAAM,iBAAiB,OAAO,KAAK;AACnC,QAAM,WAAW,OAAsB,MAAM,KAAK;AAClD,WAAS,UAAU,MAAM;AAEzB,QAAM,aAAa,OAAkC,IAAI;AAEzD,YAAU,MAAM;AACd,QAAI,CAAC,WAAY;AAEjB,UAAM,gBAAgB,kBAAkB,KAAK,eAAe,qBAAqB;AAAA,MAC/E,MAAM;AAAA,IAAA,CACP;AACD,QAAI,YAAY;AAChB,UAAM,UAAgC,CAAC,MAAM,YAAY;AACvD,UAAI,UAAW,QAAO;AACtB,kBAAY;AACZ,aAAO,cAAc,MAAM,OAAO;AAAA,IACpC;AAGA,UAAM,mCAAmB,IAAA;AAEzB,UAAM,UAAU,CAAC,UAAwB;AACvC,UAAI,CAAC,gBAAgB,EAAE,QAAQ,MAAM,QAAQ,gBAAgB,sBAAA,CAAuB,GAAG;AACrF;AAAA,MACF;AACA,UAAI,CAAC,MAAM,QAAQ,OAAO,MAAM,SAAS,SAAU;AAEnD,UAAI,MAAM,KAAK,SAAS,iBAAiB;AACvC,uBAAe,UAAU;AACzB,iBAAS,CAAC,UAAU;AAAA,UAClB,GAAG;AAAA,UACH,GAAG,yBAAyB,MAAM,MAAM,IAAI;AAAA,UAC5C,cAAc;AAAA;AAAA;AAAA;AAAA,QAAA,EAId;AAAA,MACJ;AAEA,UAAI,MAAM,KAAK,SAAS,iBAAiB,OAAO,MAAM,KAAK,UAAU,UAAU;AAC7E,gBAAQ,qBAAqB,EAAE,MAAM,EAAE,MAAM,SAAA,GAAY;AACzD,0BAAkB,KAAK,uBAAuB;AAAA,UAC5C,OAAO;AAAA,UACP,MAAM,EAAE,QAAQ,qBAAA;AAAA,QAAqB,CACtC;AACD,iBAAS,CAAC,UAAU;AAAA,UAClB,GAAG;AAAA,UACH,OAAO,MAAM,KAAK;AAAA,UAClB,OAAO;AAAA,QAAA,EACP;AACF,iBAAS,UAAU,MAAM,KAAK;AAC9B,mBAAW,WAAW,CAAC,GAAG,YAAY,EAAG,SAAQ,MAAM,KAAK,KAAK;AAAA,MACnE;AAAA,IACF;AACA,WAAO,iBAAiB,WAAW,OAAO;AAK1C,eAAW,UAAU,CAAC,EAAE,YAAY;AAClC,UAAI,CAAC,MAAO,QAAO,QAAQ,QAAQ,SAAS,OAAO;AACnD,aAAO,IAAI,QAAuB,CAAC,YAAY;AAC7C,cAAM,SAAS,CAAC,UAAyB;AACvC,uBAAaA,MAAK;AAClB,uBAAa,OAAO,MAAM;AAC1B,kBAAQ,SAAS,SAAS,OAAO;AAAA,QACnC;AACA,cAAMA,SAAQ,WAAW,MAAM,OAAO,IAAI,GAAG,+BAA+B;AAC5E,qBAAa,IAAI,MAAM;AACvB,eAAO,OAAO,YAAY,EAAE,MAAM,oBAAA,GAAuB,GAAG;AAAA,MAC9D,CAAC;AAAA,IACH;AAEA,QAAI,CAAC,gBAAgB,SAAS;AAC5B,aAAO,OAAO,YAAY,EAAE,MAAM,oBAAA,GAAuB,GAAG;AAC5D,sBAAgB,UAAU;AAAA,IAC5B;AAEA,UAAM,QAAQ,WAAW,MAAM;AAC7B,cAAQ,uBAAuB;AAAA,QAC7B,MAAM;AAAA,UACJ,MAAM;AAAA,UACN,YAAY;AAAA,UACZ,iBAAiB,eAAe;AAAA,QAAA;AAAA,MAClC,CACD;AACD,eAAS,CAAC,SAAU,KAAK,QAAQ,OAAO,EAAE,GAAG,MAAM,OAAO,MAAO;AAAA,IACnE,GAAG,iBAAiB;AAEpB,WAAO,MAAM;AACX,aAAO,oBAAoB,WAAW,OAAO;AAC7C,mBAAa,KAAK;AAClB,iBAAW,UAAU;AACrB,iBAAW,WAAW,CAAC,GAAG,YAAY,WAAW,IAAI;AAAA,IACvD;AAAA,EACF,GAAG,CAAC,YAAY,qBAAqB,CAAC;AAEtC,YAAU,MAAM;AACd,QAAI,WAAY;AAChB,QAAI,OAAO,WAAW,YAAa;AAEnC,QAAI,YAAY;AAChB,QAAI,eAAqD;AACzD,QAAI,aAAqC;AACzC,QAAI,WAA0C;AAC9C,QAAI,eAAe;AAEnB,UAAM,oBAAoB,MAAM;AAC9B,UAAI,cAAc;AAChB,qBAAa,YAAY;AACzB,uBAAe;AAAA,MACjB;AAAA,IACF;AAEA,UAAM,YAAY,MAAM;AACtB,UAAI,UAAW;AACf,eAAS,CAAC,SAAU,KAAK,QAAQ,OAAO,EAAE,GAAG,MAAM,OAAO,MAAO;AAAA,IACnE;AAEA,UAAM,kBAAkB,CAAC,cAAuB;AAC9C,wBAAA;AACA,qBAAe;AACf,YAAM,QAAQ,qBAAqB,WAAW,KAAK,KAAK;AACxD,qBAAe,WAAW,MAAM;AAC9B,aAAK,UAAU,EAAE,oBAAoB,OAAO;AAAA,MAC9C,GAAG,KAAK;AAAA,IACV;AAEA,UAAM,gBAAgB,MAAM;AAC1B,wBAAA;AACA,YAAM,QAAQ,kBAAkB,YAAY;AAC5C,sBAAgB;AAChB,qBAAe,WAAW,MAAM;AAC9B,aAAK,UAAU,EAAE,oBAAoB,OAAO;AAAA,MAC9C,GAAG,KAAK;AAAA,IACV;AAEA,UAAM,WAAW,OAAO;AAAA,MACtB;AAAA,IAAA,MAG4B;AAC5B,YAAM,oBAAoB,IAAI,gBAAA;AAC9B,mBAAa;AAKb,YAAM,YAAY;AAClB,YAAM,aAAa,YACf,kBAAkB,KAAK,eAAe,qBAAqB,EAAE,MAAM,WAAA,CAAY,IAC/E;AACJ,UAAI,YAAY;AAChB,YAAM,gBAAmC,aACrC,CAAC,MAAM,YAAY;AACjB,YAAI,UAAW,QAAO;AACtB,oBAAY;AACZ,eAAO,WAAW,MAAM,OAAO;AAAA,MACjC,IACA;AAEJ,YAAM,eAAe,WAAW,MAAM;AACpC,0BAAkB,MAAA;AAClB,uDAAgB,uBAAuB;AAAA,UACrC,MAAM,EAAE,MAAM,YAAY,YAAY,kBAAA;AAAA,QAAkB;AAE1D,YAAI,oBAAoB;AACtB,oBAAA;AAAA,QACF;AAAA,MACF,GAAG,iBAAiB;AAEpB,UAAI;AACF,cAAM,OAAO,MAAM,MAAM,iBAAiB;AAAA,UACxC,aAAa;AAAA,UACb,SAAS,EAAE,QAAQ,mBAAA;AAAA,UACnB,QAAQ,kBAAkB;AAAA,QAAA,CAC3B;AACD,YAAI,UAAW,QAAO;AAEtB,YAAI,CAAC,KAAK,IAAI;AACZ,cAAI,KAAK,WAAW,KAAK;AAEvB,2DAAgB,sBAAsB;AAAA,cACpC,MAAM,EAAE,MAAM,YAAY,QAAQ,KAAK,OAAA;AAAA,YAAO;AAEhD,sBAAA;AAAA,UACF,OAAO;AACL,kBAAM,UAAU;AAAA,cACd,OAAO;AAAA,cACP,MAAM,EAAE,MAAM,YAAY,QAAQ,KAAK,OAAA;AAAA,YAAO;AAEhD,gBAAI,eAAe;AACjB,4BAAc,qBAAqB,OAAO;AAAA,YAC5C,OAAO;AACL,gCAAkB,KAAK,6BAA6B,OAAO;AAAA,YAC7D;AACA,gBAAI,oBAAoB;AACtB,wBAAA;AAAA,YACF;AACA,0BAAA;AAAA,UACF;AACA,iBAAO;AAAA,QACT;AAEA,cAAM,OAAQ,MAAM,KAAK,KAAA;AACzB,YAAI,UAAW,QAAO;AAEtB,YAAI,eAAe;AACjB,wBAAc,qBAAqB;AAAA,YACjC,MAAM,EAAE,MAAM,YAAY,WAAW,OAAO,KAAK,UAAU,SAAA;AAAA,UAAS,CACrE;AACD,4BAAkB,KAAK,uBAAuB;AAAA,YAC5C,OAAO;AAAA,YACP,MAAM,EAAE,QAAQ,YAAY,YAAY,KAAK,WAAA;AAAA,UAAW,CACzD;AAAA,QACH,OAAO;AACL,4BAAkB,KAAK,wBAAwB;AAAA,YAC7C,OAAO;AAAA,YACP,MAAM,EAAE,QAAQ,YAAY,YAAY,KAAK,WAAA;AAAA,UAAW,CACzD;AAAA,QACH;AAEA,cAAM,QAAQ,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ;AAC5D,iBAAS;AAAA,UACP;AAAA,UACA,GAAG,iBAAiB,IAAI;AAAA,UACxB,cAAc;AAAA,UACd,OAAO;AAAA,QAAA,CACR;AACD,iBAAS,UAAU;AACnB,wBAAgB,KAAK,UAAU;AAC/B,eAAO;AAAA,MACT,SAAS,OAAO;AACd,YAAI,CAAC,aAAa,EAAE,iBAAiB,gBAAgB,MAAM,SAAS,eAAe;AACjF,gBAAM,UAAU,EAAE,OAAO,QAAiB,MAAM,EAAE,MAAM,WAAA,GAAc,MAAA;AACtE,cAAI,eAAe;AACjB,0BAAc,qBAAqB,OAAO;AAAA,UAC5C,OAAO;AACL,8BAAkB,KAAK,6BAA6B,OAAO;AAAA,UAC7D;AAAA,QACF;AACA,YAAI,oBAAoB;AACtB,oBAAA;AAAA,QACF;AACA,YAAI,CAAC,WAAW;AACd,wBAAA;AAAA,QACF;AACA,eAAO;AAAA,MACT,UAAA;AACE,qBAAa,YAAY;AACzB,YAAI,eAAe,kBAAmB,cAAa;AAAA,MACrD;AAAA,IACF;AAMA,UAAM,YAAY,CAAC,YAAqE;AACtF,8BAAa,SAAS,OAAO,EAAE,QAAQ,MAAM;AAC3C,mBAAW;AAAA,MACb,CAAC;AACD,aAAO;AAAA,IACT;AAKA,eAAW,UAAU,CAAC,EAAE,YAAY;AAClC,UAAI,CAAC,MAAO,QAAO,QAAQ,QAAQ,SAAS,OAAO;AACnD,aAAO,UAAU,EAAE,oBAAoB,OAAO,EAAE,KAAK,CAAC,UAAU,SAAS,SAAS,OAAO;AAAA,IAC3F;AAEA,SAAK,UAAU,EAAE,oBAAoB,MAAM;AAE3C,WAAO,MAAM;AACX,kBAAY;AACZ,+CAAY;AACZ,wBAAA;AACA,iBAAW,UAAU;AAAA,IACvB;AAAA,EACF,GAAG,CAAC,UAAU,CAAC;AAEf,QAAM,aAAa;AAAA,IACjB,CAAC;;AAAY,+BAAW,YAAX,oCAAqB,aAAY,QAAQ,QAAQ,SAAS,OAAO;AAAA;AAAA,IAC9E,CAAA;AAAA,EAAC;AAGH,SAAO,QAAQ,OAAO,EAAE,GAAG,OAAO,eAAe,CAAC,OAAO,UAAU,CAAC;AACtE;AAMO,SAAS,gBAA+B;AAC7C,SAAO,kBAAkB;AAC3B;"}
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const statewireAuthHeaders = require("./statewire-auth-headers.cjs");
4
+ function isCredentialRefusal(error) {
5
+ return typeof error === "object" && error !== null && "status" in error && error.status === 401;
6
+ }
7
+ async function withCredentialRenewal(getAuth, run) {
8
+ const auth = await getAuth({ force: false });
9
+ try {
10
+ return await run(auth);
11
+ } catch (error) {
12
+ if (!isCredentialRefusal(error) || !auth.token) throw error;
13
+ const renewed = await getAuth({ force: true });
14
+ if (!renewed.token || renewed.token === auth.token) throw error;
15
+ return run(renewed);
16
+ }
17
+ }
18
+ async function sendWithCredentialRenewal(source, send) {
19
+ const headers = await statewireAuthHeaders.resolveStatewireAuthHeaders(source, { staleAuth: false });
20
+ const response = await send(headers);
21
+ if (response.status !== 401 || !source.getToken || !headers.Authorization) return response;
22
+ const renewed = await statewireAuthHeaders.resolveStatewireAuthHeaders(source, { staleAuth: true });
23
+ if (!renewed.Authorization || renewed.Authorization === headers.Authorization) return response;
24
+ return send(renewed);
25
+ }
26
+ exports.isCredentialRefusal = isCredentialRefusal;
27
+ exports.sendWithCredentialRenewal = sendWithCredentialRenewal;
28
+ exports.withCredentialRenewal = withCredentialRenewal;
29
+ //# sourceMappingURL=credential-renewal.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential-renewal.cjs","sources":["../../src/runtime/credential-renewal.ts"],"sourcesContent":["import { type StatewireAuthSource, resolveStatewireAuthHeaders } from './statewire-auth-headers';\n\n/**\n * The credential for one request, resolved right before it is sent. `force`\n * means the previous credential was refused: renew it first. A resolver that\n * ignores the argument keeps working — it just cannot renew.\n */\nexport type CredentialResolver<TAuth> = (options?: { force?: boolean }) => TAuth | Promise<TAuth>;\n\n/** True for a failure that says the host refused the credential (HTTP 401). */\nexport function isCredentialRefusal(error: unknown): boolean {\n return (\n typeof error === 'object' &&\n error !== null &&\n 'status' in error &&\n (error as { status?: unknown }).status === 401\n );\n}\n\n/**\n * Run one request with the current credential and, when the host refuses it\n * (401), renew the credential once and run the request again — the REST twin\n * of the statewire transport's `staleAuth` retry. A second refusal, and every\n * other failure (a 403 included: renewing cannot grant access), propagates\n * unchanged. A request made without a token (an API key alone) is not\n * retried, and neither is one whose renewal produced the same token (a static\n * token) or none: there is nothing new to send. A refused request never ran\n * on the server, so repeating a write is safe.\n */\nexport async function withCredentialRenewal<TAuth extends { token?: string | null }, TResult>(\n getAuth: CredentialResolver<TAuth>,\n run: (auth: TAuth) => Promise<TResult>,\n): Promise<TResult> {\n const auth = await getAuth({ force: false });\n try {\n return await run(auth);\n } catch (error) {\n if (!isCredentialRefusal(error) || !auth.token) throw error;\n const renewed = await getAuth({ force: true });\n if (!renewed.token || renewed.token === auth.token) throw error;\n return run(renewed);\n }\n}\n\n/**\n * Send one raw `fetch` with the credential headers `source` resolves and, when\n * the host refuses a bearer token (401), renew it once (`force: true`) and\n * resend with fresh headers: the statewire transport's `staleAuth` contract.\n * `send` runs once per attempt, so it builds its request body each time (a\n * `FormData` or stream is single-use). A request sent with the API key alone,\n * from a source with no live getter, or whose renewal produced the same token\n * (a static one) or none, is not resent: there is nothing new to send.\n * Returns the last response either way.\n */\nexport async function sendWithCredentialRenewal(\n source: StatewireAuthSource,\n send: (headers: Record<string, string>) => Promise<Response>,\n): Promise<Response> {\n const headers = await resolveStatewireAuthHeaders(source, { staleAuth: false });\n const response = await send(headers);\n if (response.status !== 401 || !source.getToken || !headers.Authorization) return response;\n const renewed = await resolveStatewireAuthHeaders(source, { staleAuth: true });\n if (!renewed.Authorization || renewed.Authorization === headers.Authorization) return response;\n return send(renewed);\n}\n"],"names":["resolveStatewireAuthHeaders"],"mappings":";;;AAUO,SAAS,oBAAoB,OAAyB;AAC3D,SACE,OAAO,UAAU,YACjB,UAAU,QACV,YAAY,SACX,MAA+B,WAAW;AAE/C;AAYA,eAAsB,sBACpB,SACA,KACkB;AAClB,QAAM,OAAO,MAAM,QAAQ,EAAE,OAAO,OAAO;AAC3C,MAAI;AACF,WAAO,MAAM,IAAI,IAAI;AAAA,EACvB,SAAS,OAAO;AACd,QAAI,CAAC,oBAAoB,KAAK,KAAK,CAAC,KAAK,MAAO,OAAM;AACtD,UAAM,UAAU,MAAM,QAAQ,EAAE,OAAO,MAAM;AAC7C,QAAI,CAAC,QAAQ,SAAS,QAAQ,UAAU,KAAK,MAAO,OAAM;AAC1D,WAAO,IAAI,OAAO;AAAA,EACpB;AACF;AAYA,eAAsB,0BACpB,QACA,MACmB;AACnB,QAAM,UAAU,MAAMA,qBAAAA,4BAA4B,QAAQ,EAAE,WAAW,OAAO;AAC9E,QAAM,WAAW,MAAM,KAAK,OAAO;AACnC,MAAI,SAAS,WAAW,OAAO,CAAC,OAAO,YAAY,CAAC,QAAQ,cAAe,QAAO;AAClF,QAAM,UAAU,MAAMA,qBAAAA,4BAA4B,QAAQ,EAAE,WAAW,MAAM;AAC7E,MAAI,CAAC,QAAQ,iBAAiB,QAAQ,kBAAkB,QAAQ,cAAe,QAAO;AACtF,SAAO,KAAK,OAAO;AACrB;;;;"}
@@ -0,0 +1,35 @@
1
+ import { type StatewireAuthSource } from './statewire-auth-headers';
2
+ /**
3
+ * The credential for one request, resolved right before it is sent. `force`
4
+ * means the previous credential was refused: renew it first. A resolver that
5
+ * ignores the argument keeps working — it just cannot renew.
6
+ */
7
+ export type CredentialResolver<TAuth> = (options?: {
8
+ force?: boolean;
9
+ }) => TAuth | Promise<TAuth>;
10
+ /** True for a failure that says the host refused the credential (HTTP 401). */
11
+ export declare function isCredentialRefusal(error: unknown): boolean;
12
+ /**
13
+ * Run one request with the current credential and, when the host refuses it
14
+ * (401), renew the credential once and run the request again — the REST twin
15
+ * of the statewire transport's `staleAuth` retry. A second refusal, and every
16
+ * other failure (a 403 included: renewing cannot grant access), propagates
17
+ * unchanged. A request made without a token (an API key alone) is not
18
+ * retried, and neither is one whose renewal produced the same token (a static
19
+ * token) or none: there is nothing new to send. A refused request never ran
20
+ * on the server, so repeating a write is safe.
21
+ */
22
+ export declare function withCredentialRenewal<TAuth extends {
23
+ token?: string | null;
24
+ }, TResult>(getAuth: CredentialResolver<TAuth>, run: (auth: TAuth) => Promise<TResult>): Promise<TResult>;
25
+ /**
26
+ * Send one raw `fetch` with the credential headers `source` resolves and, when
27
+ * the host refuses a bearer token (401), renew it once (`force: true`) and
28
+ * resend with fresh headers: the statewire transport's `staleAuth` contract.
29
+ * `send` runs once per attempt, so it builds its request body each time (a
30
+ * `FormData` or stream is single-use). A request sent with the API key alone,
31
+ * from a source with no live getter, or whose renewal produced the same token
32
+ * (a static one) or none, is not resent: there is nothing new to send.
33
+ * Returns the last response either way.
34
+ */
35
+ export declare function sendWithCredentialRenewal(source: StatewireAuthSource, send: (headers: Record<string, string>) => Promise<Response>): Promise<Response>;
@@ -0,0 +1,29 @@
1
+ import { resolveStatewireAuthHeaders } from "./statewire-auth-headers.js";
2
+ function isCredentialRefusal(error) {
3
+ return typeof error === "object" && error !== null && "status" in error && error.status === 401;
4
+ }
5
+ async function withCredentialRenewal(getAuth, run) {
6
+ const auth = await getAuth({ force: false });
7
+ try {
8
+ return await run(auth);
9
+ } catch (error) {
10
+ if (!isCredentialRefusal(error) || !auth.token) throw error;
11
+ const renewed = await getAuth({ force: true });
12
+ if (!renewed.token || renewed.token === auth.token) throw error;
13
+ return run(renewed);
14
+ }
15
+ }
16
+ async function sendWithCredentialRenewal(source, send) {
17
+ const headers = await resolveStatewireAuthHeaders(source, { staleAuth: false });
18
+ const response = await send(headers);
19
+ if (response.status !== 401 || !source.getToken || !headers.Authorization) return response;
20
+ const renewed = await resolveStatewireAuthHeaders(source, { staleAuth: true });
21
+ if (!renewed.Authorization || renewed.Authorization === headers.Authorization) return response;
22
+ return send(renewed);
23
+ }
24
+ export {
25
+ isCredentialRefusal,
26
+ sendWithCredentialRenewal,
27
+ withCredentialRenewal
28
+ };
29
+ //# sourceMappingURL=credential-renewal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential-renewal.js","sources":["../../src/runtime/credential-renewal.ts"],"sourcesContent":["import { type StatewireAuthSource, resolveStatewireAuthHeaders } from './statewire-auth-headers';\n\n/**\n * The credential for one request, resolved right before it is sent. `force`\n * means the previous credential was refused: renew it first. A resolver that\n * ignores the argument keeps working — it just cannot renew.\n */\nexport type CredentialResolver<TAuth> = (options?: { force?: boolean }) => TAuth | Promise<TAuth>;\n\n/** True for a failure that says the host refused the credential (HTTP 401). */\nexport function isCredentialRefusal(error: unknown): boolean {\n return (\n typeof error === 'object' &&\n error !== null &&\n 'status' in error &&\n (error as { status?: unknown }).status === 401\n );\n}\n\n/**\n * Run one request with the current credential and, when the host refuses it\n * (401), renew the credential once and run the request again — the REST twin\n * of the statewire transport's `staleAuth` retry. A second refusal, and every\n * other failure (a 403 included: renewing cannot grant access), propagates\n * unchanged. A request made without a token (an API key alone) is not\n * retried, and neither is one whose renewal produced the same token (a static\n * token) or none: there is nothing new to send. A refused request never ran\n * on the server, so repeating a write is safe.\n */\nexport async function withCredentialRenewal<TAuth extends { token?: string | null }, TResult>(\n getAuth: CredentialResolver<TAuth>,\n run: (auth: TAuth) => Promise<TResult>,\n): Promise<TResult> {\n const auth = await getAuth({ force: false });\n try {\n return await run(auth);\n } catch (error) {\n if (!isCredentialRefusal(error) || !auth.token) throw error;\n const renewed = await getAuth({ force: true });\n if (!renewed.token || renewed.token === auth.token) throw error;\n return run(renewed);\n }\n}\n\n/**\n * Send one raw `fetch` with the credential headers `source` resolves and, when\n * the host refuses a bearer token (401), renew it once (`force: true`) and\n * resend with fresh headers: the statewire transport's `staleAuth` contract.\n * `send` runs once per attempt, so it builds its request body each time (a\n * `FormData` or stream is single-use). A request sent with the API key alone,\n * from a source with no live getter, or whose renewal produced the same token\n * (a static one) or none, is not resent: there is nothing new to send.\n * Returns the last response either way.\n */\nexport async function sendWithCredentialRenewal(\n source: StatewireAuthSource,\n send: (headers: Record<string, string>) => Promise<Response>,\n): Promise<Response> {\n const headers = await resolveStatewireAuthHeaders(source, { staleAuth: false });\n const response = await send(headers);\n if (response.status !== 401 || !source.getToken || !headers.Authorization) return response;\n const renewed = await resolveStatewireAuthHeaders(source, { staleAuth: true });\n if (!renewed.Authorization || renewed.Authorization === headers.Authorization) return response;\n return send(renewed);\n}\n"],"names":[],"mappings":";AAUO,SAAS,oBAAoB,OAAyB;AAC3D,SACE,OAAO,UAAU,YACjB,UAAU,QACV,YAAY,SACX,MAA+B,WAAW;AAE/C;AAYA,eAAsB,sBACpB,SACA,KACkB;AAClB,QAAM,OAAO,MAAM,QAAQ,EAAE,OAAO,OAAO;AAC3C,MAAI;AACF,WAAO,MAAM,IAAI,IAAI;AAAA,EACvB,SAAS,OAAO;AACd,QAAI,CAAC,oBAAoB,KAAK,KAAK,CAAC,KAAK,MAAO,OAAM;AACtD,UAAM,UAAU,MAAM,QAAQ,EAAE,OAAO,MAAM;AAC7C,QAAI,CAAC,QAAQ,SAAS,QAAQ,UAAU,KAAK,MAAO,OAAM;AAC1D,WAAO,IAAI,OAAO;AAAA,EACpB;AACF;AAYA,eAAsB,0BACpB,QACA,MACmB;AACnB,QAAM,UAAU,MAAM,4BAA4B,QAAQ,EAAE,WAAW,OAAO;AAC9E,QAAM,WAAW,MAAM,KAAK,OAAO;AACnC,MAAI,SAAS,WAAW,OAAO,CAAC,OAAO,YAAY,CAAC,QAAQ,cAAe,QAAO;AAClF,QAAM,UAAU,MAAM,4BAA4B,QAAQ,EAAE,WAAW,MAAM;AAC7E,MAAI,CAAC,QAAQ,iBAAiB,QAAQ,kBAAkB,QAAQ,cAAe,QAAO;AACtF,SAAO,KAAK,OAAO;AACrB;"}
@@ -5,7 +5,7 @@ const react = require("react");
5
5
  const mint = require("../collab/mint.cjs");
6
6
  const errors = require("../diagnostics/errors.cjs");
7
7
  const AthenaContext = require("../provider/AthenaContext.cjs");
8
- const statewireAuthHeaders = require("../runtime/statewire-auth-headers.cjs");
8
+ const credentialRenewal = require("../runtime/credential-renewal.cjs");
9
9
  const credentialShape = require("../threads/credential-shape.cjs");
10
10
  const ATHENA_REPO_SEARCH_PATH = "/api/search/repositories";
11
11
  function repoSearchRequestBody(params) {
@@ -24,8 +24,7 @@ function isAbortError(error) {
24
24
  async function fetchRepoSearch(backendUrl, auth, params, opts = {}) {
25
25
  const url = `${mint.getAthenaApiBaseUrl(backendUrl)}${ATHENA_REPO_SEARCH_PATH}`;
26
26
  const body = JSON.stringify(repoSearchRequestBody(params));
27
- const post = async (staleAuth) => {
28
- const headers = await statewireAuthHeaders.resolveStatewireAuthHeaders(auth, { staleAuth });
27
+ const post = async (headers) => {
29
28
  try {
30
29
  return await fetch(url, {
31
30
  method: "POST",
@@ -43,8 +42,7 @@ async function fetchRepoSearch(backendUrl, auth, params, opts = {}) {
43
42
  });
44
43
  }
45
44
  };
46
- let res = await post(false);
47
- if (res.status === 401 && auth.getToken) res = await post(true);
45
+ const res = await credentialRenewal.sendWithCredentialRenewal(auth, post);
48
46
  if (!res.ok) {
49
47
  const refused = res.status === 401 || res.status === 403;
50
48
  throw new errors.AthenaSdkError({
@@ -1 +1 @@
1
- {"version":3,"file":"useAthenaRepoSearch.cjs","sources":["../../src/search/useAthenaRepoSearch.ts"],"sourcesContent":["/**\n * Code search over the workspace's indexed repositories —\n * `POST /api/search/repositories` (agora `web/api/search/repositories.py`),\n * the HTTP face of Clio's repository source for a Devin-style \"Ask\" surface\n * that searches the monorepo without an agent turn. Athena platform staff\n * only (the account-level `is_admin`, not a workspace role) on the managed\n * cloud today: a non-staff caller reads `403`, a private deployment `404`.\n */\nimport {\n keepPreviousData,\n QueryClient,\n QueryClientContext,\n useQuery,\n type UseQueryResult,\n} from '@tanstack/react-query';\nimport { useContext, useRef, useState } from 'react';\nimport { getAthenaApiBaseUrl } from '../collab/mint';\nimport { AthenaSdkError } from '../diagnostics/errors';\nimport { useAthenaConfig } from '../provider/AthenaContext';\nimport {\n resolveStatewireAuthHeaders,\n type StatewireAuthSource,\n} from '../runtime/statewire-auth-headers';\nimport { credentialShape, sameCredentialShape } from '../threads/credential-shape';\n\nexport type AthenaRepoSearchMode = 'hybrid' | 'lexical';\n\n/** One file range of one indexed repository, as the route serves it. */\nexport interface AthenaRepoSearchHit {\n /** The GitHub catalog asset the repository is connected through. */\n catalogAssetId: string;\n /** `owner/repo`; `null` when the catalog names no repository. */\n repoFullName: string | null;\n /** Repo-relative, `/`-separated. */\n path: string;\n /** 1-based, inclusive. */\n startLine: number;\n endLine: number;\n /** The enclosing declaration (`Class.method`) when the chunk's header named one. */\n symbol: string | null;\n /** Clio's language name for the file (`python`, `typescript`, …). */\n language: string | null;\n /** `code` (an AST chunk) or `window` (a Markdown heading window). */\n unitType: string;\n score: number;\n /** The indexed commit the hit was read at. */\n commitSha: string | null;\n /** `{origin}/{owner}/{repo}/blob/{sha}/{path}#L{a}-L{b}`, GHES-aware. */\n githubUrl: string | null;\n /** The chunk's lines, header line removed, line breaks preserved. */\n snippet: string;\n /** `asset_<catalog>?anchor=file_line_range&…` — what `read_asset` and a citation take. */\n readAssetReference: string;\n}\n\nexport interface AthenaRepoSearchResponse {\n /** Best first, at most `limit`. */\n hits: AthenaRepoSearchHit[];\n tookMs: number;\n mode: AthenaRepoSearchMode;\n}\n\nexport interface AthenaRepoSearchParams {\n query: string;\n /** 1–25; the route's default is 10. */\n limit?: number;\n /** `hybrid` (default) or `lexical`. */\n mode?: AthenaRepoSearchMode;\n /** Search these GitHub catalog assets only; empty means every indexed repository. */\n repositoryAssetIds?: string[];\n /** gitignore-style globs a hit's path must match (`agora/**\\/*.py`, `docs/`). */\n paths?: string[];\n /** Clio language names a hit's file must be indexed as. */\n languages?: string[];\n}\n\nexport interface UseAthenaRepoSearchOptions extends AthenaRepoSearchParams {\n /** `false` holds the query idle (a blank `query` does too). Default `true`. */\n enabled?: boolean;\n}\n\n/** The route's JSON body — snake_case, unset filters omitted. */\nexport interface AthenaRepoSearchRequestBody {\n query: string;\n limit?: number;\n mode?: AthenaRepoSearchMode;\n repository_asset_ids?: string[];\n paths?: string[];\n languages?: string[];\n}\n\nexport const ATHENA_REPO_SEARCH_PATH = '/api/search/repositories';\n\nexport function repoSearchRequestBody(params: AthenaRepoSearchParams): AthenaRepoSearchRequestBody {\n const body: AthenaRepoSearchRequestBody = { query: params.query.trim() };\n if (params.limit !== undefined) body.limit = params.limit;\n if (params.mode !== undefined) body.mode = params.mode;\n if (params.repositoryAssetIds?.length) body.repository_asset_ids = params.repositoryAssetIds;\n if (params.paths?.length) body.paths = params.paths;\n if (params.languages?.length) body.languages = params.languages;\n return body;\n}\n\n/** A `fetch` the caller cancelled (unmount, a newer query): lifecycle, not failure. */\nfunction isAbortError(error: unknown): boolean {\n return (error instanceof DOMException || error instanceof Error) && error.name === 'AbortError';\n}\n\n/**\n * One search request. The credential rides `Authorization: Bearer` (a token,\n * resolved live through `getToken` when the host provided one) or\n * `X-API-KEY`; with neither, a same-origin host's session cookie rides\n * automatically, as the SDK's other REST helpers allow. A `401` gets one\n * forced re-mint through `getToken` (`force: true`, the statewire\n * transport's `staleAuth` contract) — a getter that caches until forced would\n * otherwise resend the same expired token on every retry. Every failure is an\n * `AthenaSdkError`, transport rejections included; only an abort keeps its\n * own identity.\n */\nexport async function fetchRepoSearch(\n backendUrl: string,\n auth: StatewireAuthSource,\n params: AthenaRepoSearchParams,\n opts: { signal?: AbortSignal } = {},\n): Promise<AthenaRepoSearchResponse> {\n const url = `${getAthenaApiBaseUrl(backendUrl)}${ATHENA_REPO_SEARCH_PATH}`;\n const body = JSON.stringify(repoSearchRequestBody(params));\n const post = async (staleAuth: boolean): Promise<Response> => {\n const headers = await resolveStatewireAuthHeaders(auth, { staleAuth });\n try {\n return await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...headers },\n body,\n signal: opts.signal ?? null,\n });\n } catch (error) {\n if (isAbortError(error)) throw error;\n throw new AthenaSdkError({\n code: 'repo_search_failed',\n message: 'Repository search could not reach the Agora API.',\n cause: error,\n hint: 'Check the network and that backendUrl points at the Agora API base.',\n });\n }\n };\n let res = await post(false);\n if (res.status === 401 && auth.getToken) res = await post(true);\n if (!res.ok) {\n const refused = res.status === 401 || res.status === 403;\n throw new AthenaSdkError({\n code: refused ? 'auth_rejected' : 'repo_search_failed',\n message: `Repository search failed: ${res.status}`,\n status: res.status,\n detail: await res.json().catch(() => undefined),\n hint: refused\n ? 'Repository search is limited to Athena platform staff on the managed cloud; check the credential.'\n : res.status === 404\n ? 'Repository search is not offered on this deployment (managed cloud only).'\n : 'Check backendUrl points at the Agora API base and that Clio is available.',\n });\n }\n return (await res.json()) as AthenaRepoSearchResponse;\n}\n\n/**\n * Search the indexed repositories through TanStack Query with the provider's\n * `backendUrl` and credentials. Keyed by the non-secret request and a\n * credential generation (a counter that advances when the host's credentials\n * change), never by the credential itself. Uses the host's `QueryClient` when\n * one is in context (`enableThreadList` mounts the SDK's own) and a\n * hook-scoped client otherwise, so it works under any `<AthenaProvider>`.\n */\nexport function useAthenaRepoSearch({\n enabled = true,\n ...params\n}: UseAthenaRepoSearchOptions): UseQueryResult<AthenaRepoSearchResponse, Error> {\n const config = useAthenaConfig();\n const { backendUrl, apiKey, getToken } = config;\n const credentialsRef = useRef(credentialShape({ apiKey, token: config.token, getToken }));\n const generationRef = useRef(0);\n const currentCredentials = credentialShape({ apiKey, token: config.token, getToken });\n if (!sameCredentialShape(credentialsRef.current, currentCredentials)) {\n credentialsRef.current = currentCredentials;\n generationRef.current += 1;\n }\n const body = repoSearchRequestBody(params);\n const contextClient = useContext(QueryClientContext);\n const [fallbackClient] = useState(() => (contextClient ? undefined : new QueryClient()));\n return useQuery(\n {\n queryKey: ['athena-sdk', 'repo-search', backendUrl, generationRef.current, body],\n // `config.token` is read at fetch time: the provider serves the latest\n // token through a getter, so rotations reach the request.\n queryFn: ({ signal }) =>\n fetchRepoSearch(backendUrl, { apiKey, token: config.token, getToken }, params, { signal }),\n enabled: enabled && body.query.length > 0 && backendUrl.length > 0,\n staleTime: 30_000,\n placeholderData: keepPreviousData,\n refetchOnWindowFocus: false,\n },\n contextClient ?? fallbackClient,\n );\n}\n"],"names":["getAthenaApiBaseUrl","resolveStatewireAuthHeaders","AthenaSdkError","useAthenaConfig","useRef","credentialShape","sameCredentialShape","useContext","QueryClientContext","useState","QueryClient","useQuery","keepPreviousData"],"mappings":"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"}
1
+ {"version":3,"file":"useAthenaRepoSearch.cjs","sources":["../../src/search/useAthenaRepoSearch.ts"],"sourcesContent":["/**\n * Code search over the workspace's indexed repositories —\n * `POST /api/search/repositories` (agora `web/api/search/repositories.py`),\n * the HTTP face of Clio's repository source for a Devin-style \"Ask\" surface\n * that searches the monorepo without an agent turn. Athena platform staff\n * only (the account-level `is_admin`, not a workspace role) on the managed\n * cloud today: a non-staff caller reads `403`, a private deployment `404`.\n */\nimport {\n keepPreviousData,\n QueryClient,\n QueryClientContext,\n useQuery,\n type UseQueryResult,\n} from '@tanstack/react-query';\nimport { useContext, useRef, useState } from 'react';\nimport { getAthenaApiBaseUrl } from '../collab/mint';\nimport { AthenaSdkError } from '../diagnostics/errors';\nimport { useAthenaConfig } from '../provider/AthenaContext';\nimport { sendWithCredentialRenewal } from '../runtime/credential-renewal';\nimport type { StatewireAuthSource } from '../runtime/statewire-auth-headers';\nimport { credentialShape, sameCredentialShape } from '../threads/credential-shape';\n\nexport type AthenaRepoSearchMode = 'hybrid' | 'lexical';\n\n/** One file range of one indexed repository, as the route serves it. */\nexport interface AthenaRepoSearchHit {\n /** The GitHub catalog asset the repository is connected through. */\n catalogAssetId: string;\n /** `owner/repo`; `null` when the catalog names no repository. */\n repoFullName: string | null;\n /** Repo-relative, `/`-separated. */\n path: string;\n /** 1-based, inclusive. */\n startLine: number;\n endLine: number;\n /** The enclosing declaration (`Class.method`) when the chunk's header named one. */\n symbol: string | null;\n /** Clio's language name for the file (`python`, `typescript`, …). */\n language: string | null;\n /** `code` (an AST chunk) or `window` (a Markdown heading window). */\n unitType: string;\n score: number;\n /** The indexed commit the hit was read at. */\n commitSha: string | null;\n /** `{origin}/{owner}/{repo}/blob/{sha}/{path}#L{a}-L{b}`, GHES-aware. */\n githubUrl: string | null;\n /** The chunk's lines, header line removed, line breaks preserved. */\n snippet: string;\n /** `asset_<catalog>?anchor=file_line_range&…` — what `read_asset` and a citation take. */\n readAssetReference: string;\n}\n\nexport interface AthenaRepoSearchResponse {\n /** Best first, at most `limit`. */\n hits: AthenaRepoSearchHit[];\n tookMs: number;\n mode: AthenaRepoSearchMode;\n}\n\nexport interface AthenaRepoSearchParams {\n query: string;\n /** 1–25; the route's default is 10. */\n limit?: number;\n /** `hybrid` (default) or `lexical`. */\n mode?: AthenaRepoSearchMode;\n /** Search these GitHub catalog assets only; empty means every indexed repository. */\n repositoryAssetIds?: string[];\n /** gitignore-style globs a hit's path must match (`agora/**\\/*.py`, `docs/`). */\n paths?: string[];\n /** Clio language names a hit's file must be indexed as. */\n languages?: string[];\n}\n\nexport interface UseAthenaRepoSearchOptions extends AthenaRepoSearchParams {\n /** `false` holds the query idle (a blank `query` does too). Default `true`. */\n enabled?: boolean;\n}\n\n/** The route's JSON body — snake_case, unset filters omitted. */\nexport interface AthenaRepoSearchRequestBody {\n query: string;\n limit?: number;\n mode?: AthenaRepoSearchMode;\n repository_asset_ids?: string[];\n paths?: string[];\n languages?: string[];\n}\n\nexport const ATHENA_REPO_SEARCH_PATH = '/api/search/repositories';\n\nexport function repoSearchRequestBody(params: AthenaRepoSearchParams): AthenaRepoSearchRequestBody {\n const body: AthenaRepoSearchRequestBody = { query: params.query.trim() };\n if (params.limit !== undefined) body.limit = params.limit;\n if (params.mode !== undefined) body.mode = params.mode;\n if (params.repositoryAssetIds?.length) body.repository_asset_ids = params.repositoryAssetIds;\n if (params.paths?.length) body.paths = params.paths;\n if (params.languages?.length) body.languages = params.languages;\n return body;\n}\n\n/** A `fetch` the caller cancelled (unmount, a newer query): lifecycle, not failure. */\nfunction isAbortError(error: unknown): boolean {\n return (error instanceof DOMException || error instanceof Error) && error.name === 'AbortError';\n}\n\n/**\n * One search request. The credential rides `Authorization: Bearer` (a token,\n * resolved live through `getToken` when the host provided one) or\n * `X-API-KEY`; with neither, a same-origin host's session cookie rides\n * automatically, as the SDK's other REST helpers allow. A refused bearer\n * (`401`) gets one forced re-mint through `getToken` (`force: true`, the\n * statewire transport's `staleAuth` contract; `sendWithCredentialRenewal`) — a\n * getter that caches until forced would otherwise resend the same expired\n * token on every retry. Every failure is an `AthenaSdkError`, transport\n * rejections included; only an abort keeps its own identity.\n */\nexport async function fetchRepoSearch(\n backendUrl: string,\n auth: StatewireAuthSource,\n params: AthenaRepoSearchParams,\n opts: { signal?: AbortSignal } = {},\n): Promise<AthenaRepoSearchResponse> {\n const url = `${getAthenaApiBaseUrl(backendUrl)}${ATHENA_REPO_SEARCH_PATH}`;\n const body = JSON.stringify(repoSearchRequestBody(params));\n const post = async (headers: Record<string, string>): Promise<Response> => {\n try {\n return await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...headers },\n body,\n signal: opts.signal ?? null,\n });\n } catch (error) {\n if (isAbortError(error)) throw error;\n throw new AthenaSdkError({\n code: 'repo_search_failed',\n message: 'Repository search could not reach the Agora API.',\n cause: error,\n hint: 'Check the network and that backendUrl points at the Agora API base.',\n });\n }\n };\n const res = await sendWithCredentialRenewal(auth, post);\n if (!res.ok) {\n const refused = res.status === 401 || res.status === 403;\n throw new AthenaSdkError({\n code: refused ? 'auth_rejected' : 'repo_search_failed',\n message: `Repository search failed: ${res.status}`,\n status: res.status,\n detail: await res.json().catch(() => undefined),\n hint: refused\n ? 'Repository search is limited to Athena platform staff on the managed cloud; check the credential.'\n : res.status === 404\n ? 'Repository search is not offered on this deployment (managed cloud only).'\n : 'Check backendUrl points at the Agora API base and that Clio is available.',\n });\n }\n return (await res.json()) as AthenaRepoSearchResponse;\n}\n\n/**\n * Search the indexed repositories through TanStack Query with the provider's\n * `backendUrl` and credentials. Keyed by the non-secret request and a\n * credential generation (a counter that advances when the host's credentials\n * change), never by the credential itself. Uses the host's `QueryClient` when\n * one is in context (`enableThreadList` mounts the SDK's own) and a\n * hook-scoped client otherwise, so it works under any `<AthenaProvider>`.\n */\nexport function useAthenaRepoSearch({\n enabled = true,\n ...params\n}: UseAthenaRepoSearchOptions): UseQueryResult<AthenaRepoSearchResponse, Error> {\n const config = useAthenaConfig();\n const { backendUrl, apiKey, getToken } = config;\n const credentialsRef = useRef(credentialShape({ apiKey, token: config.token, getToken }));\n const generationRef = useRef(0);\n const currentCredentials = credentialShape({ apiKey, token: config.token, getToken });\n if (!sameCredentialShape(credentialsRef.current, currentCredentials)) {\n credentialsRef.current = currentCredentials;\n generationRef.current += 1;\n }\n const body = repoSearchRequestBody(params);\n const contextClient = useContext(QueryClientContext);\n const [fallbackClient] = useState(() => (contextClient ? undefined : new QueryClient()));\n return useQuery(\n {\n queryKey: ['athena-sdk', 'repo-search', backendUrl, generationRef.current, body],\n // `config.token` is read at fetch time: the provider serves the latest\n // token through a getter, so rotations reach the request.\n queryFn: ({ signal }) =>\n fetchRepoSearch(backendUrl, { apiKey, token: config.token, getToken }, params, { signal }),\n enabled: enabled && body.query.length > 0 && backendUrl.length > 0,\n staleTime: 30_000,\n placeholderData: keepPreviousData,\n refetchOnWindowFocus: false,\n },\n contextClient ?? fallbackClient,\n );\n}\n"],"names":["getAthenaApiBaseUrl","AthenaSdkError","sendWithCredentialRenewal","useAthenaConfig","useRef","credentialShape","sameCredentialShape","useContext","QueryClientContext","useState","QueryClient","useQuery","keepPreviousData"],"mappings":"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"}
@@ -7,7 +7,7 @@
7
7
  * cloud today: a non-staff caller reads `403`, a private deployment `404`.
8
8
  */
9
9
  import { type UseQueryResult } from '@tanstack/react-query';
10
- import { type StatewireAuthSource } from '../runtime/statewire-auth-headers';
10
+ import type { StatewireAuthSource } from '../runtime/statewire-auth-headers';
11
11
  export type AthenaRepoSearchMode = 'hybrid' | 'lexical';
12
12
  /** One file range of one indexed repository, as the route serves it. */
13
13
  export interface AthenaRepoSearchHit {
@@ -74,12 +74,12 @@ export declare function repoSearchRequestBody(params: AthenaRepoSearchParams): A
74
74
  * One search request. The credential rides `Authorization: Bearer` (a token,
75
75
  * resolved live through `getToken` when the host provided one) or
76
76
  * `X-API-KEY`; with neither, a same-origin host's session cookie rides
77
- * automatically, as the SDK's other REST helpers allow. A `401` gets one
78
- * forced re-mint through `getToken` (`force: true`, the statewire
79
- * transport's `staleAuth` contract) — a getter that caches until forced would
80
- * otherwise resend the same expired token on every retry. Every failure is an
81
- * `AthenaSdkError`, transport rejections included; only an abort keeps its
82
- * own identity.
77
+ * automatically, as the SDK's other REST helpers allow. A refused bearer
78
+ * (`401`) gets one forced re-mint through `getToken` (`force: true`, the
79
+ * statewire transport's `staleAuth` contract; `sendWithCredentialRenewal`) — a
80
+ * getter that caches until forced would otherwise resend the same expired
81
+ * token on every retry. Every failure is an `AthenaSdkError`, transport
82
+ * rejections included; only an abort keeps its own identity.
83
83
  */
84
84
  export declare function fetchRepoSearch(backendUrl: string, auth: StatewireAuthSource, params: AthenaRepoSearchParams, opts?: {
85
85
  signal?: AbortSignal;
@@ -3,7 +3,7 @@ import { useRef, useContext, useState } from "react";
3
3
  import { getAthenaApiBaseUrl } from "../collab/mint.js";
4
4
  import { AthenaSdkError } from "../diagnostics/errors.js";
5
5
  import { useAthenaConfig } from "../provider/AthenaContext.js";
6
- import { resolveStatewireAuthHeaders } from "../runtime/statewire-auth-headers.js";
6
+ import { sendWithCredentialRenewal } from "../runtime/credential-renewal.js";
7
7
  import { credentialShape, sameCredentialShape } from "../threads/credential-shape.js";
8
8
  const ATHENA_REPO_SEARCH_PATH = "/api/search/repositories";
9
9
  function repoSearchRequestBody(params) {
@@ -22,8 +22,7 @@ function isAbortError(error) {
22
22
  async function fetchRepoSearch(backendUrl, auth, params, opts = {}) {
23
23
  const url = `${getAthenaApiBaseUrl(backendUrl)}${ATHENA_REPO_SEARCH_PATH}`;
24
24
  const body = JSON.stringify(repoSearchRequestBody(params));
25
- const post = async (staleAuth) => {
26
- const headers = await resolveStatewireAuthHeaders(auth, { staleAuth });
25
+ const post = async (headers) => {
27
26
  try {
28
27
  return await fetch(url, {
29
28
  method: "POST",
@@ -41,8 +40,7 @@ async function fetchRepoSearch(backendUrl, auth, params, opts = {}) {
41
40
  });
42
41
  }
43
42
  };
44
- let res = await post(false);
45
- if (res.status === 401 && auth.getToken) res = await post(true);
43
+ const res = await sendWithCredentialRenewal(auth, post);
46
44
  if (!res.ok) {
47
45
  const refused = res.status === 401 || res.status === 403;
48
46
  throw new AthenaSdkError({
@@ -1 +1 @@
1
- {"version":3,"file":"useAthenaRepoSearch.js","sources":["../../src/search/useAthenaRepoSearch.ts"],"sourcesContent":["/**\n * Code search over the workspace's indexed repositories —\n * `POST /api/search/repositories` (agora `web/api/search/repositories.py`),\n * the HTTP face of Clio's repository source for a Devin-style \"Ask\" surface\n * that searches the monorepo without an agent turn. Athena platform staff\n * only (the account-level `is_admin`, not a workspace role) on the managed\n * cloud today: a non-staff caller reads `403`, a private deployment `404`.\n */\nimport {\n keepPreviousData,\n QueryClient,\n QueryClientContext,\n useQuery,\n type UseQueryResult,\n} from '@tanstack/react-query';\nimport { useContext, useRef, useState } from 'react';\nimport { getAthenaApiBaseUrl } from '../collab/mint';\nimport { AthenaSdkError } from '../diagnostics/errors';\nimport { useAthenaConfig } from '../provider/AthenaContext';\nimport {\n resolveStatewireAuthHeaders,\n type StatewireAuthSource,\n} from '../runtime/statewire-auth-headers';\nimport { credentialShape, sameCredentialShape } from '../threads/credential-shape';\n\nexport type AthenaRepoSearchMode = 'hybrid' | 'lexical';\n\n/** One file range of one indexed repository, as the route serves it. */\nexport interface AthenaRepoSearchHit {\n /** The GitHub catalog asset the repository is connected through. */\n catalogAssetId: string;\n /** `owner/repo`; `null` when the catalog names no repository. */\n repoFullName: string | null;\n /** Repo-relative, `/`-separated. */\n path: string;\n /** 1-based, inclusive. */\n startLine: number;\n endLine: number;\n /** The enclosing declaration (`Class.method`) when the chunk's header named one. */\n symbol: string | null;\n /** Clio's language name for the file (`python`, `typescript`, …). */\n language: string | null;\n /** `code` (an AST chunk) or `window` (a Markdown heading window). */\n unitType: string;\n score: number;\n /** The indexed commit the hit was read at. */\n commitSha: string | null;\n /** `{origin}/{owner}/{repo}/blob/{sha}/{path}#L{a}-L{b}`, GHES-aware. */\n githubUrl: string | null;\n /** The chunk's lines, header line removed, line breaks preserved. */\n snippet: string;\n /** `asset_<catalog>?anchor=file_line_range&…` — what `read_asset` and a citation take. */\n readAssetReference: string;\n}\n\nexport interface AthenaRepoSearchResponse {\n /** Best first, at most `limit`. */\n hits: AthenaRepoSearchHit[];\n tookMs: number;\n mode: AthenaRepoSearchMode;\n}\n\nexport interface AthenaRepoSearchParams {\n query: string;\n /** 1–25; the route's default is 10. */\n limit?: number;\n /** `hybrid` (default) or `lexical`. */\n mode?: AthenaRepoSearchMode;\n /** Search these GitHub catalog assets only; empty means every indexed repository. */\n repositoryAssetIds?: string[];\n /** gitignore-style globs a hit's path must match (`agora/**\\/*.py`, `docs/`). */\n paths?: string[];\n /** Clio language names a hit's file must be indexed as. */\n languages?: string[];\n}\n\nexport interface UseAthenaRepoSearchOptions extends AthenaRepoSearchParams {\n /** `false` holds the query idle (a blank `query` does too). Default `true`. */\n enabled?: boolean;\n}\n\n/** The route's JSON body — snake_case, unset filters omitted. */\nexport interface AthenaRepoSearchRequestBody {\n query: string;\n limit?: number;\n mode?: AthenaRepoSearchMode;\n repository_asset_ids?: string[];\n paths?: string[];\n languages?: string[];\n}\n\nexport const ATHENA_REPO_SEARCH_PATH = '/api/search/repositories';\n\nexport function repoSearchRequestBody(params: AthenaRepoSearchParams): AthenaRepoSearchRequestBody {\n const body: AthenaRepoSearchRequestBody = { query: params.query.trim() };\n if (params.limit !== undefined) body.limit = params.limit;\n if (params.mode !== undefined) body.mode = params.mode;\n if (params.repositoryAssetIds?.length) body.repository_asset_ids = params.repositoryAssetIds;\n if (params.paths?.length) body.paths = params.paths;\n if (params.languages?.length) body.languages = params.languages;\n return body;\n}\n\n/** A `fetch` the caller cancelled (unmount, a newer query): lifecycle, not failure. */\nfunction isAbortError(error: unknown): boolean {\n return (error instanceof DOMException || error instanceof Error) && error.name === 'AbortError';\n}\n\n/**\n * One search request. The credential rides `Authorization: Bearer` (a token,\n * resolved live through `getToken` when the host provided one) or\n * `X-API-KEY`; with neither, a same-origin host's session cookie rides\n * automatically, as the SDK's other REST helpers allow. A `401` gets one\n * forced re-mint through `getToken` (`force: true`, the statewire\n * transport's `staleAuth` contract) — a getter that caches until forced would\n * otherwise resend the same expired token on every retry. Every failure is an\n * `AthenaSdkError`, transport rejections included; only an abort keeps its\n * own identity.\n */\nexport async function fetchRepoSearch(\n backendUrl: string,\n auth: StatewireAuthSource,\n params: AthenaRepoSearchParams,\n opts: { signal?: AbortSignal } = {},\n): Promise<AthenaRepoSearchResponse> {\n const url = `${getAthenaApiBaseUrl(backendUrl)}${ATHENA_REPO_SEARCH_PATH}`;\n const body = JSON.stringify(repoSearchRequestBody(params));\n const post = async (staleAuth: boolean): Promise<Response> => {\n const headers = await resolveStatewireAuthHeaders(auth, { staleAuth });\n try {\n return await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...headers },\n body,\n signal: opts.signal ?? null,\n });\n } catch (error) {\n if (isAbortError(error)) throw error;\n throw new AthenaSdkError({\n code: 'repo_search_failed',\n message: 'Repository search could not reach the Agora API.',\n cause: error,\n hint: 'Check the network and that backendUrl points at the Agora API base.',\n });\n }\n };\n let res = await post(false);\n if (res.status === 401 && auth.getToken) res = await post(true);\n if (!res.ok) {\n const refused = res.status === 401 || res.status === 403;\n throw new AthenaSdkError({\n code: refused ? 'auth_rejected' : 'repo_search_failed',\n message: `Repository search failed: ${res.status}`,\n status: res.status,\n detail: await res.json().catch(() => undefined),\n hint: refused\n ? 'Repository search is limited to Athena platform staff on the managed cloud; check the credential.'\n : res.status === 404\n ? 'Repository search is not offered on this deployment (managed cloud only).'\n : 'Check backendUrl points at the Agora API base and that Clio is available.',\n });\n }\n return (await res.json()) as AthenaRepoSearchResponse;\n}\n\n/**\n * Search the indexed repositories through TanStack Query with the provider's\n * `backendUrl` and credentials. Keyed by the non-secret request and a\n * credential generation (a counter that advances when the host's credentials\n * change), never by the credential itself. Uses the host's `QueryClient` when\n * one is in context (`enableThreadList` mounts the SDK's own) and a\n * hook-scoped client otherwise, so it works under any `<AthenaProvider>`.\n */\nexport function useAthenaRepoSearch({\n enabled = true,\n ...params\n}: UseAthenaRepoSearchOptions): UseQueryResult<AthenaRepoSearchResponse, Error> {\n const config = useAthenaConfig();\n const { backendUrl, apiKey, getToken } = config;\n const credentialsRef = useRef(credentialShape({ apiKey, token: config.token, getToken }));\n const generationRef = useRef(0);\n const currentCredentials = credentialShape({ apiKey, token: config.token, getToken });\n if (!sameCredentialShape(credentialsRef.current, currentCredentials)) {\n credentialsRef.current = currentCredentials;\n generationRef.current += 1;\n }\n const body = repoSearchRequestBody(params);\n const contextClient = useContext(QueryClientContext);\n const [fallbackClient] = useState(() => (contextClient ? undefined : new QueryClient()));\n return useQuery(\n {\n queryKey: ['athena-sdk', 'repo-search', backendUrl, generationRef.current, body],\n // `config.token` is read at fetch time: the provider serves the latest\n // token through a getter, so rotations reach the request.\n queryFn: ({ signal }) =>\n fetchRepoSearch(backendUrl, { apiKey, token: config.token, getToken }, params, { signal }),\n enabled: enabled && body.query.length > 0 && backendUrl.length > 0,\n staleTime: 30_000,\n placeholderData: keepPreviousData,\n refetchOnWindowFocus: false,\n },\n contextClient ?? fallbackClient,\n );\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useAthenaRepoSearch.js","sources":["../../src/search/useAthenaRepoSearch.ts"],"sourcesContent":["/**\n * Code search over the workspace's indexed repositories —\n * `POST /api/search/repositories` (agora `web/api/search/repositories.py`),\n * the HTTP face of Clio's repository source for a Devin-style \"Ask\" surface\n * that searches the monorepo without an agent turn. Athena platform staff\n * only (the account-level `is_admin`, not a workspace role) on the managed\n * cloud today: a non-staff caller reads `403`, a private deployment `404`.\n */\nimport {\n keepPreviousData,\n QueryClient,\n QueryClientContext,\n useQuery,\n type UseQueryResult,\n} from '@tanstack/react-query';\nimport { useContext, useRef, useState } from 'react';\nimport { getAthenaApiBaseUrl } from '../collab/mint';\nimport { AthenaSdkError } from '../diagnostics/errors';\nimport { useAthenaConfig } from '../provider/AthenaContext';\nimport { sendWithCredentialRenewal } from '../runtime/credential-renewal';\nimport type { StatewireAuthSource } from '../runtime/statewire-auth-headers';\nimport { credentialShape, sameCredentialShape } from '../threads/credential-shape';\n\nexport type AthenaRepoSearchMode = 'hybrid' | 'lexical';\n\n/** One file range of one indexed repository, as the route serves it. */\nexport interface AthenaRepoSearchHit {\n /** The GitHub catalog asset the repository is connected through. */\n catalogAssetId: string;\n /** `owner/repo`; `null` when the catalog names no repository. */\n repoFullName: string | null;\n /** Repo-relative, `/`-separated. */\n path: string;\n /** 1-based, inclusive. */\n startLine: number;\n endLine: number;\n /** The enclosing declaration (`Class.method`) when the chunk's header named one. */\n symbol: string | null;\n /** Clio's language name for the file (`python`, `typescript`, …). */\n language: string | null;\n /** `code` (an AST chunk) or `window` (a Markdown heading window). */\n unitType: string;\n score: number;\n /** The indexed commit the hit was read at. */\n commitSha: string | null;\n /** `{origin}/{owner}/{repo}/blob/{sha}/{path}#L{a}-L{b}`, GHES-aware. */\n githubUrl: string | null;\n /** The chunk's lines, header line removed, line breaks preserved. */\n snippet: string;\n /** `asset_<catalog>?anchor=file_line_range&…` — what `read_asset` and a citation take. */\n readAssetReference: string;\n}\n\nexport interface AthenaRepoSearchResponse {\n /** Best first, at most `limit`. */\n hits: AthenaRepoSearchHit[];\n tookMs: number;\n mode: AthenaRepoSearchMode;\n}\n\nexport interface AthenaRepoSearchParams {\n query: string;\n /** 1–25; the route's default is 10. */\n limit?: number;\n /** `hybrid` (default) or `lexical`. */\n mode?: AthenaRepoSearchMode;\n /** Search these GitHub catalog assets only; empty means every indexed repository. */\n repositoryAssetIds?: string[];\n /** gitignore-style globs a hit's path must match (`agora/**\\/*.py`, `docs/`). */\n paths?: string[];\n /** Clio language names a hit's file must be indexed as. */\n languages?: string[];\n}\n\nexport interface UseAthenaRepoSearchOptions extends AthenaRepoSearchParams {\n /** `false` holds the query idle (a blank `query` does too). Default `true`. */\n enabled?: boolean;\n}\n\n/** The route's JSON body — snake_case, unset filters omitted. */\nexport interface AthenaRepoSearchRequestBody {\n query: string;\n limit?: number;\n mode?: AthenaRepoSearchMode;\n repository_asset_ids?: string[];\n paths?: string[];\n languages?: string[];\n}\n\nexport const ATHENA_REPO_SEARCH_PATH = '/api/search/repositories';\n\nexport function repoSearchRequestBody(params: AthenaRepoSearchParams): AthenaRepoSearchRequestBody {\n const body: AthenaRepoSearchRequestBody = { query: params.query.trim() };\n if (params.limit !== undefined) body.limit = params.limit;\n if (params.mode !== undefined) body.mode = params.mode;\n if (params.repositoryAssetIds?.length) body.repository_asset_ids = params.repositoryAssetIds;\n if (params.paths?.length) body.paths = params.paths;\n if (params.languages?.length) body.languages = params.languages;\n return body;\n}\n\n/** A `fetch` the caller cancelled (unmount, a newer query): lifecycle, not failure. */\nfunction isAbortError(error: unknown): boolean {\n return (error instanceof DOMException || error instanceof Error) && error.name === 'AbortError';\n}\n\n/**\n * One search request. The credential rides `Authorization: Bearer` (a token,\n * resolved live through `getToken` when the host provided one) or\n * `X-API-KEY`; with neither, a same-origin host's session cookie rides\n * automatically, as the SDK's other REST helpers allow. A refused bearer\n * (`401`) gets one forced re-mint through `getToken` (`force: true`, the\n * statewire transport's `staleAuth` contract; `sendWithCredentialRenewal`) — a\n * getter that caches until forced would otherwise resend the same expired\n * token on every retry. Every failure is an `AthenaSdkError`, transport\n * rejections included; only an abort keeps its own identity.\n */\nexport async function fetchRepoSearch(\n backendUrl: string,\n auth: StatewireAuthSource,\n params: AthenaRepoSearchParams,\n opts: { signal?: AbortSignal } = {},\n): Promise<AthenaRepoSearchResponse> {\n const url = `${getAthenaApiBaseUrl(backendUrl)}${ATHENA_REPO_SEARCH_PATH}`;\n const body = JSON.stringify(repoSearchRequestBody(params));\n const post = async (headers: Record<string, string>): Promise<Response> => {\n try {\n return await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...headers },\n body,\n signal: opts.signal ?? null,\n });\n } catch (error) {\n if (isAbortError(error)) throw error;\n throw new AthenaSdkError({\n code: 'repo_search_failed',\n message: 'Repository search could not reach the Agora API.',\n cause: error,\n hint: 'Check the network and that backendUrl points at the Agora API base.',\n });\n }\n };\n const res = await sendWithCredentialRenewal(auth, post);\n if (!res.ok) {\n const refused = res.status === 401 || res.status === 403;\n throw new AthenaSdkError({\n code: refused ? 'auth_rejected' : 'repo_search_failed',\n message: `Repository search failed: ${res.status}`,\n status: res.status,\n detail: await res.json().catch(() => undefined),\n hint: refused\n ? 'Repository search is limited to Athena platform staff on the managed cloud; check the credential.'\n : res.status === 404\n ? 'Repository search is not offered on this deployment (managed cloud only).'\n : 'Check backendUrl points at the Agora API base and that Clio is available.',\n });\n }\n return (await res.json()) as AthenaRepoSearchResponse;\n}\n\n/**\n * Search the indexed repositories through TanStack Query with the provider's\n * `backendUrl` and credentials. Keyed by the non-secret request and a\n * credential generation (a counter that advances when the host's credentials\n * change), never by the credential itself. Uses the host's `QueryClient` when\n * one is in context (`enableThreadList` mounts the SDK's own) and a\n * hook-scoped client otherwise, so it works under any `<AthenaProvider>`.\n */\nexport function useAthenaRepoSearch({\n enabled = true,\n ...params\n}: UseAthenaRepoSearchOptions): UseQueryResult<AthenaRepoSearchResponse, Error> {\n const config = useAthenaConfig();\n const { backendUrl, apiKey, getToken } = config;\n const credentialsRef = useRef(credentialShape({ apiKey, token: config.token, getToken }));\n const generationRef = useRef(0);\n const currentCredentials = credentialShape({ apiKey, token: config.token, getToken });\n if (!sameCredentialShape(credentialsRef.current, currentCredentials)) {\n credentialsRef.current = currentCredentials;\n generationRef.current += 1;\n }\n const body = repoSearchRequestBody(params);\n const contextClient = useContext(QueryClientContext);\n const [fallbackClient] = useState(() => (contextClient ? undefined : new QueryClient()));\n return useQuery(\n {\n queryKey: ['athena-sdk', 'repo-search', backendUrl, generationRef.current, body],\n // `config.token` is read at fetch time: the provider serves the latest\n // token through a getter, so rotations reach the request.\n queryFn: ({ signal }) =>\n fetchRepoSearch(backendUrl, { apiKey, token: config.token, getToken }, params, { signal }),\n enabled: enabled && body.query.length > 0 && backendUrl.length > 0,\n staleTime: 30_000,\n placeholderData: keepPreviousData,\n refetchOnWindowFocus: false,\n },\n contextClient ?? fallbackClient,\n );\n}\n"],"names":[],"mappings":"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"}
@@ -105,7 +105,11 @@ async function setThreadReadState(backendUrl, auth, opts) {
105
105
  })
106
106
  });
107
107
  if (!res.ok) {
108
- throw new Error(`[AthenaSDK] Failed to set thread read state: ${res.status}`);
108
+ throw new errors.AthenaSdkError({
109
+ code: res.status === 401 || res.status === 403 ? "auth_rejected" : "unknown",
110
+ message: `Failed to set thread read state: ${res.status}`,
111
+ status: res.status
112
+ });
109
113
  }
110
114
  return res.json();
111
115
  }