Files
pad/internal
xarmian 82c45f24cd feat(apps): app item actions and context codes (TASK-3414, SPEC-6 U11) (#1784)
* feat(apps): app item actions and context codes (TASK-3414, SPEC-6 U11) [wip]

Item actions are written as app_item_actions rows at provisioning, kept in
step by a reviewed upgrade (a changed action gets a new revision, a
removed one is retired), deleted at uninstall and backfilled at start.
A viewer's click mints a single-use, two-minute context code; the app
redeems it with its service token. Mint and redeem take the install, item
and action rows in that order (FOR SHARE on Postgres), re-check the
install, the item's current companion collection, the action's revision
and the viewer's current visibility (the human item read's rule), and
refuse with one 404 body. Redeem consumes the code first. The frozen TS
type ItemAppAction and api.items.appActions / mintAppAction ship for U9.

Migrations 125 / pg 099.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(apps): U11 mint, redeem, refusal and revocation tests; redeem in the read census; NUL baseline (TASK-3414)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(apps): invalidating mutations wait for a redeem holding the section 6 locks (TASK-3414)

Deterministic: a hook pauses the redeem with its locks held while disable,
an upgrade's action change, a move or a delete is started; each stays
blocked until the redeem commits. On Postgres, dropping the install lock
lets disable through, dropping the item lock lets move and delete through.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(apps): U11 mutant kills: epoch, deactivation, companion moves, release, admin non-member, backfill and workspace guards (TASK-3414)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(apps): redeem at the token's workspace, service tokens only; no pool reads under the context locks; normalized base URL (codex r1 on U11, TASK-3414)

Redeem moves to POST /api/app/v1/context/redeem (§6's path), outside
/workspaces/{ws}: the workspace is the token's, injected before the same
per-route stack, so no workspace-path refusal can answer before the code
is consumed. It refuses a delegated token with the same 404.

The viewer re-admission reads the user, role, membership and grants on
the mint or redeem transaction (GetUserQ, ViewerRoleQ,
UserHasGrantsInWorkspaceQ). A one-connection pool runs mint and redeem for
a full member, a restricted member and a guest without hanging; switching
any of those reads back to the pool deadlocks it.

The link-out URL is built from the validated origin, refused before a
code is written when base_url does not normalize.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(apps): a delegated redeem attempt consumes the code before it is refused (codex r2 on U11, TASK-3414)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(apps): build the context code expiry from its constant (lint, TASK-3414)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-05 02:29:48 -04:00
..