TypeScript SDK
Connect & Grants
Connect sessions, grant lifecycle, delegation revoke, managed-secret grants, HITL approvals.
This page covers every surface for getting a credential into the system and managing its lifecycle: the OAuth provider catalog, Connect sessions (user-consent flows), grant CRUD, agent-delegation revoke, managed-secret grant creation, and human-in-the-loop approvals.
// Provider catalogconst catalog = await app.oauthProviders.list();
// Connect flowsconst results = await app.connect({ providers: ["google"] });const session = await app.createConnectSession({ allowedProviders: ["github"] });const grants = await app.pollConnectSession(session.sessionToken);const recover = await app.createConnectSessionForError(caughtError);const msSess = await app.createManagedSecretConnectSession({ /* ... */ });
// End-user sign-in (IDP login)const auth = await app.authenticate(); // CLI convenienceconst authSession = await app.createAuthSession(); // headless / remote userconst result = await app.pollAuthSession(authSession.sessionToken);
// Grantsconst page = await app.listGrants({ limit: 50 });await app.revokeGrant(grantId, { reason: "rotation" });const grant = await app.createManagedSecretGrant("ms_…", { principal });
// Delegationsawait app.revokeDelegation(grantId, agentId); // operatorawait agent.revokeDelegation(grantId); // agent self-revoke
// Approvalsconst final = await app.awaitApproval(approvalId, { timeoutMs: 600_000 });const status = await app.getApprovalStatus(approvalId);oauthProviders.list
Section titled “oauthProviders.list”Fetch the OAuth provider catalog: the connectable providers configured for the platform and, per provider, the scopes you can request. Use it to drive provider pickers and scope selection instead of hardcoding scope strings.
async list(options?: { forceRefresh?: boolean }): Promise<OAuthProviderCatalog>| Option | Type | Default | Description |
|---|---|---|---|
forceRefresh | boolean | false | Bypass the in-process cache and re-fetch. |
Results are cached in-process for 5 minutes. The catalog lists only active (connectable) providers. Available on both App and Agent as app.oauthProviders / agent.oauthProviders.
Returns OAuthProviderCatalog — a providers record keyed by id, plus getDefaultScopes(provider) / getRequiredScopes(provider) helpers. Throws BackendError on a malformed catalog response. Requires the providers:read scope.
const catalog = await app.oauthProviders.list();const provider = catalog.providers["<providerId>"];console.log(provider.displayName, provider.defaultScopes);
// Pre-fill a Connect flow with the provider's default scopes.const scopes = catalog.getDefaultScopes("<providerId>");createConnectSession
Section titled “createConnectSession”Mint a Connect session URL. The application then surfaces the URL to the user — popup, redirect, mobile webview, Slack message, etc.
async createConnectSession( options?: CreateConnectSessionOptions,): Promise<ConnectSession>| Option | Type | Description |
|---|---|---|
allowedProviders | string[] | Restrict to specific providers (e.g. ["google", "github"]). When omitted the user can pick any configured provider. |
returnUrl | string | URL the browser redirects to after consent (mobile / single-page flows). |
allowedOrigin | string | Origin allowed for the postMessage completion event (popup flow). |
metadata | { ipAddress?: string; userAgent?: string } | Forwarded to the audit row. |
grantPolicy | { maxTtlSeconds?: number; defaultTtlSeconds?: number } | TTL bounds the user picks from on the consent screen. Seconds. |
requiredScopes | Record<string, string[]> | Per-provider scope ceiling. The OAuth URL is built with these scopes (must be a subset of the application’s Dev Portal configuration). |
agent | string | Agent UUID or managed-agent name. When set, on approval the SDK returns a delegation grant that the agent uses as grantId on request(). |
userToken | string | Per-call user JWT. Overrides userTokenGetter. Required when there is no JWT context configured. |
Returns ConnectSession — connectUrl is what the user opens; sessionToken is what you pass to pollConnectSession().
const session = await app.createConnectSession({ allowedProviders: ["google"], allowedOrigin: "https://app.example.com", grantPolicy: { maxTtlSeconds: 30 * 24 * 3600, defaultTtlSeconds: 7 * 24 * 3600 },});
window.open(session.connectUrl, "alter-connect", "popup");const grants = await app.pollConnectSession(session.sessionToken);createManagedSecretConnectSession
Section titled “createManagedSecretConnectSession”Mint a Connect session for the user → agent delegation flow on a managed secret. The user consents to the named agent using the user’s existing managed-secret access.
async createManagedSecretConnectSession( options: CreateManagedSecretConnectSessionOptions,): Promise<ManagedSecretConnectSession>| Option | Type | Description |
|---|---|---|
templateSlug | string | Canonical template slug (kebab-case, e.g. "stripe-api-key"). Required. |
delegatedAgentId | string | UUID of the agent being authorized. Required. |
userToken | string | IDP JWT identifying the consenting user. Required. |
requestedTtlSeconds | number | Caller-suggested TTL. Capped by template policy and the source credential’s expiry. |
delegatedAgentName | string | Display name shown on the consent screen. Defaults to the agent’s stored display name. |
allowedOrigin | string | Origin allowed for the postMessage event. |
returnUrl | string | URL for the mobile redirect flow. |
Returns ManagedSecretConnectSession. The user opens connectUrl; on approval, delegation is created and the agent uses the returned delegationId as grantId on request().
App-only method.
pollConnectSession
Section titled “pollConnectSession”Block until a session reaches a terminal state. Use this when application code minted the session itself and needs to wait for completion.
async pollConnectSession( sessionToken: string, options?: { timeoutMs?: number; pollIntervalMs?: number },): Promise<ConnectResult[]>| Option | Type | Default | Description |
|---|---|---|---|
timeoutMs | number | 300_000 (5 min) | Maximum time to wait. Milliseconds. |
pollIntervalMs | number | 2_000 | Time between polls. Milliseconds. |
Returns one ConnectResult per provider the user completed.
Throws:
ConnectTimeoutError— the session did not complete withintimeoutMs.ConnectDeniedError— the user clicked Deny.ConnectConfigError— provider configuration issue (invalid redirect URI, unknown client, etc.).ConnectFlowError— other failure, including session expiry.
createConnectSessionForError
Section titled “createConnectSessionForError”Recover from a typed credential failure. Pass the caught exception and the method extracts the provider, the delegated agent (when present), and builds a re-authorization session.
async createConnectSessionForError( error: NoDelegatedGrantError | GrantNotFoundError | CredentialRevokedError, options?: { allowedOrigin?: string; returnUrl?: string; metadata?: { ipAddress?: string; userAgent?: string }; grantPolicy?: { maxTtlSeconds?: number; defaultTtlSeconds?: number }; requiredScopes?: Record<string, string[]>; userToken?: string; },): Promise<ConnectSession>try { await app.request("GET", url, { provider: "google" });} catch (error) { if (error instanceof NoDelegatedGrantError) { const session = await app.createConnectSessionForError(error, { allowedOrigin: "https://app.example.com", }); redirectUser(session.connectUrl); const results = await app.pollConnectSession(session.sessionToken); // Retry the original call. }}Throws AlterValueError when the typed error has no providerId context (rare — only happens when the original failure was direct-mode against a stale grantId).
connect
Section titled “connect”The all-in-one headless flow. Mints a session, opens the user’s default browser, polls until done, and returns the resulting grants.
async connect(options: ConnectOptions): Promise<ConnectResult[]>| Option | Type | Default | Description |
|---|---|---|---|
providers | string[] | — | Restrict to specific providers. |
timeout | number | 300_000 | Maximum wait. Milliseconds. |
pollInterval | number | 2_000 | Time between polls. Milliseconds. |
openBrowser | boolean | true | When false, prints the URL instead of launching the browser. |
grantPolicy | { maxTtlSeconds?: number; defaultTtlSeconds?: number } | — | TTL bounds passed to the Connect UI. Seconds. |
const results = await app.connect({ providers: ["github"], timeout: 10 * 60_000,});console.log(`Connected ${results.length} provider(s)`);Use this for CLI tools and scripts. For embedded UI flows, prefer createConnectSession() + pollConnectSession() so application code controls the rendering.
The browser launch uses the optional open peer dependency. When that package is not installed, connect() falls back to printing the URL.
authenticate
Section titled “authenticate”Open the application’s configured IDP login page in the user’s default browser, poll until the user authenticates, and return their IDP JWT.
async authenticate(options?: { timeout?: number }): Promise<AuthResult>| Option | Type | Default | Description |
|---|---|---|---|
timeout | number | 300_000 | Maximum wait. Milliseconds. |
Returns AuthResult with userToken and userInfo.
App-only method.
createAuthSession
Section titled “createAuthSession”The split, headless counterpart to authenticate(). Mints a sign-in session and returns the IDP authUrl without opening a browser or mutating the instance. Hand authUrl to a user on any channel (a chat message, an MCP client, a printed link), then poll with pollAuthSession(). Because it installs no userTokenGetter, it is safe on a shared App that resolves a JWT per request.
async createAuthSession(): Promise<AuthSession>Returns AuthSession with sessionToken, authUrl, expiresIn, and expiresAt. sessionToken is persistable: a worker can store it, poll in the background, and resume after a restart. Throws AlterSDKError (no IDP configured) or BackendError (malformed response). Requires the idp_users:write scope. App-only — agents do not start user logins.
const session = await app.createAuthSession();// Send `session.authUrl` to the user; never log it (it carries the session token).return { signInUrl: session.authUrl, session: session.sessionToken };pollAuthSession
Section titled “pollAuthSession”The polling half of the link-based flow. Resolves when the user finishes IDP login, returning their JWT. Like createAuthSession(), it installs no userTokenGetter — the caller decides what to do with the token.
async pollAuthSession( sessionToken: string, options?: { timeoutMs?: number; pollIntervalMs?: number },): Promise<AuthResult>| Option | Type | Default | Description |
|---|---|---|---|
timeoutMs | number | 300_000 | Maximum wait. Milliseconds. The Python SDK’s equivalent takes seconds. |
pollIntervalMs | number | 2_000 | Milliseconds between polls. |
Transient network blips and non-200 responses are retried until the deadline; only a terminal IDP error, an expired session, or the timeout ends the loop.
Returns AuthResult with userToken and userInfo. Throws AlterValueError (blank sessionToken), ConnectTimeoutError (deadline), or the matching typed subclass on a permanent backend failure. Requires the idp_users:read scope.
const session = await app.createAuthSession();const result = await app.pollAuthSession(session.sessionToken, { timeoutMs: 600_000 });listGrants
Section titled “listGrants”Return the calling principal’s accessible grants, paginated.
async listGrants( options?: AppListGrantsOptions | AgentListGrantsOptions,): Promise<UnifiedGrantListResult>Available on both App and Agent. The backend dispatches by principal kind:
- App — every grant the application owns: OAuth and managed-secret, across all principal kinds (user, group, system, agent). With
userTokenGetterconfigured (orendUserTokenpassed), the list narrows to that end user — their own grants plus group grants they are a live member of. - Agent — only what the agent can reach: OAuth grants delegated to it, plus managed-secret grants it owns or that are delegated to it (
accessViamarksownershipvs delegation). Other agents’ grants, non-delegating users’ grants, and generic system grants are invisible. Mixed result type.
Both AppListGrantsOptions and AgentListGrantsOptions accept the same filters — all AND-ed, each only narrowing the result:
| Option | Type | Default | Description |
|---|---|---|---|
providerId | string | — | Filter to one provider. OAuth: the provider id (e.g. "google"). Managed secret: the per-secret slug (e.g. "stripe-production"), unique per app. |
status | string | — | Filter by grant status (e.g. "active", "expired", "revoked"). |
account | string | — | Filter by accountIdentifier (multi-account disambiguation). |
endUserToken | string | — | Scope to one end user by their JWT — their direct grants plus live group-member grants. On App, takes precedence over userTokenGetter; on Agent, narrows the delegated OAuth grants (agent-owned managed-secret grants are unaffected). An invalid token is a hard 401, never a silent fall-through to app scope. |
limit | number | 100 | Page size (1..1000). |
offset | number | 0 | Page offset. |
Returns UnifiedGrantListResult. Each entry is either an OAuthGrantItem or a ManagedSecretGrantItem; branch on grantKind.
const page = await agent.listGrants({ limit: 50 });for (const grant of page.grants) { if (grant.grantKind === "oauth") { console.log(grant.providerId, grant.scopes); } else { console.log(grant.managedSecretSlug, grant.label); }}if (page.hasMore) { // Fetch next page with offset = page.offset + page.limit.}Throws AlterValueError for out-of-range limit / offset / providerId.
revokeGrant
Section titled “revokeGrant”Revoke a grant. App-only.
async revokeGrant( grantId: string, options?: RevokeGrantOptions,): Promise<RevokeGrantResult>| Option | Type | Description |
|---|---|---|
reason | string | Free-form reason stored on the audit row. |
Returns RevokeGrantResult carrying the grant ID, success flag, and revocation timestamp.
await app.revokeGrant(grantId, { reason: "User requested account deletion" });Revoking an OAuth grant also cascades the revocation to every agent delegation on it.
revokeDelegation
Section titled “revokeDelegation”Revoke an agent’s delegation on a grant. The grant itself stays active; only the named agent loses access.
// App: operator revokes a named agent's delegation.async revokeDelegation(grantId: string, agentId: string): Promise<void>
// Agent: agent revokes its own delegation on a grant.async revokeDelegation(grantId: string): Promise<void>Both paths are idempotent.
// Operator path.await app.revokeDelegation(grantId, agentId);
// Self-revoke path — the agent removes its own access.await agent.revokeDelegation(grantId);Throws AlterValueError when agentId is missing on the App path.
createManagedSecretGrant
Section titled “createManagedSecretGrant”Provision a managed-secret grant. App-only.
async createManagedSecretGrant( managedSecretId: string, options: { principal: Principal; grantPolicy?: { maxTtlSeconds?: number | null; defaultTtlSeconds?: number | null; }; },): Promise<CreateGrantResult>The principal field is a discriminated union — TypeScript narrows it based on the type discriminator. Each principal kind binds the grant to a different identity surface; the label lives on the principal itself (required for user/group bindings, optional otherwise):
| Principal kind | type | Required fields | When to use |
|---|---|---|---|
UserPrincipal | "user" | userToken, label | One end user (resolved from JWT). |
GroupPrincipal | "group" | externalGroupId, idpId, label | All members of an IDP group inherit access. |
SystemPrincipal | "system" | label (optional) | Server-to-server. No caller identity. |
AgentPrincipal | "agent" | Not accepted by this SDK method. Create agent-bound managed-secret grants via the Developer Portal flow instead. |
The optional grantPolicy overrides the TTL bounds inherited from the parent managed secret:
| Field | Type | Description |
|---|---|---|
maxTtlSeconds | number | null | Cap on requested credential TTL. |
defaultTtlSeconds | number | null | Default TTL when callers don’t request one. |
import type { Principal } from "@alter-ai/alter-sdk";
const principal: Principal = { type: "user", userToken: callingUserJwt, label: "prod-stripe-readonly",};
const grant = await app.createManagedSecretGrant("ms_…", { principal, grantPolicy: { maxTtlSeconds: 3600 },});console.log(grant.grantId, grant.principalType);Returns CreateGrantResult with the new grantId, the resolved principal type, and the label.
getApprovalStatus
Section titled “getApprovalStatus”Single-shot poll for an approval row.
async getApprovalStatus(approvalId: string): Promise<ApprovalStatus>Returns ApprovalStatus. The status field is one of:
"pending"— no approver decision yet."approved"— approver said yes; backend is about to execute."executing"— backend is calling the provider."executed"— finished; the result blob is available."denied"— approver said no."expired"— approval window elapsed without a decision."failed"— backend errored while executing the proxied call.
The returned ApprovalStatus exposes a derived isTerminal boolean — true when status is "denied", "expired", "executed", or "failed". Use result.isTerminal to check whether further polling is pointless.
Throws BackendError, NetworkError, TimeoutError.
awaitApproval
Section titled “awaitApproval”Poll until the approval reaches a terminal state, then return the result. Transient 502 / 503 / 504 responses and transport blips are retried within the deadline; permanent failures propagate immediately.
async awaitApproval( approvalId: string, options?: { timeoutMs?: number; pollIntervalMs?: number },): Promise<ApprovalResult>| Option | Type | Default | Description |
|---|---|---|---|
timeoutMs | number | 300_000 (5 min) | Maximum local wait. Milliseconds. |
pollIntervalMs | number | 2_000 | Time between polls. Milliseconds. |
Returns ApprovalResult when the row reaches executed.
Throws:
ApprovalDeniedError— approver denied.ApprovalExpiredError— approval window elapsed.ApprovalExecutionFailedError— backend failed to execute after approval was granted.ApprovalTimeoutError— local wait elapsed before any decision was recorded. When the wait elapsed because the backend was returning transient errors, the message names the underlying cause and the original exception is preserved on.cause.
try { const final = await app.awaitApproval(approvalId, { timeoutMs: 10 * 60_000 }); return final.bodyJson();} catch (error) { if (error instanceof ApprovalDeniedError) { // Surface "request was rejected" to the user. } throw error;}Reading the response body
Section titled “Reading the response body”ApprovalResult carries the provider response as base64.
final.bodyBytes(); // Uint8Arrayfinal.bodyText(); // string (utf-8 default)final.bodyText("latin1");final.bodyJson(); // parsed JSONfinal.bodyTruncated is true if the backend truncated the body (very large responses).
Surfacing PendingApproval to the user
Section titled “Surfacing PendingApproval to the user”PendingApproval .approvalUrl is the deep link to the approver’s wallet UI. Surface it through whatever channel the application uses — modal, redirect, Slack DM, push notification. Email is sent automatically when the backend has an email provider configured.
if (result instanceof PendingApproval) { await notifyApprover({ title: "Approval requested", expiresAt: result.expiresAt, url: result.approvalUrl, }); await app.awaitApproval(result.approvalId, { timeoutMs: 60 * 60_000 });}