UNPKG

run402-mcp

Version:

MCP server for Run402 — full-stack backend infrastructure for AI agents: Postgres, auth, storage, serverless functions and atomic deploys. Paid with x402/MPP. Includes $0.03 image generation.

879 lines • 39 kB
/** * Error hierarchy for the Run402 SDK. Every failure throws a subclass of * {@link Run402Error}. Consumers (MCP handlers, CLI commands, user functions) * translate these into their native error shapes at the edge. * * Branch on {@link Run402Error.kind} (or the exported `is*` type guards) rather * than `instanceof`. Discriminator-based checks survive duplicate SDK installs, * bundler chunk splits, ESM/CJS interop, and V8-isolate realm boundaries — * any setting where a class object's identity might differ from the consumer's * own class object reference. `instanceof X` continues to work for callers * holding a single SDK copy (back-compat); the guards are the recommended path. */ export class Run402Error extends Error { /** * Structural brand. Always `true` on any {@link Run402Error} subclass * instance, regardless of which SDK copy created it. The exported * {@link isRun402Error} guard checks this field instead of `instanceof`, * so cross-realm and cross-bundle errors still match. */ isRun402Error = true; /** HTTP status, or null for local/network failures that produced no response. */ status; /** * Parsed response body, or null when no body was received. Holds the raw * gateway envelope, so additive fields not lifted onto typed properties stay * reachable here — notably `correlated_platform_incident` * (`{ id, subsystem, status: "ongoing" | "resolved" }`), present ONLY while * an OPEN platform incident correlates with this error's `code` (a `poll` * action also rides in {@link nextActions}). It is a CORRELATION, not an * exoneration: the platform states it was degraded when the call failed and * leaves the judgment to you. Treat it as a strong signal to poll the events * feed (`r.events.list`) before debugging your own code; the follow-up * `platform_incident` feed event carries the project's real failed-invocation * count once the incident resolves. */ body; /** Short verb phrase identifying the attempted operation (e.g. "provisioning project"). */ context; /** Canonical machine-readable Run402 error code, when the gateway provided one. */ code; /** High-level error category, e.g. lifecycle, deploy, auth. */ category; /** Whether the same request may succeed later. */ retryable; /** Whether repeating the same request should avoid duplicating/corrupting a mutation. */ safeToRetry; /** Gateway-known mutation progress for failed mutating operations. */ mutationState; /** Trace id suitable for support/debugging. */ traceId; /** Canonical structured context. Preserved by reference from the response body. */ details; /** Advisory next actions (gateway-authored or SDK-synthesized). Rendering them must not execute them. */ nextActions; /** * Quota-denial scope (v1.46+). `"organization"` for pooled organization * denials; `"project"` for the orphan fallback (project whose organization * row was purged but cascade has not yet run). Lifted from * `details.scope` when the gateway returned it. Undefined for errors * that are not quota-related. */ quotaScope; constructor(message, status, body, context) { super(message); this.name = this.constructor.name; this.status = status; this.body = body; this.context = context; const envelope = canonicalEnvelope(body); const ctor = this.constructor; const envelopeCode = typeof envelope?.code === "string" ? envelope.code : undefined; const envelopeCategory = typeof envelope?.category === "string" ? envelope.category : undefined; const envelopeRetryable = typeof envelope?.retryable === "boolean" ? envelope.retryable : undefined; if (envelopeCode !== undefined) this.code = envelopeCode; else if (ctor.DEFAULT_CODE !== undefined) this.code = ctor.DEFAULT_CODE; if (envelopeCategory !== undefined) this.category = envelopeCategory; else if (ctor.DEFAULT_CATEGORY !== undefined) this.category = ctor.DEFAULT_CATEGORY; if (envelopeRetryable !== undefined) this.retryable = envelopeRetryable; else if (ctor.DEFAULT_RETRYABLE !== undefined) this.retryable = ctor.DEFAULT_RETRYABLE; if (typeof envelope?.safe_to_retry === "boolean") this.safeToRetry = envelope.safe_to_retry; if (typeof envelope?.mutation_state === "string") this.mutationState = envelope.mutation_state; if (typeof envelope?.trace_id === "string") this.traceId = envelope.trace_id; if (envelope && Object.prototype.hasOwnProperty.call(envelope, "details")) { this.details = envelope.details; } if (Array.isArray(envelope?.next_actions) && envelope.next_actions.length > 0) { this.nextActions = envelope.next_actions; } else { // Gateway gap: `POST /apply/v1/plans` 401 and some // validation 400s return `next_actions: []`. Synthesize the canonical // action for known codes so the relay never hands an agent an empty array. // Only fills gaps — never overrides gateway-authored actions. const synthesized = synthesizeNextActions(this.code); if (synthesized.length > 0) this.nextActions = synthesized; } const scope = extractQuotaScope(envelope); if (scope !== undefined) this.quotaScope = scope; } /** * Canonical structured envelope for `JSON.stringify`. Without this, an * `Error` instance serializes as `"{}"` (its built-in fields are * non-enumerable), losing every structured detail an agent needs for * triage. Subclasses with extra fields (e.g. {@link Run402DeployError}) * override and spread `super.toJSON()`. */ toJSON() { return { name: this.name, kind: this.kind, message: this.message, status: this.status, code: this.code, category: this.category, retryable: this.retryable, safeToRetry: this.safeToRetry, mutationState: this.mutationState, traceId: this.traceId, context: this.context, details: this.details, nextActions: this.nextActions, quotaScope: this.quotaScope, body: this.body, }; } } function canonicalEnvelope(body) { return body && typeof body === "object" && !Array.isArray(body) ? body : null; } function extractQuotaScope(envelope) { if (!envelope) return undefined; const details = envelope.details; if (!details || typeof details !== "object" || Array.isArray(details)) return undefined; const scope = details.scope; return scope === "organization" || scope === "project" ? scope : undefined; } /** * HTTP 402 — a genuine protocol payment challenge (x402 or a successor * rail): insufficient balance, or a priced quote that must be paid before * the request can proceed. style.md reserves 402 for exactly this; quota, * lifecycle (frozen/dormant lease expiry), and budget-cap denials return * 403 instead with their own canonical `code` (see the api-error-envelope * `HTTP 402 is reserved for protocol payment challenges` requirement). */ export class PaymentRequired extends Run402Error { static DEFAULT_CODE = "PAYMENT_REQUIRED"; static DEFAULT_CATEGORY = "payment_required"; static DEFAULT_RETRYABLE = false; kind = "payment_required"; } /** Project ID is not present in the credential provider (local miss) or the gateway returned 404. */ export class ProjectNotFound extends Run402Error { static DEFAULT_CODE = "PROJECT_NOT_FOUND"; static DEFAULT_CATEGORY = "not_found"; static DEFAULT_RETRYABLE = false; kind = "project_not_found"; projectId; constructor(projectId, context, status = null, body = null) { super(`Project ${projectId} not found`, status, body, context); this.projectId = projectId; } } /** * HTTP 401 or 403 — the generic denial bucket: authentication missing, * invalid, or insufficient for the operation. Also the fallback for any * other 403 that isn't one of the more specific subclasses below (e.g. * `NOT_AUTHORIZED`, `STEP_UP_REQUIRED`) — since 402 is reserved for * genuine payment challenges, this now also covers non-payment 403 denials * such as quota (`QUOTA_EXCEEDED`), lifecycle (`PROJECT_FROZEN` / * `PROJECT_DORMANT`), and grant-key spend-cap denials. Check `body.code` (or * use `formatCanonicalErrorContext`-style parsing) to distinguish these * from a true auth failure. */ export class Unauthorized extends Run402Error { static DEFAULT_CODE = "UNAUTHORIZED"; static DEFAULT_CATEGORY = "auth"; static DEFAULT_RETRYABLE = false; kind = "unauthorized"; } /** * HTTP 403 `NOT_AUTHORIZED` — the org-owned control plane (gateway v1.77+) * denied a control-plane action. A wallet *authenticates* (SIWX resolves it to * a principal); *authorization* is an org (organization) membership in the * role lattice `owner > admin > developer > billing > viewer`, or a per-project * grant for agent/CI principals — never `wallet_address == signer`. High-stakes * ops (delete, transfer-of-ownership, membership change) require an active * `owner` membership. * * Distinct from {@link Unauthorized} (authentication missing/invalid): here the * caller IS authenticated but lacks the required role/capability, so the fix is * to obtain access (a membership/grant), not to re-authenticate. The gateway * returns 403 — never 404 — even when the project does not exist, so existence * is not leaked to a non-authorized caller (surfaced as `reason: * "project_not_found"`). Branch on `kind === "not_authorized"` (or * {@link isNotAuthorized}). */ export class NotAuthorizedError extends Run402Error { static DEFAULT_CODE = "NOT_AUTHORIZED"; static DEFAULT_CATEGORY = "auth"; static DEFAULT_RETRYABLE = false; kind = "not_authorized"; /** The control-plane action that was denied, when the gateway named one. */ action; /** Org role required for the action (e.g. `"owner"`), or null when the denial is capability-based. */ requiredRole; /** Per-project capability required (e.g. `"deploy"`), or null when the denial is role-based. */ requiredCapability; /** * Why authorization failed. Known values: `"member"` (no active membership), * `"grant"` (no per-project grant), `"forbidden"` (role/grant too low), and * `"project_not_found"` (returned as 403 to avoid leaking existence). Future * strings pass through unchanged. */ reason; constructor(message, status, body, context) { super(message, status, body, context); const envelope = body && typeof body === "object" && !Array.isArray(body) ? body : null; const details = envelope && typeof envelope.details === "object" && envelope.details !== null && !Array.isArray(envelope.details) ? envelope.details : null; this.action = typeof details?.action === "string" ? details.action : null; this.requiredRole = typeof details?.required_role === "string" ? details.required_role : null; this.requiredCapability = typeof details?.required_capability === "string" ? details.required_capability : null; this.reason = typeof details?.reason === "string" ? details.reason : null; } toJSON() { return { ...super.toJSON(), action: this.action, requiredRole: this.requiredRole, requiredCapability: this.requiredCapability, reason: this.reason, }; } } /** Any other non-2xx HTTP response from the gateway. */ export class ApiError extends Run402Error { static DEFAULT_CODE = "API_ERROR"; static DEFAULT_CATEGORY = "api"; static DEFAULT_RETRYABLE = false; kind = "api_error"; } /** The underlying `fetch` threw before producing a response (DNS, connection reset, offline). */ export class NetworkError extends Run402Error { static DEFAULT_CODE = "NETWORK_ERROR"; static DEFAULT_CATEGORY = "network"; static DEFAULT_RETRYABLE = true; kind = "network_error"; cause; constructor(message, cause, context) { super(message, null, null, context); this.cause = cause; } } /** * Node paid-fetch failure with an explicit payment-effect boundary. * * A caller may repeat the request only when `safeToRetry === true`. Once a * payment-bearing request may have reached the target, the SDK reports an * ambiguous mutation and directs the caller to reconcile by * `paymentAttemptId`; it never turns a post-dispatch transport error into a * blind retry. The raw cause remains available in-process but is deliberately * omitted from `toJSON()` and the canonical body. */ /** * The underlying failure, CLASSIFIED rather than quoted (`details.failure`). A raw cause message * can carry a URL with a token, a key path, or a provider's echo of a secret * (`errors.test.ts` pins that none of that ever serializes), so only the * error's name, its code, and a recognized reason cross into the envelope. */ const CAUSE_REASONS = [ [/maxAmountPerPayment|spendControls/i, "The payment library's own per-payment spend cap refused the amount. run402 sets its caps explicitly; upgrade the run402 CLI/SDK."], [/Facilitator does not support|unsupported network|no supported payment kinds/i, "No registered signer matches the accepted network or scheme."], [/user rejected|denied|signature/i, "The signer refused or could not produce the authorization signature."], ]; function summarizeCause(cause) { if (!cause || typeof cause !== "object") return null; const c = cause; const message = typeof c.message === "string" ? c.message : ""; const reason = CAUSE_REASONS.find(([pattern]) => pattern.test(message))?.[1]; return { error_name: typeof c.name === "string" ? c.name : "Error", ...(typeof c.code === "string" ? { code: c.code } : {}), ...(reason ? { reason } : {}), }; } export class PaymentAttemptError extends Run402Error { kind = "payment_attempt_error"; code; phase; paymentAttemptId; providerStarted; responseStatus; retryable; safeToRetry; mutationState; cause; constructor(init) { const retryable = init.retryable ?? init.safeToRetry; const nextActions = init.nextActions ?? (init.safeToRetry && retryable ? [ { type: "retry", payment_attempt_id: init.paymentAttemptId, why: "No payment-bearing request was dispatched; retrying the original request is safe.", }, ] : !init.safeToRetry ? [ { type: "reconcile_payment", payment_attempt_id: init.paymentAttemptId, safe_to_auto_execute: false, why: "A payment-bearing request may have reached the provider. Reconcile this attempt before issuing another payment.", }, { type: "poll", payment_attempt_id: init.paymentAttemptId, safe_to_auto_execute: true, why: "Poll the target or payment provider for the final state of this attempt.", }, ] : []); const details = { payment_attempt_id: init.paymentAttemptId, phase: init.phase, provider_started: init.providerStarted, ...(init.transportCode ? { transport_code: init.transportCode } : {}), response_status: init.responseStatus ?? null, ...(init.request ? { request: init.request } : {}), // The underlying failure, in words. A signing refusal without its reason // ("could not be created") is undebuggable from a terminal; the cause // never carries keys, headers, or proofs, only the library's message. ...(summarizeCause(init.cause) ? { failure: summarizeCause(init.cause) } : {}), }; super(init.message, null, { error: "payment_attempt_failed", message: init.message, code: init.code, category: init.category ?? "payment", source: "sdk", retryable, safe_to_retry: init.safeToRetry, mutation_state: init.mutationState, details, next_actions: nextActions, }, "executing x402 paid fetch"); this.code = init.code; this.phase = init.phase; this.paymentAttemptId = init.paymentAttemptId; this.providerStarted = init.providerStarted; this.responseStatus = init.responseStatus ?? null; this.retryable = retryable; this.safeToRetry = init.safeToRetry; this.mutationState = init.mutationState; this.cause = init.cause; } toJSON() { return { ...super.toJSON(), phase: this.phase, paymentAttemptId: this.paymentAttemptId, providerStarted: this.providerStarted, responseStatus: this.responseStatus, }; } } /** Local/filesystem error — input validation, missing path, unreadable dir. No HTTP involved. */ export class LocalError extends Run402Error { static DEFAULT_CODE = "LOCAL_ERROR"; static DEFAULT_CATEGORY = "local"; static DEFAULT_RETRYABLE = false; kind = "local_error"; cause; /** One-line remedy for a person reading the error, when the thrower has one. */ hint; /** * @param message Human-readable error message. * @param context Short verb phrase identifying the operation (used for triage). * @param opts Optional. Pass a raw `unknown` for back-compat (treated as `cause`), * or an options bag `{ cause?, code?, hint?, details?, next_actions? }` to thread a stable error * `code` (mirrors a gateway error code so client-side validators throw with * the same `code` field consumers branch on). */ constructor(message, context, opts) { const bag = opts && typeof opts === "object" && !Array.isArray(opts) && ("cause" in opts || "code" in opts || "hint" in opts || "details" in opts || "next_actions" in opts) ? opts : null; const code = bag?.code; const hint = bag?.hint; const details = bag?.details; const nextActions = bag?.next_actions; const cause = bag ? bag.cause : opts; const envelope = code !== undefined || hint !== undefined || details !== undefined || nextActions !== undefined ? { ...(code !== undefined ? { code } : {}), ...(hint !== undefined ? { hint } : {}), ...(details !== undefined ? { details } : {}), ...(nextActions !== undefined ? { next_actions: nextActions } : {}), } : null; super(message, null, envelope, context); if (cause !== undefined) this.cause = cause; if (hint !== undefined) this.hint = hint; } toJSON() { const json = super.toJSON(); return this.hint === undefined ? json : { ...json, hint: this.hint }; } } /** Local credential-cache miss — the project may exist, but no cached keys are available locally. */ export class ProjectCredentialNotFound extends LocalError { projectId; constructor(projectId, context, details = {}) { super(`No local project credentials cached for ${projectId}`, context, { code: "PROJECT_CREDENTIAL_NOT_FOUND", details: { project_id: projectId, source: "local_cache", ...details, }, next_actions: [ { type: "run_command", command: `run402 credentials project-keys status --project ${projectId}`, why: "Inspect whether this profile has cached project keys without revealing secrets.", }, { type: "run_command", command: `run402 credentials project-keys import --project ${projectId} --service-key-stdin`, why: "Import a service key only when this operation is classified as credential-required.", }, ], }); this.name = "ProjectCredentialNotFound"; this.projectId = projectId; } } /** * `SECRET_REQUIRES_CLI` — a method that would return or consume a one-time * secret (a grant key, a Handoff or Invite Key, project credentials, a * private key, a Lightning pairing) was called on a client whose * `capabilities.returnSecrets` is false: the `sandbox` surface an MCP `run` * snippet executes on. Thrown before any request is made, so nothing secret * ever reaches the snippet or the MCP result cache. Carries exactly one * `run_cli_command` next action naming the CLI command that performs the * same operation, where the secret prints once to a person's terminal. */ export class SecretRequiresCliError extends LocalError { /** The exact CLI command line for the same operation. */ command; constructor(command, why) { const reason = why ?? "This operation prints a one-time secret, which only the CLI hands to a person."; super(`This operation returns or consumes a one-time secret and is refused on this client. Run it from the CLI: ${command}`, "checking secret-return capability", { code: "SECRET_REQUIRES_CLI", details: { command }, next_actions: [{ type: "run_cli_command", command, why: reason }], }); this.name = "SecretRequiresCliError"; this.command = command; } toJSON() { return { ...super.toJSON(), command: this.command }; } } export const PROJECT_CREDENTIAL_ERROR_CODES = [ "PROJECT_CREDENTIAL_NOT_FOUND", "PROJECT_CREDENTIAL_INVALID", "PROJECT_CREDENTIAL_EXPIRED", "PROJECT_CREDENTIAL_PROJECT_MISMATCH", ]; const PROJECT_CREDENTIAL_ERROR_CODE_SET = new Set(PROJECT_CREDENTIAL_ERROR_CODES); export class Run402DeployError extends Run402Error { kind = "deploy_error"; code; phase; resource; retryable; operationId; planId; fix; logs; rolledBack; attempts; maxRetries; lastRetryCode; constructor(message, init) { super(message, init.status ?? null, init.body ?? null, init.context); this.code = init.code; this.phase = init.phase ?? null; this.resource = init.resource ?? null; this.retryable = init.retryable ?? false; this.operationId = init.operationId ?? null; this.planId = init.planId ?? null; this.fix = init.fix ?? null; this.logs = init.logs ?? null; this.rolledBack = init.rolledBack ?? false; if (init.attempts !== undefined) this.attempts = init.attempts; if (init.maxRetries !== undefined) this.maxRetries = init.maxRetries; if (init.lastRetryCode !== undefined) this.lastRetryCode = init.lastRetryCode; } toJSON() { return { ...super.toJSON(), phase: this.phase, resource: this.resource, operationId: this.operationId, planId: this.planId, fix: this.fix, logs: this.logs, rolledBack: this.rolledBack, retryable: this.retryable, attempts: this.attempts, maxRetries: this.maxRetries, lastRetryCode: this.lastRetryCode, }; } } /** * Project has a pending transfer (v1.59+). Gateway returns 409 with * `code: "PROJECT_HAS_PENDING_TRANSFER"` from the transfer-freeze middleware * mounted on owner-side mutations (deploy, secrets, custom domains, function * CRUD, scheduled-function changes, mailbox config, CI bindings, project * rename, etc.). The pending transfer must be accepted, cancelled, or * allowed to expire (72h) before owner-side mutations resume. * * The error carries `transferId` (when the gateway resolved it) and * `cancelPath` lifted from `next_actions[].path`, so callers can present an * actionable resolution path. `previewPath` mirrors the view-transfer * next_action when present. */ export class TransferFreezeError extends Run402Error { static DEFAULT_CODE = "PROJECT_HAS_PENDING_TRANSFER"; static DEFAULT_CATEGORY = "validation"; static DEFAULT_RETRYABLE = false; kind = "transfer_freeze"; /** The pending transfer id when the gateway resolved one. */ transferId; /** API path to cancel the pending transfer (e.g. `/agent/v1/transfers/<transfer_id>/cancel`). */ cancelPath; /** API path to view the pending transfer preview. */ previewPath; projectId; constructor(message, status, body, context) { super(message, status, body, context); const envelope = body && typeof body === "object" && !Array.isArray(body) ? body : null; const details = envelope && typeof envelope.details === "object" && envelope.details !== null ? envelope.details : null; this.transferId = typeof details?.transfer_id === "string" ? details.transfer_id : null; this.projectId = typeof details?.project_id === "string" ? details.project_id : null; const actions = Array.isArray(envelope?.next_actions) ? envelope?.next_actions : []; this.cancelPath = pickNextActionPath(actions, "cancel_transfer"); this.previewPath = pickNextActionPath(actions, "view_transfer"); } } /** * Synthesize a canonical next action when the gateway returned a known error * code with an empty/absent `next_actions[]`. Mirrors the WRITE_APPROVAL_* * synthesis on {@link WriteApprovalRequiredError}; only fills gaps, never overrides. */ function synthesizeNextActions(code) { switch (code) { case "AUTH_REQUIRED": return [ { type: "authenticate", auth: "SIWX", why: "Provide SIWX wallet auth (or a sign-in session or grant-key bearer) and retry the request.", }, ]; default: return []; } } function pickNextActionPath(actions, type) { for (const a of actions) { if (a && typeof a === "object" && !Array.isArray(a)) { const obj = a; if (obj.type === type && typeof obj.path === "string") return obj.path; } } return null; } /** * HTTP 403 `STEP_UP_REQUIRED` — the gateway requires a fresh, same-client * step-up (a recent `passkey` AMR) before this high-stakes control-plane * operation (delete / transfer / membership / invite / payment drain·rotate) * may proceed. A `device`-grade session and a write approval can never satisfy * it; the caller must complete the challenge at {@link challengeUrl} (e.g. by * signing in again with `run402 login`) on the same client and retry. * * Typed fields are lifted from the gateway `details` envelope; the same * remediation pointer is also present in {@link Run402Error.nextActions} as an * `authenticate` action. */ export class StepUpRequiredError extends Run402Error { static DEFAULT_CODE = "STEP_UP_REQUIRED"; static DEFAULT_CATEGORY = "auth"; static DEFAULT_RETRYABLE = false; kind = "step_up_required"; /** AMRs that would satisfy the step-up (e.g. `["passkey"]`). Empty when the gateway omitted it. */ requiredAmr; /** Max age in seconds the satisfying auth may be; null when the gateway omitted it. */ maxAgeSeconds; /** Where to run the step-up challenge; null when the gateway omitted it. */ challengeUrl; /** Why the step-up was demanded (e.g. `"device_flow_forbidden"`); null when absent. */ reason; constructor(message, status, body, context) { super(message, status, body, context); const envelope = body && typeof body === "object" && !Array.isArray(body) ? body : null; const details = envelope && typeof envelope.details === "object" && envelope.details !== null && !Array.isArray(envelope.details) ? envelope.details : null; this.requiredAmr = Array.isArray(details?.required_amr) ? details.required_amr.filter((x) => typeof x === "string") : []; this.maxAgeSeconds = typeof details?.max_age_seconds === "number" ? details.max_age_seconds : null; this.challengeUrl = typeof details?.challenge_url === "string" ? details.challenge_url : null; this.reason = typeof details?.reason === "string" ? details.reason : null; } toJSON() { return { ...super.toJSON(), requiredAmr: this.requiredAmr, maxAgeSeconds: this.maxAgeSeconds, challengeUrl: this.challengeUrl, reason: this.reason, }; } } /** * A signed-in person's command-line write (no wallet) needs a passkey-signed * **write approval** scoped to a specific `(capability, target)`. Maps the * gateway codes `WRITE_APPROVAL_REQUIRED` (none attached), * `WRITE_APPROVAL_SCOPE_MISMATCH` (the attached approval covers a different * action or target), `WRITE_APPROVAL_BINDING_MISMATCH` (bound to another sign-in * session), and `WRITE_APPROVAL_SESSION_INVALID` (expired or revoked). * * The SDK synthesizes a fully-resolved remediation from the failing request's * capability + target: read {@link approveCommand} (e.g. `run402 approve * --action project.deploy --project prj_x`) or the structured * {@link nextActions}. The SIWX wallet path never triggers this. A write * approval never satisfies step-up. Branch on * `kind === "write_approval_required"` (or {@link isWriteApprovalRequired}). */ export class WriteApprovalRequiredError extends Run402Error { static DEFAULT_CODE = "WRITE_APPROVAL_REQUIRED"; static DEFAULT_CATEGORY = "auth"; static DEFAULT_RETRYABLE = false; kind = "write_approval_required"; /** The gateway write capability needing approval, when known from the request. */ capability; /** The capability's target (`{ org_id }` or `{ project_id }`), when known. */ target; /** Fully-resolved CLI command that mints the missing approval, or null if unresolvable. */ approveCommand; constructor(message, status, body, context, meta) { super(message, status, body, context); this.capability = meta?.capability ?? null; this.target = meta?.target ?? null; this.approveCommand = buildApproveCommand(this.capability, this.target); // The gateway WRITE_APPROVAL_* envelope carries no next_actions — synthesize // a structured one so generic consumers get the remediation too. if ((!this.nextActions || this.nextActions.length === 0) && this.approveCommand) { this.nextActions = [ { type: "approve_write", command: this.approveCommand, why: approveWhy(this.code) }, ]; } } toJSON() { return { ...super.toJSON(), capability: this.capability, target: this.target, approveCommand: this.approveCommand, }; } } function buildApproveCommand(capability, target) { if (!capability) return null; if (target?.org_id) return `run402 approve --action ${capability} --org ${target.org_id}`; if (target?.project_id) return `run402 approve --action ${capability} --project ${target.project_id}`; return `run402 approve --action ${capability}`; } function approveWhy(code) { if (code === "WRITE_APPROVAL_SCOPE_MISMATCH" || code === "WRITE_APPROVAL_BINDING_MISMATCH") return "The attached write approval covers a different action, target, or sign-in session. Approve this exact target."; if (code === "WRITE_APPROVAL_SESSION_INVALID") return "The write approval expired or was revoked. Approve again."; return "This write needs a passkey write approval."; } // ─── Type guards ───────────────────────────────────────────────────────────── // // Identity-free guards. Each one checks the structural brand and (for subclass // guards) the `kind` discriminator. Use these instead of `instanceof X` so the // check survives duplicate SDK installs, bundler chunk splits, ESM/CJS interop, // and V8-isolate realm boundaries — anywhere the consumer's class object // reference might differ from the throw site's. /** True if `e` is any {@link Run402Error} subclass instance, regardless of which SDK copy created it. */ export function isRun402Error(e) { return Boolean(e && typeof e === "object" && e.isRun402Error === true); } /** True if `e` is a {@link PaymentRequired}. Survives duplicate SDK copies and realms. */ export function isPaymentRequired(e) { return isRun402Error(e) && e.kind === "payment_required"; } /** True if `e` is a {@link ProjectNotFound}. */ export function isProjectNotFound(e) { return isRun402Error(e) && e.kind === "project_not_found"; } /** True if `e` is a local project credential-cache miss. */ export function isProjectCredentialNotFound(e) { return isLocalError(e) && e.code === "PROJECT_CREDENTIAL_NOT_FOUND"; } /** True if `e` carries any stable project-credential error code. */ export function isProjectCredentialError(e) { return isRun402Error(e) && typeof e.code === "string" && PROJECT_CREDENTIAL_ERROR_CODE_SET.has(e.code); } /** True if cached or supplied project credentials were rejected as invalid. */ export function isProjectCredentialInvalid(e) { return isRun402Error(e) && e.code === "PROJECT_CREDENTIAL_INVALID"; } /** True if cached or supplied project credentials were rejected as expired. */ export function isProjectCredentialExpired(e) { return isRun402Error(e) && e.code === "PROJECT_CREDENTIAL_EXPIRED"; } /** True if service-key auth was supplied for a different explicit project id. */ export function isProjectCredentialProjectMismatch(e) { return isRun402Error(e) && e.code === "PROJECT_CREDENTIAL_PROJECT_MISMATCH"; } /** True if `e` is an {@link Unauthorized}. */ export function isUnauthorized(e) { return isRun402Error(e) && e.kind === "unauthorized"; } /** True if `e` is a {@link NotAuthorizedError} (org-owned control-plane denial, gateway v1.77+). */ export function isNotAuthorized(e) { return isRun402Error(e) && e.kind === "not_authorized"; } /** True if `e` is an {@link ApiError}. */ export function isApiError(e) { return isRun402Error(e) && e.kind === "api_error"; } /** True if `e` is a {@link NetworkError}. */ export function isNetworkError(e) { return isRun402Error(e) && e.kind === "network_error"; } /** True if `e` is a phase-aware automatic x402 payment failure. */ export function isPaymentAttemptError(e) { return isRun402Error(e) && e.kind === "payment_attempt_error"; } /** True if `e` is a {@link LocalError}. */ export function isLocalError(e) { return isRun402Error(e) && e.kind === "local_error"; } /** True if `e` is a {@link Run402DeployError}. */ /** True for the local refusal a `sandbox` client throws instead of returning a one-time secret. */ export function isSecretRequiresCli(e) { return isLocalError(e) && e.code === "SECRET_REQUIRES_CLI"; } export function isDeployError(e) { return isRun402Error(e) && e.kind === "deploy_error"; } /** True if `e` is a {@link TransferFreezeError}. */ export function isTransferFreezeError(e) { return isRun402Error(e) && e.kind === "transfer_freeze"; } /** True if `e` is a {@link StepUpRequiredError}. Survives duplicate SDK copies and realms. */ export function isStepUpRequired(e) { return isRun402Error(e) && e.kind === "step_up_required"; } /** True if `e` is a {@link WriteApprovalRequiredError} (a wallet-less write needs a passkey write approval). */ export function isWriteApprovalRequired(e) { return isRun402Error(e) && e.kind === "write_approval_required"; } /** * Extract the v1.46+ quota-denial scope from an error. Returns `"organization"` * for pooled denials, `"project"` for the orphan fallback, or `undefined` * when the error is not quota-related (or originated from a pre-v1.46 * gateway that did not set `details.scope`). Safe to call with `unknown`. */ export function getQuotaScope(e) { return isRun402Error(e) ? e.quotaScope : undefined; } /** * Canonical "should I retry this?" policy. Returns true when `e` is a * {@link Run402Error} AND any of: * - `e.retryable === true` (gateway flagged it) * - `e.kind === "network_error"` (fetch never produced a response) * - `e.status` is 408 (Request Timeout), 425 (Too Early), or 429 (Too Many * Requests) * - `e.status` is a 5xx server error * * `safeToRetry` is deliberately NOT sufficient by itself: it means a repeated * mutation should not duplicate/corrupt state, not that the request can succeed * without a lifecycle/payment/auth action first. * * Returns false for non-Run402 errors so it can be safely called with * `unknown` from a catch block. Used as the default `retryIf` in * {@link withRetry}. */ export function isRetryableRun402Error(e) { if (!isRun402Error(e)) return false; if (e.retryable === true) return true; if (e.kind === "deploy_error" && e.retryable === false) return false; if (hasExplicitRetryableFalse(e.body)) return false; if (e.kind === "network_error") return true; const s = e.status; if (s === 408 || s === 425 || s === 429) return true; if (typeof s === "number" && s >= 500) return true; return false; } function hasExplicitRetryableFalse(body) { return Boolean(body && typeof body === "object" && !Array.isArray(body) && Object.prototype.hasOwnProperty.call(body, "retryable") && body.retryable === false); } //# sourceMappingURL=errors.js.map