import * as v from "@atcute/lexicons/validations"; import type { Did, Handle } from "@atcute/lexicons/syntax"; import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; import { buildUrl, toResponseError } from "$lib/api/_request"; import type { QueryValue, XrpcRequestInit } from "$lib/api/client"; export const CREATE_DELEGATE_NSID = "farm.tranquil.delegation.createAccount"; export const LIST_DELEGATES_NSID = "farm.tranquil.delegation.listControlledAccounts"; export const LIST_DELEGATES_ENDPOINT = "_delegation.listControlledAccounts"; export const CREATE_DELEGATE_ENDPOINT = "_delegation.createDelegatedAccount"; const delegatedAccountSchema = v.object({ did: v.didString(), grantedAt: v.datetimeString(), grantedScopes: v.string(), handle: v.optional(v.handleString()) }); export const listDelegatesSchema = v.object({ get accounts() { return v.array(delegatedAccountSchema); } }); export type DelegatedAccountInfo = v.InferInput; export interface CreateDelegatedAccountInput { /** Full handle or local handle prefix. */ handle: string; /** Scopes granted to the initial controller. */ controllerScopes: string; email?: string; } export interface CreateDelegatedAccountOutput { did: Did; handle: Handle; } // agent.handle resolves a relative path against the session's PDS, so only // the path and query of the built URL are kept; the origin is a placeholder. const pathFor = (nsid: string, params?: Record): string => { const url = buildUrl("http://pds.invalid", nsid, params); return `${url.pathname}${url.search}`; }; const tranquilGet = async ( agent: OAuthUserAgent, nsid: string, params?: Record, init?: XrpcRequestInit ): Promise => { const headers = new Headers(init?.headers); if (!headers.has("accept")) headers.set("accept", "application/json"); const response = await agent.handle(pathFor(nsid, params), { headers, signal: init?.signal }); if (!response.ok) throw await toResponseError(response); return (await response.json()) as T; }; const tranquilPost = async ( agent: OAuthUserAgent, nsid: string, body?: unknown, init?: XrpcRequestInit ): Promise => { const headers = new Headers(init?.headers); if (!headers.has("content-type")) headers.set("content-type", "application/json"); if (!headers.has("accept")) headers.set("accept", "application/json"); const response = await agent.handle(pathFor(nsid), { method: "POST", headers, body: body === undefined ? undefined : JSON.stringify(body), signal: init?.signal }); if (!response.ok) throw await toResponseError(response); return (await response.json()) as T; }; // ---- shared types, mirrored from tranquil-pds's delegation api ---- export type DelegationScope = string; export interface ScopePreset { name: string; label: string; description: string; scopes: DelegationScope; } export interface ControllerInfo { did: Did; handle?: Handle; grantedScopes: DelegationScope; grantedAt: string; isActive: boolean; isLocal: boolean; } export type DelegationActionType = | "GrantCreated" | "GrantRevoked" | "ScopesModified" | "TokenIssued" | "RepoWrite" | "BlobUpload" | "AccountAction"; export interface AuditLogEntry { id: string; delegatedDid: Did; actorDid: Did; controllerDid?: Did; actionType: DelegationActionType; actionDetails?: unknown; createdAt: string; } export interface ResolvedIdentity { did: Did; handle?: Handle; pdsUrl?: string; isLocal: boolean; } // ---- controllers on *this* account ---- export const listControllers = ( agent: OAuthUserAgent, init?: XrpcRequestInit ): Promise => tranquilGet<{ controllers: ControllerInfo[] }>( agent, "_delegation.listControllers", undefined, init ).then((r) => r.controllers); export const addController = ( agent: OAuthUserAgent, input: { controllerDid: Did; grantedScopes: DelegationScope }, init?: XrpcRequestInit ): Promise<{ success: boolean }> => tranquilPost( agent, "_delegation.addController", { controller_did: input.controllerDid, granted_scopes: input.grantedScopes }, init ); export const removeController = ( agent: OAuthUserAgent, controllerDid: Did, init?: XrpcRequestInit ): Promise<{ success: boolean }> => tranquilPost(agent, "_delegation.removeController", { controller_did: controllerDid }, init); export const updateControllerScopes = ( agent: OAuthUserAgent, input: { controllerDid: Did; grantedScopes: DelegationScope }, init?: XrpcRequestInit ): Promise<{ success: boolean }> => tranquilPost( agent, "_delegation.updateControllerScopes", { controller_did: input.controllerDid, granted_scopes: input.grantedScopes }, init ); // ---- accounts this account controls ---- export const listControlledAccounts = ( agent: OAuthUserAgent, init?: XrpcRequestInit ): Promise => tranquilGet<{ accounts: DelegatedAccountInfo[] }>( agent, LIST_DELEGATES_ENDPOINT, undefined, init ).then((r) => r.accounts); export const createDelegatedAccount = ( agent: OAuthUserAgent, input: CreateDelegatedAccountInput, init?: XrpcRequestInit ): Promise => tranquilPost(agent, CREATE_DELEGATE_ENDPOINT, input, init); // ---- audit log, scope presets, identity resolution ---- export const getAuditLog = ( agent: OAuthUserAgent, params?: { limit?: number; offset?: number }, init?: XrpcRequestInit ): Promise<{ entries: AuditLogEntry[]; total: number }> => tranquilGet(agent, "_delegation.getAuditLog", params as Record, init); export const getScopePresets = ( agent: OAuthUserAgent, init?: XrpcRequestInit ): Promise => tranquilGet<{ presets: ScopePreset[] }>( agent, "_delegation.getScopePresets", undefined, init ).then((r) => r.presets); export const resolveController = ( agent: OAuthUserAgent, identifier: string, init?: XrpcRequestInit ): Promise => tranquilGet(agent, "_delegation.resolveController", { identifier }, init);