Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cspell.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ words:
- skohub
- solidcommunity
- solidproject
- solidweb
- SRON
- Tahoma
- Warchoł
Expand Down
24 changes: 23 additions & 1 deletion docs/onboarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 | <https://igrant.io/datapod.html> |
| solidcommunity.net | <https://solidcommunity.net/.account/login/password/register/> |
| Inrupt PodSpaces | <https://start.inrupt.com/> |
| solidweb.me | <https://solidweb.me/.account/login/password/register/> |

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
Expand Down
3 changes: 2 additions & 1 deletion docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -897,7 +897,8 @@ these. `npm run check:providers`
provider instead. Its issuer must answer
`<issuer>/.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
Expand Down
15 changes: 10 additions & 5 deletions packages/domain/src/podProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
3 changes: 3 additions & 0 deletions packages/domain/src/podProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/",
},
];
29 changes: 18 additions & 11 deletions packages/ui/src/ui/onboarding/OnboardingFlow.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,27 +39,34 @@ 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();
});

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", () => {
Expand Down
12 changes: 12 additions & 0 deletions scripts/checkProviders.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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("<html>")],
});
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;
Expand Down
17 changes: 13 additions & 4 deletions scripts/checkProviders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Response> {
async function get(fetch: typeof globalThis.fetch, url: string, accept: string): Promise<Response> {
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;
Expand Down Expand Up @@ -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 {
Expand All @@ -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}.`,
Expand Down
Loading