Skip to content

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 catalog
const catalog = await app.oauthProviders.list();
// Connect flows
const 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 convenience
const authSession = await app.createAuthSession(); // headless / remote user
const result = await app.pollAuthSession(authSession.sessionToken);
// Grants
const page = await app.listGrants({ limit: 50 });
await app.revokeGrant(grantId, { reason: "rotation" });
const grant = await app.createManagedSecretGrant("ms_…", { principal });
// Delegations
await app.revokeDelegation(grantId, agentId); // operator
await agent.revokeDelegation(grantId); // agent self-revoke
// Approvals
const final = await app.awaitApproval(approvalId, { timeoutMs: 600_000 });
const status = await app.getApprovalStatus(approvalId);

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>
OptionTypeDefaultDescription
forceRefreshbooleanfalseBypass 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>");

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>
OptionTypeDescription
allowedProvidersstring[]Restrict to specific providers (e.g. ["google", "github"]). When omitted the user can pick any configured provider.
returnUrlstringURL the browser redirects to after consent (mobile / single-page flows).
allowedOriginstringOrigin 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.
requiredScopesRecord<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).
agentstringAgent UUID or managed-agent name. When set, on approval the SDK returns a delegation grant that the agent uses as grantId on request().
userTokenstringPer-call user JWT. Overrides userTokenGetter. Required when there is no JWT context configured.

Returns ConnectSessionconnectUrl 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);

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>
OptionTypeDescription
templateSlugstringCanonical template slug (kebab-case, e.g. "stripe-api-key"). Required.
delegatedAgentIdstringUUID of the agent being authorized. Required.
userTokenstringIDP JWT identifying the consenting user. Required.
requestedTtlSecondsnumberCaller-suggested TTL. Capped by template policy and the source credential’s expiry.
delegatedAgentNamestringDisplay name shown on the consent screen. Defaults to the agent’s stored display name.
allowedOriginstringOrigin allowed for the postMessage event.
returnUrlstringURL 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.

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[]>
OptionTypeDefaultDescription
timeoutMsnumber300_000 (5 min)Maximum time to wait. Milliseconds.
pollIntervalMsnumber2_000Time between polls. Milliseconds.

Returns one ConnectResult per provider the user completed.

Throws:

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).

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[]>
OptionTypeDefaultDescription
providersstring[]Restrict to specific providers.
timeoutnumber300_000Maximum wait. Milliseconds.
pollIntervalnumber2_000Time between polls. Milliseconds.
openBrowserbooleantrueWhen 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.

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>
OptionTypeDefaultDescription
timeoutnumber300_000Maximum wait. Milliseconds.

Returns AuthResult with userToken and userInfo.

App-only method.

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 };

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>
OptionTypeDefaultDescription
timeoutMsnumber300_000Maximum wait. Milliseconds. The Python SDK’s equivalent takes seconds.
pollIntervalMsnumber2_000Milliseconds 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 });

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 userTokenGetter configured (or endUserToken passed), 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 (accessVia marks ownership vs 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:

OptionTypeDefaultDescription
providerIdstringFilter to one provider. OAuth: the provider id (e.g. "google"). Managed secret: the per-secret slug (e.g. "stripe-production"), unique per app.
statusstringFilter by grant status (e.g. "active", "expired", "revoked").
accountstringFilter by accountIdentifier (multi-account disambiguation).
endUserTokenstringScope 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.
limitnumber100Page size (1..1000).
offsetnumber0Page 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.

Revoke a grant. App-only.

async revokeGrant(
grantId: string,
options?: RevokeGrantOptions,
): Promise<RevokeGrantResult>
OptionTypeDescription
reasonstringFree-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.

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.

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 kindtypeRequired fieldsWhen to use
UserPrincipal"user"userToken, labelOne end user (resolved from JWT).
GroupPrincipal"group"externalGroupId, idpId, labelAll 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:

FieldTypeDescription
maxTtlSecondsnumber | nullCap on requested credential TTL.
defaultTtlSecondsnumber | nullDefault 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.


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.

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>
OptionTypeDefaultDescription
timeoutMsnumber300_000 (5 min)Maximum local wait. Milliseconds.
pollIntervalMsnumber2_000Time between polls. Milliseconds.

Returns ApprovalResult when the row reaches executed.

Throws:

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;
}

ApprovalResult carries the provider response as base64.

final.bodyBytes(); // Uint8Array
final.bodyText(); // string (utf-8 default)
final.bodyText("latin1");
final.bodyJson(); // parsed JSON

final.bodyTruncated is true if the backend truncated the body (very large responses).

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 });
}