From 4ea9ab8acb527b7f031eca7fa05d2bc5c251c2b4 Mon Sep 17 00:00:00 2001 From: Anton Wiklund Date: Sat, 24 Oct 2026 17:05:12 +0200 Subject: [PATCH 1/2] fix(ci): ask for a provider's sign-up page as a browser does npm run check:providers asked every URL for JSON first, the sign-up pages too. The Community Solid Server, which solidcommunity.net and solidweb.me run, serves its "Create account" page only to a request that prefers HTML and answers 404 to one that prefers JSON. A sign-up link there would fail the weekly check although it works in every browser. The discovery document is now asked for as JSON and a sign-up page as HTML, as a newcomer's browser asks for it. Refs: #283 --- docs/testing.md | 3 ++- scripts/checkProviders.test.ts | 12 ++++++++++++ scripts/checkProviders.ts | 17 +++++++++++++---- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/docs/testing.md b/docs/testing.md index 7a031367..d1d882f6 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -897,7 +897,8 @@ these. `npm run check:providers` provider instead. Its issuer must answer `/.well-known/openid-configuration` with JSON whose `issuer` is the entry's `oidcIssuer`, character for character, as the login compares -them. Its `signUpUrl`, where it has one, must answer too. A request that +them. Its `signUpUrl`, where it has one, must answer too, to a request +that asks for HTML as a browser does. A request that fails or answers 5xx is tried three times. The [Links workflow](../.github/workflows/links.yml) runs the check weekly, and by hand, in a job of its own. It is not a gate, so a provider that diff --git a/scripts/checkProviders.test.ts b/scripts/checkProviders.test.ts index ee5d34e9..869cbf60 100644 --- a/scripts/checkProviders.test.ts +++ b/scripts/checkProviders.test.ts @@ -42,6 +42,18 @@ describe("providerProblems", () => { expect(await providerProblems(PROVIDER, fetch)).toEqual([]); }); + it("asks for the discovery document as JSON and for the sign-up page as a browser does", async () => { + const fetch = fakeFetch({ + [CONFIGURATION]: [json({ issuer: "https://login.example/" })], + "https://example/sign-up": [new Response("")], + }); + await providerProblems(PROVIDER, fetch); + const accept = (url: string) => + fetch.mock.calls.find(([input]) => String(input) === url)?.[1]?.headers.accept; + expect(accept(CONFIGURATION)).toBe("application/json"); + expect(accept("https://example/sign-up")).toBe("text/html, */*;q=0.8"); + }); + it("reads no sign-up page for a provider that has none", async () => { const fetch = fakeFetch({ [CONFIGURATION]: [json({ issuer: "https://login.example/" })] }); const { signUpUrl: _, ...withoutSignUp } = PROVIDER; diff --git a/scripts/checkProviders.ts b/scripts/checkProviders.ts index 405c7dfd..5b8468be 100644 --- a/scripts/checkProviders.ts +++ b/scripts/checkProviders.ts @@ -22,13 +22,22 @@ export function configurationUrl(issuer: string): string { return `${issuer.replace(/\/$/, "")}/.well-known/openid-configuration`; } +/** What the discovery document is asked for as. */ +const JSON_ACCEPT = "application/json"; +/** + * What a sign-up page is asked for as: a browser's request, since a newcomer + * opens it in one. Some servers (the Community Solid Server) answer a page + * of theirs only to a request that prefers HTML. + */ +const PAGE_ACCEPT = "text/html, */*;q=0.8"; + /** A GET, tried again while it fails or the server answers 5xx; the last error is thrown. */ -async function get(fetch: typeof globalThis.fetch, url: string): Promise { +async function get(fetch: typeof globalThis.fetch, url: string, accept: string): Promise { let failure: unknown; for (let attempt = 1; attempt <= ATTEMPTS; attempt++) { try { const response = await fetch(url, { - headers: { accept: "application/json, text/html;q=0.9, */*;q=0.8" }, + headers: { accept }, signal: AbortSignal.timeout(TIMEOUT_MS), }); if (response.status < 500 || attempt === ATTEMPTS) return response; @@ -62,7 +71,7 @@ export async function providerProblems( const problems: string[] = []; const url = configurationUrl(provider.oidcIssuer); try { - const response = await get(fetch, url); + const response = await get(fetch, url, JSON_ACCEPT); if (!response.ok) { problems.push(`${provider.name}: ${url} answers ${response.status}.`); } else { @@ -78,7 +87,7 @@ export async function providerProblems( } if (provider.signUpUrl !== undefined) { try { - const response = await get(fetch, provider.signUpUrl); + const response = await get(fetch, provider.signUpUrl, PAGE_ACCEPT); if (!response.ok) problems.push( `${provider.name}: its sign-up page ${provider.signUpUrl} answers ${response.status}.`, From bc566ffe9f8444c0d275fb29100b7830dd19f74c Mon Sep 17 00:00:00 2001 From: Anton Wiklund Date: Sat, 24 Oct 2026 17:05:12 +0200 Subject: [PATCH 2/2] feat(domain): offer every suggested provider for creating a Pod Onboarding's first step offered only iGrant.io Data Pod for creating a Pod, since it was the only provider with a signUpUrl. A newcomer who did not want a Pod there, or whose sign-up failed, had nowhere else to go from the app, which undercuts Solid's promise that you choose where your data lives. Nothing said why the other three had no link. solidcommunity.net, Inrupt PodSpaces and solidweb.me all let anyone sign up today, with no invite and no waiting list, so each gets its sign-up page. The two Community Solid Servers link straight to their "Create account" page. onboarding.md lists the providers offered, notes that PodSpaces is a Developer Preview, and says what to do when a provider's sign-up closes. The weekly check:providers requests every new link. Closes: #283 --- cspell.config.yaml | 1 + docs/onboarding.md | 24 ++++++++++++++- packages/domain/src/podProvider.test.ts | 15 ++++++---- packages/domain/src/podProvider.ts | 3 ++ .../src/ui/onboarding/OnboardingFlow.test.tsx | 29 ++++++++++++------- 5 files changed, 55 insertions(+), 17 deletions(-) diff --git a/cspell.config.yaml b/cspell.config.yaml index 615fc2ee..cebcc528 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -179,6 +179,7 @@ words: - skohub - solidcommunity - solidproject + - solidweb - SRON - Tahoma - Warchoł diff --git a/docs/onboarding.md b/docs/onboarding.md index 7886a8d1..53a90cbf 100644 --- a/docs/onboarding.md +++ b/docs/onboarding.md @@ -59,7 +59,7 @@ instead, in `VITE_POD_PROVIDERS` ([configuration.md](configuration.md#vite_pod_p `AppShell` takes the list as its `providers`: - providers **with a `signUpUrl`** are offered on the first step for - creating a Pod (currently iGrant.io Data Pod); + creating a Pod ([which ones](#offered-for-creating-a-pod)); - **every** provider is suggested on the WebID step under "Or pick your provider", for users who do not want to type their WebID. Picking one calls `loginWithProvider(oidcIssuer)`, which starts the same Solid-OIDC @@ -69,6 +69,28 @@ instead, in `VITE_POD_PROVIDERS` ([configuration.md](configuration.md#vite_pod_p Issuers are the exact values each provider advertises in its `/.well-known/openid-configuration`. +### Offered for creating a Pod + +Every suggested provider is offered for creating a Pod: each lets anyone +sign up, with no invite and no waiting list (checked on 2026-10-24). + +| Provider | Sign-up page | +| --- | --- | +| iGrant.io Data Pod | | +| solidcommunity.net | | +| Inrupt PodSpaces | | +| solidweb.me | | + +solidcommunity.net and solidweb.me run the Community Solid Server: its +"Create account" page makes the account, and the Pod is created from the +account's page after that. Inrupt PodSpaces is a Developer Preview, which +Inrupt says is not for production use or for sensitive or personal data. + +`npm run check:providers` requests each sign-up page weekly +([testing.md](testing.md#pod-providers)). A provider whose sign-up +closes, or comes to need an invite, loses its `signUpUrl` and stays +suggested for logging in; say here why it was left out. + ## WebID validation A chosen provider's issuer passes `isSecureUrl` in the use case, like every diff --git a/packages/domain/src/podProvider.test.ts b/packages/domain/src/podProvider.test.ts index 5b8e55c8..1bc7f447 100644 --- a/packages/domain/src/podProvider.test.ts +++ b/packages/domain/src/podProvider.test.ts @@ -7,11 +7,16 @@ import { POD_PROVIDERS } from "./podProvider"; import { isSecureUrl } from "./webId"; describe("POD_PROVIDERS", () => { - it("offers iGrant.io Data Pod for creating a Pod", () => { - expect(POD_PROVIDERS.find((p) => p.id === "igrant")).toMatchObject({ - name: "iGrant.io Data Pod", - signUpUrl: "https://igrant.io/datapod.html", - }); + it("offers every suggested provider for creating a Pod", () => { + expect(POD_PROVIDERS.map(({ id, signUpUrl }) => ({ id, signUpUrl }))).toEqual([ + { id: "igrant", signUpUrl: "https://igrant.io/datapod.html" }, + { + id: "solidcommunity", + signUpUrl: "https://solidcommunity.net/.account/login/password/register/", + }, + { id: "inrupt", signUpUrl: "https://start.inrupt.com/" }, + { id: "solidweb", signUpUrl: "https://solidweb.me/.account/login/password/register/" }, + ]); }); it("only suggests https issuers and sign-up pages, with unique ids", () => { diff --git a/packages/domain/src/podProvider.ts b/packages/domain/src/podProvider.ts index 4a435864..0bdcf6c1 100644 --- a/packages/domain/src/podProvider.ts +++ b/packages/domain/src/podProvider.ts @@ -36,15 +36,18 @@ export const POD_PROVIDERS: readonly PodProvider[] = [ id: "solidcommunity", name: "solidcommunity.net", oidcIssuer: "https://solidcommunity.net/", + signUpUrl: "https://solidcommunity.net/.account/login/password/register/", }, { id: "inrupt", name: "Inrupt PodSpaces", oidcIssuer: "https://login.inrupt.com", + signUpUrl: "https://start.inrupt.com/", }, { id: "solidweb", name: "solidweb.me", oidcIssuer: "https://solidweb.me/", + signUpUrl: "https://solidweb.me/.account/login/password/register/", }, ]; diff --git a/packages/ui/src/ui/onboarding/OnboardingFlow.test.tsx b/packages/ui/src/ui/onboarding/OnboardingFlow.test.tsx index 3b174a77..518858a0 100644 --- a/packages/ui/src/ui/onboarding/OnboardingFlow.test.tsx +++ b/packages/ui/src/ui/onboarding/OnboardingFlow.test.tsx @@ -39,7 +39,9 @@ describe("OnboardingFlow", () => { expect(buttons[0]).toHaveTextContent("Try it without logging in"); expect(buttons[0]).toHaveClass("primary"); // Creating a Pod stays offered, no longer as the main action. - expect(screen.getByRole("link", { name: /Create a Pod with/ })).not.toHaveClass("primary"); + for (const link of screen.getAllByRole("link", { name: /Create a Pod with/ })) { + expect(link).not.toHaveClass("primary"); + } fireEvent.click(screen.getByRole("button", { name: "Try it without logging in" })); expect(props.onTryAsGuest).toHaveBeenCalledOnce(); }); @@ -47,19 +49,24 @@ describe("OnboardingFlow", () => { it("offers no guest's study unless it may", () => { renderFlow(); expect(screen.queryByRole("button", { name: "Try it without logging in" })).toBeNull(); - expect(screen.getByRole("link", { name: /Create a Pod with/ })).toHaveClass("primary"); + for (const link of screen.getAllByRole("link", { name: /Create a Pod with/ })) { + expect(link).toHaveClass("primary"); + } }); - it("links to iGrant.io sign-up in a new tab, without leaking the opener", () => { + it("links to each provider's sign-up in a new tab, without leaking the opener", () => { renderFlow(); - expect(screen.getByText("iGrant.io Data Pod")).toBeInTheDocument(); - const link = screen.getByRole("link", { - name: "Create a Pod with iGrant.io Data Pod (opens in a new tab)", - }); - expect(link).toHaveTextContent("Create a Pod ↗"); - expect(link).toHaveAttribute("href", "https://igrant.io/datapod.html"); - expect(link).toHaveAttribute("target", "_blank"); - expect(link).toHaveAttribute("rel", "noopener noreferrer"); + expect(screen.getAllByRole("link")).toHaveLength(POD_PROVIDERS.length); + for (const provider of POD_PROVIDERS) { + expect(screen.getByText(provider.name)).toBeInTheDocument(); + const link = screen.getByRole("link", { + name: `Create a Pod with ${provider.name} (opens in a new tab)`, + }); + expect(link).toHaveTextContent("Create a Pod ↗"); + expect(link).toHaveAttribute("href", provider.signUpUrl); + expect(link).toHaveAttribute("target", "_blank"); + expect(link).toHaveAttribute("rel", "noopener noreferrer"); + } }); it("lists whichever providers it is given", () => {