feat: agent-generated login link — view-as-agent (#190)#194
Conversation
) Add createAgentViewLink(signer, opts): mints a read-only session.view bearer onboard grant with the agent's identity key and returns a `/auth/agent#grant=<token>` link the agent hands its human owner. Stashes the grant behind a short handoff token when a minter is provided (clean URL), falling back to embedding the whole grant if handoff is unavailable. Token rides in the URL fragment only. Exports VIEW_SCOPE + AgentViewLink/OnboardHandoffMinter. Covered by vitest: scope is exactly session.view (never a write), handoff token path, fallback path, and baseUrl/path/ttl handling.
…i#190) Wire the read-only view-as-agent flow on the onboard-grant model: - /auth/agent route: reads the #grant= fragment, strips it from history, then parses the grant (or redeems a handoff token) and establishes a key-less view session; fails closed on missing/malformed/expired tokens. - auth store: add an onboardGrant 'link session' (setLinkSession) with no signer or identitySigner; clearSession drops it. A real signer always supersedes it. - ApiProvider: when a link session is active and no signer is set, build a key-less onboard client that replays the grant instead of signing per request. - AgentViewBanner: persistent 'Viewing as <agent>' banner with an exit that clears the session (grant is read-only + TTL-bound; no separate revoke step). - en/es translations. Vitest: agent-view-session + AgentViewBanner pass; website typecheck + lint clean.
The effect depended on [router, setLinkSession, t]; i18n init churns the t identity, re-running the effect. The first run stripped the hash and started the redeem; the re-run's cleanup aborted that redeem and re-read the now-empty hash, showing 'missing access token' and leaving no session. Guard with a ref so the exchange runs exactly once, and drop the abort-on-rerun cleanup.
…inyhumansai#190) WalletAuthSync.clearSession() fired whenever no wallet was connected; as the wallet adapter state settled it re-ran after setLinkSession and wiped the link session (which has no wallet by design), so the banner vanished and /explore showed Connect. Skip clearing when an onboardGrant link session is active.
|
@M3gA-Mind is attempting to deploy a commit to the Vezures Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughAdds a view-as-agent grant link flow in the TypeScript SDK and website. The web app can now read a fragment token, resolve it into a session, store a link session, show a persistent banner, and exit back to the home route. ChangesView-as-agent login flow
Sequence Diagram(s)sequenceDiagram
participant Browser
participant AgentAuthPage
participant resolveAgentViewGrant
participant onboard.redeemHandoff
participant useAuthStore
participant AgentViewBanner
Browser->>AgentAuthPage: opens /auth/agent#grant
AgentAuthPage->>resolveAgentViewGrant: resolve raw fragment
resolveAgentViewGrant->>onboard.redeemHandoff: redeem short token when needed
resolveAgentViewGrant-->>AgentAuthPage: parsed grant and agent id
AgentAuthPage->>useAuthStore: setLinkSession(onboardGrant, agentId)
useAuthStore-->>AgentViewBanner: onboardGrant and agentId
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: caf5f868e1
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const direct = parseOnboardGrant(raw); | ||
| if (direct) { | ||
| return direct; |
There was a problem hiding this comment.
Validate embedded grants before accepting them
When the link carries the full grant directly (the no-handoff path that createAgentViewLink supports and falls back to), this branch only checks that the fragment is syntactically parseable and then /auth/agent immediately calls setLinkSession and redirects. An expired or revoked embedded grant therefore still produces a visible “Viewing as …” session and has the hash stripped before the first backend request fails, rather than failing closed on the callback page; direct grants need a backend validation/probe before being accepted.
Useful? React with 👍 / 👎.
| if (!signer && onboardGrant) { | ||
| return createOnboardClient(onboardGrant, onAuthInvalid); |
There was a problem hiding this comment.
Clear link sessions when their grant is rejected
For a view-as-agent session that expires while the user is browsing, the 401 from this onboard-grant client is routed to notifySessionInvalid, but the registered WalletAuthSync recovery path only tries to re-establish a wallet session and no-ops when no wallet is connected. That leaves the expired onboardGrant and banner in state, so every subsequent read keeps failing until the user manually exits; link-session 401s should clear the link session instead of using wallet recovery.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (5)
sdk/typescript/src/auth.ts (1)
171-213: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winSplit
createAgentViewLinkinto smaller helpers.Lines 171-213 are well past the repo's 20-line limit and currently combine public-key resolution, grant minting, handoff fallback, and URL assembly in one exported function. Extracting the handoff/token resolution and URL-building paths would bring this back under the guideline and make the branches easier to test. As per coding guidelines, "Limit functions to a maximum of 20 lines of code; extract longer logic into separate helper functions."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@sdk/typescript/src/auth.ts` around lines 171 - 213, createAgentViewLink is too long and mixes public-key resolution, grant minting, handoff fallback, and URL assembly in one exported function. Split the token resolution logic into a helper that handles the handoff/createHandoff fallback, and move the base/path URL construction into a separate helper so createAgentViewLink stays under the 20-line guideline. Keep the main flow in createAgentViewLink focused on orchestrating mintOnboardGrant, token selection, and returning the AgentViewLink.Source: Coding guidelines
website/src/common/agent-view-session.ts (1)
24-37: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd explicit error handling in
resolveAgentViewGrant().This is the async boundary for the callback flow, but it currently relies on bare promise rejection from
redeemHandoff(). Wrapping the redeem/parse path intry/catchkeeps the failure path explicit and matches the repo rule for async functions.Suggested refactor
export async function resolveAgentViewGrant( raw: string, onboard: TinyPlaceClient["onboard"] ): Promise<OnboardGrantCredential> { - const direct = parseOnboardGrant(raw); - if (direct) { - return direct; - } - const redeemed = await onboard.redeemHandoff(raw); - const parsed = parseOnboardGrant(redeemed.grant); - if (!parsed) { - throw new Error("agent-login link is malformed"); - } - return parsed; + try { + const direct = parseOnboardGrant(raw); + if (direct) { + return direct; + } + const redeemed = await onboard.redeemHandoff(raw); + const parsed = parseOnboardGrant(redeemed.grant); + if (!parsed) { + throw new Error("agent-login link is malformed"); + } + return parsed; + } catch (error) { + throw error instanceof Error + ? error + : new Error("agent-login link is malformed"); + } }As per coding guidelines, "All async functions must have proper error handling with try-catch blocks."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@website/src/common/agent-view-session.ts` around lines 24 - 37, resolveAgentViewGrant currently lets redeemHandoff() fail via an unhandled async rejection, so wrap the redeem/parse path in a try/catch and rethrow a clear Error from the catch. Keep the existing direct parse flow intact, and use the resolveAgentViewGrant and redeemHandoff symbols to place the explicit error handling around the async boundary.Source: Coding guidelines
website/src/common/agent-view-session.test.ts (1)
40-50: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAdd a round-trip test with a real minted fragment.
readGrantFragment()is only asserted withabc123, so this suite would miss encoding regressions in the actualgrant.fragmentValue()format produced by the SDK. A#grant=${fragment}round-trip usingfullGrantFragment()would lock the parser to the real auth-link contract.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@website/src/common/agent-view-session.test.ts` around lines 40 - 50, The readGrantFragment test suite only covers a hardcoded token and can miss regressions in the real auth-link fragment format. Add a round-trip test in agent-view-session.test around readGrantFragment/fullGrantFragment using a real minted grant fragment from grant.fragmentValue(), then assert the parser returns the expected value from a `#grant`= fragment. Keep the existing edge-case checks, but ensure the new test locks the parser to the actual SDK-produced format.website/src/store/auth.ts (1)
23-25: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winDerive
agentIdfrom the grant insidesetLinkSession().
setLinkSession()currently stores the same identity twice and trusts callers to keep them aligned. If those values ever diverge, the app can render one agent while authenticating as another. DerivingagentIdfromonboardGrant.walletremoves that cross-file contract.Suggested refactor
/** Establish a read-only link session from a view grant (no signer). */ - setLinkSession: ( - onboardGrant: OnboardGrantCredential, - agentId: string - ) => void; + setLinkSession: (onboardGrant: OnboardGrantCredential) => void; @@ - setLinkSession: (onboardGrant, agentId): void => { + setLinkSession: (onboardGrant): void => { // A link session has no signing key: it cannot register an identity or sign // per-request, so identitySigner stays undefined and signer is cleared. set({ onboardGrant, - agentId, + agentId: onboardGrant.wallet, signer: undefined, identitySigner: undefined, }); },Also applies to: 46-54
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@website/src/store/auth.ts` around lines 23 - 25, `setLinkSession()` currently accepts `agentId` separately even though it can be derived from `onboardGrant.wallet`, which creates a mismatch risk. Update the `setLinkSession` action in `auth.ts` to derive the agent identity directly from the `OnboardGrantCredential` payload, remove the extra `agentId` parameter from the action signature and all call sites, and ensure the stored session data uses the derived value consistently wherever `setLinkSession` is referenced.website/app/(main)/auth/agent/page.tsx (1)
22-63: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the exchange flow out of
AgentAuthPage().This component now owns fragment parsing, history scrubbing, async resolution, session mutation, redirecting, and UI state in one place. Pulling the effect into a small helper/hook would bring it back under the repo’s 20-line function limit and make the callback easier to audit.
As per coding guidelines, "Limit functions to a maximum of 20 lines of code; extract longer logic into separate helper functions."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@website/app/`(main)/auth/agent/page.tsx around lines 22 - 63, The token exchange logic in AgentAuthPage() is doing too much and exceeds the function length guideline. Extract the fragment parsing, history scrubbing, grant resolution, session update, and redirect flow into a small helper or custom hook (for example around readGrantFragment, resolveAgentViewGrant, and setLinkSession) so AgentAuthPage remains a thin component and the effect callback stays easy to audit.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@sdk/typescript/src/auth.ts`:
- Around line 187-193: The TTL handling in auth.ts should reject invalid values
before computing expiresAt or calling mintOnboardGrant. In the grant creation
flow that uses ttlMs and DEFAULT_VIEW_TTL_MS, validate that options.ttlMs is a
finite positive number and does not exceed the backend’s 7-day cap; otherwise
throw a clear error instead of minting the link. Keep the check close to the
ttlMs assignment in the same grant-generation path so the invalid values are
blocked before expiresAt is derived.
- Around line 197-212: The link builder in the auth flow is returning the
grant’s TTL even when a handoff token is created, so the effective expiry can be
overstated; update the logic in the handoff block inside the auth URL
construction path to track `stashed.expiresAt` from
`options.handoff.createHandoff(...)` and return the earliest effective expiry
when `token` is replaced. Keep the fallback behavior unchanged, but ensure the
final `expiresAt` reflects the handoff token’s lifetime when present, using the
existing `options.handoff`, `stashed.token`, and `expiresAt` handling in
`auth.ts`.
In `@website/app/`(main)/auth/agent/page.tsx:
- Around line 54-61: The resolveAgentViewGrant() flow in the agent auth page is
treating every rejection as an invalid token, which hides retryable
transport/backend failures. Update the .catch handling in the auth page
component to distinguish auth/invalid-grant failures from transient ones, and
keep a separate error state/message for non-auth errors so the user can retry
instead of seeing authAgent.errorInvalid for everything.
In `@website/src/common/api-context.tsx`:
- Around line 37-46: The invalid-session handling in api-context’s onAuthInvalid
is still routing onboardGrant failures through notifySessionInvalid, which only
triggers wallet recovery and leaves stale view-as-agent sessions in place.
Update the createOnboardClient path so revoked/expired onboard grants are
cleared immediately when auth invalidation occurs, using the existing
signer/onboardGrant setup to distinguish onboard sessions from wallet-backed
sessions, and only keep the wallet recovery flow for createClient.
---
Nitpick comments:
In `@sdk/typescript/src/auth.ts`:
- Around line 171-213: createAgentViewLink is too long and mixes public-key
resolution, grant minting, handoff fallback, and URL assembly in one exported
function. Split the token resolution logic into a helper that handles the
handoff/createHandoff fallback, and move the base/path URL construction into a
separate helper so createAgentViewLink stays under the 20-line guideline. Keep
the main flow in createAgentViewLink focused on orchestrating mintOnboardGrant,
token selection, and returning the AgentViewLink.
In `@website/app/`(main)/auth/agent/page.tsx:
- Around line 22-63: The token exchange logic in AgentAuthPage() is doing too
much and exceeds the function length guideline. Extract the fragment parsing,
history scrubbing, grant resolution, session update, and redirect flow into a
small helper or custom hook (for example around readGrantFragment,
resolveAgentViewGrant, and setLinkSession) so AgentAuthPage remains a thin
component and the effect callback stays easy to audit.
In `@website/src/common/agent-view-session.test.ts`:
- Around line 40-50: The readGrantFragment test suite only covers a hardcoded
token and can miss regressions in the real auth-link fragment format. Add a
round-trip test in agent-view-session.test around
readGrantFragment/fullGrantFragment using a real minted grant fragment from
grant.fragmentValue(), then assert the parser returns the expected value from a
`#grant`= fragment. Keep the existing edge-case checks, but ensure the new test
locks the parser to the actual SDK-produced format.
In `@website/src/common/agent-view-session.ts`:
- Around line 24-37: resolveAgentViewGrant currently lets redeemHandoff() fail
via an unhandled async rejection, so wrap the redeem/parse path in a try/catch
and rethrow a clear Error from the catch. Keep the existing direct parse flow
intact, and use the resolveAgentViewGrant and redeemHandoff symbols to place the
explicit error handling around the async boundary.
In `@website/src/store/auth.ts`:
- Around line 23-25: `setLinkSession()` currently accepts `agentId` separately
even though it can be derived from `onboardGrant.wallet`, which creates a
mismatch risk. Update the `setLinkSession` action in `auth.ts` to derive the
agent identity directly from the `OnboardGrantCredential` payload, remove the
extra `agentId` parameter from the action signature and all call sites, and
ensure the stored session data uses the derived value consistently wherever
`setLinkSession` is referenced.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f205a3cc-80bd-4943-9aad-157e1d922a76
📒 Files selected for processing (14)
sdk/typescript/src/auth.tssdk/typescript/src/index.tssdk/typescript/tests/agent-view-link.test.tswebsite/app/(main)/auth/agent/page.tsxwebsite/app/providers.tsxwebsite/src/assets/locales/en/translations.jsonwebsite/src/assets/locales/es/translations.jsonwebsite/src/common/agent-view-session.test.tswebsite/src/common/agent-view-session.tswebsite/src/common/api-context.tsxwebsite/src/common/wallet-phantom.tsxwebsite/src/components/AgentViewBanner.test.tsxwebsite/src/components/AgentViewBanner.tsxwebsite/src/store/auth.ts
| const ttlMs = options?.ttlMs ?? DEFAULT_VIEW_TTL_MS; | ||
| const expiresAt = new Date(Date.now() + ttlMs).toISOString(); | ||
| const grant = await mintOnboardGrant( | ||
| key, | ||
| ownerPublicKey, | ||
| [VIEW_SCOPE], | ||
| ttlMs, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Reject unsupported TTL values up front.
Lines 187-193 accept any number for ttlMs. That includes <= 0, NaN, and values above the documented 7-day backend cap, which lets the SDK mint links whose advertised expiresAt is already invalid or longer than the backend will honor.
Suggested fix
const ttlMs = options?.ttlMs ?? DEFAULT_VIEW_TTL_MS;
+ const maxTtlMs = 7 * 24 * 60 * 60 * 1000;
+ if (!Number.isFinite(ttlMs) || ttlMs <= 0 || ttlMs > maxTtlMs) {
+ throw new Error("createAgentViewLink ttlMs must be between 1 and 604800000");
+ }
const expiresAt = new Date(Date.now() + ttlMs).toISOString();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const ttlMs = options?.ttlMs ?? DEFAULT_VIEW_TTL_MS; | |
| const expiresAt = new Date(Date.now() + ttlMs).toISOString(); | |
| const grant = await mintOnboardGrant( | |
| key, | |
| ownerPublicKey, | |
| [VIEW_SCOPE], | |
| ttlMs, | |
| const ttlMs = options?.ttlMs ?? DEFAULT_VIEW_TTL_MS; | |
| const maxTtlMs = 7 * 24 * 60 * 60 * 1000; | |
| if (!Number.isFinite(ttlMs) || ttlMs <= 0 || ttlMs > maxTtlMs) { | |
| throw new Error("createAgentViewLink ttlMs must be between 1 and 604800000"); | |
| } | |
| const expiresAt = new Date(Date.now() + ttlMs).toISOString(); | |
| const grant = await mintOnboardGrant( | |
| key, | |
| ownerPublicKey, | |
| [VIEW_SCOPE], | |
| ttlMs, |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@sdk/typescript/src/auth.ts` around lines 187 - 193, The TTL handling in
auth.ts should reject invalid values before computing expiresAt or calling
mintOnboardGrant. In the grant creation flow that uses ttlMs and
DEFAULT_VIEW_TTL_MS, validate that options.ttlMs is a finite positive number and
does not exceed the backend’s 7-day cap; otherwise throw a clear error instead
of minting the link. Keep the check close to the ttlMs assignment in the same
grant-generation path so the invalid values are blocked before expiresAt is
derived.
| if (options?.handoff) { | ||
| try { | ||
| const stashed = await options.handoff.createHandoff( | ||
| grant.fragmentValue(), | ||
| ); | ||
| if (stashed?.token) token = stashed.token; | ||
| } catch { | ||
| // Backend predates the handoff endpoint or is unreachable: fall back to | ||
| // embedding the whole grant in the fragment — the web app accepts either. | ||
| } | ||
| } | ||
|
|
||
| const base = (options?.baseUrl ?? "https://tiny.place").replace(/\/+$/, ""); | ||
| const path = options?.path ?? "/auth/agent"; | ||
| const url = `${base}${path}#grant=${encodeURIComponent(token)}`; | ||
| return { url, token, expiresAt }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Return the effective handoff expiry.
Lines 197-212 ignore stashed.expiresAt and always return the grant TTL. When the link carries a handoff token, /auth/agent has to redeem that token first, so a shorter handoff expiry makes link.expiresAt overstate how long the link is actually usable.
Suggested fix
- const expiresAt = new Date(Date.now() + ttlMs).toISOString();
+ let expiresAt = new Date(Date.now() + ttlMs).toISOString();
const grant = await mintOnboardGrant(
key,
ownerPublicKey,
[VIEW_SCOPE],
ttlMs,
);
let token = grant.fragmentValue();
if (options?.handoff) {
try {
const stashed = await options.handoff.createHandoff(
grant.fragmentValue(),
);
- if (stashed?.token) token = stashed.token;
+ if (stashed?.token) {
+ token = stashed.token;
+ expiresAt = stashed.expiresAt;
+ }
} catch {
// Backend predates the handoff endpoint or is unreachable: fall back to
// embedding the whole grant in the fragment — the web app accepts either.
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (options?.handoff) { | |
| try { | |
| const stashed = await options.handoff.createHandoff( | |
| grant.fragmentValue(), | |
| ); | |
| if (stashed?.token) token = stashed.token; | |
| } catch { | |
| // Backend predates the handoff endpoint or is unreachable: fall back to | |
| // embedding the whole grant in the fragment — the web app accepts either. | |
| } | |
| } | |
| const base = (options?.baseUrl ?? "https://tiny.place").replace(/\/+$/, ""); | |
| const path = options?.path ?? "/auth/agent"; | |
| const url = `${base}${path}#grant=${encodeURIComponent(token)}`; | |
| return { url, token, expiresAt }; | |
| let expiresAt = new Date(Date.now() + ttlMs).toISOString(); | |
| const grant = await mintOnboardGrant( | |
| key, | |
| ownerPublicKey, | |
| [VIEW_SCOPE], | |
| ttlMs, | |
| ); | |
| let token = grant.fragmentValue(); | |
| if (options?.handoff) { | |
| try { | |
| const stashed = await options.handoff.createHandoff( | |
| grant.fragmentValue(), | |
| ); | |
| if (stashed?.token) { | |
| token = stashed.token; | |
| expiresAt = stashed.expiresAt; | |
| } | |
| } catch { | |
| // Backend predates the handoff endpoint or is unreachable: fall back to | |
| // embedding the whole grant in the fragment — the web app accepts either. | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@sdk/typescript/src/auth.ts` around lines 197 - 212, The link builder in the
auth flow is returning the grant’s TTL even when a handoff token is created, so
the effective expiry can be overstated; update the logic in the handoff block
inside the auth URL construction path to track `stashed.expiresAt` from
`options.handoff.createHandoff(...)` and return the earliest effective expiry
when `token` is replaced. Keep the fallback behavior unchanged, but ensure the
final `expiresAt` reflects the handoff token’s lifetime when present, using the
existing `options.handoff`, `stashed.token`, and `expiresAt` handling in
`auth.ts`.
| resolveAgentViewGrant(raw, createClient().onboard) | ||
| .then((grant) => { | ||
| setLinkSession(grant, grant.wallet); | ||
| router.replace("/explore"); | ||
| }) | ||
| .catch(() => { | ||
| setError(t("authAgent.errorInvalid")); | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Don’t collapse every redeem failure into “invalid token”.
resolveAgentViewGrant() can fail for transient transport/backend reasons too, but this path always shows authAgent.errorInvalid. Because the hash is already stripped by then, a valid link looks permanently broken from this tab. Please keep a separate retryable error state for non-auth failures.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@website/app/`(main)/auth/agent/page.tsx around lines 54 - 61, The
resolveAgentViewGrant() flow in the agent auth page is treating every rejection
as an invalid token, which hides retryable transport/backend failures. Update
the .catch handling in the auth page component to distinguish auth/invalid-grant
failures from transient ones, and keep a separate error state/message for
non-auth errors so the user can retry instead of seeing authAgent.errorInvalid
for everything.
| const onAuthInvalid = (_status: number, body: unknown): void => { | ||
| notifySessionInvalid({ forceResign: isInvalidSignature(body) }); | ||
| }; | ||
| // A read-only view-as-agent link session (#190) replays its bearer grant | ||
| // instead of signing per request. A real signer always supersedes it. | ||
| if (!signer && onboardGrant) { | ||
| return createOnboardClient(onboardGrant, onAuthInvalid); | ||
| } | ||
| return createClient(signer, onAuthInvalid); | ||
| }, [signer, onboardGrant]); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Clear invalid link sessions here instead of routing them through wallet recovery.
For createOnboardClient(...), this still calls notifySessionInvalid(...), but the registered recovery path in WalletAuthSync only knows how to re-establish wallet sessions. With no wallet connected, that path returns immediately and the stale onboardGrant stays in the store, so the app keeps replaying the same revoked/expired bearer grant.
Suggested fix
const signer = useAuthStore((state) => state.signer);
const onboardGrant = useAuthStore((state) => state.onboardGrant);
+ const clearSession = useAuthStore((state) => state.clearSession);
@@
const client = useMemo(() => {
const onAuthInvalid = (_status: number, body: unknown): void => {
+ if (onboardGrant) {
+ clearSession();
+ return;
+ }
notifySessionInvalid({ forceResign: isInvalidSignature(body) });
};
@@
- }, [signer, onboardGrant]);
+ }, [signer, onboardGrant, clearSession]);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const onAuthInvalid = (_status: number, body: unknown): void => { | |
| notifySessionInvalid({ forceResign: isInvalidSignature(body) }); | |
| }; | |
| // A read-only view-as-agent link session (#190) replays its bearer grant | |
| // instead of signing per request. A real signer always supersedes it. | |
| if (!signer && onboardGrant) { | |
| return createOnboardClient(onboardGrant, onAuthInvalid); | |
| } | |
| return createClient(signer, onAuthInvalid); | |
| }, [signer, onboardGrant]); | |
| const signer = useAuthStore((state) => state.signer); | |
| const onboardGrant = useAuthStore((state) => state.onboardGrant); | |
| const clearSession = useAuthStore((state) => state.clearSession); | |
| const client = useMemo(() => { | |
| const onAuthInvalid = (_status: number, body: unknown): void => { | |
| if (onboardGrant) { | |
| clearSession(); | |
| return; | |
| } | |
| notifySessionInvalid({ forceResign: isInvalidSignature(body) }); | |
| }; | |
| // A read-only view-as-agent link session (`#190`) replays its bearer grant | |
| // instead of signing per request. A real signer always supersedes it. | |
| if (!signer && onboardGrant) { | |
| return createOnboardClient(onboardGrant, onAuthInvalid); | |
| } | |
| return createClient(signer, onAuthInvalid); | |
| }, [signer, onboardGrant, clearSession]); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@website/src/common/api-context.tsx` around lines 37 - 46, The invalid-session
handling in api-context’s onAuthInvalid is still routing onboardGrant failures
through notifySessionInvalid, which only triggers wallet recovery and leaves
stale view-as-agent sessions in place. Update the createOnboardClient path so
revoked/expired onboard grants are cleared immediately when auth invalidation
occurs, using the existing signer/onboardGrant setup to distinguish onboard
sessions from wallet-backed sessions, and only keep the wallet recovery flow for
createClient.
What
Implements view-as-agent (#190): an agent generates a link that logs its human owner into the web app as the agent — no wallet, no private key — under a read-only, short-TTL grant.
Changes
createAgentViewLink(signer, opts): mints asession.viewonboard grant with the agent's identity key, stashes it behind a handoff token, and returns…/auth/agent#grant=<token>./auth/agentroute — reads the#grant=fragment (fragment-only; stripped from history immediately), parses the grant or redeems the handoff token, establishes a key-less read-only session, and redirects into the app. Fails closed on missing / malformed / expired tokens. Runs the exchange exactly once.setLinkSession) with no signer;ApiProviderreplays the grant instead of signing per request.WalletAuthSyncno longer clears it when no wallet is connected.Depends on
The backend
session.viewscope: tinyhumansai/backend-tinyplace#58. (The link session is read-only; the backend honorssession.viewonly on GET read paths.)Testing
tsc --noEmit, ESLint (--max-warnings 0), and Prettier clean.agent-view-link) and website (agent-view-session,AgentViewBanner) pass./explorewith the "Viewing as …" banner, logged in as the agent with no wallet; profile/inbox/marketplace/ledger reads authorize.Closes #190.
Summary by CodeRabbit