import type { MachineCredentials } from "./machine-credentials.js"; /** * OAuth 2.1 Device Authorization Grant (RFC 8628) client for the ai-game.dev authorization server — * the TypeScript port of the plugin's C# `DeviceAuthService` + `DeviceAuthFlow` * (auth-fixes design 02 T1 / 03 Flow B). It POSTs `client_id` + `scope` (form-encoded) to * `{base}/oauth/device_authorization`, then redeems the grant at `{base}/oauth/token` with the * device-code grant type — yielding an ES256 hub JWT plus a **rotating refresh token**, which the * caller persists as full {@link MachineCredentials}. This replaces the legacy `/api/auth/device/*` * JSON flow and **never mints a PAT** (personal access tokens remain a manual, human-only tool). * * The RFC 8628 client-MUSTs (design 02 T1, §3.3 / §3.5) are codified here and unit-tested: * - the caller is handed BOTH the `user_code` AND the `verification_uri` to display (§3.3); * - the poll interval defaults to 5s and honours a larger server `interval` (§3.5); * - a `slow_down` error bumps the interval by 5s (§3.5); * - `authorization_pending` keeps polling; `expired_token` / `access_denied` stop cleanly. * * MCP conformance (additive): an optional RFC 8707 `resource` indicator is threaded into the * device-authorization request AND every token poll — exactly one `resource` per request, so the AS * mints single-audience tokens — and `scope=mcp:agent` ({@link MCP_AGENT_SCOPE}) is first-class * alongside the default {@link DEFAULT_PLUGIN_SCOPE}. * * Nothing here touches the machine credential store — a caller writes the returned credentials only * on success, so a network failure or a denied/expired grant never corrupts the store (design 03 F4). */ /** RFC 8628 `device_authorization` response document. */ export interface DeviceAuthorizeResponse { device_code: string; user_code: string; verification_uri: string; verification_uri_complete?: string; expires_in: number; interval?: number; } /** * OAuth 2.1 token response for the device-code (and refresh-token) grant. On success it carries the * access token + rotating refresh token + `expires_in`; while authorization is pending it carries an * RFC 6749 §5.2 `error` (`authorization_pending` / `slow_down` / `access_denied` / `expired_token`). */ export interface DeviceTokenResponse { access_token?: string; refresh_token?: string; token_type?: string; expires_in?: number; scope?: string; error?: string; error_description?: string; } /** Path (relative to the AS root) of the RFC 8628 device-authorization endpoint. */ export declare const OAUTH_DEVICE_AUTHORIZATION_PATH = "/oauth/device_authorization"; /** Path (relative to the AS root) of the OAuth 2.1 token endpoint. */ export declare const OAUTH_TOKEN_PATH = "/oauth/token"; /** RFC 8628 device-code grant type redeemed at the token endpoint. */ export declare const DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code"; /** Default scope selecting the MCP-plugin JWT + refresh-token response. */ export declare const DEFAULT_PLUGIN_SCOPE = "mcp:plugin"; /** First-class agent-plane scope: `scope=mcp:agent` selects an agent-plane token response. */ export declare const MCP_AGENT_SCOPE = "mcp:agent"; /** RFC 8628 §3.5 default polling interval when the server does not specify one (5 seconds). */ export declare const DEFAULT_POLL_INTERVAL_MS = 5000; /** RFC 8628 §3.5 amount the polling interval grows by on each `slow_down` (5 seconds). */ export declare const SLOW_DOWN_INCREMENT_MS = 5000; /** * The device-authorization transport seam. Split from the flow so the RFC 8628 state machine can be * exercised against a mocked authorization server with no live network. */ export interface DeviceAuthTransport { /** `POST /oauth/device_authorization` → device/user code document. */ requestDeviceCode(signal?: AbortSignal): Promise; /** `POST /oauth/token` (device-code grant). Pending/slow-down come back as a soft error body. */ pollToken(deviceCode: string, signal?: AbortSignal): Promise; } /** Absolute device-authorization URL for an AS root. */ export declare function deviceAuthorizationUrl(serverBaseUrl: string): string; /** Absolute token URL for an AS root. */ export declare function tokenUrl(serverBaseUrl: string): string; /** Options for the default fetch-backed transport. */ export interface HttpDeviceAuthTransportOptions { serverBaseUrl: string; clientId: string; scope?: string; /** * RFC 8707 resource indicator — the MCP resource server the token is minted for (e.g. * `https://ai-game.dev/mcp`). When set, exactly ONE `resource` parameter is sent on the * device-authorization request AND on every token poll, yielding a single-audience token. * Omitted → the legacy wire shape (no `resource`) is preserved. */ resource?: string; /** Injectable for tests; defaults to the global `fetch`. */ fetchImpl?: typeof fetch; /** Per-request network timeout (ms). Default 30s. */ timeoutMs?: number; } /** * The default {@link DeviceAuthTransport}: form-encoded POSTs to the real OAuth endpoints via * `fetch`. `client_id` + `scope` go to `/oauth/device_authorization`; `grant_type=device_code` + * `device_code` + `client_id` go to `/oauth/token`. The token endpoint is NOT status-checked — * `authorization_pending` / `slow_down` come back as HTTP 400 with an RFC 6749 §5.2 error body the * flow inspects. */ export declare class HttpDeviceAuthTransport implements DeviceAuthTransport { private readonly _serverBaseUrl; private readonly _clientId; private readonly _scope; private readonly _resource; private readonly _fetch; private readonly _timeoutMs; constructor(options: HttpDeviceAuthTransportOptions); requestDeviceCode(signal?: AbortSignal): Promise; pollToken(deviceCode: string, signal?: AbortSignal): Promise; private post; } /** Callbacks + injectable seams for {@link deviceLogin}. */ export interface DeviceLoginOptions { /** The AS root (e.g. `https://ai-game.dev`) — NOT the `/mcp` hub URL. Used to build the transport. */ serverBaseUrl: string; /** Product client id (`unity-mcp-cli` / `unreal-mcp-cli` / `godot-cli`). Required. */ clientId: string; /** Scope; defaults to `mcp:plugin`. Pass {@link MCP_AGENT_SCOPE} (`mcp:agent`) for the agent plane. */ scope?: string; /** * RFC 8707 resource indicator threaded into the default transport: exactly ONE `resource` on the * device-authorization request AND every token poll (single-audience tokens). Omitted → legacy * wire shape. Ignored when a custom `transport` is supplied (the transport owns its wire shape). */ resource?: string; /** Injectable `fetch` for the default transport (mock-AS tests). Ignored when `transport` is supplied. */ fetchImpl?: typeof fetch; /** * The server target recorded on the resulting credential (hosted vs local). Defaults to * `serverBaseUrl`. Kept distinct so a caller can record the hub URL if it prefers. */ serverTarget?: string; /** * REQUIRED (RFC 8628 §3.3): display the `user_code` AND the `verification_uri` to the user. The * flow calls this exactly once, before polling begins. */ onUserCode: (userCode: string, verificationUri: string, response: DeviceAuthorizeResponse) => void; /** Optional: called once when polling starts (e.g. to show a spinner). */ onPolling?: () => void; /** Optional: open the verification URL in a browser (`verification_uri_complete` when present). */ openBrowser?: (url: string) => void; /** Injectable transport; defaults to {@link HttpDeviceAuthTransport}. */ transport?: DeviceAuthTransport; /** Injectable delay (ms); defaults to a cancellable `setTimeout`. For tests. */ delay?: (ms: number, signal?: AbortSignal) => Promise; /** Injectable clock (ms since epoch); defaults to `Date.now`. For deadline tests. */ now?: () => number; /** Default poll interval floor (ms); defaults to {@link DEFAULT_POLL_INTERVAL_MS}. */ defaultPollIntervalMs?: number; /** Cancellation. */ signal?: AbortSignal; } /** The outcome of {@link deviceLogin}. Failures are values, not throws (network errors included). */ export type DeviceLoginResult = { ok: true; credentials: MachineCredentials; } | { ok: false; reason: "expired" | "denied" | "error" | "cancelled"; message: string; }; /** * Run the RFC 8628 device-authorization flow end to end and return full {@link MachineCredentials} * on success. The caller persists them (this function never writes the store), so an early failure * leaves the store untouched (design 03 F4). */ export declare function deviceLogin(options: DeviceLoginOptions): Promise; /** * Best-effort extraction of the `sub` claim from a JWT for the diagnostic `subject` field. This does * NOT verify the signature (that is the server's job on every request) — it only reads the already * server-issued token to record which account it resolves to. Returns undefined on any malformed * input. */ export declare function decodeJwtSubject(accessToken: string | undefined): string | undefined; //# sourceMappingURL=oauth-device-flow.d.ts.map