authRequired
JavaScript · package toolnexus · SPEC §10 · js/src/types.ts
function authRequired(url: string, prompt?: string): ToolResult// = pending({ kind: "authorization", prompt, url })Sugar for the single most common suspension: pending({ kind: "authorization", prompt, url })
with prompt defaulting to "Authorization required to continue". Nothing about it is special
machinery — it is pending called with one fixed kind.
When to use it
Section titled “When to use it”Use authRequired any time a tool discovers, mid-call, that the session it needs isn’t
authenticated — an expired token, a login that never happened, a scope the current session lacks.
By SPEC §10 convention, kind: "authorization" follows OAuth2/OIDC authorization-code semantics:
url is the address to send the user to, and the host’s waitFor performs the
redirect → consent → callback out-of-band. toolnexus itself stays OIDC-agnostic — there is no auth
library in the kernel, only this one shaped suspension.
Why this and not the alternative
Section titled “Why this and not the alternative”Examples
Section titled “Examples”1. The shape — kind and default prompt fixed for you
Section titled “1. The shape — kind and default prompt fixed for you”import assert from "node:assert"import { authRequired, pendingOf } from "toolnexus"
const res = authRequired("https://example.com/login?token=abc")const req = pendingOf(res)
assert.equal(req?.kind, "authorization")assert.equal(req?.url, "https://example.com/login?token=abc")assert.equal(req?.prompt, "Authorization required to continue") // the defaultassert.equal(res.output, "Authorization required to continue\nhttps://example.com/login?token=abc")
// The second argument overrides the default prompt.const custom = authRequired("https://x.com/login", "Log in to view your balance")assert.equal(pendingOf(custom)?.prompt, "Log in to view your balance")
console.log("ok:", req?.kind, "->", req?.url)2. A tool that logs in via waitFor, then the retry succeeds
Section titled “2. A tool that logs in via waitFor, then the retry succeeds”The retry carries ctx.answer — this tool ignores its data (a login has no payload to read) and
just proceeds, because the session is now valid.
import assert from "node:assert"import { createClient, createToolkit, defineTool, authRequired } from "toolnexus"
let authed = falseconst getBalance = defineTool({ name: "get_balance", description: "Return the account balance. Requires login first.", inputSchema: { type: "object", properties: {} }, run: () => { if (!authed) return authRequired("https://example.com/login?token=abc", "Log in to view your balance") return "balance: $500" },})
const canned: typeof fetch = async (_url, init) => { const body = JSON.parse(String((init as any).body)) const sawToolResult = body.messages.some((m: any) => m.role === "tool") const message = sawToolResult ? { role: "assistant", content: "Your balance is $500." } : { role: "assistant", content: null, tool_calls: [{ id: "c1", type: "function", function: { name: "get_balance", arguments: "{}" } }] } return new Response(JSON.stringify({ choices: [{ message }], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } }), { status: 200, headers: { "content-type": "application/json" } })}
const seenUrls: string[] = []const client = createClient({ apiKey: "test-key", baseUrl: "http://stub", style: "openai", model: "m", fetch: canned, waitFor: async (request) => { seenUrls.push(request.url!) // in real life: open a browser, or message a channel authed = true // the world changed out-of-band — the tool retries into a valid session return { id: request.id, ok: true } },})
const tk = await createToolkit({ builtins: false, extraTools: [getBalance] })const res = await client.run("what is my balance?", { toolkit: tk })
assert.equal(res.status, "done")assert.ok(seenUrls[0].includes("example.com/login"))assert.match(res.toolCalls[0].output, /balance: \$500/)
await tk.close()console.log("ok:", seenUrls[0], "->", res.text)3. No waitFor — the durable halt carries pending.url for a host to deliver later
Section titled “3. No waitFor — the durable halt carries pending.url for a host to deliver later”import assert from "node:assert"import { createClient, createToolkit, defineTool, authRequired } from "toolnexus"
const getBalance = defineTool({ name: "get_balance", description: "Return the account balance. Requires login first.", inputSchema: { type: "object", properties: {} }, run: () => authRequired("https://example.com/login?token=abc", "Log in to view your balance"),})
const canned: typeof fetch = async () => new Response(JSON.stringify({ choices: [{ message: { content: null, tool_calls: [{ id: "c1", type: "function", function: { name: "get_balance", arguments: "{}" } }] } }], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, }), { status: 200, headers: { "content-type": "application/json" } })
const client = createClient({ apiKey: "test-key", baseUrl: "http://stub", style: "openai", model: "m", fetch: canned }) // no waitForconst tk = await createToolkit({ builtins: false, extraTools: [getBalance] })
const res = await client.run("what is my balance?", { toolkit: tk })
assert.equal(res.status, "pending")assert.equal(res.pending?.kind, "authorization")assert.ok(res.pending?.url?.includes("example.com/login"))// A durable host persists `res.pending` (and `res.messages`) here and delivers the link elsewhere —// see agents.AgentRuntime.resume for the shipped, in-process way back in.
await tk.close()console.log("ok:", res.status, "|", res.pending?.url)Fields
Section titled “Fields”authRequired(url, prompt?) builds a Request with:
| Field | Value |
|---|---|
kind |
Always "authorization". |
url |
The first argument, verbatim — the authorize endpoint. |
prompt |
The second argument, or "Authorization required to continue". |
id |
Auto-generated, same as pending. |