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. The docs: field on every R402_* envelope deep-links 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 #

Astro version outside the adapter's pinned peer range (>=6.0.0 <7.0.0). Thrown at the start of astro:config:setup before any other adapter work runs.

When

Build start, when assertAstroVersionSupported() runs.

Fix

Pin Astro to ^6 in package.json. The adapter re-validates before widening the range; track support state in @run402/astro CHANGELOG.md.

R402_ASTRO_DYNAMIC_IMAGE_UNSUPPORTED #

Astro <Image> component with a runtime-resolved src (e.g., <Image src={page.heroUrl}>). Detected by source-tree scan before vite build.

When

Build, by detectDynamicImage().

Fix

For CMS-driven runtime image refs, upload via r.assets.put(file, { variants: ['webp', 'display_jpeg', 'blurhash'] }) and render an <img> against the returned AssetRef.cdn_url. Static-import form (import hero from "../assets/hero.svg"; <Image src={hero} />) is allowed.

For build-time-known but data-driven cases (a section config from a JSON/MD/CMS export that contains image refs), the recommended pattern in @run402/astro v0.1 is the r.assets.fromRef() SDK helper + an <img> tag. A first-class <Run402Image> component for runtime-resolved refs is on the v0.2 roadmap.

R402_ASTRO_SERVER_ISLAND_UNSUPPORTED #

Astro server-island directive (server:defer or server:only) detected. Server islands are deferred to a future Run402 release (v1.5+).

When

Build, by detectServerIslands().

Fix

Use client islands (client:load, client:idle, client:visible) or move the rendering into the page's frontmatter. SSR is still the default — only the island mechanism is rejected.

R402_ASTRO_SESSIONS_UNSUPPORTED #

Astro Sessions API used — either Astro.session.* in source files OR experimental.session in astro.config.*. Deferred to a future Run402 release.

When

Build, by detectSessionsApi().

Fix

Use signed HTTP-only cookies via Astro.cookies or a custom DB-backed session via db(). Remove experimental: { session: ... } from your astro config.

R402_ASTRO_MIDDLEWARE_UNSUPPORTED #

Astro middleware shape the SSR runtime can't honor (rare — most middleware passes through unchanged).

Fix

Replace the unsupported middleware feature with one of the documented patterns in kychee-com/run402astro/README.md.

R402_ASTRO_BUILD_FAILED #

Catchall for astro:build:* hook failures not covered by a more specific code. Inspect message + log for the underlying cause.

R402_ASTRO_UNSUPPORTED_OUTPUT #

output: 'static' configured but the project has SSR-only routes (or vice versa). Typically surfaces when migrating from a different Astro adapter.

Fix

Use output: 'server' (the preset default) and mark per-route static via export const prerender = true. To migrate an SSG site, pass run402({ output: 'static' }) and opt routes INTO SSR per-page.

R402_ASTRO_ADAPTER_MANIFEST_MISSING #

The @run402/astro/release-slice SDK helper (or the run402 deploy apply --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 apply --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 apply --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 #

Function bundle includes a native (.node) dependency that can't run on the Lambda runtime.

When

Deploy, by the bundle scanner in services/functions.ts.

Fix

Remove the native dep OR replace with a Run402 primitive (r.assets.put for image work, r.ai.* for ML inference, r.email.send for SES).

R402_BUNDLE_UNRESOLVED_IMPORT #

User code imports a module name that's neither a Node builtin, @run402/functions, nor declared in the function's --deps.

Fix

Add the dep to --deps (per-function package.json) OR remove the import.

R402_SNAPSTART_INIT_IO #

Lambda SnapStart snapshot capture failed because user code performed network/DB IO at module scope.

When

Deploy. The function still deploys and runs, but SnapStart cold-start optimization will not apply until the next deploy with module-scope IO removed.

Fix

Move network / DB calls from module scope into the request handler (Astro frontmatter or APIRoute body).

R402_SDK_OUTSIDE_REQUEST_CONTEXT #

A @run402/functions SDK call (e.g., db(), getUser(), r.assets.put, r.cache.invalidate, r.email.send) was made outside an active request context. The SDK uses AsyncLocalStorage to resolve host, projectId, auth state.

When

Runtime, on any SDK call from a setTimeout, post-response background work, or module-scope IIFE.

Fix

Move the call into a request handler OR pass an absolute URL where applicable (e.g., r.cache.invalidate(new URL('https://eagles.kychon.com/the-guys'))).

R402_SSR_RUNTIME_ERROR #

Uncaught exception during Astro render. Public response carries only code, message, requestId, releaseId — no stack.

Fix

Full stack trace via run402 logs --request-id <req> against the project. Filter by request_id in the function's CloudWatch log group.

R402_CACHE_UNSUPPORTED_VARY #

Response carries a Vary: header value the cache layer can't safely partition on (e.g., Vary: User-Agent). Response is BYPASSED (not stored), but the request still serves normally.

Fix

Drop the Vary: header OR limit it to one of the cache-aware values: Accept-Encoding, Accept-Language, Cookie (treated as auth-tainted), x-run402-locale.

R402_CACHE_AUTH_TAINTED #

Informational diagnostic only — the cache layer is logging that this response was bypassed because user code called a payment / auth primitive during render. No fix needed unless you intended the response to be cacheable.

R402_CACHE_INVALIDATION_HOST_REQUIRED #

r.cache.invalidate(path) called outside a request context with the path-string form. The SDK has no host to scope against.

Fix

Pass an absolute URL — r.cache.invalidate(new URL('https://your.run402.com/path')) — OR move the call into a request handler.

R402_CACHE_INVALIDATION_HOST_FORBIDDEN #

r.cache.invalidate(...) or POST /cache/v1/invalidate targeted a host that is not owned by the caller's project. Cross-project invalidation is rejected to preserve tenant isolation.

Fix

Invalidate only hosts attached to your project (list via r.domains.list()). Cross-project / operator-scope invalidation is on the roadmap as a separate primitive — track via the cross-project-cache-invalidation change in openspec/changes/.

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 #

A specific deploy stage (stage field on the envelope: staging / migrating / schema_settling / activating) failed. Mostly forward-fixable: re-apply with a corrected migration / spec.

Fix

Stage-specific. Inspect error.message and error.code on the operation row via GET /deploy/v2/operations/:id.

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 #

auth.* helper called from a prerendered Astro page (export const prerender = true). Prerendered pages run at deploy time — there is no request, no cookie, no actor.

Fix

Drop export const prerender = true (the page becomes SSR), OR move the auth-aware widget into a server island (<UserMenu server:defer />).

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 #

Consumer code accessed a hallucinated SDK name. Bare imports like getUser / getSession / currentUser / getServerSession are throwing-sentinel exports as of @run402/functions v3.0. Property access on the auth object like auth.protect / auth.signIn / auth.logout / auth.middleware goes through a Proxy that throws.

Fix

The error envelope's details.canonical_name field carries the canonical replacement. Use auth.user() / auth.requireUser() / auth.requireRole(role) / auth.sessions.endResponse() — see llms-full.txt's "In-handler helpers" section for the full canonical surface.

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_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 the capability on this function's apply-spec entry: "capabilities": ["auth.sessionMint"] (a sibling of schedule in run402.config.json / the /apply/v1 function entry). 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.