feat(auth): signup hook for new-user provisioning notifications - #680
Open
jiashuoz wants to merge 1 commit into
Open
feat(auth): signup hook for new-user provisioning notifications#680jiashuoz wants to merge 1 commit into
jiashuoz wants to merge 1 commit into
Conversation
Fire an HMAC-signed HTTP POST when a brand-new user account is created via Google OAuth signup, so an external service (e.g. a hosted deployment's billing sidecar) can run first-touch provisioning like a welcome email. Mirrors the account-deletion billing hook precisely: same X-E2A-Internal-Signature HMAC-SHA256 scheme over the JSON body, same limits.internal_api_secret, 5s timeout, log-and-continue. - identity: CreateOrGetUserWithCreated surfaces a created signal from the users upsert via RETURNING (xmax = 0); CreateOrGetUser stays a thin wrapper so its many callers are untouched. - config: limits.signup_hook_url (E2A_SIGNUP_HOOK_URL), empty = disabled, symmetric to billing_hook_url. - auth: HandleCallback fires the hook asynchronously (goroutine with its own timeout-bounded context) only when the upsert INSERTed — best-effort, so login latency and success never depend on the hook. Placing the trigger in the OAuth callback (not the store) keeps prober seeds, -bootstrap-email, and test-harness accounts silent. - docs: config.example.yaml + deployment.md entries. - tests: hook fires with correct payload+signature for a new user, stays silent for returning users and when unconfigured, login succeeds with the endpoint down; store-level xmax discriminator regression test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
What
Adds an optional signup hook: when a brand-new user account is created via Google OAuth signup, the server fires an HMAC-signed HTTP POST to a configured URL. This lets an external service — e.g. a hosted deployment's billing sidecar — run first-touch provisioning such as sending a welcome email. Self-host default is off (empty URL = disabled, zero behavior change).
Why
The hosted deployment wants to welcome-email new signups. The server is the only component that knows at the moment of signup whether the OAuth upsert created a new user or matched a returning one, so it exposes that moment as an internal webhook, mirroring the existing account-deletion billing hook.
Design notes
notifyBillingUserDeletedininternal/agent/user_data_rights_api.go): JSON body,X-E2A-Internal-Signature: hex(HMAC-SHA256(internal_api_secret, body)), 5s timeout, expects204 No Content, logs and continues on any failure.CreateOrGetUser'sINSERT … ON CONFLICT DO UPDATEupsert gains a(xmax = 0) AS insertedRETURNING column — Postgres's system columnxmaxis 0 only on a freshly inserted row. Exposed asCreateOrGetUserWithCreatedreturning(user, created, err);CreateOrGetUserstays a thin wrapper so its many existing callers (prober seeds, test harness, dozens of tests) are untouched.HandleCallback, not the store upsert — deliberately, so synthetic accounts (prober seeds,-bootstrap-email, test harnesses) never fire the hook. OIDC login never provisions users, so it's out of scope by construction.context.Background()(the request context is canceled the instant the login response is written). Any failure is logged and dropped.limits.signup_hook_url(yaml) /E2A_SIGNUP_HOOK_URL(env), sitting next tobilling_hook_urland signed with the samelimits.internal_api_secret. Likebilling_hook_url, no extra validation — empty disables.Payload:
{"user_id": "…", "email": "…", "name": "…"}Test coverage
internal/auth/signup_hook_test.go(drivesHandleCallbackthrough the existing fake-Google harness, real test DB):{user_id, email, name}with a matching independently-recomputed HMAC; login still 302s to/dashboardgoogle_subject) → hook stays silentinternal/identity/user_upsert_test.go: xmax discriminator regression —created=trueon first insert,falseon the DO UPDATE path (with profile-field refresh and stable user ID),trueagain for a distinct subject; wrapper contract preserved.internal/identity,internal/auth,internal/config,internal/agent) green against an isolated test DB;make test-unitgreen;go build ./...+go veton changed packages clean.Not a client API change — server-internal config + internal webhook only; SDKs/CLI/MCP untouched.
🤖 Generated with Claude Code