-
Notifications
You must be signed in to change notification settings - Fork 24
[codex] expose wallet email verification in SDK #26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
936448e
c9e4c87
2df2d3c
10dcac8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,12 @@ import type { SigningKey } from "../auth.js"; | |
| import { signFreshCanonicalPayload } from "../auth.js"; | ||
| import { canonicalPayload } from "../crypto.js"; | ||
| import type { HttpClient } from "../http.js"; | ||
| import type { User, UserProfileUpdate } from "../types/index.js"; | ||
| import type { | ||
| User, | ||
| UserEmailVerificationConfirmRequest, | ||
| UserEmailVerificationRequest, | ||
| UserProfileUpdate, | ||
| } from "../types/index.js"; | ||
|
|
||
| /** | ||
| * UsersApi reads and writes the per-wallet User profile — the single source of | ||
|
|
@@ -14,6 +19,7 @@ export class UsersApi { | |
| constructor( | ||
| private readonly http: HttpClient, | ||
| private readonly signingKey?: SigningKey, | ||
| private readonly harnessKey?: string, | ||
| ) {} | ||
|
|
||
| /** Fetch a wallet's profile by its cryptoId. */ | ||
|
|
@@ -30,6 +36,7 @@ export class UsersApi { | |
| cryptoId: string, | ||
| update: UserProfileUpdate, | ||
| ): Promise<User> { | ||
| update = withDefaultHarnessKey(update, this.harnessKey); | ||
| if (this.signingKey && !update.signature) { | ||
| const payload = userProfileSignaturePayload(cryptoId, update); | ||
| update = { | ||
|
|
@@ -42,6 +49,51 @@ export class UsersApi { | |
| update, | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Start email verification for a wallet. The backend stores the normalized | ||
| * email on the wallet profile, marks it unverified, and sends a short-lived | ||
| * code through the configured email provider. | ||
| */ | ||
| async startEmailVerification( | ||
| cryptoId: string, | ||
| request: UserEmailVerificationRequest, | ||
| ): Promise<User> { | ||
| request = withDefaultHarnessKey(request, this.harnessKey); | ||
| if (this.signingKey && !request.signature) { | ||
| const payload = userEmailStartSignaturePayload(cryptoId, request); | ||
| request = { | ||
| ...request, | ||
| signature: await signFreshCanonicalPayload(this.signingKey, payload), | ||
| }; | ||
| } | ||
| return this.http.postDirectoryAuth<User>( | ||
| `/users/${encodeURIComponent(cryptoId)}/email/verification`, | ||
| request, | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Confirm a wallet email verification code. Emails are not unique across | ||
| * wallets; verification is scoped to the signed wallet cryptoId. | ||
| */ | ||
| async confirmEmailVerification( | ||
| cryptoId: string, | ||
| request: UserEmailVerificationConfirmRequest, | ||
| ): Promise<User> { | ||
| request = withDefaultHarnessKey(request, this.harnessKey); | ||
| if (this.signingKey && !request.signature) { | ||
| const payload = userEmailConfirmSignaturePayload(cryptoId, request); | ||
| request = { | ||
| ...request, | ||
| signature: await signFreshCanonicalPayload(this.signingKey, payload), | ||
| }; | ||
| } | ||
| return this.http.postDirectoryAuth<User>( | ||
| `/users/${encodeURIComponent(cryptoId)}/email/verification/confirm`, | ||
| request, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -60,7 +112,41 @@ function userProfileSignaturePayload( | |
| bio: update.bio ?? null, | ||
| cryptoId, | ||
| displayName: update.displayName ?? null, | ||
| harnessKey: update.harnessKey ?? null, | ||
| link: update.link ?? null, | ||
| tags: update.tags ?? null, | ||
| }); | ||
| } | ||
|
|
||
| function userEmailStartSignaturePayload( | ||
| cryptoId: string, | ||
| request: UserEmailVerificationRequest, | ||
| ): string { | ||
| return canonicalPayload("user.email.start", { | ||
| cryptoId, | ||
| email: request.email, | ||
| harnessKey: request.harnessKey ?? null, | ||
| }); | ||
| } | ||
|
|
||
| function userEmailConfirmSignaturePayload( | ||
| cryptoId: string, | ||
| request: UserEmailVerificationConfirmRequest, | ||
| ): string { | ||
| return canonicalPayload("user.email.confirm", { | ||
| code: request.code, | ||
| cryptoId, | ||
| email: request.email, | ||
| harnessKey: request.harnessKey ?? null, | ||
| }); | ||
| } | ||
|
|
||
| function withDefaultHarnessKey<T extends { harnessKey?: string }>( | ||
| value: T, | ||
| harnessKey: string | undefined, | ||
| ): T { | ||
| if (value.harnessKey || !harnessKey) { | ||
| return value; | ||
| } | ||
| return { ...value, harnessKey }; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When callers pass a precomputed Useful? React with 👍 / 👎. |
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the paid registration path,
registry.registerhas already returned success and the x402 payment has been accepted before this extra profile update runs. If/users/{id}/profileis unavailable or rejects the harness update,buyDomainnow throws even though the handle and payment succeeded, which can make agents retry/pay again or report a false failure; treat harness recording as best-effort or return the registration result.Useful? React with 👍 / 👎.