import { MachineCredentialLock } from "./credential-lock.js"; import { type MachineCredentials, type MachineCredentialStore } from "./machine-credentials.js"; import type { TokenRefresher } from "./token-refresher.js"; /** * `MachineCredentialProvider` — **the single TypeScript entry point for machine-credential access * and refresh** (unified-machine-auth 02/04). The three engine CLIs (d2/e2/f2, W2) and the desktop * App (W3) consume credentials EXCLUSIVELY through this class; nothing else re-implements refresh. * It owns the family-aware machine store view, the cross-process lock, and the 04 §3 refresh rules: * * 1. **Proactive** refresh inside the expiry skew (60 s) via {@link getAccessToken}; **reactive** * refresh on a server 401 via {@link refresh}. Both run the 04 §2 critical section under the * {@link MachineCredentialLock}: acquire → **re-read the store** (double-checked — a peer that * already refreshed is adopted without a network call) → refresh (≤15 s) → write → release. * 2. The refresh request presents the **family's stored `clientId`** (04 §1/D8). The component * default (`defaultClientId` option) is used ONLY when the family stores none — the * `families.legacy` case (§3.7, status-quo behavior) — never over a stored id. * 3. `scope`/`resource` are omitted entirely (P0-3; enforced by the {@link TokenRefresher} seam, * which does not even carry them). * 4. A rotation response missing `refresh_token` keeps the previous one. * 5. `invalid_grant` → the lock is released and the store re-read once more (a racer may have won * legitimately — its rotation is adopted); if the family is genuinely dead: the provider marks * it signed-out for this process, emits ONE structured telemetry event * ({@link CredentialTelemetryEvent}), never deletes the store or any other family, and never * loops. * 6. Rate discipline: at most one network refresh attempt per family per skew window per * provider (one provider per CLI process). * 7. `families.legacy` refreshes with the component-default `clientId` and, on success, is * written back as a v2 document's `families.legacy` (+ the v1 mirror while it is the sole * plugin-plane credential) — F11.1 adoption under the lock. * * Failure honesty (04 §1 / c1 review A3): a store that turns **unreadable** during refresh is * busy/transient — the provider NEVER overwrites or deletes it; only an explicit re-auth flow * (login-commit.ts) may replace it. Lock-budget exhaustion surfaces as * {@link CredentialLockBusyError} ("busy — retry"), never as a lock-free refresh (D9) and never * as a spurious "sign in again". */ /** * Raised when the CLI has no usable credential and the user must sign in again — an expired/revoked * refresh family, a missing credential, or an unrecoverable refresh failure. Adapters translate this * into the user-facing `login required` message. */ export declare class LoginRequiredError extends Error { constructor(reason?: string); } /** Default proactive-refresh skew: refresh once the token is within 60s of expiry. */ export declare const DEFAULT_REFRESH_SKEW_MS = 60000; /** * Which credential a caller needs — resolved to a concrete store family by the provider: * * - `"agent"` — the `families.agent` credential ONLY (`scope=mcp:agent`, `aud` = the `/mcp` * resource). The desktop App's plane. * - `"plugin"` (the default) — **the engine CLI's credential for the MCP server's HTTP API**: * `/api/tools/*`, `/api/system-tools/*`, and the streamable `/mcp` endpoint. Resolves * `families.agent` FIRST, then `families.plugin`, then `families.legacy`. * * Why agent-first: the MCP server authorizes every HTTP route on its AGENT plane, which * requires `aud` = its canonical `/mcp` resource and refuses the hub audience `urn:agd:hub` * outright (MCP-Plugin-dotnet `AccessTokenValidator.AudienceEntryAccepted`). The * exchange-derived `families.plugin` credential always carries `aud=urn:agd:hub` * (AI-Game-Dev-Server `mint_plugin_tokens_for_sub`), so presenting it to `/api/tools/*` is * a guaranteed `401 invalid_token` — which is what every Cloud-mode `run-tool` did while this * plane resolved `plugin → legacy`. The engine CLIs shipped passing `"plugin"` for exactly * those HTTP calls, so the fix lives in this resolution rather than in each consumer. A store * with no agent family (a `--tools-only` login, an enroll-minted or adopted v1 credential) * still falls back to the plugin-plane families, unchanged. * - `"hub"` — the plugin-plane credential proper (`families.plugin`, then `families.legacy`): * for a caller that connects to the SignalR hub (`/hub/mcp-server`, validated on the PLUGIN * plane). No TypeScript consumer does today; the engine plugins read the store themselves. */ export type CredentialPlane = "agent" | "plugin" | "hub"; /** A concrete store family a refresh operates on (see {@link CredentialPlane} for the resolution). */ export type CredentialFamilyName = "agent" | "plugin" | "legacy"; /** * Structured telemetry event (04 §3 rule 5). Carries NO token material. Exactly one * `family-dead` event is emitted per family death per provider. */ export interface CredentialTelemetryEvent { type: "family-dead"; family: CredentialFamilyName; /** The server error that killed the family (e.g. `invalid_grant`). Never token material. */ reason: string; } export interface MachineCredentialProviderOptions { /** Proactive-refresh skew (ms); defaults to {@link DEFAULT_REFRESH_SKEW_MS}. */ refreshSkewMs?: number; /** Injectable clock (ms since epoch); defaults to `Date.now`. For deterministic expiry tests. */ clock?: () => number; /** Optional structured warning sink (never receives token material). */ onWarning?: (message: string) => void; /** * The cross-process store lock (04 §2). Defaults to a {@link MachineCredentialLock} on the * store's own directory — pass one only to share an instance or to tune TEST-ONLY timings. */ lock?: MachineCredentialLock; /** * The component's own OAuth client id (`unity-mcp-cli` / `unreal-mcp-cli` / `godot-cli` / the * App's DCR id). Used ONLY for families that store no `clientId` — `families.legacy` (§3.7) and * transitional enroll-minted plugin families from servers that predate a6. A **stored** * `clientId` always wins (04 §3 rule 2); this is never presented over one. */ defaultClientId?: string; /** Structured telemetry sink (04 §3 rule 5); defaults to a no-op. Never receives token material. */ onTelemetry?: (event: CredentialTelemetryEvent) => void; } export declare class MachineCredentialProvider { private readonly _store; private readonly _refresher; private readonly _skewMs; private readonly _clock; private readonly _onWarning; private readonly _lock; private readonly _defaultClientId; private readonly _onTelemetry; /** Rate discipline (04 §3 rule 6): last network-attempt instant per family. */ private readonly _lastAttemptAt; /** Families this process observed dying (rule 5): family → {reason, the dead refresh token}. */ private readonly _deadFamilies; private _lastFailureReason; constructor(store: MachineCredentialStore, refresher: TokenRefresher, options?: MachineCredentialProviderOptions); /** The underlying store (for callers that need identity fields / sign-out). */ get store(): MachineCredentialStore; /** The cross-process lock guarding this provider's store writes (04 §2). */ get lock(): MachineCredentialLock; /** * True when a usable (access-token-bearing) credential is present on disk for `plane` * (default `plugin` — see {@link CredentialPlane} for how each plane resolves to a family). * Never throws — an unreadable store reads as signed-out here (04 §1: surface "sign in * required"). */ isSignedIn(plane?: CredentialPlane): boolean; /** * Return a valid access token for `options.family` (default `plugin` — the engine CLI's * MCP-server HTTP credential, resolved agent → plugin → legacy; see {@link CredentialPlane}), * proactively refreshing under the lock when it is within the skew window of expiry. The * refresh operates on the family that SERVED the request, presenting that family's stored * `clientId`. * * Throws {@link LoginRequiredError} when signed out or when the token is expired and the family * is dead / the refresh failed; {@link CredentialLockBusyError} when the token is expired and * the lock stayed contended (retry later — NOT a sign-in problem); and * {@link MachineCredentialStoreUnreadableError} when the token is expired and the store is * unreadable (04 §1 "sign in required (store unreadable)" — the file is never touched). * A refresh failure while the current token is still valid returns the current token. */ getAccessToken(options?: { family?: CredentialPlane; signal?: AbortSignal; }): Promise; /** * Reactively refresh now (driven by a server 401) for `options.family` (default `plugin`), * refreshing the SAME family {@link getAccessToken} resolves for that plane — the one whose * token the server just rejected — with that family's stored `clientId`. * * Returns the machine-store document holding the rotated family on success. Its top-level * `accessToken`/`refreshToken`/`expiresAt` are those of the family that was refreshed — for a * plugin/legacy family that is exactly the on-disk v1 compat mirror, for the agent family it is * a view (the on-disk mirror keeps following the plugin plane). This holds even when the * rotation could NOT be persisted: the top level then carries the in-memory rotated token, * never the stale token still on disk. Throws * {@link LoginRequiredError} when refresh is impossible or the family is dead (expiry / * family-revoke), {@link CredentialLockBusyError} on a contended lock (retry later), and * {@link MachineCredentialStoreUnreadableError} on an unreadable store. */ refresh(options?: { family?: CredentialPlane; signal?: AbortSignal; }): Promise; /** * One full family-refresh attempt: dead-family memo → rate discipline → lock → critical section * (re-read → decide → network → write) → post-failure re-read for `invalid_grant` (F3.5). */ private refreshFamily; /** * The 04 §2 critical section, entered ONLY while holding the lock: re-read store → decide * (adopt a peer's refresh without a network call when possible) → network refresh presenting * the stored `clientId` → write the rotated family (same lock hold — c1 review A1). */ private criticalSection; /** Mark a family dead (rule 5): memoized, exactly ONE structured telemetry event, one warning. */ private markFamilyDead; private failureToError; /** * Resolve the concrete family serving `plane` (see {@link CredentialPlane}): agent → `agent`; * plugin → `agent`, then `plugin`, then `legacy`; hub → `plugin`, then `legacy`. */ private resolvePlane; /** Read for the entry points: unreadable → distinct "sign in required (store unreadable)". */ private readOrThrowLoginRequired; private safeRead; private safeReadState; /** * The document {@link refresh} hands back: `document` with its top-level token triple set to * the family that was refreshed, so a caller reading `document.accessToken` retries with the * token it asked for — never the plugin-plane mirror when the agent family was refreshed, and * never the stale on-disk token when the rotation could not be persisted. */ private servedDocument; /** Build the in-memory fallback document when a rotated family could not be persisted. */ private inMemoryDocument; private shouldRefresh; private isExpired; private parseExpiry; } //# sourceMappingURL=credential-provider.d.ts.map