Skip to content

Fix/tap hold wake and remount - #13

Merged
0dillon merged 3 commits into
mainfrom
fix/tap-hold-wake-and-remount
Aug 25, 2026
Merged

Fix/tap hold wake and remount#13
0dillon merged 3 commits into
mainfrom
fix/tap-hold-wake-and-remount

Conversation

@0dillon

@0dillon 0dillon commented Aug 25, 2026

Copy link
Copy Markdown
Owner

No description provided.

aide-user was a bare cookie carrying an account id. Setting it to
somebody else's id made you that account: no password, no session, and
nothing in any route to get past, because every route trusts whatever
userIdFrom() returns. Account ids are eight hex characters and appear in
ordinary responses, so this was not a theoretical leak away from being an
account takeover of anyone whose id you had seen.

It carries the same HMAC as the login session now, with the id inside the
signed payload so a signature cannot be lifted from one account onto
another. It still confers a weaker identity — no password was ever
presented for it — but it can no longer be invented, only replayed by
whoever already holds the cookie. Both cookies are HttpOnly, both expire,
and both are Secure outside development: they are bearer tokens for an
account that can move money, and they must not travel in clear text.

The signing key no longer falls back to a string in this repository. A
default that ships here is a default an attacker also has, and with it
they can mint a cookie for any account on the platform — so in production
a missing SESSION_SECRET now refuses to start instead. It also no longer
falls back to MONNIFY_SECRET_KEY: reusing a payment provider's credential
to sign cookies means one leak costs both.

DEPLOY NOTE: SESSION_SECRET must be set in the environment before this
ships, or the app will not boot. Existing cookies are invalidated by the
new format, so everyone signs in again once.
lib/store/applications.ts began with `const owner = () => worker.id`, and
every function in it called that instead of asking who was signed in. So
there was, in effect, one worker. Your application list was the demo
worker's. Applying applied as them. An employer reviewing applicants saw
that account's name, skills and bio attached to whatever had been applied
for, and hiring, rejecting or marking paid acted on them no matter who
had actually applied. Payment coverage checked their wallet, so one
person's confirmed money could release another person's gig. The same
constant appeared again in the external-jobs route, where one worker's
web-scan results were everyone's.

The account is a parameter everywhere now. That is the point: leaving it
out is a type error rather than a silent fallback to the wrong person.
Employers act on a named applicant, resolved through a new by_job index —
the one you named, the only one there is, or a refusal asking which.

Message threads were worse, because the system prompt tells employers to
send credentials through them. The route asked `acc.role === "worker"`
and returned "worker", making every worker a party to every gig's
conversation, and the agent tools carried their own copy of the same
check. Both now ask whether this account is THIS thread's hired worker,
or the employer who actually posted the gig.

Ownership no longer rests on display names, which two accounts can share.
Gigs record employerAccountId and messages record authorAccountId; where
neither exists — the seeded demo gigs, rows written before these fields —
the checks fall back to the old name comparison for reads and refuse
outright for writes, because refusing is the safe direction.

The Convex layer already keyed applications by accountId. None of this
needed new storage; it needed the server to stop answering "who?" with a
constant.
Three things the platform could do in one direction only. Each one is a
write, so each one asks the same question first: is this yours?

Withdrawing an application is allowed while it is still just an
application. Once the assessment clock has started it is refused — an
attempt row means the questions have been handed out, and deleting the
record would be a way to quietly retake a test meant to be taken once.
Cancelled applications are refused too, or the permanent lockout could be
erased by withdrawing and applying again.

Removing a gig is refused once anyone has been hired or paid on it: at
that point the gig is the record of work that was agreed, and taking it
away would strand the worker's application and their onboarding thread
with nothing to point at. Pending applications go with the gig, because
an application to a listing that no longer exists is a job the worker can
never hear about again. The employer is told the count before they
confirm, aloud as well as on screen.

Messages can only be deleted by the account that wrote them, checked
against the stored author id rather than a display name. Rows written
before that field existed carry no author, so nobody can delete them —
refusing is the safe direction when ownership is unknown.

All three are reachable by voice as well as by button, since a worker who
cannot see the screen cannot press "Withdraw". Refusals are spoken, not
just displayed: a button that silently does nothing is worse than one
that is not there.
@0dillon
0dillon merged commit bf45da2 into main Aug 25, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant