Skip to content

refactor(convex): consolidate share/harness authorization into shareAuthCore - #157

Merged
DIodide merged 1 commit into
stagingfrom
refactor/deepen-shareauth
Jul 4, 2026
Merged

refactor(convex): consolidate share/harness authorization into shareAuthCore#157
DIodide merged 1 commit into
stagingfrom
refactor/deepen-shareauth

Conversation

@DIodide

@DIodide DIodide commented Jul 3, 2026

Copy link
Copy Markdown
Owner

🔴 HOLD — do not merge until v1.0 (PR #154) is cut. This is live authorization code; keeping it off the release keeps v1.0 clean. It lands as the first post-1.0 improvement.

Third deepening from the improve-codebase-architecture skill (design-it-twice → Design #2), after event_encoder (#155) and ProvisioningContext (#156).

What

The grant-based access model was mirrored across shares.ts and harnessShares.ts (two ~700-line files) — grantForToken, assertOwned*, resolveRole, activeGrantForCaller all near-identical — and the mirror already rotted once (the sharedLocked cleanup fix landed on only one side; that was a code-review finding earlier this cycle).

New shareAuthCore.ts owns the ONE security machine, wired per-entity via makeShareAuth(descriptor) with concretely-typed closures (zero casts on the cross-entity gate):

  • empty-token→null guard, isActiveGrant (single impl) on the token lookup AND the per-user scan, opaque "Not found" (never leak existence), owner-wins-first, the cross-entity fk gate, editor-short-circuit / viewer-floor / max-role-wins, the truthy-userId guard (an anonymous caller rides the token path only — never matches a grantedToUserId===undefined row), and a required-projection sharedHeader (an unredacted egress is unrepresentable at the type level).

Entity policy stays in the wrappers: publicMessage/publicHarnessProjection redaction, authorizeConversationWrite (owner|editor) vs editSharedHarness (editor-only + the sharedLocked lock + field allowlist), mint/rotate/list payloads, the harness lock-cleanup tail. Exported signatures unchanged (resolveConversationRole, authorizeConversationWrite, isActiveGrant re-exported) so messages.ts, checkConversationAccess, and the FastAPI backend need zero edits.

Net: −174 lines from the two share files → +211 core.

Why (glossary)

  • locality: the whole authorization model lives in one auditable module; a token/expiry/precedence fix lands once for both entities (and any future third).
  • leverage: callers pass (ctx, userId, entityId, token) and get a role; the mirror can't rot.
  • depth: a small interface hides the entire precedence/activeness/opaque-null machine.

Verification

  • 48 existing share/role tests pass with ZERO expectation changes (shares.test 32 + harnessShares.test 16) — the primary safety net; a changed assertion would mean behavior moved.
  • 12 new shareAuthCore.test.ts cases pin the invariants directly, incl. the anonymous-privilege-escalation negative (userId=null must never match grantedToUserId===undefined) and the cross-entity gate.
  • A 7-agent verify + xhigh review gauntlet walked all invariants → behaviorPreserving: yes, 0 findings. convex 231 passed, tsc -p convex + biome clean.

No user-facing change → no CHANGELOG entry.

…uthCore

Third deepening from the improve-codebase-architecture skill (design-it-twice →
Design #2). The grant-based access model was mirrored across shares.ts and
harnessShares.ts (two ~700-line files) — grantForToken, assertOwned*,
resolveRole, activeGrantForCaller all near-identical — and the mirror had
already rotted once (the sharedLocked cleanup fix landed on only one side).

New shareAuthCore.ts owns the ONE security machine: empty-token→null guard,
isActiveGrant (single impl) on the token lookup AND the per-user scan, opaque
"Not found" (never leak existence), owner-wins-first, the cross-entity fk gate,
editor-short-circuit / viewer-floor / max-role-wins, the truthy-userId guard
(an anonymous caller rides the token path only — never matches a
grantedToUserId===undefined row), and a required-projection sharedHeader (an
unredacted egress is unrepresentable). Each entity wires it with concretely-
typed closures via makeShareAuth(descriptor) — zero casts on the fk gate.

Entity POLICY stays in the wrappers: publicMessage / publicHarnessProjection
redaction, authorizeConversationWrite (owner|editor) vs editSharedHarness
(editor-only + the sharedLocked lock + field allowlist), mint/rotate/list
payloads, the harness lock-cleanup tail. Exported signatures unchanged
(resolveConversationRole / authorizeConversationWrite / isActiveGrant re-
exported from shares.ts) so messages.ts and other importers need zero edits.

Behaviour-preserving: the 48 existing share/role tests (shares 32 +
harnessShares 16) pass with ZERO expectation changes. New shareAuthCore.test.ts
(12 cases) pins the invariants directly, incl. the anonymous-privilege-
escalation negative and the cross-entity gate. convex 231 passed, tsc + biome
clean.

HOLD merge until AFTER v1.0 is cut (live authorization code — keep the release
clean; a human should read the final diff).
@DIodide
DIodide merged commit 6a82324 into staging Jul 4, 2026
4 checks passed
@DIodide
DIodide deleted the refactor/deepen-shareauth branch July 4, 2026 02:52
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