Commit graph

214 commits

Author SHA1 Message Date
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
25d43d1121 Merge security/fastify-upgrade: fastify 5.12.7 advisories
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 23:06:25 +02:00
Meta Vens
899ea0b8fe Heimdall: fastify 5.8.5 to 5.12.7, find-my-way 9.5.0 to 9.10.0 (clears 5 High advisories)
GHSA-p68q-wchp-6fh7 authentication bypass via malformed URLs, header and request validation bypass, request body replacement, find-my-way HTTP/2 DoS. pnpm audit --prod: 8 vulnerabilities before, none after.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 21:00:47 +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
c1dfe15dd5 Merge readme-front-door: README opens with value, problem, entry points, live versus planned 2026-10-22 20:04:06 +02:00
Meta Vens
c7cf78bbbc README: front door with value, problem, entry points, live versus planned
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 20:03:50 +02:00
Meta Vens
5952cce8a0 State: live d51fa6b proven by a human Twitch sign-in; Joystick consent remains
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:03:17 +02:00
Meta Vens
30a1ace6fd State: close eureka/joystick-h1 as accepted (merged, PR #1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 17:56:34 +02:00
Meta Vens
df05570ef6 State: Joystick H1 merged and live at d51fa6b; follow-ups recorded by id
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 17:56:25 +02:00
Meta Vens
d51fa6b999
Merge pull request #1 from GameCult/eureka/joystick-h1
Joystick.tv provider and serialized managed-credential refresh (cut H1)
2026-10-19 17:47:39 +02:00
Meta Vens
778eabaedc Docs: map managed credential custody, the callback mint and the open follow-ups
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>
2026-10-19 15:49:34 +02:00
Meta Vens
6db84bfae8 Pin the year boundary, the typed exchange failure and the start refusal's code
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>
2026-10-19 14:45:35 +02:00
Meta Vens
1d856ebd40 Code exchange: a rotating provider's grant without a refresh token fails the callback
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>
2026-10-19 14:35:11 +02:00
Meta Vens
4e312828c3 Pin the handoff strip: a custody-only backend_callback delivery carries no refresh token
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>
2026-10-19 14:34:44 +02:00
Meta Vens
09ea233592 Token answers: an expires_in beyond a year is unusable, so the default lifetime applies
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>
2026-10-19 14:34:43 +02:00
Meta Vens
e1ac84fb70 Joystick custody needs a creator binding, at start and at the callback
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>
2026-10-19 14:34:24 +02:00
Meta Vens
26f264877a Pin the hybrid capability listing a viewer sign-in carries
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:04:12 +02:00
Meta Vens
a911da93d8 Claims: facts are the one owner of a capability-free claim
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>
2026-10-19 13:34:41 +02:00
Meta Vens
655fcc7d22 Credential rotate decisions carry a required expiry
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>
2026-10-19 13:29:10 +02:00
Meta Vens
32208cc8e2 Pin the callback's refusal of a signed state the provider has no role for
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>
2026-10-19 13:27:28 +02:00
Meta Vens
a32be41878 Token answers: a default lifetime replaces an unknown expiry; a malformed refresh_token is typed
A token answer without a usable expires_in (absent, non-numeric, a string that
is not a positive integer) now takes the descriptor's
defaultAccessTokenLifetimeSeconds (3600 for every provider), so no issued
token has a NULL expiry that dueForRefresh would never refresh. A 2xx whose
refresh_token is present but not a string is the typed
provider_body_not_token_set (502 managed_credential_refresh_failed, tokens
kept); an empty string stays a missing token, so a rotating provider still
fails closed. The expiry rule is now pinned on memory and both live Postgres
rigs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:24:21 +02:00
Meta Vens
79ea0007d3 OAuth callback: mode and provider role decide what a completion mints
The start gate only refused early. The callback ignored the signed state's
mode, so a Joystick connect for StreamPixels redeemed into a viewer_access
session. callbackMint(profile, provider, mode) now decides at the callback:
identity providers keep a viewer session (Bifrost discord connect unchanged);
a connect for a managed-connection-only provider stores custody and mints a
claim with no facts, no capabilities and no refresh token (the refresh route
would recompute viewer_access from the account); anything else is refused.
StreamPixels' redeem reads only account, provider, mode, connection and
claimSet.identities, so it accepts the capability-free claim unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:23:53 +02:00
Meta Vens
6bdaed2a9b Token set check: drop the array clause the access_token check already decides
A JSON array has no string access_token, so the typeof check refuses it;
the separate Array.isArray clause was an equivalent mutant's worth of code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 12:44:20 +02:00
Meta Vens
c1fbc1261a OAuth start: connect keeps admitting identity providers, as Bifrost uses it
The first gate refused connect for a provider outside managedConnectionProviders,
which broke Bifrost's existing Discord connect. sign_in and link still take
identityProviders only, so a managed-only provider (Joystick) can never sign
anyone in or link; connect takes either list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 12:38:13 +02:00
Meta Vens
b15bf33b21 Refresh: one attempt per identity per process; waiters share its outcome
Callers that arrive while a refresh of one linked identity is in flight in
this process join it and receive its success or failure, instead of queuing
on the row lock and presenting the same refresh token again behind a failed
holder. The entry lives as long as the attempt, so a later request retries.
The store lock stays the cross-process guard; the residual risk (one
presentation per process per burst) is written into the service contract.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 12:34:39 +02:00
Meta Vens
d989860163 Refresh classification: revoke only on a 400 invalid_grant; any other answer keeps the tokens
A grant verdict is read from a 400 body by exact code, never from a 401, a
5xx or a 2xx. A 2xx that is not an object with a non-empty string
access_token and no error is refused as provider_body_not_token_set, so a
malformed success keeps the tokens and surfaces as the typed 502 for every
provider, rotating or not. A refresh answer without expires_in stores the
expiry as unknown rather than keeping the already-due one; an unknown expiry
is never due, so the next resolve does not refresh again.

Pins Soul's three surviving mutants: substring code match, the dropped
expiry rule, and the snapshot refresh token on a non-rotating provider.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 12:34:17 +02:00
Meta Vens
c554c00697 OAuth start: the profile's role lists decide which provider a mode may use
sign_in and link now take only identityProviders; connect takes only
managedConnectionProviders. supportsProvider(), which unioned the role lists
with entitlementSources and never looked at the mode, is deleted: a Joystick
sign_in for streampixels no longer starts, so a Deru sign-in cannot rewrite
his managed credential row. No built-in profile had a provider reachable
only through the union.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 12:31:59 +02:00
Meta Vens
34a8c6a81d Tests: a late decider still queues behind the one waiting on the in-memory chain
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 11:52:27 +02:00
Meta Vens
6b8c4a9c63 Tests: pin the refresh fast paths, boundaries and connection hygiene
Added after a diff-scoped StrykerJS run left survivors: no-lock paths, the
skew boundary, decision kinds, a vanished row, open-transaction hygiene on
the live store, and the token request's method, signal, token type and
scope handling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 11:47:37 +02:00
Meta Vens
9b3d44f56a Tests: satisfy exactOptionalPropertyTypes in the Joystick stub
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 11:25:01 +02:00
Meta Vens
f6baadec0c Tests: refresh failures surface as the typed ManagedCredentialRefreshFailed; seven providers
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 11:20:18 +02:00
Meta Vens
7ce9588503 Postgres store: return created_at and updated_at as ISO strings too
The credential write returns updated_at; a Date there broke the equality the
port promises against the in-memory store.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 11:18:12 +02:00
Meta Vens
3eaa9ef265 Declare Joystick in the Idunn recipe and document the locked refresh
Recipe optional_environment gains the Joystick client id and secret file;
service-contract documents the row lock, rotation fail-closed and the 410/502
answers; the StreamPixels profile names Heimdall as sole holder of provider
refresh tokens (F5).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 11:15:42 +02:00
Meta Vens
43b7072dfb Postgres store: return token_expires_at as the ISO string the port promises
pg returns a Date for TIMESTAMPTZ; a byte-identity check on a kept credential
saw a Date where the port types a string.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 11:15:12 +02:00
Meta Vens
1c38d12050 Add Joystick as a confidential, rotating managed-connection provider
Joystick joins the catalog (Basic client auth, rotatesRefreshTokens), resolves
identity by channel id, and is a streampixels managed-connection provider only:
it is not an identity provider for the profile (ruling signin-waits-for-public-bot).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 11:13:40 +02:00
Meta Vens
c81f923890 Refresh managed credentials under a row lock, with typed revocation
refreshLinkedIdentityCredentialIfNeeded is deleted. refreshManagedCredential
owns the policy; HeimdallStore.updateLinkedIdentityCredential owns the row
(SELECT ... FOR UPDATE in Postgres, a per-id chain in memory), so a refresh
token is presented once across concurrent requests and processes. invalid_grant
clears the tokens (410 managed_credential_revoked); any other failure keeps
them (502 managed_credential_refresh_failed). The "?? refreshToken" keep-old
fallback is gone; keeping it is an explicit branch for non-rotating providers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 11:11:34 +02:00
Meta Vens
0dbcd98019 Collapse ten per-provider token functions into one descriptor-driven request
Endpoint, client-auth method (RFC 7591 names) and rotation now live on
ProviderDescriptor; requestProviderTokens is the only code that talks to a
token endpoint. Failure labels are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 11:06:32 +02:00
Meta Vens
2108d1088b Handoff: rehydrate with plain python, not the repixelizer venv on E:
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 10:44:29 +02:00
Meta Vens
8066bcbbee State: Joystick H1 is the active cut; Ghostlight canaries demoted behind it
Points at campaign joystick:campaign:self; flags the 1086aee deploy fact as
last-recorded (2026-08-22, ~100 commits behind HEAD); drops the stale E:
path and repixelizer-venv rehydrate command from the handoff.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 10:44:24 +02:00
Meta Vens
7904e17cd2 Pin the predicate splitter on a quoted identifier
Soul pass 7's Y2, treating only single quotes as quoted text, survived:
no test gave the splitter a double-quoted identifier. One case now holds
an identifier containing " AND (" and checks it stays whole.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:18:53 +02:00
Meta Vens
996f21f58c Schema check: deterministic column collations, a named TEMP refusal, and kills for the printer
Soul pass 7, S7-3 and S7-4, P7-3 and P7-5.

Columns (S7-3): every declared column must be under a deterministic
collation. A case-insensitive provider_user_id whose key index compares
under "C" passed, and the store's own lookups then matched other users'
ids.

The check becomes checkSchemaRequirements(pool, requirements), with
Heimdall's store calling it with HEIMDALL_SCHEMA_REQUIREMENTS (its tables,
columns, keys and privileges). That seam lets a live test check a
predicate Postgres prints differently from how it is written (a literal
gains its cast): the check must still pass, so comparing the raw text
instead of the printed form (P7-3's Y1) now fails.

A role that may not create temporary tables was refused with 42501 and
told to check GC_ACCESS_DATABASE_URL_FILE (P7-5). The predicate printer now
refuses it as SCHEMA_TEMP_PRIVILEGE, and startup says to grant the role
TEMPORARY on its database, in fixed text.

New fixtures (S7-4): a case-insensitive collation on a key's first column
(Y3), NOT NULL added to a nullable column of linked_identities (Y5), and
every privilege on auth_completions except on attempt_id (Y8) are refused;
a key index that INCLUDEs an undeclared case-insensitive column starts
(Y4).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 22:52:41 +02:00
Meta Vens
588739857a Schema: name what the startup check does not prove
Soul pass 7, S7-2. The out-of-scope list named CHECK constraints, triggers
and row-level security. It now also names unique indexes beyond or
stricter than the required keys (including leftovers of an interrupted
concurrent build, which are enforced once ready), NOT NULL columns the
schema does not declare and that have no default, and column-level SELECT
under SELECT * or RETURNING *, which needs every column of the table.
Documentation only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 22:52:40 +02:00
Meta Vens
d029e08a2e Recipe test: explore loadConfig as Idunn starts Heimdall
Soul pass 7, S7-1. The exploration always called loadConfig with no
arguments, and the candidate socket alone is refused (it needs the base
URL), so neither the Idunn candidate path nor --state-root was ever run: a
secret read only there (Y6, Y7) went unseen.

The recipe's [service] arguments after the entry script, each binding
given a probe path, are now the argv of an Idunn-shaped run, together with
a loopback candidate socket and its base URL. That run is added on its own
and to every combined run. A test pins the parsed argv, that both
environment names are declared in the recipe, and that loadConfig accepts
the shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 22:52:40 +02:00
Meta Vens
ca2366cecf Schema check: validity is the rule; pin the printed predicates
The mutations dropping indisvalid or indisready each survived alone,
because a failed CREATE INDEX CONCURRENTLY clears both. Postgres never
marks an index valid before it is ready (DROP INDEX CONCURRENTLY clears
validity first), so indisready adds nothing and is deleted. A fixture now
holds an index that is ready but not valid, the state an interrupted DROP
INDEX CONCURRENTLY leaves, set directly in pg_index by the admin role.

Comparing the raw declared predicate instead of Postgres's printing of it
survived too: the schema's one predicate splits into the same conditions
either way. A live test now pins printedDeclaredPredicates to the printed
predicate of the applied attempt index, so the printing itself is
checked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 21:28:50 +02:00
Meta Vens
97632b7cfa Print the declared predicates in a module function, not a store method
The store's coverage test lists every method on PostgresStore as an
operation the workload must call, and the private predicate printer
counted as one. It is a helper of checkSchema, not a store operation, so
it becomes a function of the pool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 21:11:18 +02:00