diff --git a/docker-compose.yml b/docker-compose.yml index c4e0366c..b6106fb7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -712,6 +712,7 @@ services: PRIMARY_SPINDLE: spindle.tngl.boltless.dev DELEGATION_URL: https://delegates.tngl.boltless.dev DELEGATION_DID: did:web:tranquil.tngl.boltless.dev + DELEGATION_HANDLE_DOMAIN: tranquil.tngl.boltless.dev CAMO_URL: https://camo.tngl.boltless.dev CAMO_SHARED_SECRET: localinfra-camo-secret AVATAR_URL: https://avatar.tngl.boltless.dev diff --git a/web/src/app.d.ts b/web/src/app.d.ts index e4fedd74..8a45af84 100644 --- a/web/src/app.d.ts +++ b/web/src/app.d.ts @@ -16,6 +16,7 @@ declare global { deliberiUrl: string; delegationUrl: string; delegationDid: string; + delegationHandleDomain: string; sitesDomain: string; moderationServiceDid: string; primaryKnotHostname: string; diff --git a/web/src/lib/api/delegation.ts b/web/src/lib/api/delegation.ts index 9dfcc980..a1893504 100644 --- a/web/src/lib/api/delegation.ts +++ b/web/src/lib/api/delegation.ts @@ -26,6 +26,7 @@ export const GATE_CREATE_DELEGATE_NSID = "sh.tangled.delegation.createAccount"; export interface DelegationService { serviceUrl: string; serviceDid?: string; + handleDomain?: string; fetch?: typeof globalThis.fetch; } diff --git a/web/src/lib/api/orgCreate.test.ts b/web/src/lib/api/orgCreate.test.ts index 46eac064..6f78dc7f 100644 --- a/web/src/lib/api/orgCreate.test.ts +++ b/web/src/lib/api/orgCreate.test.ts @@ -3,12 +3,7 @@ import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { getOrCreateDelegatedSession } from "$lib/api/actAs"; import { provisionDelegate } from "$lib/api/delegation"; -import { - createOrg, - createOrgAccount, - ORG_HANDLE_SUFFIX, - ORG_PROFILE_MARKER -} from "$lib/api/orgCreate"; +import { createOrg, createOrgAccount, ORG_PROFILE_MARKER } from "$lib/api/orgCreate"; import { ensureProfile } from "$lib/api/profile"; vi.mock("$lib/api/delegation", () => ({ provisionDelegate: vi.fn() })); @@ -18,13 +13,13 @@ vi.mock("$lib/api/profile", () => ({ ensureProfile: vi.fn() })); -const service = { serviceUrl: "https://gate.example" }; +const service = { serviceUrl: "https://gate.example", handleDomain: "orgs.example" }; const controllerDid = "did:plc:controller" as Did; const controller = { sub: controllerDid } as OAuthUserAgent; const orgAgent = {} as OAuthUserAgent; const orgDid = "did:plc:org" as Did; const orgLabel = "acme"; -const fullHandle = `${orgLabel}${ORG_HANDLE_SUFFIX}`; +const fullHandle = `${orgLabel}.orgs.example`; beforeEach(() => { vi.resetAllMocks(); @@ -49,6 +44,13 @@ describe("createOrgAccount", () => { expect(provisionDelegate).not.toHaveBeenCalled(); }); + it("refuses to guess a handle domain the deployment does not set", async () => { + await expect( + createOrgAccount(controller, orgLabel, { serviceUrl: "https://gate.example" }) + ).rejects.toThrow(/not configured/); + expect(provisionDelegate).not.toHaveBeenCalled(); + }); + it("rejects a name carrying its own domain", async () => { await expect(createOrgAccount(controller, "acme.owner.example", service)).rejects.toThrow( /name/ diff --git a/web/src/lib/api/orgCreate.ts b/web/src/lib/api/orgCreate.ts index b23887d7..2541a33b 100644 --- a/web/src/lib/api/orgCreate.ts +++ b/web/src/lib/api/orgCreate.ts @@ -6,8 +6,6 @@ import { ensureProfile, PROFILE_COLLECTION } from "$lib/api/profile"; import type { ProfileRecord } from "$lib/api/records"; import { isValidSubdomain, SUBDOMAIN_MAX_LENGTH, SUBDOMAIN_MIN_LENGTH } from "$lib/subdomain"; -export const ORG_HANDLE_SUFFIX = ".tranquil.tngl.boltless.dev"; - export const ORG_PROFILE_MARKER: ProfileRecord = { $type: PROFILE_COLLECTION, bluesky: false, @@ -23,7 +21,8 @@ export interface CreatedOrg extends OrgAccount { warnings: string[]; } -const validateOrgHandle = (raw: string): string => { +const validateOrgHandle = (raw: string, handleDomain: string | undefined): string => { + if (!handleDomain) throw new Error("Organization creation is not configured."); const label = raw.trim().toLowerCase(); if (label.length < SUBDOMAIN_MIN_LENGTH || label.length > SUBDOMAIN_MAX_LENGTH) { throw new Error( @@ -35,7 +34,7 @@ const validateOrgHandle = (raw: string): string => { "Organization name can only contain lowercase letters, digits, and hyphens, and cannot start or end with a hyphen." ); } - return `${label}${ORG_HANDLE_SUFFIX}`; + return `${label}.${handleDomain}`; }; export const createOrgAccount = async ( @@ -43,7 +42,7 @@ export const createOrgAccount = async ( rawHandle: string, service: DelegationService ): Promise => { - const handle = validateOrgHandle(rawHandle); + const handle = validateOrgHandle(rawHandle, service.handleDomain); const account = await provisionDelegate(agent, handle, service); return { did: account.did, handle: account.handle }; diff --git a/web/src/lib/components/organization/OrganizationCreationForm.svelte b/web/src/lib/components/organization/OrganizationCreationForm.svelte index b92047e1..e12d74c0 100644 --- a/web/src/lib/components/organization/OrganizationCreationForm.svelte +++ b/web/src/lib/components/organization/OrganizationCreationForm.svelte @@ -8,7 +8,7 @@ import X from "$icon/x"; import { createAction } from "$lib/action.svelte"; import { getAuth } from "$lib/auth.svelte"; - import { createOrg, ORG_HANDLE_SUFFIX, type CreatedOrg } from "$lib/api/orgCreate"; + import { createOrg, type CreatedOrg } from "$lib/api/orgCreate"; import { isValidSubdomain } from "$lib/subdomain"; import AccountSelector from "$lib/components/auth/AccountSelector.svelte"; import Button from "$lib/components/ui/Button.svelte"; @@ -56,7 +56,8 @@ const delegation = $derived({ serviceUrl: (page.data.publicConfig?.delegationUrl as string | undefined) ?? "", - serviceDid: page.data.publicConfig?.delegationDid as string | undefined + serviceDid: page.data.publicConfig?.delegationDid as string | undefined, + handleDomain: page.data.publicConfig?.delegationHandleDomain as string | undefined }); let created = $state(); @@ -98,14 +99,16 @@ disabled={action.loading || created !== undefined} class="min-w-0 flex-1 rounded-none border-0 bg-transparent focus-within:ring-0" /> - - - + {#if delegation.handleDomain} + + + + {/if} {/snippet} diff --git a/web/src/lib/server/config.ts b/web/src/lib/server/config.ts index be40ed6a..12b9cf1c 100644 --- a/web/src/lib/server/config.ts +++ b/web/src/lib/server/config.ts @@ -29,6 +29,7 @@ export type WebConfig = { deliberiUrl: string; delegationUrl: string; delegationDid: string; + delegationHandleDomain: string; /** the domain user sites are served under, e.g. "tngl.io" */ sitesDomain: string; camoUrl: string; @@ -59,6 +60,7 @@ export type PublicWebConfig = Pick< | "deliberiUrl" | "delegationUrl" | "delegationDid" + | "delegationHandleDomain" | "sitesDomain" | "moderationServiceDid" | "primaryKnotHostname" @@ -80,6 +82,7 @@ type WebConfigEnv = { DELIBERI_URL?: string; DELEGATION_URL?: string; DELEGATION_DID?: string; + DELEGATION_HANDLE_DOMAIN?: string; KNOT_RESOLVER_URL?: string; SITES_DOMAIN?: string; CAMO_URL?: string; @@ -107,6 +110,7 @@ export const resolveConfig = (values: WebConfigEnv): WebConfig => ({ deliberiUrl: cleanUrl(values.DELIBERI_URL, "https://notifs-dev.tangled.network"), delegationUrl: cleanUrl(values.DELEGATION_URL, ""), delegationDid: values.DELEGATION_DID?.trim() ?? "", + delegationHandleDomain: values.DELEGATION_HANDLE_DOMAIN?.trim().toLowerCase() ?? "", sitesDomain: values.SITES_DOMAIN?.trim() || "tngl.io", camoUrl: cleanUrl(values.CAMO_URL, "https://camo.tangled.sh"), avatarUrl: cleanUrl(values.AVATAR_URL, "https://avatar.tangled.sh"), @@ -142,6 +146,7 @@ export const getPublicConfig = (platform?: App.Platform): PublicWebConfig => { deliberiUrl: config.deliberiUrl, delegationUrl: config.delegationUrl, delegationDid: config.delegationDid, + delegationHandleDomain: config.delegationHandleDomain, sitesDomain: config.sitesDomain, moderationServiceDid: config.moderationServiceDid, primaryKnotHostname: config.primaryKnotHostname,