R402_* — error reference
run402.com →

R402_* error reference

Stable error codes emitted by the Run402 gateway and the @run402/astro SSR adapter. Each carries code, message, suggestedFix, docs, and (when statically determinable) file, line on the wire envelope.

Codes are protocol-stable — the exact uppercase string is what's emitted in JSON envelopes, response headers, logs, and CLI output. When present, a docs field can deep-link into this page via #R402_FOO anchors, so build output and gateway error responses are one click away from a fix.

Errors that can precede your handler #

A request to one of your functions can fail before your code runs. The Run402 gateway parses the request body, authenticates the apikey (or Bearer token), and applies rate limits at the boundary — so a transport, JSON-parse, or auth failure is answered by run402, not by your handler. Your function cannot rewrap these: it never executes (auth is the gate), or never sees the raw bytes (the body is pre-parsed).

The contract is therefore two-layered: run402 owns the transport / JSON-parse / authentication / rate-limit boundary; your function owns its domain errors. To make the two legible, every gateway-boundary envelope is stamped "source": "gateway". A caller branches on it deterministically: source: "gateway" → resolve against this catalog; no source (or your own value) → resolve against your app's contract. Boundary envelopes carry the same rich shape as the rest of this reference — code, category, trace_id, safe_to_retry, and an actionable next_actions[].

The pre-handler boundary codes:

Canonical vocabulary (recommended for apps) #

run402 keeps its own protocol-stable codes (this page) — it does not mimic any app's error vocabulary, and it does not require you to change yours. But if you want a caller to see one consistent envelope across both layers, the simplest path is to adopt run402's canonical shape for your own (post-handler) errors too. This is a recommendation, not a mandate.

The canonical envelope: { error, message, code, category, source, retryable, safe_to_retry, mutation_state, trace_id, details, next_actions[] }. code is a SCREAMING_SNAKE string; category is one of validation · auth · billing · quota · rate_limit · lifecycle · not_found · conflict · deploy · storage · upstream · internal; next_actions[] each name a machine-actionable type (retry · authenticate · submit_payment · renew_tier · check_usage · resume_deploy · edit_request · edit_migration · contact_support) and a human why. App errors should omit source (or set their own), reserving "gateway" for run402's boundary so the two layers stay distinguishable.

Index

R402_ASTRO_VERSION_UNSUPPORTED #

The installed Astro version is outside the adapter peer range. Check the installed @run402/astro package; this source revision supports Astro >=6 <8. Install a version inside that range and rebuild.

R402_ASTRO_DYNAMIC_IMAGE_UNSUPPORTED #

Found <Image src={expr}> where expr is not statically resolvable (DB-row property access, function call, environment variable, frontmatter value, etc.).

**Suggested fix:** for CMS images, store the full AssetRef JSON from assets.put() at upload time and render with <Run402Picture asset={page.hero_asset} />. For build-time static images, use <Image src="./hero.jpg"> with a string literal OR import hero from "../assets/hero.png"; <Image src={hero} /> with a static import binding.

R402_ASTRO_SERVER_ISLAND_UNSUPPORTED #

Found a server:defer or server:only directive. Server islands are unsupported.

**Suggested fix:** use client islands (client:load, client:idle, client:visible) instead, OR move the rendering into the page's frontmatter at SSR time.

R402_ASTRO_SESSIONS_UNSUPPORTED #

The installed adapter rejects Astro Sessions API usage. Remove that configuration and use the supported hosted authentication/session helpers for app login. Application-owned session systems require their own cookie, CSRF, expiry and revocation design; a plain cookie is not an equivalent replacement.

R402_ASTRO_MIDDLEWARE_UNSUPPORTED #

An unsupported middleware pattern was detected. Inspect the diagnostic and use the supported request-context middleware or page/endpoint auth helpers. This code alone does not prove the middleware ran successfully.

R402_ASTRO_BUILD_FAILED #

Astro's own build pipeline threw an unrecovered error. message carries the compiler error summary; file / line when statically determinable.

**Suggested fix:** check the build log for the specific Astro compiler error and address it (typically a syntax error, type error, or missing import in a .astro page).

R402_ASTRO_UNSUPPORTED_OUTPUT #

The requested output mode is unsupported by the installed adapter. Use the supported server preset and per-route export const prerender = true for build-time pages. Consult the installed package peer range before upgrading Astro.

R402_ASTRO_ADAPTER_MANIFEST_MISSING #

The @run402/astro/release-slice SDK helper (or the run402 deploy --dir CLI shortcut) could not locate, read, or parse dist/run402/adapter.json. Thrown by loadAstroAdapterManifest() when the file is absent, unreadable, not JSON, or not a JSON object.

When

Deploy-script call to buildAstroReleaseSlice(distDir) or loadAstroAdapterManifest(distDir); also surfaced by run402 deploy --dir <path> when the directory does not contain a Run402-built Astro tree.

Fix

Run astro build with @run402/astro registered in astro.config.mjs first (the adapter writes dist/run402/adapter.json in its astro:build:done hook). If the build did run, verify the helper is reading the correct distDir (relative paths resolve against process.cwd()). If dist/run402/adapter.json is present but malformed, delete it and re-run the build — the adapter rewrites it from scratch.

R402_ASTRO_ADAPTER_MANIFEST_VERSION_UNSUPPORTED #

The dist/run402/adapter.json manifest's version field is not in the helper's supported set. The helper is conservative on version skew: @run402/astro/release-slice only consumes manifest shapes it has been tested against.

When

Deploy-script call to buildAstroReleaseSlice(distDir) or loadAstroAdapterManifest(distDir); also surfaced by run402 deploy --dir <path>.

Fix

Upgrade @run402/astro and the helper to compatible versions in lockstep, or pin the adapter to a manifest version the helper accepts. The error's observedVersion field names the manifest's actual declared version; the message lists the helper's accepted set.

R402_ASTRO_IMAGE_ASSET_MISSING #

The asset prop on <Run402Image> was null/undefined.

Fix

Pass a typed AssetRef: rehydrate from a DB JSONB value via r.assets.fromRef(rawJsonbValue), OR get one fresh from an upload via r.assets.put(file, key).

R402_ASTRO_IMAGE_ASSET_STRING_URL #

The asset prop was a string URL, not an AssetRef object. <Run402Image> requires the typed AssetRef so it can wire variants, dimensions, the blurhash placeholder, etc. — a raw URL carries none of that.

Fix

If you have a stored AssetRef in your DB, rehydrate via r.assets.fromRef(rawJsonbValue). If you have raw bytes to upload, call r.assets.put(file, key) server-side first and pass the returned ref. The error message includes the offending URL (truncated to 120 chars) to help locate the misuse.

R402_ASTRO_IMAGE_ASSET_WRONG_SHAPE #

The asset prop was an object but is missing the required cdn_url field — the only truly-required field on an AssetRef. Every other field is optional per the field-level fallback rules.

Fix

Re-fetch the AssetRef via r.assets.fromRef(...) or r.assets.get(key). The error's fix.missing_fields envelope names the column.

R402_ASTRO_IMAGE_NON_IMAGE_ASSET #

The AssetRef's content_type is present but isn't an image/* MIME (e.g., application/pdf). <Run402Image> renders only image assets.

Fix

For PDFs / videos / generic downloads, use a plain <a href={asset.cdn_url} download>...</a> link instead. The suggested-fix line in the error message includes a paste-ready snippet.

R402_ASTRO_IMAGE_ALT_REQUIRED #

The alt prop was missing. TypeScript catches this at compile time; the runtime guard catches JS consumers who bypass TS.

Fix

Pass an alt string. Empty string alt="" is explicitly allowed for decorative images per HTML5 §4.7.4.4.

R402_ASTRO_IMAGE_CONFLICTING_CLASS_PROPS #

Both class and className were passed on the same call. Common during a React → Astro port. Silent resolution would surprise (which wins?), so the component fails loudly.

Fix

Pass one or the other. class is the canonical Astro form; className is the React port alias and is normalized to class in the rendered HTML.

R402_ASTRO_IMAGE_CONFLICTING_LOADING_PROPS #

priority={true} + loading="lazy" are contradictory. priority implies loading="eager" + fetchpriority="high"; the explicit loading="lazy" alongside is ambiguous.

Fix

Drop one of the two. For above-fold heroes use priority alone; for below-fold images use loading="lazy" alone (the default).

R402_ASTRO_IMAGE_HEIC_NO_TRANSCODE #

The AssetRef has content_type: "image/heic" (or "image/heif") AND is missing the display_jpeg variant. The component cannot render — <img src="<heic-url>"> shows ZERO pixels in Firefox + Chrome (~75% of global browser share). This hard-fail is the HEIC correctness floor: it fires unconditionally, regardless of strict mode, schema filter, or any other prop.

Fix

Three operator paths forward:

R402_ASTRO_IMAGE_SIZES_REQUIRED #

The AssetRef has multiple variants but the sizes prop wasn't passed. Without sizes, browsers over-fetch the largest variant on mobile — defeating the variant ladder.

Fix

Pass a sizes attribute matching your layout. Common patterns:

R402_ASTRO_IMAGE_STRICT_DEGRADED #

Strict-mode is enabled (via per-call strict prop OR project-level imageDefaults.strict) AND the AssetRef is missing fields below the full v1.49+ target. The component refuses to render the degraded output.

The envelope carries a subcode identifying the specific failure:

Fix

Three paths forward depending on the subcode:

R402_ASTRO_IMAGE_RESERVED_DATA_ATTR #

The caller passed a data-run402-image attribute. This key is reserved by the component — it emits the major version ("1" for v1.x) on the outermost rendered element so CSS selectors and test hooks can target Run402-rendered images stably.

Fix

Drop the prop or rename to a different data-* key (e.g., data-component-marker). TypeScript catches this at compile time via the DataAttributes mapped type's Exclude clause; the runtime guard is for pure-JS consumers.

R402_ASTRO_IMAGE_WRONG_ENTRY_POINT #

The component was imported from the wrong entry point — typically a .tsx file importing from @run402/astro/components (the Astro entry) OR a .astro file importing from @run402/astro/react (the React entry). TypeScript's brand types catch this at compile time; the runtime guard is the defense-in-depth tier for JS consumers.

Fix

// Astro context (.astro file):
import { Run402Image } from "@run402/astro/components";

// React context (.tsx / .jsx file):
import { Run402Image } from "@run402/astro/react";

R402_BUNDLE_NATIVE_DEP_UNSUPPORTED #

The function bundler rejected a native binary dependency. Replace it with a supported dependency or application runtime helper. Use the CLI asset workflow for operating uploads; application-side image, database, AI and email calls belong in their supported runtime APIs.

R402_BUNDLE_UNRESOLVED_IMPORT #

The deploy bundler couldn't resolve an import in a function file.

**Suggested fix:** check the import path is correct; ensure the package is in package.json's dependencies (NOT devDependencies); run npm install.

R402_SNAPSTART_INIT_IO #

Move network and database work from module initialization into the request handler. Inspect the deployment report for whether activation occurred and whether a resume is available. This diagnostic alone does not establish activation, snapshot success or a latency guarantee.

R402_SDK_OUTSIDE_REQUEST_CONTEXT #

A request-scoped runtime helper was called during module initialization or after its request ended. Move the awaited call into a request handler. Use a declared scheduled trigger and durable function run for work that must outlive a response; do not rely on an unawaited timer.

R402_SSR_RUNTIME_ERROR #

The SSR render failed. Use run402 logs --request-id <actual-request-id> --project <project-id> and inspect the available application logs. Fix the reported source or configuration issue and redeploy. Public diagnostics omit private stack information; exact log detail depends on the runtime and its redaction policy.

R402_CACHE_UNSUPPORTED_VARY #

The cache cannot safely partition this response by its requested Vary dimensions, so it bypasses storage. Preserve the response’s correctness and privacy. Use supported cache-key dimensions or separate public paths only when that preserves the application semantics; do not delete Vary merely to force caching.

R402_CACHE_AUTH_TAINTED #

An auth-dependent render bypassed public cache storage. This is expected isolation behavior. Keep personalized responses uncacheable; do not bypass auth helpers or read cookies directly merely to avoid cache taint.

R402_CACHE_INVALIDATION_HOST_REQUIRED #

cache.invalidate('/path') (path-string form) was called outside a request context. The path-string form needs the current host from the ALS request context to scope correctly.

**Suggested fix:** use the absolute-URL form: cache.invalidate(new URL('https://eagles.kychon.com/the-guys')) OR cache.invalidate('https://eagles.kychon.com/the-guys'). Or move the call into a request handler.

R402_CACHE_INVALIDATION_HOST_FORBIDDEN #

cache.invalidate(<absolute URL>) targeted a host that is NOT owned by the caller's authenticated project. Cross-project cache mutation is rejected for tenant isolation.

**Suggested fix:** use a host attached to your project. Run run402 domains list to see your project's attached hosts.

R402_LOCALE_NOT_CANONICAL #

A spec.i18n.locales[] entry or spec.i18n.defaultLocale isn't in RFC 5646 §2.1.1 canonical casing. The envelope's fix: field carries { input, canonical } so an automated agent can apply the suggested fix without parsing the message.

When

Deploy, at POST /apply/v1/plans validation (before any DB write).

Fix

Use the canonical form named in the error message. Rules per RFC 5646 §2.1.1:

The platform rejects rather than auto-canonicalizes so your spec and DB-stored translation keys (e.g., section_translations.language) stay byte-identical. Silent canonicalization on input would split the spec from the DB column, surfacing as silent runtime 404s instead of a clear deploy-time error.

R402_DEPLOY_STAGE_FAILED #

Inspect the reported stage, operation identity, field diagnostics and next actions. Resolve the failing input or dependency, then follow the returned resume/retry action only when permitted. An ambiguous activation result requires inspection; promotion does not reverse migrations.

R402_AUTH_REQUIRED #

auth.requireUser() called on an anonymous request. The platform decides the response shape from the Accept header: text/html gets a 303 to /auth/sign-in?returnTo=<encoded>; everything else gets a 401 envelope.

Fix

Don't catch — the platform handles the redirect-vs-envelope split. For optional personalization use auth.user() (returns null on anonymous instead of throwing).

R402_AUTH_INSUFFICIENT_ROLE #

auth.requireRole(role) called by an authenticated user who doesn't hold the named role. HTTP 403. NOT a sign-in error — the platform does NOT redirect to /auth/sign-in (the user IS signed in).

Fix

Request access via your app's onboarding flow. The error envelope's details.required_role names the missing grant.

R402_AUTH_INSUFFICIENT_MEMBERSHIP #

auth.requireMembership(m) called by an authenticated user who doesn't hold the named membership. HTTP 403. Same shape as INSUFFICIENT_ROLE — not a sign-in error.

R402_AUTH_FRESHNESS_REQUIRED #

auth.requireFresh({ maxAge, amr? }) called for a proof that's stale. Per-method: a recent password proof does NOT satisfy {amr: ["passkey"]}.

Fix

HTML: 303 → /auth/re-auth?returnTo=. JSON: details.max_age + details.amr name what would satisfy. Re-prove the named AMR method.

R402_AUTH_SESSION_EXPIRED #

The cookie passed strict parser, but expires_at (sliding) or hard_expires_at (30d cap) is in the past. Cookie is cleared on the response.

R402_AUTH_SESSION_INVALID #

Cookie failed strict regex (^v1\.[uuid]\.[43-char base64url]$), or multiple __Host-Http-r402_session cookies were present, or the cookie value contained control characters, or the HMAC verifier mismatched. Cookie is cleared.

The gateway's response-validation layer rejected an outgoing Set-Cookie: __Host-Http-r402_session=... missing HttpOnly / Secure / SameSite=Lax / Path=/, or carrying a Domain= attribute. Server-side bug.

R402_AUTH_CSRF_ORIGIN_MISMATCH #

Cookie-authenticated unsafe-method request (POST/PUT/PATCH/DELETE) with Origin mismatched or absent. Origin: null always fails (sandboxed iframes, opaque origins, file:// URIs). Referer fallback uses full-origin equality (scheme + host + port — NOT host-only). Both absent fails closed.

Fix

Submit the form from the same origin. For routes that legitimately accept cross-origin Bearer requests, declare auth.precedence: "bearer" on the route — the CSRF gate is cookie-only.

R402_AUTH_CSRF_TOKEN_MISMATCH #

Hosted-auth form (POST /auth/sign-out, POST /auth/re-auth) submitted without the platform CSRF token, or with a token that didn't match the double-submit derivation. Missing token AND mismatched token both produce this error so attackers can't differentiate.

Fix

Render the form with auth.csrfField() so the _csrf hidden input is present. The <UserButton /> component from @run402/astro does this for you.

Request presents both a session cookie AND a Authorization: Bearer token, but they resolve to different (user_id, project_id). HTTP 400.

Fix

Send one credential, not both. Or declare auth.precedence on the route to pick which wins (browser code shouldn't carry both — the SDK never attaches Bearer in cookie-context calls).

R402_AUTH_INVALID_BEARER #

Valid cookie present alongside a malformed or expired Authorization: Bearer token. Rejects rather than silently proceeding as the cookie's actor — silent fall-through would hide bugs and create inconsistent downstream state.

R402_AUTH_ACTOR_HEADER_SPOOF #

A client-supplied reserved actor-context header (x-run402-*, run402-*, x-r402-*, run402.actor.*) reached the gateway. The header is stripped at ingress; the diagnostic is logged so spoof attempts surface in metrics.

Fix

Don't set these headers from the client. The gateway populates them via its trusted ingress pipeline.

R402_AUTH_RETURN_TO_INVALID #

A hosted-auth route received a returnTo value that's not path-relative (begins / but not //) or same-origin absolute. Rejected: protocol-relative (//evil.example), foreign origins, embedded credentials, control characters, unsupported schemes (javascript:, data:, file:, ftp:).

Fix

Pass a relative path (/forum/topic-1) or a same-origin absolute URL (https://<your-host>/forum/topic-1). The error's details.attempted_returnTo echoes what was sent.

R402_AUTH_STATE_CHANGING_GET #

Build-time / lint-time diagnostic. run402 doctor source scan detected a GET handler that mutates state (db().insert(), adminDb().sql("UPDATE …"), etc.). GETs are safe by HTTP convention; mutating in a GET handler is a footgun (link previews, prefetch, crawlers).

Fix

Move the mutation to POST. The hosted UI already does this (GET /auth/sign-out is redirect-safe; only POST /auth/sign-out revokes).

auth.identities.link({provider, subject, proof}) would create a duplicate (project_id, provider, subject) link. HTTP 409.

Fix

First valid linker wins. Surface a recovery flow on the second attempt — "this wallet is already linked to another account".

R402_AUTH_SESSION_BRIDGE_UNVERIFIED #

Either auth.sessions.createResponseFromIdentity({...}) couldn't verify the supplied proof, OR consumer code attempted to access an internal-only session-creation primitive (auth.sessions._unsafeCreate).

Fix

Supply a verifiable proof shape: {kind: "siwx", signature, message, nonce?} for wallets, {kind: "oidc_jwt", token, nonce?} for OIDC, {kind: "custom", payload, nonce?} for admin-registered providers. Raw-userId session minting is NOT in the public SDK.

R402_AUTH_UNKNOWN_IDENTITY #

createResponseFromIdentity resolved the proof but no internal.identities row matched AND createUser: true was not set.

Fix

Pass createUser: true if first-sign-in should create the user; otherwise route the visitor to a sign-up flow.

R402_AUTH_TENANT_SUFFIX_REQUIRED #

The gateway refuses to set __Host-Http-r402_session cookies on hosts under *.run402.com for projects not in the internal staging allowlist. PSL-registered tenant suffix hosts (*.run402.app) and verified custom domains are always allowed.

Fix

Move to a verified custom domain (kychon.com) or wait for the PSL-registered tenant suffix to be allocated for your project. SameSite is scoped to eTLD+1 — sibling subdomains on *.run402.com are same-site, which is why we refuse session cookies there.

R402_AUTH_PRERENDERED #

An auth helper ran during prerendering, where there is no request actor. Render the auth-dependent page through SSR or move the interaction to a supported client flow. Server islands are currently unsupported; server:defer is not a remedy.

R402_AUTH_FETCH_ABSOLUTE_URL #

auth.fetch(input, init?) rejected a URL synchronously (before network I/O). Rejected shapes: cross-origin absolute URLs, embedded credentials (https://user:pass@host/), non-HTTP(S) schemes (javascript:, data:, file:), protocol-relative URLs (//evil/...), subdomain spoofs (https://kychon.run402.app.evil.example/), port mismatches.

Fix

auth.fetch is for same-origin SSR fetches. Cross-origin calls belong in your own service module (no actor-context propagation across origins anyway — the actor header is NEVER attached to cross-origin redirect hops).

R402_AUTH_UNKNOWN_EXPORT #

An unsupported auth namespace member or sentinel export was accessed. Follow the diagnostic’s canonical replacement, such as auth.user() or auth.requireUser(). Do not invent auth.getUser or auth.protect. The legacy top-level getUser(req) is a separate compatibility helper, not the same export as a namespace member.

R402_AUTH_REDUNDANT_USER_FILTER #

Build-time deploy-fail (or runtime warning). .eq("user_id", user.id) against an RLS-bound table — RLS already binds the visitor's rows via run402.current_user_id(). The redundant filter risks masking RLS misconfiguration.

Fix

Remove the .eq("user_id", ...) filter. If the filter is intentional (e.g., admin cross-user query intentionally narrowed), opt out with // run402-allow-user-filter: <reason> on the preceding line.

R402_AUTH_AUTHZ_VERSION_PROHIBITED #

Build-time deploy-fail. Consumer migration contains UPDATE internal.sessions SET authz_version = .... Only platform-owned grant-mutation triggers bump authz_version.

Fix

Register your custom grants table in the project's authz manifest so the platform installs the bump trigger automatically. The platform bumps authz_version for all active sessions of (project_id, user_id) in the same transaction as the grant mutation — guaranteeing instant authorization revocation.

R402_AUTH_INVALID_CREDENTIALS #

The supplied credential did not verify for a tenant-owned credential check — i.e. your function verified an email/password (or equivalent) against your OWN store and it failed. HTTP 401. Raised by calling auth.invalidCredentials(). Distinct from R402_AUTH_MAGIC_LINK_INVALID (that's the platform's own token); no session is minted.

Fix

Surface the canonical failure from your verify function: if (!ok) throw auth.invalidCredentials();. It's a function, not a constructor — never new auth.InvalidCredentialsError(). On success, mint with auth.sessions.createResponseFromTenantAssertion({ tenant, user, method }).

A magic-link token is unknown, expired, already consumed, or its continuation nonce didn't match at POST /auth/magic-link/confirm. HTTP 400/410 with a uniform body — single-use replay looks identical to an unknown token so attackers can't differentiate. The GET /auth/magic-link interstitial is side-effect-free; only confirm consumes.

Fix

Request a fresh link. In Astro, render the magic-link entry via the <SignIn methods={["magic_link"]} /> component (or POST /auth/magic-link/send from the slot) — never hand-roll the confirm fetch.

R402_AUTH_EMAIL_CODE_INVALID #

The email-code challenge is unknown, expired, superseded, already consumed, bound to another project/host, or the supplied well-formed six-digit code did not verify. HTTP 401 with a uniform, no-store body. A failed guess may count toward the challenge's fixed attempt budget.

Fix

Check the six-digit code and retry with the same opaque challenge_id. Do not look up by email and do not put the code in a URL, log, or browser storage. If the email was replaced or expired, request a new email-auth challenge.

R402_AUTH_EMAIL_CODE_EXHAUSTED #

The fifth well-formed incorrect code burned the challenge. HTTP 410, no-store, terminal and not retryable with that handle. A concurrent link/code winner can also make the other credential terminal without minting a second session.

Fix

Request a fresh email-auth challenge, retain its new challenge_id, and verify the newly delivered credential. In Astro, let <SignIn methods={["magic_link"]} /> manage resend and handle replacement.

R402_AUTH_UNTRUSTED_CONTEXT #

auth.sessions.createResponseFromTenantAssertion(...) ran in a function that did NOT declare the auth.sessionMint capability in its deploy spec. HTTP 403. A service key alone is not sufficient — minting a host-bound cookie by vouching for a tenant subject is a privileged action and must be explicitly granted per-function. The mint is also audited (function id, route, host, issuer, subject id, amr, IP, UA, request id).

Fix

Declare "capabilities": ["auth.sessionMint"] on the function entry in your release manifest and validate it with run402 up --check. run402 doctor static-detects the mint call without the capability at deploy time (R402_DOCTOR_AUTH_SESSION_MINT_CAPABILITY_MISSING) so you catch it before runtime.

R402_AUTH_PASSKEY_CHALLENGE_INVALID #

A hosted passkey /verify (POST /auth/passkeys/login/verify or /auth/passkeys/register/verify) ran without a valid, same-origin, actor-or-pending-signup-bound challenge. HTTP 400. Common causes: skipped the /options call, the challenge expired, or you verified on a different host than you minted the challenge on (passkeys are bound to the exact request host as rpId — a challenge from tenant-a.run402.app is invalid on tenant-b.run402.app).

Fix

Drive the ceremony through the component: <SignIn methods={["passkey"]} />, or always call /auth/passkeys/*/options immediately before /verify on the same host. The register /verify stores the credential ONLY for the challenge-bound actor — body user/email are ignored.

R402_AUTH_TENANT_SUBJECT_INVALID #

auth.sessions.createResponseFromTenantAssertion(...) was called with a malformed subject: missing tenant, missing user, a method other than "password"/"sso", or — the classic mistake — user.id set to a bare email instead of a stable primary key. HTTP 400. Platform identity uniqueness is (project_id, issuer, user.id); an email is not a subject id and is never used to implicitly link accounts.

Fix

Pass a structured user with a stable id: auth.sessions.createResponseFromTenantAssertion({ tenant: "<t>", user: { id: user.id, email: user.email, emailVerified: true }, method: "password" }). The platform derives issuer: "tenant:<t>" and the amr from method — you never hand-build those.

R402_AUTH_RENAMED_EXPORT #

Consumer code called the removed top-level auth.identities.link. HTTP 400 (deploy/runtime). Identity link/unlink were consolidated under auth.account.identities.*, and linking became a redirect+proof ceremony (startLink) that links an OAuth identity to the already-signed-in account (not a new sign-in).

Fix

The envelope's details.canonical_name names the move. Use auth.account.identities.startLink({ provider: "google", redirectUrl: "/settings/security" }), or — the everyday Astro path — render <AccountSecurity sections={["identities"]} /> which drives link/unlink for you.

See also: the @run402/astro README (npmjs.com/package/@run402/astro), CLI reference at /llms-cli.txt, and full HTTP API reference at /llms-full.txt.

Current code destinations

Gateway, client and application runtime errors have distinct contracts. Follow the owning topic for applicable fields and recovery guidance. Existing anchors remain valid.