/** * `session` — a person's **sign-in session** (wire token class * `control_plane_session`). One session, bound to the principal, graded by how * it was minted (`grade` on every mint response and on `GET /agent/v1/whoami`): * * - `browser` — the console: email magic link, passkey, Google, or GitHub. * - `loopback` — `run402 login`: the loopback-PKCE browser ceremony * ({@link Session.buildCliAuthorizeUrl} + {@link Session.exchangeCliToken}). * Full and step-up-able. * - `device` — `run402 login --device`: RFC 8628 device authorization * ({@link Session.deviceStart} + {@link Session.devicePoll}). Read-only: the * gateway refuses every mutation with `403 SESSION_READ_ONLY`. * * Reached as `r.session.*`. Isomorphic — no Node APIs; the loopback server and * PKCE generation live in the Node CLI. The public *mint* methods send no auth * (the body, the magic-link token, or the code + verifier is the credential); * the *session-bound* methods take `opts.token` to send the bearer explicitly, * and fall back to the credential provider's default auth (e.g. * {@link controlPlaneSessionCredentials} or a SIWX wallet) when omitted. * * WebAuthn option/assertion payloads are opaque passthroughs (`unknown`) — the * browser runs the actual ceremony; a headless client cannot. * * High-stakes writes (invite, membership, transfer, delete) require a **fresh * passkey** — a magic-link/OAuth session does NOT satisfy step-up, so the * gateway returns {@link StepUpRequiredError}; `stepUpOptions`/`stepUpVerify` * are how a long-lived session re-establishes that freshness. A write approval * ({@link WriteApproval}) never satisfies step-up. */ import type { Client } from "../kernel.js"; import type { SessionGrade, WhoAmIResult } from "./org.types.js"; /** * A minted sign-in session (`POST /agent/v1/control-plane/cli/token`, * `…/cli/device/token`, and the browser mint routes). Forward-compatible. */ export interface ControlPlaneSession { control_plane_session_token: string; token_type?: string; /** Relative lifetime in seconds. */ expires_in?: number; /** How it was minted: `browser`, `loopback`, or `device` (read-only). */ grade?: SessionGrade | (string & {}); /** The principal id. */ principal_id?: string; /** Auth methods satisfied (e.g. `["passkey"]`). */ amr?: string[]; /** ISO-8601 absolute expiry, when the mint route reports one. */ absolute_expires_at?: string; [key: string]: unknown; } /** RFC 8628 device-authorization start response (`POST …/cli/device`). */ export interface DeviceAuthStart { device_code: string; user_code: string; verification_uri: string; /** Pre-fills the user_code so the person can click straight through. */ verification_uri_complete?: string; expires_in: number; /** Minimum seconds between `devicePoll` calls. */ interval: number; } /** * Result of one {@link Session.devicePoll}. The non-approved states are the * RFC 8628 token error codes — expected polling states, NOT thrown errors, so * callers can run the poll loop without try/catch. An approved session carries * `grade: "device"`. */ export type DevicePollResult = { kind: "approved"; session: ControlPlaneSession; } | { kind: "authorization_pending"; } | { kind: "slow_down"; } | { kind: "access_denied"; } | { kind: "expired_token"; }; /** Parameters for {@link Session.buildCliAuthorizeUrl}. */ export interface CliAuthorizeParams { /** The CLI's loopback redirect, e.g. `http://127.0.0.1:54321/callback`. */ redirectUri: string; /** PKCE S256 challenge = base64url(sha256(verifier)). */ codeChallenge: string; /** Opaque CSRF state echoed back on the redirect. */ state: string; /** Replay nonce. */ nonce: string; } /** Parameters for {@link Session.exchangeCliToken}. */ export interface CliTokenExchange { /** Authorization code received on the loopback redirect. */ code: string; /** The PKCE verifier whose hash was sent as `codeChallenge`. */ codeVerifier: string; /** Must match the `redirectUri` used at authorize time. */ redirectUri: string; /** Must match the `state` used at authorize time. */ state: string; } /** OAuth identity providers bridged for sign-in. */ export type ControlPlaneOAuthProvider = "google" | "github"; /** Generic, non-enumerating response from {@link Session.email}. */ export interface MagicLinkSendResult { status: string; message: string; [key: string]: unknown; } /** * Result of {@link Session.consumeRecoveryCode} — a minted session that * cannot perform high-stakes ops until a passkey is enrolled * (`must_enroll_passkey: true`). Recovery `amr` never satisfies step-up. */ export interface RecoveryConsumeResult extends ControlPlaneSession { must_enroll_passkey?: boolean; note?: string; } /** Result of {@link Session.refresh} (`POST …/session/refresh`). */ export interface ControlPlaneRefreshResult { control_plane_session_token: string; token_type?: string; expires_in?: number; [key: string]: unknown; } /** WebAuthn options envelope (`{ options }`) — opaque; handed to the browser. */ export interface WebAuthnOptionsResult { options: unknown; [key: string]: unknown; } /** Result of {@link Session.enrollPasskeyVerify}. */ export interface EnrollPasskeyResult { status: string; credential_id: string; [key: string]: unknown; } /** Result of {@link Session.stepUpVerify}. */ export interface StepUpVerifyResult { status: string; stepped_up: boolean; [key: string]: unknown; } /** Result of {@link Session.issueRecoveryCodes} — shown ONCE. */ export interface RecoveryCodesResult { status: string; recovery_codes: string[]; note?: string; [key: string]: unknown; } /** One active authenticator (no secret material). Forward-compatible. */ export interface Authenticator { id: string; kind: string; /** Provider-side subject hint (e.g. masked email / credential label); absent when unset. */ subject_hint?: string; /** ISO-8601 enrollment time. */ added_at?: string; /** ISO-8601 last-use time; absent when never used. */ last_used_at?: string; [key: string]: unknown; } /** Result of {@link Session.revokeAuthenticator}. */ export interface AuthenticatorRevokeResult { status: string; kind: string; [key: string]: unknown; } /** Options bag carrying the optional `control_plane_session` bearer. */ export interface SessionTokenOpts { /** * The `control_plane_session` bearer. When omitted, the request falls back to * the credential provider's default auth (e.g. {@link controlPlaneSessionCredentials}). */ token?: string; } export declare class Session { private readonly client; constructor(client: Client); /** * Build the loopback-PKCE authorize URL `run402 login` opens in the browser * (`GET /agent/v1/control-plane/cli/authorize`). Pure — no network. The * caller generates `codeChallenge`/`state`/`nonce` and runs the redirect * server on `127.0.0.1`. */ buildCliAuthorizeUrl(params: CliAuthorizeParams): string; /** * Exchange the loopback authorization code (+ PKCE verifier) for a * `loopback`-grade sign-in session (`POST …/cli/token`). Unauthenticated — * the code + verifier are the credential. */ exchangeCliToken(params: CliTokenExchange): Promise; /** * Begin RFC 8628 device authorization (`POST …/cli/device`) for * `run402 login --device`. Unauthenticated. Returns the codes the CLI prints * (`user_code` + `verification_uri`) plus the poll `interval` and * `expires_in`. The person approves in the console, which needs a recent * sign-in (`403 RECENT_SIGN_IN_REQUIRED` otherwise). */ deviceStart(): Promise; /** * Poll once for approval (`POST …/cli/device/token`). Bypasses the kernel's * error mapping on purpose: the RFC 8628 error codes (`authorization_pending`, * `slow_down`, ...) are normal polling states returned as data, not * exceptions. Only an unexpected response shape throws. The approved session * is `device` grade (read-only). */ devicePoll(deviceCode: string): Promise; /** * Send a control-plane sign-in magic link to `email` * (`POST /agent/v1/control-plane/session/email`). Non-enumerating: an * identical response whether or not the email can sign in. Rate-limited. */ email(input: { email: string; }): Promise; /** * Exchange a magic-link token for a sign-in session * (`POST …/session/email/verify`). Verifies the email, resolves/creates the * principal, **auto-claims any pending invites**, and mints the session * (`amr: ["email"]`). */ verifyEmail(input: { token: string; }): Promise; /** * Get WebAuthn login options for an email's passkeys * (`POST …/session/passkey/options`). Opaque — pass `options` to the browser's * `navigator.credentials.get`. */ passkeyOptions(input: { email: string; }): Promise; /** * Verify a WebAuthn assertion and mint a session (`amr: ["passkey"]`) * (`POST …/session/passkey/verify`). `response` is the opaque assertion from * the browser. */ passkeyVerify(input: { email: string; response: unknown; }): Promise; /** * Build the browser OAuth start URL for `provider` * (`GET …/oauth/:provider/start`). Pure — no network. Open it in a browser; * the gateway 302s to the provider, then the callback lands on the console * with the session token in the URL fragment. * * Note: the live bridge can return `503` until the gateway provisions the * provider's `CONTROL_PLANE_{GOOGLE,GITHUB}_*` client credentials. */ oauthUrl(provider: ControlPlaneOAuthProvider): string; /** * Run the recovery-code ceremony (`POST …/recovery/consume`). Mints a session * with `amr: ["recovery_code"]` which **cannot** do high-stakes ops * (`must_enroll_passkey: true`) — enrol a passkey to restore full access. */ consumeRecoveryCode(input: { code: string; }): Promise; /** * Resolve the session's principal, memberships, and `session` — * `{ grade, amr, amr_times }` (`GET /agent/v1/whoami`). The `memberships` * reflect any invites auto-claimed at sign-in. With no `token` the request * uses the credential provider, and `session` is `null` for a wallet caller. */ whoami(opts?: SessionTokenOpts): Promise; /** Rotate the access token (`POST …/session/refresh`). */ refresh(opts?: SessionTokenOpts): Promise; /** Sign out — revoke the session server-side (`POST …/session/revoke`). Idempotent. */ revoke(opts?: SessionTokenOpts): Promise<{ status: string; [key: string]: unknown; }>; /** WebAuthn registration options for a new passkey (`POST …/passkey/enroll/options`). */ enrollPasskeyOptions(opts?: SessionTokenOpts): Promise; /** Verify a passkey registration (`POST …/passkey/enroll/verify`). `label` names the authenticator. */ enrollPasskeyVerify(input: { response: unknown; label?: string | null; } & SessionTokenOpts): Promise; /** * WebAuthn step-up options for a high-stakes op (`POST …/step-up/options`). * `opClass` binds the elevation, e.g. `"org.invite"` / `"org.membership"` / * `"project.transfer"` (see {@link StepUpRequiredError.requiredAmr}). */ stepUpOptions(input?: { opClass?: string; } & SessionTokenOpts): Promise; /** * Verify a step-up assertion (`POST …/step-up/verify`) → refreshes session * passkey-freshness and records an action-bound elevation when `opClass` (and * optionally `objectKind`/`objectId`) are given. Retry the gated write after. */ stepUpVerify(input: { response: unknown; opClass?: string; objectKind?: string | null; objectId?: string | null; } & SessionTokenOpts): Promise; /** (Re)issue recovery codes — shown ONCE (`POST …/recovery/issue`). */ issueRecoveryCodes(opts?: SessionTokenOpts): Promise; /** List my active authenticators — no secret material (`GET …/authenticators`). */ listAuthenticators(opts?: SessionTokenOpts): Promise; /** * Revoke an authenticator (`DELETE …/authenticators/:id`). Step-up enforced; * the gateway refuses to remove the last passkey of a sole org owner * (`OWNER_NEEDS_PASSKEY`). */ revokeAuthenticator(input: { id: string; } & SessionTokenOpts): Promise; /** * My own source-access key + wrapper set, ciphertext included * (`GET /agent/v1/source-access/wrappers`) — the states/scheme read behind * `run402 repos access`. Principal-scoped structurally: only the * caller's own wrappers ever come back. Enrollment/activation/revocation * are console ceremonies (WebAuthn); this SDK surface is read-only. */ sourceAccessWrappers(opts?: SessionTokenOpts): Promise; /** * Export my versioned member recovery bundle * (`GET /agent/v1/source-access/recovery-bundle`, format * `r402s-member-recovery-bundle/v1`) — key identity + every ACTIVE wrapper * ciphertext. A server-side wrapper row alone is NOT offline backup; this * bundle kept in the member's own storage (separately from the source * recovery code) is what `r402s-recover` opens with no run402 server. The * gateway stamps the export as recovery-posture evidence. */ sourceAccessRecoveryBundle(opts?: SessionTokenOpts): Promise; } /** One wrapper row as `GET /agent/v1/source-access/wrappers` returns it. */ export interface SourceAccessWrapper { wrapper_id: string; encryption_key_id: string; kind: "webauthn_prf" | "recovery_code"; state: "pending" | "active" | "revoked"; format_version: string; credential_subject: string | null; wrapper_ciphertext: string; blob_sha256: string; created_at: string; activated_at: string | null; } export interface SourceAccessWrappersResult { principal_id: string; /** Null when the principal has never enrolled a source-access key. */ encryption_key: { encryption_key_id: string; ek_fingerprint: string; public_key: string; suite: string; custody_scheme: string; state: string; created_at: string; } | null; wrappers: SourceAccessWrapper[]; } /** `r402s-member-recovery-bundle/v1` exactly as the gateway returns it (see the node-side `VaultMemberRecoveryBundle` for the recover-input twin). */ export interface SourceAccessRecoveryBundleResult { format: "r402s-member-recovery-bundle/v1"; exported_at: string; principal_id: string; encryption_key_id: string; ek_fingerprint: string; public_key: string; suite: string; custody_scheme: string; wrappers: Array>; note: string; } //# sourceMappingURL=session.d.ts.map