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>
Bifrost's browser binding is the secrecy of a nonce carried readable in the signed state, so only Bifrost may mint a Bifrost state. The gate sits in startOAuthFlow beside the entitlementPolicy caller check, so HTTP and the private command plane share it. Follows the red tests.
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>
architecture.md gains a Managed credential custody section: the single
refresh owner, the store-port lock, per-process single flight, typed
revoked and refresh-failed answers, the provider descriptor token fields
and the one token request, the mode x role gates, custody-only
completions, verification layer and follow-ups by id. The spotify finding
now matches the removed spiverse profile; JOYSTICK joins the provider env
lists. implementation-plan and service-contract lose two stale lines.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Closes StrykerJS survivors on the diff: the cap at exactly 365 days against a
second over, the rotating-exchange failure's typed code (and that a
non-rotating provider is unaffected), and the start refusal's error word.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Joystick rotates refresh tokens, so a code-exchange answer whose refresh_token
is absent or empty would store a credential the first refresh revokes. The
shared exchange now throws a typed ProviderResponseError before the identity
is resolved or anything is written. The residual-risk comment on the in-flight
refresh map cites its follow-up.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Delivers a joystick connect through a backend_callback handoff and checks the
posted body has no refreshToken or refresh and empty capabilities, so the
handoff spreading `issued` instead of `delivered` fails.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A value like 3e11 overflowed the Date the expiry is stored as and surfaced
as an untyped error. usableExpiresIn now caps a usable value at 365 days;
the refresh path is pinned on memory and live Postgres.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
callbackMint now takes the connection: custody_only is a connect for a
managed-connection-only provider bound to connection.kind creator with a
targetId. Anything else is refused at start with creator_connection_required
before any redirect, and at the callback from the signed state before the
code exchange. The streampixels profile note says what Heimdall guarantees
and records the S1 rule that SP's redeem treats a non-creator Joystick
completion as an error, never a sign-in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every shared capability needs a non-empty anyOf, so an empty fact set already
yields no capabilities; the explicit empty list beside it was a second owner
no test could tell apart from the first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refresh and exchange answers now type as ExpiringTokenSet, and a rotate
decision requires tokenExpiresAt, so neither store has a no-expiry rule left
to mutate: a NULL expiry can only be written by a revoke.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Re-signs a joystick connect state as sign_in and link with the app's own key
and shows the callback refuses before the code exchange and writes nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>