Skip to content

fix: persist self-hosted address provider bindings - #217

Merged
andrei-hasna merged 2 commits into
mainfrom
fix/c3964d-address-provider-binding
Aug 9, 2026
Merged

fix: persist self-hosted address provider bindings#217
andrei-hasna merged 2 commits into
mainfrom
fix/c3964d-address-provider-binding

Conversation

@andrei-hasna

@andrei-hasna andrei-hasna commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Carrier task: c3964d26-5256-4a24-ab5b-24636e261e91
Incident: 690284

Root cause: the self-hosted address client omitted provider_id from POST /v1/addresses and then overlaid it only on the create response; the server schema/store did not persist or return the binding.

Fix: persist nullable provider_id with tenant-scoped provider validation, preserve omitted-provider compatibility, return scoped unknown/cross-tenant 404s, regenerate the self-hosted SDK/contracts, and add CLI/HTTP/store regressions including duplicate idempotency and exact readback.

Validation: focused matrix 71 pass / 0 fail; full Bun build passed; self-hosted SDK type gate passed; no-cloud source 26 pass / 0 fail; packed artifact scan passed. The hermetic wrapper was attempted twice and both runs hit the pre-existing auth stub connection-refused failure after earlier files; auth.test.ts passes standalone 16 pass / 0 fail. No production address was mutated.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Persist address provider_id through the self-hosted API and store, validate tenant scope, and return exact readback.

Carrier task: c3964d26-5256-4a24-ab5b-24636e261e91
Incident: 690284

Agent: quintilianus
@andrei-hasna

Copy link
Copy Markdown
Contributor Author

NO_GO for exact head dd803f18680519af19fb052ce93046fc66c07704 (base 72ed341ec77f12e2b102b821254fb862043589d5, candidate tree 75b6dfea8178ea6d123abc4adbe6a7f8309e6d76).

I found two concrete in-scope P1 blockers material to the supplied lanes:

  1. Required gate failure (lane 6), candidate-caused/reachable. GitHub verify fails on the exact PR merge for the full local suite because this PR appends migration 0025_address_provider_binding but src/server/self-hosted/inbound.test.ts still asserts the last migration is 0024_idp_principal_tenants_multi_grant. Exact local reproduction on the pinned worktree:
Expected: "0024_idp_principal_tenants_multi_grant"
Received: "0025_address_provider_binding"
(fail) Emails self-hosted inbound messages > migration set includes the inbound schema migration [0.36ms]
0 pass
27 filtered out
1 fail

GitHub check evidence on the exact head:

verify	fail	5m11s	https://github.com/hasna/emails/actions/runs/31311119270/job/93238988396
container-runtime	pass	1m46s	https://github.com/hasna/emails/actions/runs/31311119270/job/93238988395
selfhost-postgres	pass	52s	https://github.com/hasna/emails/actions/runs/31311119270/job/93238988363
  1. Provider-selection violation in the self-hosted CLI path (lanes 1 and 4). emails address add <same-email> --provider <new-provider> checks for an existing address by email only in both self-hosted adapters, ignoring the requested provider parameter, so it can silently return the old provider's address and never POST the requested provider binding. This is reachable through the real self-hosted CLI transport/stub path. Exact reproduction output:
requested_provider=provider-two
returned_provider=provider-one
stored_rows=1
stored_providers=provider-one

Supporting code evidence:

src/cli/commands/address.ts:215:        const existing = getAddressByEmail(opts.provider, email);
src/db/addresses.remote.ts:92:export function getAddressByEmail(_provider_id: string, email: string): EmailAddress | null {
src/db/addresses.local.ts:97:export function getAddressByEmail(provider_id: string, email: string, db?: Database): EmailAddress | null {

Additional gate evidence I ran locally on the pinned worktree:

56 pass
0 fail
642 expect() calls
Ran 56 tests across 4 files. [954.00ms]
$ tsc -p tsconfig.selfhost-sdk-type-tests.json
26 pass
0 fail
308 expect() calls
Ran 26 tests across 2 files. [1092.00ms]
$ node scripts/scan-staged-secrets.mjs

Worktree cleanliness after review commands: git status --short produced no output.

Non-blocking note: the direct service/store path for a valid in-tenant provider_id does persist and exact-readback the value, and omitted provider stays legacy-null, but the required full verify gate and the supported CLI provider-selection path above are blockers.

Make self-hosted address lookup and uniqueness provider-aware, and advance the migration ordering gate to 0025.

Remediation: PR #217 review cycle 1

Agent: quintilianus
@andrei-hasna

Copy link
Copy Markdown
Contributor Author

[REVIEW] NO_GO — #217 @ dd803f1 — lens: correctness+security+gates, reviewer unresolved-account002 (1 of 1)

Reviewed the complete origin/main...HEAD diff for all 13 changed files and surrounding CLI, local/remote address stores, self-hosted migration/service/store, OpenAPI/generated contracts, provider-health code, and regression/parity tests. The provider binding path is tenant-scoped, validates provider existence before insertion, persists provider_id, and returns scoped 404s for missing or cross-tenant provider references.

Commands and results:

  • bun install — exit 0; setup only, not a repository gate.
  • Typecheck — no typecheck script is declared; none was invented or run.
  • bun run test — exit 1; 4359 pass, 156 skip, 1 fail; Ran 4516 tests across 291 files.

Blocking P0/P1 findings:

  • P1 required test gate failure: src/server/self-hosted/inbound.test.ts:218 still requires migration 0024_idp_principal_tenants_multi_grant to be last, but this candidate adds 0025_address_provider_binding. The repository gate fails with Expected: "0024_idp_principal_tenants_multi_grant" and Received: "0025_address_provider_binding".

Non-blocking follow-ups: none.

The named remedy is to retain the 0024 membership assertion and update the terminal migration assertion to 0025_address_provider_binding, then rerun the declared full test gate.

@andrei-hasna

Copy link
Copy Markdown
Contributor Author

[REMEDIATION] #217 — migration gate fixed

The blocking migration-order assertion is resolved at current PR head 32297d9ca347ffccef4847ef1dcf4a9a89360bb1: migration 0024_idp_principal_tenants_multi_grant remains asserted as present, and 0025_address_provider_binding is asserted as later and terminal. The remediation also preserves provider-scoped address identity across same-email rows.

Focused re-validation expanded to the full declared gate because the change affects the shared migration contract: bun run test exited 0 with 4362 pass, 157 skip, 0 fail; Ran 4519 tests across 291 files.

@andrei-hasna

Copy link
Copy Markdown
Contributor Author

[REVIEW] GO — #217 @ 32297d9 — lens: correctness+security+gates, reviewer unresolved-account002 (1 of 1)

Focused re-review of the named migration-gate defect, its fix, and direct provider-identity regressions. I read the complete original candidate diff, the full remediation diff, and surrounding CLI, local/remote address lookup, tenant-scoped store, migration, service, OpenAPI/generated contract, provider-health, parity, integration, and CLI regression code.

Commands and results at this exact head:

  • bun install — exit 0; setup only, not a repository gate.
  • Typecheck — no typecheck script is declared; none was invented or run.
  • bun run test — exit 0; 4362 pass, 157 skip, 0 fail; Ran 4519 tests across 291 files.

Blocking P0/P1 findings: none. The previous required-gate failure is fixed, provider bindings are validated within the authenticated tenant, same-email bindings are provider-scoped, and the declared full gate passes.

Non-blocking follow-ups: none.

@andrei-hasna
andrei-hasna merged commit aeb1eb7 into main Aug 9, 2026
4 checks passed
@andrei-hasna
andrei-hasna deleted the fix/c3964d-address-provider-binding branch August 9, 2026 12:15
@andrei-hasna

Copy link
Copy Markdown
Contributor Author

GO for remediation cycle one on exact head 32297d9ca347ffccef4847ef1dcf4a9a89360bb1 (parent/reviewed head dd803f18680519af19fb052ce93046fc66c07704, tree d9b62f26721d591c9fa120a5aa62c2d2c0ad776c).

Focused re-review covered only the two named blockers and direct regressions. Both are fixed, required gates pass, and I found zero concrete reachable in-scope P0/P1 defects in that scope.

Named blocker 1, stale migration assertion: fixed. Migration 0025_address_provider_binding is explicitly ordered after 0024, the stale last-migration assertion now passes, and the migration replaces tenant/email uniqueness with provider-aware uniqueness while retaining a separate legacy-null uniqueness path.

(pass) Emails self-hosted inbound messages > migration set includes the inbound schema migration [0.18ms]
(pass) self-hosted parity: new migrations > 0025 appends provider-aware address uniqueness after 0024 [0.20ms]

Named blocker 2, provider-two substituted by provider-one: fixed on the original self-hosted CLI reproduction.

requested_provider=provider-two
returned_provider=provider-two
stored_rows=2
stored_providers=provider-one,provider-two

Focused local validation:

116 pass
18 skip
0 fail
406 expect() calls
Ran 134 tests across 6 files. [4.00s]

The skipped local tests require Postgres; exact-head Postgres CI executed the direct migration/tenant test:

(pass) self-hosted Postgres integration > 0025 preserves provider-scoped address identity with tenant isolation [1078.64ms]
105 pass
0 fail
Ran 106 tests across 7 files. [27.08s]

All required exact-head CI lanes:

container-runtime	pass	1m18s	https://github.com/hasna/emails/actions/runs/31312209582/job/93241618256
selfhost-postgres	pass	50s	https://github.com/hasna/emails/actions/runs/31312209582/job/93241618258
verify	pass	12m14s	https://github.com/hasna/emails/actions/runs/31312209582/job/93241618242

Direct contract/artifact, attribution, and secret-scan evidence:

openapi_generated_delta=none
author=Andrei Hasna <andrei@hasna.com>
agent=quintilianus
staged_secret_scan_exit=0

The staged scan was non-vacuous: it ran the repository's staged-secret scanner with a temporary index representing exactly dd803f1..32297d9. The review worktree remained clean; git status --short produced no output.

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