Skip to content

refactor(deep-link): classify verified as an attenuated trust class - #155

Merged
kipavy merged 1 commit into
devfrom
feat/trust-attenuated
Aug 19, 2026
Merged

refactor(deep-link): classify verified as an attenuated trust class#155
kipavy merged 1 commit into
devfrom
feat/trust-attenuated

Conversation

@kipavy

@kipavy kipavy commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Part 2B of #144 — the argument, not the link change.

TRUST's silent class claims a route carries no capability. verified only satisfies that by accident of how the link is built today: the portal spends the verification token server-side and hands the app an inert user id. Once the mail link opens the app directly the app receives the raw single-use token, and verified leaves silent under silent's own definition — with confirm the only other home, which puts a consent sheet on a tap the user just asked for by pressing "verify my email".

So: a fourth class, attenuated — a real capability worn down by scope, lifetime and blast radius until acting on it unprompted is safe — argued into the TRUST doc comment beside the other three. The argued core is that a hostile verified link carries the attacker's own token, so tapping it verifies the attacker's address (which they can already do unaided) and touches nothing of the tapper's; and that the class stops being honest the moment such a link carries something a forged one could spend against the tapper.

silent stays, unpopulated, as the home for a route that genuinely carries nothing.

No behaviour change. verified still carries u=<userId> and still runs unprompted. The intent shape, the codec, the builder and handleUnpromptedIntent are untouched. landing/app/open/fragment.ts in VoltiusApp/web needs no matching change — no route was added or renamed.

Also in this commit:

  • The four class guards collapse into one isOfClass factory instead of gaining a fourth copy of the same three-line predicate.
  • isUnpromptedIntent lists its classes rather than testing "not confirm", so a class added later has to be admitted deliberately instead of by default.
  • Follow-up 3 from the Deep links: https fallback in front of every route, plus the next routes #144 handoff: the stray double blank line at deepLinkUrl.ts:183-184.

Still gating Part 2B's shipping (not this PR)

Enterprise mail rewriters. Outlook SafeLinks / Defender ATP re-encode the link onto their own logging host, so the tap resolves against that host: no App Link fires, the mail client's browser follows the redirect, and the app never sees the link at all. Whether a fragment survives the round trip is undocumented — and SafeLinks is already known to mangle URL structure in other ways (it has re-encoded ? as &, breaking magic links). Any 2B design that actually moves the token into the mail link has to keep working when the link lands in a browser instead of the app.

Verification

  • node ./node_modules/typescript/bin/tsc --noEmit -p tsconfig.json — clean, exit 0.
  • CI=true npx vitest run480 files, 3720 tests, all passing, exit 0. (pluginBundleBuild included.)
  • The doc-comment wording of the mail-rewriter paragraph was corrected after that run; comment-only, no code touched.
  • Not exercised in a running app: this changes no runtime path, so there is nothing new to click.

`silent` claims a route carries no capability at all. That is only true of
`verified` by accident of how the link is built today: the portal spends the
verification token server-side and hands the app an inert user id. Once the
mail link opens the app directly the app receives the raw token, and `verified`
leaves `silent` under its own definition — with `confirm` the only other home,
which puts a consent sheet on a tap the user just asked for.

Add a fourth trust class for a capability worn down by scope, lifetime and
blast radius until acting on it unprompted is safe, and argue `verified` into
it in the TRUST doc comment beside the other three. `silent` stays, unpopulated,
for a route that genuinely carries nothing.

No behaviour change: `verified` still carries `u=<userId>` and still runs
unprompted. The four class guards collapse into one `isOfClass` factory rather
than gaining a fourth copy, and `isUnpromptedIntent` lists its classes so a
class added later must be admitted deliberately.
@kipavy
kipavy merged commit 39eebb2 into dev Aug 19, 2026
4 checks passed
@kipavy
kipavy deleted the feat/trust-attenuated branch August 19, 2026 17:32
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