Commit graph Heimdall/tests
Author SHA1 Message Date
Heimdall
6ed6d8fa0d Step-up assertion: pin per-app not-permitted budget and the 10.5 min prune horizon, name the tripped budget in the audit row, correct validation-order and audit docs
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-13 20:19:41 +00:00
Heimdall
1259911632 Step-up assertion: audit a flood once per window, budget the not-permitted branch, base64url nonce, honest nonce retention and precondition docs
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-13 20:13:13 +00:00
Heimdall
33a83c34c4 App-asserted step-up: POST /v1/apps/{app}/step-up mints a same-session access token with step_up_at and step_up_method
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>
2026-11-13 19:59:26 +00:00
Heimdall
e9eef09446 Pin the proof window's 600 s edge, foreign-id 404 over last_identity, and the sign_in-only step_up_at guard
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-13 19:21:45 +00:00
Heimdall
3afc956f92 Member identity list and step-up-gated unlink: step_up_at on a step-up sign-in's access claim, GET /v1/me/identities, POST /v1/me/identities/:id/unlink
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>
2026-11-13 18:59:21 +00:00
Heimdall
46d5044925 Store: an upsert never moves an identity between accounts; unlinkLinkedIdentity is the one delete, under an account lock, refusing the last identity
STORE_TABLE_PRIVILEGES.linked_identities gains DELETE; startup refuses a role without it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-13 18:59:21 +00:00
Heimdall
4aa9d03c4f Consent bomb test: pin the inflate output cap, not a wall-clock bound
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>
2026-11-13 07:03:30 +00:00
96256d2839 Prune oauth_state_uses on claim: bounded delete past exp + skew + margin (#12)
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>
2026-11-13 05:58:45 +00:00
Heimdall
673713a015 Step-up: build the authorize URL before claiming the launch (a refused step-up no longer burns the state); pin the strict step_up parse with a signed forgery; document backend_callback and mixed-version limits
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-12 23:21:12 +00:00
Heimdall
7a69d7567b Opt-in sign_in step-up: app-authenticated, signed-state flag, provider re-auth parameters, typed refusal
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-12 23:10:27 +00:00
5fc626c7ec Connect start: pin the app-secret gate on every managed provider, refuse blank targets (#9)
The target is stored and compared raw by design (no trim).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-09 12:18:26 +00:00
Heimdall
a021b02310 Test first: redeem refuses a target the completion never recorded
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-09 12:03:14 +00:00
Heimdall
246f2d4f71 Tests: rotate fixtures carry consumedRefreshToken
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-09 10:27:35 +00:00
Heimdall
d4cd112193 Blank the refresh token in the fallback write only when it is spent
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>
2026-11-09 10:22:14 +00:00
Heimdall
f2f1dabc06 Blank the consumed refresh token in the fallback marker write
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>
2026-11-09 10:07:24 +00:00
Heimdall
9c7d980165 Leave the failed-refresh marker when the revoke a rotating provider's answer called for cannot be written
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>
2026-11-08 21:56:40 +00:00
Heimdall
9da4b12bc2 Make the failed-refresh marker durable and stamp it with the failure time
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>
2026-11-08 21:48:13 +00:00
Heimdall
a0bd9836b1 Count the new linked_identities column in the schema inventory test
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 21:40:39 +00:00
Heimdall
f5f4b2830f Record a failed refresh on the linked identity row so another process does not re-present the token
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>
2026-11-08 21:35:13 +00:00
Heimdall
0ea6691cd3 Refusal return contract: pin the fragment, carry the attempt id, document it
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>
2026-11-08 13:50:58 +00:00
Heimdall
b6372e5a4f Named revocation-conflict tests on pg-mem, first-create doc line, npm fallback in test-live-pg
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 13:45:57 +00:00
Heimdall
af554ca013 Addendum: per-index predicate check, live second-revocation and missing-table cases, test-live-pg prerequisites
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 13:38:35 +00:00
Heimdall
767852ef96 WIP t10: inflate cap, consent_text_missing before claim, scope caps, revocation FK
Tests green locally; mutants not yet run; not pushed; PR body not updated.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 13:37:13 +00:00
Heimdall
d4f7dce098 Check the shown text before any code exchange; pin link expiry with a clock
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 13:31:43 +00:00
Heimdall
71ce9c16e4 Consent review fixes: ask-only cookie, fixed-expiry grants links, wording carried to the receipt, one revocation per grant
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>
2026-11-08 13:30:39 +00:00
Heimdall
48207268d0 Consent flow doc: steward rulings and the built design; pin repeat-revoke
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 13:24:48 +00:00
Heimdall
7ca4e2355f Joystick consent: the ask, typed refusal, receipts, and a signed-in grants page
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>
2026-11-08 13:22:36 +00:00
Heimdall
a7cdf0fea7 Draft: Joystick consent flow design and skipped test sketch
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>
2026-11-06 12:17:14 +00:00
Heimdall
70fd6396f4 Exercise the pending-link and findAuthCompletion store methods in the live-Postgres privilege tests
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>
2026-11-05 23:30:23 +00:00
Heimdall
f1ddc2fffc Refuse an unbound or replayed state with a page back to the app, and keep use records through the skew
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>
2026-11-05 17:16:38 +00:00
Heimdall
b541e094eb Merge heimdall-binds-completion (r2 state binding) into main's link and Google work; gate connect starts that carry a connection
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>
2026-11-05 12:46:05 +00:00
heimdall
9123f2def0 Merge main (link-initiator binding) and reconcile the Google link with it
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>
2026-11-05 10:58:02 +00:00
Heimdall
c829429cd5 Google link is two-phase; callback evaluates all linked identities
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>
2026-11-04 17:15:33 +00:00
Heimdall
8e870d20a0 Google OIDC provider: link-only, sub-keyed, PKCE and nonce
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>
2026-11-04 16:10:23 +00:00
Heimdall
84c54fbb5e Refuse a connection target on sign_in start
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>
2026-11-03 13:30:22 +00:00
Heimdall
0f2e294fcb Pin completion expiry at redeem: 410 at and after expiresAt, for any caller and target
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 09:50:34 +00:00
Heimdall
9f5a0e6938 Bind link mode to its initiator and redeem to its target
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>
2026-11-02 13:38:51 +00:00
Meta Vens
5146c722af Boot test: fail on a logged publication fault, and require presence at startup
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>
2026-10-23 17:50:37 +02:00
Meta Vens
1a36175f3a Presence authority test: the Rust fixture's target is the fixture's own, not heimdall
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 17:49:23 +02:00
Meta Vens
c044155855 Heimdall opens its Idunn presence authority once, before it listens
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>
2026-10-23 17:48:08 +02:00
Meta Vens
c0d30e8081 Test fixtures: the Rust owner's Idunn runtime records, and one fake Idunn RUDP endpoint
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>
2026-10-23 17:48:02 +02:00
Meta Vens
e8548614c8 Generation test: bundle the generation process so a uid that cannot read the checkout can run it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 14:26:13 +02:00
Meta Vens
55732a901d Presence: the publisher sequence is in-process, the state root keeps no owner-only file, an Idunn generation never self-enrolls
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>
2026-10-23 14:25:11 +02:00
Meta Vens
c15fb97706 Test: malformed URL must not reach a sibling prefix's not-found handler (GHSA-p68q-wchp-6fh7)
Red on fastify 5.8.5, the live lockfile.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 21:00:46 +02:00
Meta Vens
4185e657bb Tests: state time windows to the second, requested_scopes shapes, the launch route's validation, the phases each route claims
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 20:25:52 +02:00
Meta Vens
3def0fb426 OAuth states complete once, only in the browser that launched them: a launch hop sets a signed binding cookie, launch and callback each consume the state, and the callback checks expiry
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 20:13:17 +02:00
Meta Vens
14e0cde26c Store: claimOAuthStateUse claims one use of a signed OAuth state per (jti, phase), on both stores and an INSERT-only table
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 20:13:16 +02:00
Meta Vens
ece6ba1d62 Tests: a state completes once and only in the browser that launched it; the gate holds for every mode, provider and handoff (red against the callback that accepts any signed state)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 19:04:10 +02:00
Meta Vens
9fd152feff Tests: a foreign app's caller cannot start a Bifrost flow, with no policy attached
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>
2026-10-22 18:24:42 +02:00
Meta Vens
6d43eacd84 Tests: apps that bind browser completion refuse unauthenticated starts (red; does not pass yet)
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>
2026-10-22 18:13:02 +02:00