Skip to content

feat: agent-generated login link — view-as-agent (#190)#194

Open
M3gA-Mind wants to merge 4 commits into
tinyhumansai:mainfrom
M3gA-Mind:feat/view-as-agent
Open

feat: agent-generated login link — view-as-agent (#190)#194
M3gA-Mind wants to merge 4 commits into
tinyhumansai:mainfrom
M3gA-Mind:feat/view-as-agent

Conversation

@M3gA-Mind

@M3gA-Mind M3gA-Mind commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

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

  • SDKcreateAgentViewLink(signer, opts): mints a session.view onboard grant with the agent's identity key, stashes it behind a handoff token, and returns …/auth/agent#grant=<token>.
  • /auth/agent route — 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.
  • Auth store / API client — a read-only "link session" (setLinkSession) with no signer; ApiProvider replays the grant instead of signing per request. WalletAuthSync no longer clears it when no wallet is connected.
  • "Viewing as @agent" banner + exit that clears the session. en/es translations.

Depends on

The backend session.view scope: tinyhumansai/backend-tinyplace#58. (The link session is read-only; the backend honors session.view only on GET read paths.)

Testing

  • tsc --noEmit, ESLint (--max-warnings 0), and Prettier clean.
  • Vitest: SDK (agent-view-link) and website (agent-view-session, AgentViewBanner) pass.
  • Verified end-to-end in a headless browser against a local full stack: opening the link lands on /explore with the "Viewing as …" banner, logged in as the agent with no wallet; profile/inbox/marketplace/ledger reads authorize.

Closes #190.

Summary by CodeRabbit

  • New Features
    • Added a new agent view link flow, including shared link generation, session resolution, and a dedicated callback page.
    • Added an on-screen banner to show when you’re viewing as an agent, with a quick exit option.
  • Bug Fixes
    • Preserved read-only agent view sessions when a wallet disconnects.
    • Improved session handling so agent-view links can be resolved from either a direct grant or a short handoff token.
  • Documentation
    • Added new localized text for the agent view sign-in and error states.

)

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.
@vercel

vercel Bot commented Jun 25, 2026

Copy link
Copy Markdown

@M3gA-Mind is attempting to deploy a commit to the Vezures Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds 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.

Changes

View-as-agent login flow

Layer / File(s) Summary
SDK link minting and exports
sdk/typescript/src/auth.ts, sdk/typescript/src/index.ts
Adds createAgentViewLink, the view-scoped grant constants and handoff interfaces, and re-exports the new auth surface from the package root.
SDK link tests
sdk/typescript/tests/agent-view-link.test.ts
Covers direct fragments, handoff token stashing and fallback, and base URL, path, and TTL handling for createAgentViewLink.
Grant parsing and auth state
website/src/common/agent-view-session.ts, website/src/common/agent-view-session.test.ts, website/src/store/auth.ts
Adds fragment parsing and grant resolution helpers, extends auth state with onboardGrant and setLinkSession, and tests the grant parsing and redemption paths.
Client selection and session cleanup
website/src/common/api-context.tsx, website/src/common/wallet-phantom.tsx
Selects createOnboardClient when an onboardGrant exists and skips Phantom session clearing for link sessions.
Callback page and banner UI
website/app/(main)/auth/agent/page.tsx, website/src/components/AgentViewBanner.tsx, website/src/components/AgentViewBanner.test.tsx, website/app/providers.tsx, website/src/assets/locales/en/translations.json, website/src/assets/locales/es/translations.json
Adds the /auth/agent callback page, the persistent view banner, provider wiring, locale strings, and banner coverage.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

I hopped from hash to banner bright,
With grant-fragments tucked out of sight.
I nibbled paths from #grant= glow,
Now “Viewing as” helps the human know.
Hop hop—agent links take flight! 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 69.23% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately describes the new agent view-as-agent login link feature.
Linked Issues check ✅ Passed The PR implements the agent view link flow, client callback, no-wallet session handling, and banner/exit UI required by #190.
Out of Scope Changes check ✅ Passed The changes stay focused on the agent view session feature, with supporting tests, translations, and UI wiring.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +28 to +30
const direct = parseOnboardGrant(raw);
if (direct) {
return direct;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +42 to +43
if (!signer && onboardGrant) {
return createOnboardClient(onboardGrant, onAuthInvalid);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (5)
sdk/typescript/src/auth.ts (1)

171-213: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Split createAgentViewLink into 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 win

Add 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 in try/catch keeps 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 win

Add a round-trip test with a real minted fragment.

readGrantFragment() is only asserted with abc123, so this suite would miss encoding regressions in the actual grant.fragmentValue() format produced by the SDK. A #grant=${fragment} round-trip using fullGrantFragment() 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 win

Derive agentId from the grant inside setLinkSession().

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. Deriving agentId from onboardGrant.wallet removes 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 win

Extract 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

📥 Commits

Reviewing files that changed from the base of the PR and between b34d00e and caf5f86.

📒 Files selected for processing (14)
  • sdk/typescript/src/auth.ts
  • sdk/typescript/src/index.ts
  • sdk/typescript/tests/agent-view-link.test.ts
  • website/app/(main)/auth/agent/page.tsx
  • website/app/providers.tsx
  • website/src/assets/locales/en/translations.json
  • website/src/assets/locales/es/translations.json
  • website/src/common/agent-view-session.test.ts
  • website/src/common/agent-view-session.ts
  • website/src/common/api-context.tsx
  • website/src/common/wallet-phantom.tsx
  • website/src/components/AgentViewBanner.test.tsx
  • website/src/components/AgentViewBanner.tsx
  • website/src/store/auth.ts

Comment on lines +187 to +193
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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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.

Comment on lines +197 to +212
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 };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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`.

Comment on lines +54 to +61
resolveAgentViewGrant(raw, createClient().onboard)
.then((grant) => {
setLinkSession(grant, grant.wallet);
router.replace("/explore");
})
.catch(() => {
setError(t("authAgent.errorInvalid"));
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Comment on lines +37 to +46
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]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

Suggested change
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: agent-generated login link — let an agent's owner sign into the web app as the agent (view-as-agent)

1 participant