From cb4cd53acfd99cca962a6ef4d8ffb33a7120628f Mon Sep 17 00:00:00 2001 From: eti Date: Mon, 26 Oct 2026 20:53:25 +0100 Subject: [PATCH] web/api: update the account handle on the pds Signed-off-by: eti --- web/.storybook/MockAuth.svelte | 5 +++++ web/src/lib/api/handle.ts | 35 ++++++++++++++++++++++++++++++++++ web/src/lib/auth.svelte.ts | 16 ++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 web/src/lib/api/handle.ts diff --git a/web/.storybook/MockAuth.svelte b/web/.storybook/MockAuth.svelte index 2c5aa823c..e1a38ebc9 100644 --- a/web/.storybook/MockAuth.svelte +++ b/web/.storybook/MockAuth.svelte @@ -78,6 +78,11 @@ return knownAccounts; }, hasAccount: (did: string) => knownAccounts.some((account) => account.did === did), + adoptHandle: (handle: string) => { + knownAccounts = knownAccounts.map((account, index) => + index === 0 ? { ...account, handle } : account + ); + }, agentFor: (did: string) => { if (!expired.includes(did)) return Promise.resolve(agent); const handle = knownAccounts.find((account) => account.did === did)?.handle ?? did; diff --git a/web/src/lib/api/handle.ts b/web/src/lib/api/handle.ts new file mode 100644 index 000000000..b1f687244 --- /dev/null +++ b/web/src/lib/api/handle.ts @@ -0,0 +1,35 @@ +import { ClientResponseError, ok } from "@atcute/client"; +import { mainSchema as describeServerSchema } from "@atcute/atproto/types/server/describeServer"; +import { mainSchema as updateHandleSchema } from "@atcute/atproto/types/identity/updateHandle"; +import type { Handle } from "@atcute/lexicons/syntax"; +import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; +import { createClient } from "#lib/auth/agent.ts"; +import type { Permission } from "#lib/auth/scopes.ts"; +import { clear } from "#lib/api/cache.ts"; +import { sentence } from "#lib/format.ts"; + +export const HANDLE_PERMISSION: Permission = { resource: "identity", attr: "handle" }; + +export const handleDomains = async (agent: OAuthUserAgent): Promise => { + const { availableUserDomains } = await ok(createClient(agent).call(describeServerSchema, {})); + return availableUserDomains; +}; + +const updateHandleError = (cause: ClientResponseError): string => { + if (cause.error === "HandleNotAvailable") return "This handle is taken. Choose another one."; + if (cause.error === "InvalidHandle") return "This handle isn't allowed. Choose another one."; + if (cause.description) return sentence(cause.description); + return "Could not change your handle. Try again."; +}; + +export const updateHandle = async (agent: OAuthUserAgent, handle: Handle): Promise => { + try { + await ok(createClient(agent).call(updateHandleSchema, { input: { handle } })); + } catch (cause) { + if (cause instanceof ClientResponseError) { + throw new Error(updateHandleError(cause), { cause }); + } + throw cause; + } + clear(); +}; diff --git a/web/src/lib/auth.svelte.ts b/web/src/lib/auth.svelte.ts index 2ccb5c827..0f8d8a513 100644 --- a/web/src/lib/auth.svelte.ts +++ b/web/src/lib/auth.svelte.ts @@ -111,6 +111,7 @@ export interface Auth { hasAccount(did: string): boolean; bobbinUrl: string; refresh(): Promise; + adoptHandle(handle: string): void; signIn(identifier: string, returnTo?: string): Promise; addAccount(identifier: string, returnTo?: string): Promise; startAuthorization(serviceUrl: string, returnTo?: string): Promise; @@ -305,6 +306,20 @@ export const createAuth = ( persistActive(did, nextProfile.handle); }; + const adoptHandle = (handle: string) => { + if (state.kind !== "authenticated") return; + const did = state.profile.did; + state = { ...state, profile: { ...state.profile, handle } }; + const meta = upsertAccount(loadAccounts(), { + did, + handle, + addedAt: Math.floor(Date.now() / 1000) + }); + saveAccounts(meta); + accounts = reconcileAccounts(listStoredSessions(), meta, loadOrgDids()); + persistActive(did, handle); + }; + const adoptSession = (session: OAuthSession) => { const nextAgent = new OAuthUserAgent(session); const did = nextAgent.sub as Did; @@ -633,6 +648,7 @@ export const createAuth = ( hasAccount: (did: string) => accounts.some((account) => account.did === did), agentFor, refresh, + adoptHandle, signIn, addAccount: signIn, startAuthorization, -- knotmirror 0.0.1