feat(server,cli): auth.md discovery + lobu login --email headless claim#1073
Conversation
Server: advertise the auth.md user_claimed flow. Add an agent_auth block to the OAuth authorization-server metadata (flows_supported, claim_email_endpoint, device_authorization_endpoint, token_endpoint, auth_md), an auth_md pointer in the protected-resource metadata, and GET /auth.md serving the agent-readable walkthrough generated from the deployment base URL. CLI: add 'lobu login --email <address>'. Reuses the whole device-code login path (register -> device_authorization -> poll -> save), but POSTs the user_code+email to the discovered claim endpoint instead of printing a code, and keeps polling without a TTY because approval rides the emailed link. This is the headless front door an agent uses to log in on a user's behalf without a pre-minted PAT. discoverOAuth now parses agent_auth.claim_email_endpoint. Tests: server discovery + full user_claimed loop (register -> device/email -> consent approve -> token), CLI discovery parsing. Skill documents the flow.
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThis PR implements a headless "email-claim" device-code login: the CLI can POST {user_code, email} to a discovered claim endpoint, then poll /oauth/token until approval; the server advertises agent_auth metadata and serves an auth.md walkthrough; tests cover discovery, CLI flows, and end-to-end approval/token issuance. ChangesEmail-Claim Headless Device-Code Authentication
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 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. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint skipped: no ESLint configuration detected in root package.json. To enable, add Comment |
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
|
bug_free 86, simplicity 84, slop 5, bugs 0, 0 blockers Suites passed. Explored CLI bin help -> --email listed; buildAuthMd emits user_claimed + device/email. Skipped full server boot; integration hit metadata and full user_claimed loop. Local worktree has formatter-only edits in 2 CLI tests not part of HEAD. Suggested fixes
Full verdict JSON{
"bug_free_confidence": 86,
"bugs": 0,
"slop": 5,
"simplicity": 84,
"blockers": [],
"change_type": "feat",
"behavior_change_risk": "medium",
"tests_adequate": true,
"suggested_fixes": [
{
"file": "packages/cli/src/commands/login.ts",
"line": 122,
"change": "For --email, check discovery.claimEmailEndpoint before registerClient/startDeviceAuthorization so unsupported servers fail without creating a client/device-code request; remove the later duplicate support check."
}
],
"notes": "Suites passed. Explored CLI bin help -> --email listed; buildAuthMd emits user_claimed + device/email. Skipped full server boot; integration hit metadata and full user_claimed loop. Local worktree has formatter-only edits in 2 CLI tests not part of HEAD.",
"categories": {
"src": 278,
"tests": 337,
"docs": 9,
"config": 0,
"deps": 0,
"migrations": 0,
"ci": 0,
"generated": 0
}
}Local review gate — branch protection can require the |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@packages/cli/src/commands/login.ts`:
- Around line 135-152: The code currently uses Boolean(options.email) so passing
--email "" or whitespace bypasses explicit-mode checks; change email detection
to check option presence and non-empty trimmed content (e.g., treat emailClaim
as options.email !== undefined) and then validate that options.email.trim() is
not empty before proceeding; if empty, print the same error/usage message and
set process.exitCode = 1; only call sendEmailClaim (via tryOAuthStep) with the
validated trimmed value (references: options.email, emailClaim, sendEmailClaim,
tryOAuthStep, discovery.claimEmailEndpoint, authorization.userCode).
🪄 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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: db4d4f97-7407-44fb-bf28-6d150cc5dcfb
📒 Files selected for processing (9)
packages/cli/src/commands/login.tspackages/cli/src/index.tspackages/cli/src/internal/__tests__/oauth.test.tspackages/cli/src/internal/oauth.tspackages/server/src/__tests__/integration/oauth/agent-claim-discovery.test.tspackages/server/src/auth/oauth/auth-md.tspackages/server/src/auth/oauth/provider.tspackages/server/src/auth/oauth/routes.tsskills/lobu/SKILL.md
| const emailClaim = Boolean(options.email); | ||
| if (emailClaim) { | ||
| if (!discovery.claimEmailEndpoint) { | ||
| console.log( | ||
| chalk.red( | ||
| `\n ${discovery.issuer} does not support email login (no agent_auth.claim_email_endpoint).` | ||
| ) | ||
| ); | ||
| console.log(chalk.dim(" Use plain `lobu login` or `--token <pat>`.\n")); | ||
| process.exitCode = 1; | ||
| return; | ||
| } | ||
| const sent = await tryOAuthStep(() => | ||
| sendEmailClaim( | ||
| discovery.claimEmailEndpoint as string, | ||
| authorization.userCode, | ||
| options.email as string | ||
| ) |
There was a problem hiding this comment.
Treat --email as explicit mode and validate non-empty input.
Line 135 uses Boolean(options.email), so an explicit --email "" can drop into interactive mode, and whitespace-only values are sent as-is. Gate on option presence and validate trimmed content before calling the claim endpoint.
Proposed patch
- const emailClaim = Boolean(options.email);
+ const emailClaim = options.email !== undefined;
+ const email = options.email?.trim();
if (emailClaim) {
+ if (!email) {
+ console.log(chalk.red("\n --email requires a non-empty address.\n"));
+ process.exitCode = 1;
+ return;
+ }
if (!discovery.claimEmailEndpoint) {
console.log(
chalk.red(
`\n ${discovery.issuer} does not support email login (no agent_auth.claim_email_endpoint).`
)
);
console.log(chalk.dim(" Use plain `lobu login` or `--token <pat>`.\n"));
process.exitCode = 1;
return;
}
const sent = await tryOAuthStep(() =>
sendEmailClaim(
discovery.claimEmailEndpoint as string,
authorization.userCode,
- options.email as string
+ email
)
);
if (!sent) return;
console.log(
chalk.dim(
- `\n Sent a confirmation link to ${chalk.white(options.email as string)}.`
+ `\n Sent a confirmation link to ${chalk.white(email)}.`
)
);🤖 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 `@packages/cli/src/commands/login.ts` around lines 135 - 152, The code
currently uses Boolean(options.email) so passing --email "" or whitespace
bypasses explicit-mode checks; change email detection to check option presence
and non-empty trimmed content (e.g., treat emailClaim as options.email !==
undefined) and then validate that options.email.trim() is not empty before
proceeding; if empty, print the same error/usage message and set
process.exitCode = 1; only call sendEmailClaim (via tryOAuthStep) with the
validated trimmed value (references: options.email, emailClaim, sendEmailClaim,
tryOAuthStep, discovery.claimEmailEndpoint, authorization.userCode).
tryOAuthStep returns the callback value or undefined on error; sendEmailClaim resolved void, so the success result read as falsy and the command returned before polling — the email sent but no credential was ever collected. Return a truthy sentinel from the wrapped call. Add a regression test that drives the full --email flow (claim -> poll -> save) and the no-claim-endpoint error path.
There was a problem hiding this comment.
♻️ Duplicate comments (1)
packages/cli/src/commands/login.ts (1)
135-136:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winHandle
Boolean(options.email)still allows explicit empty input to fall back to interactive mode, and whitespace-only values can be sent/logged untrimmed.Suggested patch
- const emailClaim = Boolean(options.email); + const emailClaim = options.email !== undefined; + const email = options.email?.trim(); if (emailClaim) { + if (!email) { + console.log(chalk.red("\n --email requires a non-empty address.\n")); + process.exitCode = 1; + return; + } if (!discovery.claimEmailEndpoint) { console.log( chalk.red( `\n ${discovery.issuer} does not support email login (no agent_auth.claim_email_endpoint).` ) ); console.log(chalk.dim(" Use plain `lobu login` or `--token <pat>`.\n")); process.exitCode = 1; return; } const sent = await tryOAuthStep(async () => { await sendEmailClaim( discovery.claimEmailEndpoint as string, authorization.userCode, - options.email as string + email ); return true; }); if (!sent) return; console.log( chalk.dim( - `\n Sent a confirmation link to ${chalk.white(options.email as string)}.` + `\n Sent a confirmation link to ${chalk.white(email)}.` ) );Also applies to: 154-154, 161-161
🤖 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 `@packages/cli/src/commands/login.ts` around lines 135 - 136, The current check using Boolean(options.email) treats an explicit --email "" as false and allows whitespace-only values; change the detection to treat the flag as explicit when the option is present (e.g., options.email !== undefined or Object.prototype.hasOwnProperty.call(options, 'email')) and set emailClaim from that, then trim the provided value (const rawEmail = options.email?.toString().trim() ?? '') and validate it: if emailClaim is true but rawEmail is empty or only whitespace, surface a validation error (or prompt) instead of falling back to interactive mode; apply the same change to the other places referencing options.email/emailClaim in this module (e.g., where emailClaim is checked and where the email value is used).
🤖 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.
Duplicate comments:
In `@packages/cli/src/commands/login.ts`:
- Around line 135-136: The current check using Boolean(options.email) treats an
explicit --email "" as false and allows whitespace-only values; change the
detection to treat the flag as explicit when the option is present (e.g.,
options.email !== undefined or Object.prototype.hasOwnProperty.call(options,
'email')) and set emailClaim from that, then trim the provided value (const
rawEmail = options.email?.toString().trim() ?? '') and validate it: if
emailClaim is true but rawEmail is empty or only whitespace, surface a
validation error (or prompt) instead of falling back to interactive mode; apply
the same change to the other places referencing options.email/emailClaim in this
module (e.g., where emailClaim is checked and where the email value is used).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: fb64abaa-7dad-4100-a1b8-5b1b072a8186
📒 Files selected for processing (2)
packages/cli/src/__tests__/login-email.test.tspackages/cli/src/commands/login.ts
…ice code Move the --email claimEmailEndpoint check ahead of registerClient/ startDeviceAuthorization so an unsupported server fails fast without leaving an orphan OAuth client + device-code row; drop the now-duplicate later check. (pi review polish, non-blocking.)
These landed format-dirty via #1073 (the pre-commit --write didn't reach the committed blob), turning the format-lint check red on main. Expand the inline arrays/objects to match `biome format`. Verified clean with the exact CI command (biome format --config-path config/biome.config.json .). No logic change. Folded into this pointer-bump PR since it was blocked on the same red.
* chore: bump packages/owletto pointer to 2ffdefa Picks up: fix(oauth): frame the device consent page as an access request when arriving via a link (#224) Before: b3d3a64 After: 2ffdefa * style(cli): biome-format login-email + oauth tests These landed format-dirty via #1073 (the pre-commit --write didn't reach the committed blob), turning the format-lint check red on main. Expand the inline arrays/objects to match `biome format`. Verified clean with the exact CI command (biome format --config-path config/biome.config.json .). No logic change. Folded into this pointer-bump PR since it was blocked on the same red.
What
Makes the
user_claimedagent-registration flow (shipped in #1071) discoverable and usable, on both the HTTP and CLI front doors.Server — auth.md discovery surface:
agent_authblock in the OAuth authorization-server metadata (flows_supported,claim_methods_supported,registration_endpoint,device_authorization_endpoint,claim_email_endpoint,token_endpoint,auth_md). Onlyuser_claimedis advertised; the ID-JAGagent_verifiedflow is deliberately absent.auth_mdpointer in the protected-resource metadata (RFC 9728 allows extra members).GET /auth.mdserving an agent-readable walkthrough, generated from the deployment base URL so it's correct for self-hosted installs.CLI —
lobu login --email <address>:user_code+ email to the discovered claim endpoint instead of printing a code, and keeps polling without a TTY because approval rides the emailed link.discoverOAuthnow parsesagent_auth.claim_email_endpoint;--emailerrors clearly if the server doesn't advertise it.This closes the gap where the new claim flow existed only as a raw HTTP endpoint —
lobu loginhad no mode for it, so it looked like agents couldn't log in headlessly. Now they can.Why this shape
The email claim is the same device-code grant
lobu loginalready runs; the only difference is approval delivery (emailed link vs terminal code). So the CLI change is glue over existing, tested machinery, and the HTTP discovery lets non-CLI agents (OpenAI/Cursor) find the flow via standard OAuth metadata.Test evidence
agent-claim-discovery.test.ts, 4/4 green vs real PG17+pgvector under Node 22): theagent_authblock, the PRMauth_mdpointer,GET /auth.md, and the full user_claimed loop — register → device_authorization → device/email → consent approve (as the signed-in user) → token poll yields a scoped credential.oauth.test.ts, 31/31 viabun test):discoverOAuthparsesagent_auth.claim_email_endpointand leaves it undefined when absent.Still deferred
The ID-JAG
agent_verifiedzero-touch flow, and the owletto consent-page copy for the agent-initiated context (separate cross-repo PR).Summary by CodeRabbit
New Features
Documentation
Tests