MCPcopy Create free account
hub / github.com/Mnexa-AI/e2a / ClaimOrCreateDomain

Method ClaimOrCreateDomain

internal/identity/store.go:480–554  ·  view source on GitHub ↗

ClaimOrCreateDomain implements the atomic create/claim logic from the design doc. Creates if new, returns the existing row when the same user already owns it (verified or not), and errors if a different user owns it. The verification_token and DKIM keypair are minted on first INSERT and remain stabl

(ctx context.Context, domain, userID string)

Source from the content-addressed store, hash-verified

478// domain before invoking this — the store has no concept of a reserved
479// domain.
480func (s *Store) ClaimOrCreateDomain(ctx context.Context, domain, userID string) (*Domain, error) {
481 domain = normalizeDomain(domain)
482
483 verificationToken := "e2a-verify=" + generateID()
484
485 // Generate a DKIM keypair for this domain. Failures here are
486 // non-fatal — the columns are nullable and the outbound signer
487 // treats a missing key as "skip DKIM". We still log because key gen
488 // failing is a hard signal (entropy exhaustion or an OS-level
489 // CSPRNG bug) that ops should see.
490 var dkimSelector string
491 var dkimPubKey string
492 var dkimPrivKey []byte
493 if kp, kerr := dkim.GenerateKeypair(); kerr == nil {
494 // Encrypt the private key at rest (#144). On seal failure (catastrophic
495 // RNG) drop ALL three DKIM columns so we never publish a public key /
496 // selector without a usable private key — non-fatal, same posture as a
497 // keygen failure (the signer treats a missing key as "skip DKIM").
498 sealed, serr := s.sealDKIM(kp.PrivateKeyDER, domain)
499 if serr != nil {
500 log.Printf("[identity] dkim key seal failed for %s: %v", domain, serr)
501 } else {
502 dkimSelector = kp.Selector
503 dkimPubKey = kp.PublicKeyDNS
504 dkimPrivKey = sealed
505 }
506 } else {
507 log.Printf("[identity] dkim keygen failed for %s: %v", domain, kerr)
508 }
509
510 // Atomic upsert. The conflict branch only fires for a same-user
511 // re-claim of an unverified row, and runs as a no-op SET so
512 // RETURNING surfaces the existing row. DKIM columns and the
513 // verification_token are only written on a true INSERT, so they
514 // stay stable across re-claims — DKIM stability avoids
515 // invalidating signatures on mail in flight, and token stability
516 // means a caller who already published the TXT record on DNS
517 // isn't silently invalidated. A different-user conflict falls
518 // through to the SELECT below and returns "domain not available",
519 // preventing squatting on an unverified row whose TXT record the
520 // original owner may have already published.
521 d := &Domain{}
522 err := s.pool.QueryRow(ctx,
523 `INSERT INTO domains (domain, user_id, verified, verification_token, dkim_selector, dkim_public_key, dkim_private_key)
524 VALUES ($1, $2, false, $3, $4, $5, $6)
525 ON CONFLICT (domain) DO UPDATE
526 SET user_id = domains.user_id
527 WHERE domains.verified = false AND domains.user_id = $2
528 RETURNING domain, user_id, verified, verification_token, created_at, verified_at, is_primary, last_checked_at, COALESCE(dkim_selector, ''), COALESCE(dkim_public_key, ''), sending_status, COALESCE(sending_error, ''), sending_dns_records, sending_last_checked_at, COALESCE(sending_dkim_status, ''), COALESCE(sending_mail_from_status, '')`,
529 domain, userID, verificationToken, nullIfEmpty(dkimSelector), nullIfEmpty(dkimPubKey), nullIfEmptyBytes(dkimPrivKey),
530 ).Scan(&d.Domain, &d.UserID, &d.Verified, &d.VerificationToken, &d.CreatedAt, &d.VerifiedAt, &d.IsPrimary, &d.LastCheckedAt, &d.DKIMSelector, &d.DKIMPublicKey, &d.SendingStatus, &d.SendingError, &d.SendingDNSRecordsJSON, &d.SendingLastCheckedAt, &d.SendingDkimStatus, &d.SendingMailFromStatus)
531
532 if err == nil {
533 return d, nil
534 }
535
536 // No row returned — the row exists but the conflict UPDATE was
537 // skipped because either it's already verified or a different user

Implementers 1

Storeinternal/identity/store.go

Calls 8

sealDKIMMethod · 0.95
GenerateKeypairFunction · 0.92
normalizeDomainFunction · 0.85
generateIDFunction · 0.85
nullIfEmptyFunction · 0.85
nullIfEmptyBytesFunction · 0.85
ScanMethod · 0.80
QueryRowMethod · 0.80