/** * entraIdentity — the {@link CredentialProvider} port over Microsoft Entra ID * (peer-dep `@azure/identity`). * * import { entraIdentity } from 'agentfootprint/security'; * const credentials = entraIdentity(); * * ── What it is, and what it deliberately is not ───────────────────────────── * This is the **narrow** adapter: it vends *Entra* access tokens for *Azure* * APIs, from whatever credential the environment already has — the * DefaultAzureCredential chain walks environment service principal, workload * identity, managed identity, VS Code, Azure CLI, Azure PowerShell and the * Azure Developer CLI, in that order. That is one job and it is done * completely. * * It is **not** a user-delegation surface. Entra's on-behalf-of flow (and any * 3-legged consent dance) needs a confidential client app registration that * this adapter does not hold, so `mode: 'user'` is **refused by name** rather * than quietly served with a machine token. A machine token returned where a * user token was asked for is the exact silent downgrade the port exists to * prevent: the call succeeds, the data comes back, and it was the agent's * access rather than the person's. OBO is a later train; when it lands it will * be its own provider, not a flag here. * * ── The audience split, and where it bites ────────────────────────────────── * Azure tokens are minted for ONE audience. {@link AZURE_AI_SCOPE} * (`https://ai.azure.com/.default`) is the data plane — every Foundry and * Azure OpenAI inference call takes it. {@link AZURE_MANAGEMENT_SCOPE} * (`https://management.azure.com/.default`) is the ARM control plane — * listing deployments, creating resources. A token for one audience is a 401 * on the other, which is why BOTH are exported by name instead of leaving the * caller to guess a string. The default here is the data-plane scope, because * vending inference credentials is what an agent runtime does all day. * * ── Caching: the credential, never a token ────────────────────────────────── * One `DefaultAzureCredential` is constructed for the life of the provider and * every `getToken` call goes through it. MSAL — the machinery underneath * `@azure/identity` — caches and proactively refreshes tokens internally, so * caching a token HERE would mean owning an expiry this adapter did not * compute and cannot see revoked. Unlike the Google adapter (where scopes are * fixed at client construction and a different scope set needs its own * client), Azure scopes travel per `getToken` call, so the ONE cached * credential serves every scope set. * * ── Secrets ───────────────────────────────────────────────────────────────── * The `sdkFailure` law, same as every other credential-touching adapter here: * the library's own message never comes through, because auth libraries echo * request detail into 401/403 text and a message thrown from a * `CredentialProvider` reaches the LLM as a tool result AND rides * `agentfootprint.credential.failed` to every sink. What comes through is the * operation that failed and the error's NAME. The original is not attached as * `cause` — a cause travels into every serializer that walks own properties, * which would undo all of it in one `JSON.stringify`. * * Pattern: Adapter (GoF) + lazy peer-dep load — `@azure/identity` is required * the first time `getCredential` runs, or never if you inject a credential. */ import type { CredentialProvider } from '../../identity/types.js'; /** * The data-plane scope for ALL Foundry / Azure OpenAI inference * (`https://ai.azure.com/.default`). This is the default scope this provider * requests. * * The audience split matters: a token minted for this scope does NOT work on * the ARM control plane, and a {@link AZURE_MANAGEMENT_SCOPE} token does not * work here — Azure validates the audience on every call. Both are exported by * name so nobody has to remember which string is which. */ export declare const AZURE_AI_SCOPE = "https://ai.azure.com/.default"; /** * The ARM control-plane scope (`https://management.azure.com/.default`) — * listing deployments, managing resources. A DIFFERENT audience from * {@link AZURE_AI_SCOPE}: a token for one is a 401 on the other, which is why * both are named rather than leaving the caller to guess. */ export declare const AZURE_MANAGEMENT_SCOPE = "https://management.azure.com/.default"; /** * The CLASSIC Azure OpenAI data-plane scope * (`https://cognitiveservices.azure.com/.default`) — the audience Microsoft's * own keyless guidance names for the older deployment-scoped route * (`{endpoint}/openai/deployments/{d}/…`), which is the route `azureOpenai()` * builds and therefore its default. Current resources widely accept * {@link AZURE_AI_SCOPE} too, but an older `*.openai.azure.com` resource may * not — and a door should default to the audience ITS route documents, not the * one its sibling uses. (Azure Government spells this * `https://cognitiveservices.azure.us/.default`.) */ export declare const AZURE_COGNITIVE_SERVICES_SCOPE = "https://cognitiveservices.azure.com/.default"; /** * The `@azure/core-auth` `TokenCredential` duck type — the slice this adapter * calls. Anything `@azure/identity` exports (DefaultAzureCredential, * ManagedIdentityCredential, ClientSecretCredential, …) satisfies it. * * These are THE shared Azure credential duck-types for the whole repo: sibling * Azure adapters `import type` them from this file rather than re-declaring * their own spelling of the same SDK surface. */ export interface TokenCredentialLike { /** * Mint (or serve from MSAL's internal cache) an access token for the given * scope(s). May resolve to `null` — the SDK's spelling of "no token * available" — which this adapter refuses by name rather than passing along. */ getToken(scopes: string | readonly string[], options?: unknown): Promise; } /** * The `@azure/core-auth` `AccessToken` duck type. `expiresOnTimestamp` is unix * MILLISECONDS — the port reports unix SECONDS, and the conversion lives in * exactly one place ({@link entraIdentity}'s vend path). * * Shared repo-wide alongside {@link TokenCredentialLike} — sibling Azure * adapters `import type` it from here. */ export interface AccessTokenLike { /** The bearer token itself. A SECRET — never echoed, never logged. */ readonly token: string; /** Expiry in unix MILLISECONDS epoch (the SDK's unit, not the port's). */ readonly expiresOnTimestamp: number; /** MSAL's proactive-refresh hint, when the SDK provides one. */ readonly refreshAfterTimestamp?: number; /** 'Bearer' | 'pop'; absent on older SDK versions. */ readonly tokenType?: string; } /** The slice of `@azure/identity` this adapter loads. */ export interface AzureIdentitySdkModule { readonly DefaultAzureCredential?: new () => TokenCredentialLike; } /** Options for {@link entraIdentity}. */ export interface EntraIdentityOptions { /** * The scopes to request. Default `[AZURE_AI_SCOPE]` — the data-plane * audience every Foundry / Azure OpenAI inference call accepts. * * A request's own `scopes` win when it names any — a tool that knows it * needs the control plane says so, and this is where that is honoured. */ readonly scopes?: readonly string[]; /** * Which downstream services this provider will answer for. * * Unset — the default — it answers for ANY `service`, because the token it * vends is an Entra credential and the caller knows better than this * adapter which Azure API they are about to call. * * Set it and a request for a service outside the list is refused BY NAME * rather than served. That is the useful setting in a deployment where * tools declare `needs: [{ credential: 'github' }]` alongside Azure ones: * without it, this provider would happily hand an Entra access token to the * tool that wanted a GitHub one, and the failure would surface as a * puzzling 401 from GitHub rather than as a wiring error here. */ readonly services?: readonly string[]; /** Stable provider id (default `'entra-identity'`). */ readonly id?: string; /** * @internal Test seam — a pre-built credential. Bypasses the SDK entirely, * so the suite runs with no package and no Azure account. */ readonly _credential?: TokenCredentialLike; /** @internal Test seam — the SDK module, to exercise the real construction. */ readonly _sdk?: AzureIdentitySdkModule; } /** * Vend Entra access tokens from whatever credential this environment has — * the DefaultAzureCredential chain: environment service principal, workload * identity, managed identity, VS Code, Azure CLI, Azure PowerShell, Azure * Developer CLI. (`AZURE_TOKEN_CREDENTIALS` can restrict the chain; that is * the SDK's own dial and this adapter does not second-guess it.) * * @throws when `mode: 'user'` is requested — no user-delegation surface is * wired for Entra yet (on-behalf-of is a later train), and a machine token * returned in its place would be a silent downgrade. * @throws when `services` is configured and the request names another one. * * @example A tool that calls an Azure API with the deployment's own identity * const agent = Agent.create({ provider, credentials: entraIdentity() }) * .tool(defineTool({ * name: 'ask_foundry', * needs: [{ credential: 'azure-ai' }], * execute: async (args, ctx) => * fetch(url, { headers: ctx.credential!.toHeaders() }).then((r) => r.text()), * })) * .build(); * * @example A control-plane token, without touching the data-plane default * entraIdentity({ scopes: [AZURE_MANAGEMENT_SCOPE] }); */ export declare function entraIdentity(options?: EntraIdentityOptions): CredentialProvider; //# sourceMappingURL=azure.d.ts.map