Skip to content

fix(auth): require authenticated access across browser and HTTP - #241

Open
brianorwhatever wants to merge 4 commits into
mainfrom
codex/authenticated-boundary
Open

fix(auth): require authenticated access across browser and HTTP#241
brianorwhatever wants to merge 4 commits into
mainfrom
codex/authenticated-boundary

Conversation

@brianorwhatever

@brianorwhatever brianorwhatever commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Private reads and protected writes now require a verified human session or a scoped agent key across browser, mobile, and HTTP callers. Asserting an owner's current or legacy DID no longer grants access to their data or permission to change it.

Authorization and compatibility

  • Browser and HTTP operations share authentication, scope, and resource checks before invoking internal business operations.
  • Current and legacy identities come from authenticated account records, preserving migrated accounts and their bookmarks without accepting new caller-asserted identity links.
  • Logout and expiry invalidate database-backed private subscriptions; an indexed cleanup sweep recovers missed expiry jobs while retaining unexpired revocation tombstones.
  • Published lists remain publicly readable; shared editing, attachments, and publishing require authenticated access. Upload references are bound to the authorized resource and reject path traversal.
  • Compatibility entry-point names remain, but identity arguments confer no authority. Unauthenticated direct clients must upgrade; OTP login uses the HTTP flow.

Canonical login-account selection and duplicate-email signup protections remain intact.

Login and offline recovery

Session establishment during restore and OTP verification has a 15-second timeout, and failed verification clears local credentials. Login and logout are serialized. Shared-list controls require sign-in and display write failures.

Offline session failures pause synchronization without spending retry counts or discarding remaining edits. Resource denials use the affected edit's existing five-retry budget and backoff, allow later edits to run, and discard the denied edit with a warning when retries are exhausted. Successful edits do not erase the failure status. Missing and inaccessible resources have identical responses, so these errors do not reveal private resource existence.

Migrated bookmark IDs include both account identities without duplicates. Users can inspect and remove their own bookmarks after unpublishing without gaining access to the private list.

Related: #235

Validation

  • 232 Node tests and 256 Bun tests pass, covering forged identities, private reads, revoked keys, scopes, session lifecycle, login recovery, shared editing, attachments, publishing, migrated bookmarks, offline retries, and deployment gates.
  • TypeScript and the production build pass; the build retains its large-chunk warnings.
  • The prior full source lint comparison found 49 errors versus 51 on the baseline, with no new error diagnostics. Lint checks for the four subsequently changed source files pass; repository-wide lint is not clean.

The security reproduction and regression tests exercise handlers, signed JWTs, HTTP dispatch, and browser components locally. Live OTP delivery, subscription invalidation timing, object storage, supported native releases, and coordinated staging behavior remain unverified.

Rollout gate

This PR is open for review, and Railway has deployed a PR preview. Production cutover remains unapproved; this work has not deployed the production authentication change.

Confirm deployed browser, iOS, Android, and integration versions and record staging evidence before merging. The pending release attestation blocks every configured production Convex deployment and Railway build after merge, including unrelated hotfixes, until the evidence is recorded. Complete the gate first or ship urgent unrelated fixes before this PR. Manual production deployments must apply the same check.

The record is an auditable attestation, not authenticated owner approval. main currently has no branch protection, repository rulesets, or CODEOWNERS; required owner review is not enforced. No live repository policy was changed.

The rollout checklist covers the coordinated backend/client release and later compatibility retirement. Retire entry points only after supported versions are confirmed and the required observation window shows no compatibility use.

Post-Deploy Monitoring & Validation

Release owner: Brian (brianorwhatever). Monitor login success, private-query failures, offline retries, HTTP 401/403 rates, and session-expiry scheduler failures for the first 30 minutes and again at 24 hours. Pause rollout if supported clients cannot authenticate or any private-access bypass succeeds; retain the authenticated boundary while repairing the release.

Note

Require authenticated access across browser and HTTP operations

  • Introduces an accessSessions table and actorSession internal functions to validate JWT tokens against stored session records and resolve the authenticated actor.
  • Replaces direct Convex query, mutation, and action definitions with shared actorQuery, actorMutation, and actorAction wrappers that authenticate the actor, enforce scopes, and authorize resources before executing business logic.
  • Removes caller-supplied identity arguments (e.g., ownerDid, userDid) from backend operations and derives ownership from the server-resolved actor.
  • Adds client-side authenticated Convex adapters in authenticatedConvex.ts that inject the session token and strip legacy identity fields from operation arguments.
  • Risk: Public OTP session and user auth endpoints in convex/authSessions.ts and convex/auth.ts are now stubs that throw authentication errors; callers must migrate to the HTTP login endpoints and authenticated client wrappers.

Macroscope summarized 7c904d1.

Resolve current and migrated identities at a shared authenticated boundary,
protect private reads and scoped writes, and remove acting DIDs from clients.
Track session revocation and expiry so reactive subscriptions lose access.

Preserve compatibility names pending deployed-client confirmation. Add forged
identity, scope, session lifecycle, shared editing, migration and upload tests,
and document the coordinated rollout and remaining live verification gates.
@railway-app
railway-app Bot temporarily deployed to Friends / boop-pr-241 September 9, 2026 06:39 Destroyed
@railway-app

railway-app Bot commented Sep 9, 2026

Copy link
Copy Markdown

🚅 Deployed to the boop-pr-241 environment in Friends

Service Status Web Updated
boop ✅ Success (View Logs) Web Sep 9, 2026 at 8:51 am UTC

@brianorwhatever
brianorwhatever marked this pull request as ready for review September 9, 2026 06:39
@brianorwhatever

Copy link
Copy Markdown
Contributor Author

@pullfrog review this

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Two changes create states a user cannot recover from on their own: unbookmarking a list stops working the moment its owner unpublishes, and a login restore that never settles leaves the app permanently stuck in "Authentication is already in progress".

Reviewed changes

Every private read and protected write moves behind a server-resolved actor, so caller-supplied DIDs no longer confer authority anywhere in the backend.

  • Shared operation wrappersactorQuery/actorMutation/actorAction in convex/lib/authenticated.ts resolve the actor, enforce a scope, verify identity assertions, and authorize declared resources before the business handler runs, exporting a public/internal pair per operation.
  • Database-backed sessions — a new accessSessions table plus convex/actorSession.ts validate JWTs against stored rows so logout, revocation, and expiry invalidate live reactive subscriptions rather than just clearing client state.
  • Identity arguments removedownerDid, userDid, checkedByDid and friends are stripped from operation args across ~40 backend modules; ownership now derives from ctx.actor.
  • Resource authorizationauthorizeResources in convex/lib/permissions.ts resolves items and anchors back to their list and requires ownership or an active publication.
  • Client adapterssrc/lib/authenticatedConvex.ts wraps useQuery/useMutation/useAction to inject the session token and strip legacy identity fields, gated by a generated 132-entry registry that CI checks for staleness.
  • Public auth entry points stubbedconvex/authSessions.ts and convex/auth.ts replace their public session/user functions with unconditional throws directing callers to the HTTP login endpoints.
  • Storage path hardeningisDirectChildKey constrains site upload and asset keys to a direct child of the actor's own prefix.
  • Boundary test suitescripts/auth-boundary.test.mjs bundles and invokes the real handlers with real signed JWTs against an in-memory database with working index predicates; these tests can genuinely fail.

⚠️ The compatibility layer does not keep un-upgraded clients working

The PR body and docs/authentication-rollout.md frame the retained public names as compatibility, but they are not backward compatible in practice. convex/authSessions.ts re-adds createSession, getSession, markSessionVerified and deleteSession as stubs that throw unconditionally, and convex/auth.ts does the same for upsertUser. Any deployed client still calling those names hard-fails at the first login attempt rather than degrading.

The same holds for every converted operation: Convex arg validators reject undeclared args, so an old client sending userDid gets a validation error, and a client sending no authToken gets an auth error. The retained names preserve the API surface, not the behavior.

The rollout doc is explicit that the deployed browser/iOS/Android/integration inventory is not yet confirmed, and the PR is correctly marked draft. The gap worth closing before cutover is that nothing in the code enforces the gate — there is no version check, no feature flag, and no staged path. Whoever merges this can deploy it without the inventory ever being confirmed.

# Rollout sequencing has no enforcement mechanism

## Affected sites
- `convex/authSessions.ts` — public session functions replaced with unconditional throws
- `convex/auth.ts` — `upsertUser` replaced with an unconditional throw
- `docs/authentication-rollout.md` — states the client inventory is unconfirmed

## Required outcome
The cutover cannot reach production before the deployed-client inventory is confirmed, and the confirmation is recorded somewhere durable rather than in a doc.

## Open questions for the human
- Is there a mechanism to hold this at staging, or is the draft status the only gate?
- iOS and Android ship a webview over the same bundle, but the native shell version still gates which bundle loads — has the minimum shipped version been checked?
- Are there third-party or scripted HTTP integrations calling the Convex function endpoints directly that would not appear in an app-version inventory?

⚠️ Removing the legacy-DID migration branch closes account-migration onboarding

upsertUserInternal previously had two branches this PR deletes: migrating a legacy DID onto a Turnkey account, and linking Turnkey to an existing user found by DID. In their place is if (args.legacyDid) throw new Error("Identity migration requires verified account linking").

Existing legacyDid links on user rows keep working — authorize and isResourceOwner both consult them — so already-migrated users are unaffected. What is gone is the ability to create a new one. startOtp in src/hooks/useAuth.tsx still accepts a legacyDid parameter and the OTP flow still threads it through, so the client-side entry point survives while the server-side path now throws.

This reads as intentional (the old path let a caller claim any DID), but there is no replacement flow in this PR, and a user mid-migration hits a bare error string.

# Legacy-DID migration is closed with no replacement path

## Affected sites
- `convex/auth.ts` — `upsertUserInternal`, legacy-DID migration and DID-linking branches removed
- `src/hooks/useAuth.tsx` — `startOtp(email, legacyDid?)` still accepts and forwards `legacyDid`

## Required outcome
Either a verified account-linking flow exists, or the client stops offering migration and the dead `legacyDid` parameter is removed so users cannot reach the throw.

## Open questions for the human
- Is closing new migrations intended for this PR, or is a verified-linking flow expected to land alongside it?
- How many users are still un-migrated? If the answer is zero, removing the client-side `legacyDid` plumbing is the cleaner fix.

ℹ️ No owner or retirement date for the compatibility surface

convex/lib/clientAuth.ts lists ten identityAssertionFields accepted-and-ignored on every operation, commented "retained until deployed client versions are confirmed". checkAssertions validates them against the resolved actor, so they are not a security hole — but they are ten optional string args on the public signature of ~130 operations, and the client adapter strips them on the way out, so nothing this PR ships actually sends them.

The stubbed public names in convex/authSessions.ts and convex/auth.ts are in the same position. docs/authentication-rollout.md says to retire them "only after supported clients have migrated" without naming who checks or when.

# Compatibility surface has no removal trigger

## Affected sites
- `convex/lib/clientAuth.ts` — `identityAssertionFields`
- `convex/authSessions.ts`, `convex/auth.ts` — throwing stubs
- `docs/authentication-rollout.md` — retirement described but unscheduled

## Required outcome
A tracked follow-up with an owner and a concrete trigger, so this does not become permanent surface area by default.

ℹ️ Nitpicks

  • src/pages/PriorityFocus.tsxuserDid and legacyDid are removed from PriorityItem's destructure but userDid: string remains in its prop type, so the type still requires a prop the component ignores.
  • convex/bitcoinAnchors.ts — the spread ...ctx.credentials, around new line 247 is indented one level deeper than its siblings.
  • The PR reports npm run lint at 49 errors (down from 51). Not introduced here, and the count moved the right way, but a boundary change of this size lands on a codebase where lint is not a signal.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread convex/publication.ts Outdated
Comment thread src/hooks/useAuth.tsx Outdated
Comment thread src/lib/sync.ts
Comment thread convex/didResourcesHttp.ts
Comment thread convex/schema.ts Outdated
@railway-app
railway-app Bot temporarily deployed to Friends / boop-pr-241 September 9, 2026 07:43 Destroyed

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed changes

  • Re-read the two new commits (7c904d1afb3e6f) against the full 128-file diff, then verified each claimed fix against the files at HEAD rather than trusting the range-diff rendering.
  • Confirmed all five prior review threads are genuinely addressed and retired them: SharedListResource now gates on token with a sign-in nudge and a role="alert" rollback path; unbookmarkList/isBookmarked/getPublicationStatus dropped their caller-supplied userDid and check ctx.actor instead; accessSessions gained a by_expires_at index plus a bounded cleanupExpiredSessions cron; withAuthTimeout bounds the establish call on both the restore and OTP paths.
  • Traced the new AuthError extends ConvexError boundary through convex/lib/authError.ts, actor.ts, session.ts, permissions.ts, httpResponses.ts and src/lib/sync.ts, including the establishInternal lazy-registration path for pre-rollout HTTP JWTs and the revoked-tombstone guard that stops a logged-out token re-establishing itself.
  • Audited the release gate (scripts/check-authentication-cutover.mjs, release/authentication-cutover.json, railway.json, deploy-convex.yaml) for fail-open paths, and confirmed RAILWAY_ENVIRONMENT_NAME is the variable Railway actually injects at build time.
  • Checked that the tests added in this delta can fail: scripts/sync-auth.test.mjs, scripts/shared-list-auth.test.mjs, scripts/authentication-cutover.test.mjs, and the new bookmark/cleanup/ConvexError-round-trip cases in scripts/auth-boundary.test.mjs.

Important

The gate, the session lifecycle and the resource-declaration model all hold up. One issue introduced by this commit is worth resolving before merge: a permanently-denied offline mutation can wedge the entire sync queue with no recovery path for the user.

🚦 Rollout sequencing

The gate fails closed on every path I could enumerate — GitHub Actions never passes --railway, so needsApproval is unconditionally true there; process.exitCode = 1 does short-circuit the && in railway.json; the check runs before npm ci and before deployment secrets are used; and no other workflow deploys Convex (mobile-build.yml only runs convex codegen). docs/authentication-rollout.md states the production block is intentional, which resolves the main question. Two residual points:

  • The doc says merging "will intentionally fail production deployment", but the gate is scoped to every push touching convex/**, not to the cutover. An unrelated Convex hotfix after this merges will fail with an error about authenticated-client rollout evidence. Worth a sentence in the doc so the next person isn't confused, or scoping the trigger.
  • checkApproval only requires non-empty strings and approvedBy === record.owner — both fields live in the same file, and whoever opens a PR can set them. There is no CODEOWNERS entry on release/, so "only the named release owner may flip this" rests entirely on branch protection configured outside this repo. Worth confirming that protection exists, since this gate is the only thing between a breaking auth change and production.
  • Minor: the ^(boop-pr-\d+|staging|development)$ allowlist encodes a Railway environment-naming convention that lives in the Railway project, not here. If preview environments are named differently, previews get needlessly blocked — fail-closed, so not a security issue, but a surprising one.

ℹ️ Nitpicks

  • convex/attachments.ts:60,92,126 and several "Not authorized to …" throws in convex/auth.ts are still plain Error, so they surface as 500 through handlerErrorResponse and are invisible to accessFailure in the sync queue. Either convert them or note why they're deliberately out of scope.
  • Leftovers from the mechanical argument removal: src/pages/NoteEditor.tsx:28 has a stray trailing comma and padding ({ itemId: itemId as Id<"items">, }), and blank lines were left inside object literals in src/components/Attachments.tsx (three call sites), convex/agentReadHttp.ts, and convex/referrals.ts (a doubled blank line where the _generated/server import was).
  • Removing the AuthError branch from every *Http.ts handler changes authorization denials from a uniform 401 to 403 (handlerErrorResponse now splits on message, and scripts/sync-auth.test.mjs:47-48 pins that). That reads like the intended REST semantics, but it is a behavior change for any API-key consumer branching on 401 — worth a line in API.md if it isn't there already.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread src/lib/sync.ts
Comment thread convex/lib/permissions.ts Outdated
Comment thread .github/workflows/deploy-convex.yaml
Comment thread convex/publication.ts Outdated
@railway-app
railway-app Bot temporarily deployed to Friends / boop-pr-241 September 9, 2026 08:18 Destroyed

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ The head-of-line block is genuinely fixed. Two smaller things worth a look, both inline.

Reviewed changes

  • Read the range-diff for 99e995f against the full 129-file diff, then verified each claimed fix against the files at HEAD rather than trusting the range-diff rendering.
  • Confirmed all four prior threads are addressed and retired them: authorizeResources now throws a uniform resourceUnavailable() on every branch (lines 57/63/69/73/79/82), getUserBookmarkIds unions did + legacyDid with dedupe and has a real test in scripts/bookmark-identities.test.mjs, docs/authentication-rollout.md now states outright that the gate blocks every subsequent production Convex deploy including unrelated hotfixes, and the sync queue no longer returns out of the loop on a resource denial.
  • Traced the new FORBIDDEN code end to end: convex/lib/authError.tsresourceUnavailable()authorizeResources / getItemForSync / the convex/auth.ts compat shims → authErrorData on the client. Checked that no credential failure was reclassified into FORBIDDENrequireSession, authenticate, establishOperation and requireScope all still emit UNAUTHORIZED/INVALID_TOKEN, so the "sign in again" toast still means what it says.
  • Verified the retry accounting the fix depends on: getQueuedMutations() re-reads IndexedDB each pass and updateMutationRetry persists, so a FORBIDDEN mutation's budget survives restarts and only that mutation is discarded. The new scripts/sync-auth.test.mjs cases assert on applied-mutation contents, exact retryCount and queue length, so they can fail.
  • Re-checked the existence-oracle surface across permissions.ts, lists.ts, items.ts, attachments.ts, comments.ts, assignees.ts and the *Http.ts handlers. getList and listWithItemsOperation return null uniformly, handlerErrorResponse maps FORBIDDEN to a flat 403 before any message matching, and the leftover plain-Error "not found" throws in lists.ts / assignees.ts are unreachable because authorizeResources already covers those IDs. One handler is not covered — see the demoteItem comment.

🔁 Sync queue

The wedge is fixed, but closing it made the deletion path unreachable. getItemForSync now throws resourceUnavailable() where it used to return null, and authorizeResources throws the same for a missing item or list — so a remotely-deleted item no longer produces "Item not found", it produces FORBIDDEN. Details inline at src/lib/sync.ts:243. This is a UX regression, not data loss: the edit is still discarded, just five sync cycles later and behind a misleading message.

ℹ️ Nitpicks

  • The FORBIDDEN branch (src/lib/sync.ts:229-240) continues with no this.delay(...), unlike the generic retry path below it which backs off 1s/2s/4s/8s/16s. sync() is called from useOffline.tsx:79 on every networkStatusChange and from manualSync behind the "Sync now" button, which re-enables as soon as isSyncing clears — and a FORBIDDEN-only pass now completes near-instantly. Five taps in a few seconds, or Wi-Fi/cellular flapping, burns the whole budget against a denial that might have resolved a minute later. Reusing RETRY_DELAYS[mutation.retryCount] here would make the two paths symmetric.
  • checkAssertions in convex/lib/authenticated.ts throws AuthError("Identity assertion does not match authenticated account", "UNAUTHORIZED"), but httpResponses.ts:80 matches Identity assertion and returns 403 — the structured code and the HTTP status disagree. Pre-existing in the regex, but this commit is the one that writes the 401/403 split down as a contract in API.md, so it is now a documented inconsistency rather than an unnoticed one.
  • convex/comments.ts:102 deleteComment declares resources: () => ({}) and authorizes by hand, throwing plain Error("Comment not found") vs Error("Not authorized to delete this comment") for a guessed commentId. Production redaction collapses both to "Server Error", so this is not a prod oracle — but it is the one file in the conversion that still relies on redaction rather than on resourceUnavailable().

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread src/lib/sync.ts Outdated
Comment thread convex/items.ts Outdated
@brianorwhatever

Copy link
Copy Markdown
Contributor Author

Reply to the initial review:

The cutover cannot reach production before the deployed-client inventory is confirmed, and the confirmation is recorded somewhere durable rather than in a doc.

Addressed in afb3e6f: the configured Convex and Railway production paths check release/authentication-cutover.json before deploying. The record remains pending because deployed browser/mobile/integration inventory and live staging evidence have not been confirmed. Preview builds remain available. Retained function names preserve the API surface; they do not keep unauthenticated direct clients working. Matching identity assertions are accepted with verified credentials, but cannot grant access.

Either a verified account-linking flow exists, or the client stops offering migration and the dead legacyDid parameter is removed so users cannot reach the throw.

Removed the unused startOtp parameter, OTP state field, migration logging, and comments in afb3e6f. It was already absent from HTTP requests. Existing database-owned legacy associations remain usable; a new identity link cannot be created by asserting a DID. No unverified replacement linking flow was introduced.

A tracked follow-up with an owner and a concrete trigger, so this does not become permanent surface area by default.

The rollout document now tracks AUTH-COMPAT-RETIREMENT, owned by Brian (brianorwhatever), with review on 2026-09-16. Retirement requires confirmed supported versions and 14 consecutive days without compatibility calls or legacy assertions from supported clients. Missing telemetry does not satisfy that trigger.

The unused PriorityItem prop and credential-spread indentation were also fixed in afb3e6f. Production approval and live validation remain outstanding; none of these code fixes claim deployed-client confirmation.

@brianorwhatever

Copy link
Copy Markdown
Contributor Author

Reply to the follow-up review:

An unrelated Convex hotfix after this merges will fail with an error about authenticated-client rollout evidence.

Addressed in 99e995f: the rollout document explicitly says the pending gate blocks every subsequent production Convex deployment and Railway build, including unrelated hotfixes. A later full deployment contains this authentication change, so limiting approval to the latest changed files would bypass the gate. PR/staging deployments can collect evidence while production remains unapproved.

Worth confirming that protection exists, since this gate is the only thing between a breaking auth change and production.

It does not currently provide enforced owner-only review. The 2026-09-09 inspection found main unprotected, no repository rulesets, and no CODEOWNERS file. The rollout document records this limitation: the approval file is an auditable attestation, not proof of the approver's identity. Required owner review must be configured by the repository owner before relying on that claim. No live repository policy was changed, and no approval evidence was fabricated.

The documented non-production Railway names are boop-pr-<number>, staging, and development; the observed boop-pr-241 preview matches. Unknown names remain blocked intentionally.

Either convert them or note why they're deliberately out of scope.

The attachment and account authorization throws identified here were converted to structured resource denials in 99e995f. That commit also removed the listed argument-removal formatting leftovers and documented the HTTP 401/403 split in API.md. The offline queue now processes later edits after a resource denial and exhausts only the denied edit's retry budget; credential failures preserve the queue. Commit 0425318 adds retry backoff and makes the missing-or-inaccessible message accurate.

@brianorwhatever

Copy link
Copy Markdown
Contributor Author

Reply to the latest review:

Reusing RETRY_DELAYS[mutation.retryCount] here would make the two paths symmetric.

Addressed in 0425318. Retryable resource denials use the existing 1/2/4/8/16-second backoff; exhausted edits are discarded without another delay. Deterministic tests check the delay sequence, preserved progress for later edits, and that rapid sync attempts cannot spend additional retries while backoff is pending. The obsolete deletion branches and impossible missing-item test fixture are removed; the message now covers resources that may have been removed or access that is unavailable.

The structured code and the HTTP status disagree.

HTTP responses now honor structured error codes before inspecting legacy error messages. An identity assertion that does not match the authenticated account remains UNAUTHORIZED and returns 401 consistently; missing scopes and resource denials remain FORBIDDEN/403. A regression runs the real assertion check, serializes its Convex error, and verifies the resulting HTTP status.

It is the one file in the conversion that still relies on redaction rather than on resourceUnavailable().

Comment deletion now returns the same structured resource denial for missing comments, orphaned comments, and comments the caller cannot delete. Author, migrated-author, list-owner, and shared-editor deletion behavior remains intact. The inline demotion issue is fixed by declaring the new parent as a resource before the handler runs.

Validation for 0425318: Node 232/232 and Bun 256/256 tests pass; TypeScript, the production build, and lint on the changed source files pass. The build retains the existing large-chunk warnings. Production cutover remains unapproved.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

Re-read the one new commit (0425318, 6 files, +98/−31) against the full 129-file diff, verified each claimed fix against the files at HEAD rather than trusting the range-diff rendering, and dispatched three independent specialist passes — an HTTP-status-contract lens, an offline-sync-queue lens, and an authorization lens. All three returned clean negatives.

  • Closed the demoteItem parent oracleconvex/items.ts:942 now declares resources: args => ({ items: [args.itemId, args.newParentId] }), so authorizeResources resolves the new parent to its owning list before the handler runs. The distinguishable "Parent item not found" / "Items must be in the same list" probe is gone, and no authorization outcome changed: legitimate demotion always targets a parent in the same already-owned list.
  • Unified deleteComment's three error pathsconvex/comments.ts:111,116,136 replace "Comment not found" / "Item not found" / "Not authorized to delete this comment" with a single resourceUnavailable(). The gating condition (isAuthor || canUserEditList) is untouched, so this closes the enumeration oracle without changing who can delete what.
  • Corrected the 401/403 splitconvex/lib/httpResponses.ts:76-79 hoists the structured AuthError branch above the message regexes. Exactly one status flipped: checkAssertions (code UNAUTHORIZED) went 403 → 401. That resolves the nit in the right direction — API.md:38-42 defines 401 as a credential problem and 403 as scope-or-resource, so the old regex-driven 403 was the latent bug. Every other AuthError call site across actorSession.ts, lib/auth.ts, lib/actor.ts and lib/session.ts produces the same status as before.
  • Removed the dead deletion paths in the sync queuesrc/lib/sync.ts drops both the "Item not found" catch arm and checkForConflict's if (!serverItem) guard, and softens the two toasts to "may have been removed or access is unavailable". Confirmed getItemForSync (convex/items.ts:588-597) genuinely cannot return null any more, so the guard removal is safe rather than merely type-clean.
  • Backed off the FORBIDDEN retry pathsrc/lib/sync.ts:237-238 reuses RETRY_DELAYS[mutation.retryCount], making the two retry paths symmetric. Checked that this does not reopen the head-of-line block: it is still a continue, so healthy edits queued behind a denial apply in the same pass, and the per-mutation budget is unchanged.
  • Confirmed the new tests can fail — the demoteItem and deleteComment cases in scripts/auth-boundary.test.mjs exercise genuinely distinct inputs against the fixture (I1L1 owned by the actor, I2L2 owned by did:other), not two spellings of the same case. In scripts/sync-auth.test.mjs, assert.deepEqual(delays, [1000,2000,4000,8000,16000]) is exact and driven by an override of the real this.delay call, and the offline fixture now returns structuredClone(s.queue) so a mutated in-loop copy can no longer mask a persistence bug.

ℹ️ Nothing left that another review pass can settle

Both threads from the prior review are genuinely addressed and have been retired. The remaining gate on this PR is operational, not code: release/authentication-cutover.json still ships all-null, checkApproval accepts any non-empty approvedBy === owner pair editable in this same PR, and docs/authentication-rollout.md records that main had no protection or CODEOWNERS as of 2026-09-09. So the deployed browser/iOS/Android/integration inventory and the reviewer enforcement behind that record are the merge blockers, and they are yours to confirm rather than mine to verify. This approval covers the code.

Pullfrog  | View workflow run | Using Claude Opus𝕏

@brianorwhatever

Copy link
Copy Markdown
Contributor Author

So the deployed browser/iOS/Android/integration inventory and the reviewer enforcement behind that record are the merge blockers, and they are yours to confirm rather than mine to verify.

The code review is complete, but these confirmations are still missing. The release record has no client inventory, staging evidence, or approval. The documented repository inspection found no enforced owner-only review, and this task has not changed live repository policy.

This needs a release-owner decision: hold merge until deployed-client/staging evidence is recorded and required owner review is enforced, or explicitly accept merging the code while production stays blocked and record acceptance of the approval record's current enforcement limits. The first option satisfies the stated merge gate; the second allows code integration but does not authorize production cutover or retirement of compatibility entry points.

I recommend holding merge until those confirmations are complete, consistent with the rollout document. No production approval, client-version evidence, or owner-review enforcement is being claimed here.

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