import { describe, expect, it, vi } from "vitest"; import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; import { provisionDelegate, CREATE_DELEGATE_NSID, GATE_CREATE_DELEGATE_NSID, LIST_DELEGATES_NSID } from "#lib/api/delegation.ts"; import metadata from "#lib/oauth-client-metadata.ts"; import type { Fetch } from "#lib/fetch.ts"; const makeAgent = () => { const handle = vi.fn<(path: string, init: RequestInit) => Promise>(async () => Response.json({ token: "service-jwt" }) ); return { handle, agent: { sub: "did:plc:alice", handle } as unknown as OAuthUserAgent }; }; const account = { did: "did:plc:org", handle: "org.example", controllerDid: "did:plc:alice" }; describe("delegate provisioning", () => { it("mints one method-bound service auth on the user's PDS for the create call", async () => { const { agent, handle } = makeAgent(); const fetch = vi.fn(async () => Response.json(account)); await expect( provisionDelegate(agent, "org.example", { serviceUrl: "https://gate.example/", serviceDid: "did:web:tranquil.example", fetch }) ).resolves.toEqual(account); const params = new URL(String(handle.mock.calls[0][0]), "https://pds.example").searchParams; expect(params.get("aud")).toBe("did:web:tranquil.example"); expect(params.get("lxm")).toBe(CREATE_DELEGATE_NSID); // Tranquil enforces the delegate cap itself, so no listing token is minted. expect(handle).toHaveBeenCalledTimes(1); expect(String(fetch.mock.calls[0][0])).toBe( `https://gate.example/xrpc/${GATE_CREATE_DELEGATE_NSID}` ); expect(fetch.mock.calls[0][1]).toMatchObject({ method: "POST", headers: { authorization: "Bearer service-jwt" }, body: JSON.stringify({ handle: "org.example" }) }); }); it("allows the advertised DID to differ from a forwarded URL port", async () => { const { agent, handle } = makeAgent(); await provisionDelegate(agent, "org.example", { serviceUrl: "https://gate.example:8443", serviceDid: "did:web:tranquil.example", fetch: vi.fn(async () => Response.json(account)) }); expect( new URL(String(handle.mock.calls[0][0]), "https://pds.example").searchParams.get("aud") ).toBe("did:web:tranquil.example"); }); it.each([ [ "VerifiedEmailRequired", 403, "Verify an email address in Settings → Emails before creating an organization." ], [ "UpstreamUnavailable", 502, "Organization creation is temporarily unavailable. Try again in a few minutes." ], ["HandleNotAvailable", 400, "This organization name is taken. Choose another one."], ["InvalidHandle", 400, "This organization name isn't allowed. Choose another one."] ])("explains %s", async (error, status, message) => { const { agent } = makeAgent(); await expect( provisionDelegate(agent, "org.example", { serviceUrl: "https://gate.example", serviceDid: "did:web:tranquil.example", fetch: vi.fn(async () => Response.json({ error, message: "Handle already taken" }, { status }) ) }) ).rejects.toThrow(message); }); it("falls back to a plain sentence when an unknown error has no message", async () => { const { agent } = makeAgent(); const error = provisionDelegate(agent, "org.example", { serviceUrl: "https://gate.example", serviceDid: "did:web:tranquil.example", fetch: vi.fn(async () => Response.json({ error: "RateLimitExceeded" }, { status: 429 })) }); await expect(error).rejects.toThrow("Unable to create the organization. Try again."); await expect(error).rejects.not.toThrow(/unspecified description/); }); it.each([ [401, "Sign in again to grant Tangled permission to create organizations."], [403, "Sign in again to grant Tangled permission to create organizations."], [500, "Unable to confirm your account with your PDS. Try again."] ])("explains a %i from the user's PDS while minting service auth", async (status, message) => { const { agent, handle } = makeAgent(); handle.mockResolvedValue(Response.json({ error: "Whatever" }, { status })); await expect( provisionDelegate(agent, "org.example", { serviceUrl: "https://gate.example", serviceDid: "did:web:tranquil.example", fetch: vi.fn() }) ).rejects.toThrow(message); }); it("shows Tranquil's reason for InvalidDelegation, which is not only the delegate cap", async () => { const { agent } = makeAgent(); const message = "Invite-only registration requires an existing account on this PDS"; await expect( provisionDelegate(agent, "org.example", { serviceUrl: "https://gate.example", serviceDid: "did:web:tranquil.example", fetch: vi.fn(async () => Response.json({ error: "InvalidDelegation", message }, { status: 400 }) ) }) ).rejects.toThrow(message); }); it("does not request a token when the sidecar is not configured", async () => { const { agent, handle } = makeAgent(); await expect(provisionDelegate(agent, "org.example", { serviceUrl: "" })).rejects.toThrow( "Organization creation isn't set up on this server." ); expect(handle).not.toHaveBeenCalled(); }); it("rejects an unexpected controller", async () => { const { agent } = makeAgent(); await expect( provisionDelegate(agent, "org.example", { serviceUrl: "https://gate.example", serviceDid: "did:web:tranquil.example", fetch: vi.fn(async () => Response.json({ ...account, controllerDid: "did:plc:other" }) ) }) ).rejects.toThrow(/different account than the one you chose/); }); it("requests the permission needed to mint delegation service auth", () => { expect(metadata.scope.split(" ")).toContain(`rpc:${CREATE_DELEGATE_NSID}?aud=*`); expect(metadata.scope.split(" ")).toContain(`rpc:${LIST_DELEGATES_NSID}?aud=*`); }); }); it("asks older sessions to reauthorize before any account is created", async () => { const handle = vi.fn(async () => Response.json({ error: "InsufficientScope", message: "missing scope" }, { status: 403 }) ); const fetch = vi.fn(); await expect( provisionDelegate( { handle, sub: "did:plc:alice" } as unknown as OAuthUserAgent, "org.example", { serviceUrl: "https://gate.example", serviceDid: "did:web:tranquil.example", fetch } ) ).rejects.toThrow(/Sign in again/); expect(fetch).not.toHaveBeenCalled(); }); describe("controlled organization accounts", () => { it("lists on Tranquil with method-bound auth from the controller's PDS", async () => { const { listDelegatedAccounts } = await import("./delegation"); const { agent, handle } = makeAgent(); const accounts = [ { did: "did:plc:org", handle: "org.example", grantedScopes: "atproto repo:*", grantedAt: "2026-09-09T00:00:00Z" } ]; const fetch = vi.fn(async () => Response.json({ accounts })); await expect( listDelegatedAccounts(agent, "did:web:tranquil.example", fetch) ).resolves.toEqual(accounts); const params = new URL(String(handle.mock.calls[0][0]), "https://pds.example").searchParams; expect(params.get("aud")).toBe("did:web:tranquil.example"); expect(params.get("lxm")).toBe(LIST_DELEGATES_NSID); expect(String(fetch.mock.calls[0][0])).toBe( "https://tranquil.example/xrpc/_delegation.listControlledAccounts" ); expect(fetch.mock.calls[0][1]).toEqual({ headers: { authorization: "Bearer service-jwt" } }); }); it("reports a failed listing instead of treating it as no organizations", async () => { const { listDelegatedAccounts } = await import("./delegation"); const { agent } = makeAgent(); await expect( listDelegatedAccounts(agent, "did:web:tranquil.example", async () => Response.json({ error: "InvalidToken" }, { status: 401 }) ) ).rejects.toThrow(); }); });