fix(self-hosted): unblock address ownership; stop reporting owned addresses as unowned#56
Merged
Merged
Conversation
…addresses as unowned `emails address owner`, `set-owner`, `transfer-owner`, `unassign-owner` and `owner-history` threw unconditionally via serverOnly(), and the five matching MCP tools were blocked by assertMcpLocalStateAllowed(). Both stated reasons were false: ownership does have a /v1 implementation and those rows are not local in self_hosted mode. src/db/owners.remote.ts already serves getAddressOwnership, assignAddressOwner, transferAddressOwner, unassignAddressOwner and listAddressOwnershipEvents from /v1/owners, owner_id/administrator_id on /v1/addresses/<id>, and /v1/address-ownership-events; src/db/owners.ts routes to it whenever isSelfHostedMode(); the server serves the address-ownership-events resource; and `emails owner list` / `emails owner addresses` already used that same path with no guard. Worse than the refusals, `emails address list` / `emails addresses` hardcoded owner: null / administrator: null into every row and printed "-" in the Owner column, so an owned address was silently reported as unowned in the table AND in --json, with no error. - replace the five CLI guards with the existing lib/address-ownership.ts calls (mode-routed, so both local and self_hosted work), keeping the pre-guard output shape so scripts reading --json do not break - drop the ownership guards from the five MCP tools; they now return real data or a real error (e.g. "Address not found") instead of a mode refusal - hydrate the list projection through enrichAddresses so owner/administrator/ provider_name are real; the Owner column shows owner[:admin] - make --verbose do what its help text says (expanded owner/admin/quota) instead of being accepted and ignored; quota shows the configured LIMIT only, never a fabricated client-side "used" count, because the send ledger is server-owned - keep `address provision` server-only (the provisioning orchestration genuinely has no client-side equivalent) and rewrite the comment to say only that - delete the duplicate suggestAddressLocalParts copy in favour of the lib one Tests: new /v1-stub coverage for all five CLI subcommands and all five MCP tools plus the list projection; the old tests that asserted the refusals are replaced. All six MCP ownership tests and the three list-ownership tests fail on the pre-fix source and pass after. Full suite 2057 pass / 0 fail, tsc --noEmit clean.
andrei-hasna
added a commit
that referenced
this pull request
Jul 25, 2026
Only CHANGELOG.md conflicted. Resolved by keeping BOTH sides: - every send-path entry from main (#55, #54, #56) is preserved verbatim and kept first — part one wins on the send path; - this branch's rename-revert and dead-scaffolding entries follow; - the "BREAKING (aliased): rename @hasna/emails -> @hasna/mailery" entry is dropped rather than carried alongside its own revert. That rename never shipped — it exists only under [Unreleased] — so recording it and undoing it in the same unreleased section would describe a change no consumer can observe. Verified on the merge result: tsc --noEmit clean, bun run build exit 0, bun test 2101 pass / 0 fail / 108 skip, and the six Postgres suites against a real PG 16 at 86 pass / 0 fail.
andrei-hasna
added a commit
that referenced
this pull request
Jul 25, 2026
Only CHANGELOG.md conflicted. Kept both sides: main's entries (part one #55/#54, #56, and #57's rename revert) first, then this branch's dead-code removals. The never-shipped 'BREAKING (aliased): rename' entry is dropped, as it was on #57 — it is undone in the same unreleased section, so no consumer can observe it. Verified on the merge result: tsc --noEmit clean, bun run build exit 0, bun test 1994 pass / 0 fail / 108 skip, six Postgres suites 86 pass / 0 fail.
This was referenced Jul 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The whole address-ownership capability was blocked in
self_hostedby hand-written guards whose stated reason is factually false, even though a complete/v1-backed implementation already ships in the same tree.CLI —
src/cli/commands/address.ts:137-181:address owner,set-owner,transfer-owner,unassign-ownerandowner-historyall threw unconditionally viaserverOnly()(:29), and the comment at:24-28asserted these "have no /v1 equivalent in this self-hosted-only client".That is untrue:
src/db/owners.remote.tsimplementslistAddressOwnershipEvents(:230),assignAddressOwner(:245, PATCH/v1/addressesowner_id+administrator_idthen POST/v1/address-ownership-events),transferAddressOwner(:269),unassignAddressOwner(:292) andgetAddressOwnership(:311).src/db/owners.tsroutes to it wheneverisSelfHostedMode().src/server/self-hosted/resources.ts:451serves theaddress-ownership-eventsresource.parity.test.ts:530,:594,:615.emails owner list/emails owner addresses <owner>(src/cli/commands/owner.ts) carry no guard and DO work inself_hostedthrough the same routed module.MCP — the same false claim was duplicated:
src/mcp/tools/domains-impl.ts:89-95assertMcpLocalStateAllowedblockedget_address_owner(:408),set_address_owner(:431),transfer_address_owner(:456),unassign_address_owner(:479) andlist_address_owner_history(:501) with the reason "it reads/writes local address ownership rows" — those rows are not local inself_hostedmode.Worse than the refusals —
src/cli/commands/address.ts:52-58hardcodedowner: null/administrator: nullinto everyemails address list/emails addressesrow, and:74printed-in the Owner column. An owned address was reported as unowned in both the table and--json, with no error, whilelib/address-ownership.ts:enrichAddressresolves owner and administrator through the same mode-routed modules and works fine.Fix
lib/address-ownership.tscalls (getAddressOwnershipDetail,setAddressOwnerByRef,transferAddressOwnerByRef,unassignAddressOwnerByRef,getAddressOwnershipHistoryByRef). They are mode-routed, so both local and self_hosted work, and the output shape matches what the commands returned before the guards were introduced — scripts reading--jsondo not silently break.Address not found,A human-owned address requires an agent administrator), instead of a mode refusal.enrichAddressesinto the list projection:owner,administratorandprovider_nameare real, and the Owner column showsowner[:admin].--verbosenow does what its help text promises (expanded owner/admin/quota) instead of being accepted and ignored. The quota field shows the configured limit only — never a client-side "used" count, because the daily send ledger is server-owned andcountSendsTodayByAddressreturns zeros in self_hosted.address provisionserver-only — the provisioning orchestration genuinely has no client-side equivalent — and rewrite the comment so it claims only that.suggestAddressLocalPartscopy inaddress.tsin favour of thelib/address-ownership.tsone.Every user-facing string touched here was verified true afterwards. No misleading message replaces another.
Tests
New
/v1-stub coverage (realcurltransport, out-of-process stub — no module mocks):src/cli/commands/address.test.ts— all five ownership subcommands end to end (assign persistsowner_id/administrator_idon the/v1address; transfer writes an audit event with its reason; unassign clears both columns; history is newest-first and honest when empty; a human owner without an agent administrator is refused), plus the list projection in table,--verboseand--json.src/cli/commands/address.self-hosted.test.ts—address owneranswers from/v1;addressessurfaces the recorded owner. Onlyaddress provisionis still asserted to block.src/mcp/address-ownership-self-hosted.test.ts(new) — the five MCP tools against the stub, including "reports a real not-found error rather than a mode refusal".src/mcp/domain-address-self-hosted.test.ts— the five ownership tools removed from the local-only blocked list.Red before / green after (verified by stashing only the two source files):
src/mcp/address-ownership-self-hosted.test.ts→0 pass, 6 failbefore,6 passafter.address list reports real ownership→2 failbefore (receivedowner: null, and--verbosefell through to the compact table), pass after.addresses surfaces the owner recorded on the /v1 record→1 failbefore, pass after.Full suite:
bun test→ 2057 pass, 85 skip, 0 fail (2142 tests, 205 files).bunx tsc --noEmit→ clean.Notes for the reviewer
Adjacent findings left out of this PR to keep it to one cluster — same false-reason class, different surfaces:
suggest_address,remove_address,suspend_address,activate_address,set_address_quotaare still blocked in MCP as "local rows", but their CLI equivalents (address suggest/remove/suspend/activate/quota) all work inself_hostedover/v1.src/cli/commands/address.tsaddress addcomment still says "the /v1 API exposes no /v1/providers", butresources.ts:102servesprovidersandsrc/db/providers.remote.tsreads it.countSendsToday/countSendsTodayByAddressreturn a hardcoded0in self_hosted; any surface that renders "used/limit" from them would be lying. That is why this PR shows the limit only.Rebased on
origin/main@1ac2d15. Not merging myself.Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.