App secret plus the member's live bearer for that app, a per-profile grant and method allowlist (bifrost only), a nonce spent once per session through the existing oauth_state_uses claim (no schema change), an in-process fixed-window rate per account and per app, and an audit row per assertion or post-secret refusal. Provider step-up sign-ins now label their proof provider:<name>.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
resolveBearer is the one bearer verifier; the link race maps to link_initiator_mismatch and links/confirm 409.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The 1500 ms bound measured 3330 ms under parallel load. What the test proves is that
a decompression bomb is read under the 16 KB cap; record the inflate options and assert
maxOutputLength directly. Mutant (drop the cap) fails the test.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Not in the 74004bf release list. The runtime role needs GRANT SELECT, DELETE ON oauth_state_uses before this build starts.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A provider that keeps its refresh token valid (Twitch) keeps it when the
rotation cannot be committed: the marker is left, the token stays, and the
next caller after the backoff presents it. Blanking stays for a revoke and
for a provider that rotates (the rotate decision now carries
consumedRefreshToken). Live-Postgres test for Twitch written first and seen
failing; the contract states the rule inline.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
When the rotation or revocation the provider's answer called for cannot be
committed, the fallback write now clears refresh_token_encrypted as well as
stamping the marker, so the end of the 60 s backoff cannot replay the consumed
token. Tests (live Postgres, trigger-refused revoke) assert no replayable token
and no second provider call. Docs: the marker is a best-effort write on a
second connection after rollback, the skew cap, the blanking write's residual.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A rotating provider that answered without a new refresh token has consumed the old one; the revoke
that follows is rolled back if its UPDATE fails, and no marker remained. The fallback marker now
covers revoke as well as rotate. The residual-risk paragraph says the window opens when the request
is sent, cites the steward ruling, and ends its sentences.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The marker carried the time the attempt began, so a slow failure ate the backoff; it now carries the
time the provider's answer (or failure) was known. A throw after the provider answered (custody
encrypt) records the marker instead of rolling it back, and a rotation Postgres refuses to write
leaves the marker in a transaction of its own. A stamp from a writer's clock ahead of the reader's by
the backoff or more is treated as skew, not a fresh failure.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A failed or timed-out refresh attempt now writes refresh_failed_at under the row lock; a process
that takes the lock inside a 60 s backoff answers managed_credential_refresh_failed without calling
the provider. Rotation, revocation and re-link clear the marker. Closes the cross-process residual
that refresh-once-per-token left to a follow-up, and pins it on the two-pool rig.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The consent_refused page's return link is tested for heimdall_status,
heimdall_error, heimdall_refused_by and no completion code, for both a decline
and a provider access_denied. A browser_completion refusal now carries
heimdall_attempt_id. The contract for app authors is in the consent doc.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The ask sets its own cookie and no launch binding; Continue sets the binding
and carries the exact text shown into it; the receipt stores that text and its
hash. Grants-page form tokens keep the link's expiry. Revocations are unique
per grant and revoke reports only what it wrote. access_denied's attempt
denial code is pinned for Bifrost and Repixelizer.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Managed custody (callbackMint custody_only) now stops at a Heimdall page before
the provider redirect. A decline or a provider access_denied is consent_refused
(by viewer|provider), distinct from provider_error. A granted completion writes
an append-only consent receipt with the asked and granted scopes and the digest
of the wording shown. GET /v1/me/grants lists every grant and receipt of the
signed-in account; revoking one strips the credential, so the app's next token
request is refused, and writes a revocation receipt.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Documents what a viewer sees today on the Joystick path and what consent
is missing, then drafts a visible ask, typed refusal and auditable receipt.
Design only; the test file is a skipped sketch and was not run.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The live suite asserts that exerciseStore calls every PostgresStore method and
that dropping any one table privilege makes it fail with 42501. createPendingLink,
consumePendingLink and findAuthCompletion landed without a call there, so the
coverage assertion failed, and with no operation touching pending_links the
privilege test saw no error when SELECT on it was revoked. Both failures were
invisible because the live files skip without HEIMDALL_TEST_PG_ADMIN_URL.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Browsers (Accept: text/html) refused for state_not_bound, state_already_launched
or state_already_used get the handoff page linking to return_to, re-checked
against the app's allowed origins; other callers keep the JSON code. A callback
with no binding denies the attempt its signed state names (pending only).
The memory store prunes a use record at exp + 30 s, as the state itself is
accepted that long; the schema comment says the same.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Launch hop, signed per-flow cookie and one-use claims are merged with main's
link-initiator binding and Google OIDC (secrets now derived at launch).
A connect start carrying a connection target now needs the app's own secret.
Google, link and managed-credential tests go through the launch helper.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
One initiator field (initiator_account_id) replaces link_account_id: a bearer
names it for requires_link providers, the authenticated app for the rest. One
refusal class (LinkRefusal, 403) replaces AccountLinkRefused. Provider count in
the verse test is 8.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The callback records a typed pending link (short TTL, single use) and
writes no linked identity; POST /v1/apps/:appSlug/links/confirm with the
Heimdall access token of the initiating account attaches it. A victim who
completes an attacker's link URL attaches nothing. The sign-in callback
also evaluates the account's other linked identities with the start-time
policy, so a Discord-origin member returning via Google keeps app_access.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Google is a Heimdall provider behind Bifrost, with no direct upstream.
Scopes openid email profile; the callback verifies the id_token
(RS256 against Google's keys, iss, aud, exp, iat, nonce) and requires
a verified email. The identity key is sub, never the email.
PKCE verifier and nonce are derived from the state jti and Heimdall's
signing key, so nothing is stored between start and callback.
Google never mints an account. mode=link needs a Heimdall access token
for the app and attaches to that account only; sign_in signs in an
identity already linked; an unknown sub is refused, and a sub held by
another account is never moved. The six existing providers still mint
on first login.
Admitted to the bifrost profile only. The Idunn recipe declares the
Google client id and secret file.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Connection targets ride only on connect and link starts, which carry
x-heimdall-app-secret. A sign_in start is open to any browser; with a
connection it now returns 400 connection_not_allowed_in_sign_in.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Link start needs an authenticated app and an initiatingAccountId, carried in
the signed state. The callback refuses an identity held by another account
(link_initiator_mismatch) and attaches an unlinked one to the initiator.
auth_completions stores target_id; redeem compares it and answers
completion_target_mismatch without consuming the code. Link completions are
redeemed only by the authenticated app.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A silent early return in the startup publication would otherwise pass, the 10 s
pulse loop filling in the frames later.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
index.ts declared the presence sequence below the startup try, so the bundle's
hoisted var was undefined at the first publish; decodePrivateIdentity refused the
Rust owner's integer-array key fields; both descriptors were re-read every pulse and
returned nothing after the first. openPresenceAuthority now reads the bundle facts and
each descriptor once (whole, from offset 0, then closed), owns the one publisher
sequence, and signs through them; a launch-contract fault throws before anything
listens, so the process exits non-zero. decodePrivateIdentity takes the shape of its
source and names the failed check, never a value. openActivationSigner and the
per-pulse descriptor reads are deleted.
tests/idunn-boot.test.ts boots the bundle under the Idunn launch contract and checks
the signed warming frames and the fail-fast exits. The descriptor, sequence and
generation tests now use the Rust-written fixtures.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tests/fixtures/idunn-runtime holds expected.cc, activation.cc, the activation seed,
the presence identity credential and the machine-id it is bound to, copied from
CultLib 217a9480 (Rust-written; test keys only). fake-idunn-rudp.ts is the accept
server idunn-rudp-health.test.ts built inline, now keeping a session per remote and
every delivered frame, so a boot test can receive several pulses.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nextPublisherSequence read and rewrote <dataRoot>/runtime-presence-sequence at
mode 0600, which the next generation (a fresh DynamicUser uid in the shared
state group) cannot open. Odin compares publisher_sequence only within one
activation and Idunn mints a new activation per start, so PublisherSequence
derives it from the observation clock, never below the previous value plus
one, as Muninn does. openProviderHealthIdentity under
GAMECULT_IDUNN_RUNTIME_BUNDLE now refuses when the presence descriptor is
absent instead of enrolling an identity Idunn does not trust.
Adds the cross-generation test (root only) and the sequence, no-dataRoot and
missing-descriptor tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Kills the surviving mutant on the caller.appSlug comparison in startOAuthFlow: over HTTP a foreign secret never resolves to a caller, so only a direct call reaches that arm.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The replay of finding known-nonce-login-csrf at the HTTP surface, plus authenticated, cross-app and public-app cases.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>