fedipod 1.43.0 → 1.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,38 @@
1
+ // forum-form.mjs — what the sign-up page's forum fields say, read into what
2
+ // the forum is made from. No imports, so a test reads it as the page does.
3
+
4
+ export const SLUG = /^[a-z0-9][a-z0-9-]{0,62}$/u;
5
+
6
+ // One category per line: a short name for the address, then a colon and the
7
+ // name readers see; a line with no colon is both.
8
+ export function categoriesFrom(text) {
9
+ const lines = String(text || '').split('\n').map((l) => l.trim()).filter(Boolean);
10
+ if (!lines.length) throw new Error('Categories: at least one is required.');
11
+ const out = [];
12
+ const seen = new Set();
13
+ for (const line of lines) {
14
+ const i = line.indexOf(':');
15
+ const slug = (i < 0 ? line : line.slice(0, i)).trim().toLowerCase();
16
+ const name = (i < 0 ? '' : line.slice(i + 1)).trim() || slug;
17
+ if (!SLUG.test(slug)) throw new Error(`Categories: "${slug}" is not a short name. Letters, digits and hyphens, starting with a letter or digit.`);
18
+ if (seen.has(slug)) throw new Error(`Categories: "${slug}" is listed twice.`);
19
+ seen.add(slug);
20
+ out.push({ slug, name: name.slice(0, 200) });
21
+ }
22
+ return out;
23
+ }
24
+
25
+ // Who moderates, as an actor id: an address at this site, or a web address
26
+ // given outright. Empty is nobody named yet.
27
+ export function moderatorFrom(text, frontOrigin) {
28
+ const s = String(text || '').trim();
29
+ if (!s) return null;
30
+ if (/^https?:\/\//iu.test(s)) {
31
+ try { return new URL(s).href; } catch { throw new Error('Moderator: not a valid web address.'); }
32
+ }
33
+ const m = /^@?([a-z0-9][a-z0-9._-]*)@([a-z0-9.-]+(?::\d+)?)$/iu.exec(s);
34
+ if (!m) throw new Error('Moderator: write it as @name@server, or as a web address.');
35
+ const front = frontOrigin ? new URL(frontOrigin) : null;
36
+ if (front && m[2].toLowerCase() === front.host.toLowerCase()) return `${front.origin}/u/${m[1].toLowerCase()}/ap/actor`;
37
+ throw new Error(`Moderator: only an address at ${front ? front.host : 'this site'} can be named here. Others can be added on the forum's settings page.`);
38
+ }
@@ -0,0 +1,11 @@
1
+ // forum-support.mjs — whether this site's sign-up page offers a forum. The
2
+ // page's own code never imports the forum; this file is the one place that
3
+ // names the forum's setup module, and the page hides the choice when it is
4
+ // null. The counterpart of netlify/forum-support.mjs, which decides whether
5
+ // the Gateway behind this page keeps forums: the two go together.
6
+ //
7
+ // with forums import * as setup from '../../packages/fedipod-bb/src/setup.mjs'; export default setup;
8
+ // without forums export default null;
9
+
10
+ import * as setup from '../../packages/fedipod-bb/src/setup.mjs';
11
+ export default setup;
@@ -41,13 +41,14 @@ code { background: rgba(120,120,120,.18); padding: .05em .35em; border-radius: 4
41
41
  fieldset { border: 1px solid var(--line); border-radius: .4rem; margin: 1rem 0; padding: .75rem 1rem 1rem; }
42
42
  legend { padding: 0 .3rem; font-weight: 600; }
43
43
  label { display: block; margin: .75rem 0 .2rem; }
44
- input[type=text], input[type=email], input[type=url], input[type=password], select {
44
+ input[type=text], input[type=email], input[type=url], input[type=password], select, textarea {
45
45
  font: inherit; font-size: 1rem; width: 100%; padding: .55rem; box-sizing: border-box;
46
46
  border: 1px solid var(--field-border); border-radius: .3rem;
47
47
  background: var(--field-bg); color: var(--field-text); }
48
48
  /* A list to pick from looks different from a box to type in: its own tint,
49
49
  the accent border, and the same on its options. */
50
50
  select { background: var(--select-bg); border-color: var(--select-border); }
51
+ textarea { min-height: 6.5rem; resize: vertical; }
51
52
  select option { background: var(--select-bg); color: var(--field-text); }
52
53
  ::placeholder { color: var(--sub); opacity: 1; }
53
54
  .choice { display: block; margin: .4rem 0; font-weight: normal; }
@@ -145,6 +146,14 @@ a { color: var(--accent); }
145
146
  <fieldset>
146
147
  <legend>Your Solid Pod</legend>
147
148
 
149
+ <div role="radiogroup" aria-labelledby="making-label" id="making-group">
150
+ <span id="making-label" class="grouplabel">What you are making</span>
151
+ <label class="choice"><input type="radio" name="making" value="account" checked> An account for yourself</label>
152
+ <label class="choice"><input type="radio" name="making" value="forum"> A forum</label>
153
+ <p class="hint" id="making-hint" hidden>A forum has a pod of its own. Make a new pod for it at the
154
+ provider, or sign in with a pod that holds nothing else. The forum's address will be at this site.</p>
155
+ </div>
156
+
148
157
  <div role="radiogroup" aria-labelledby="podtype-label">
149
158
  <span id="podtype-label" class="grouplabel">Pod type</span>
150
159
  <label class="choice"><input type="radio" name="mode" value="existing"> An existing pod</label>
@@ -186,7 +195,7 @@ a { color: var(--accent); }
186
195
  <!-- Screen 2: Your Fediverse Identity — shown on return from the pod's login -->
187
196
  <div class="step" id="step-2" hidden>
188
197
  <fieldset>
189
- <legend>Your Fediverse Identity</legend>
198
+ <legend id="identity-legend">Your Fediverse Identity</legend>
190
199
 
191
200
  <span id="signed-pod-label" class="grouplabel">Your pod</span>
192
201
  <p id="signed-pod" role="status" aria-labelledby="signed-pod-label"></p>
@@ -204,12 +213,27 @@ a { color: var(--accent); }
204
213
  Your posts, followers and key come with you, and your followers are told the
205
214
  new address. Keep the same handle, or choose a new one here.</p>
206
215
 
207
- <label for="handle">Fediverse handle</label>
216
+ <label for="handle" id="handle-label">Fediverse handle</label>
208
217
  <input type="text" id="handle" name="handle" autocomplete="off"
209
218
  placeholder="the handle part of your address: @HANDLE@server" aria-describedby="handle-hint">
210
219
  <p class="hint" id="handle-hint">The name in your Fediverse address. This is permanent.
211
220
  Letters, digits, and hyphens only.</p>
212
221
 
222
+ <div id="forum-fields" hidden>
223
+ <label for="forumName">Forum name</label>
224
+ <input type="text" id="forumName" name="forumName" autocomplete="off" placeholder="The name readers see">
225
+ <label for="categories">Categories, one per line</label>
226
+ <textarea id="categories" name="categories" autocomplete="off" spellcheck="false" aria-describedby="categories-hint"
227
+ placeholder="general: General Discussion&#10;software: Software"></textarea>
228
+ <p class="hint" id="categories-hint">Each line is a short name for the category's address, then a colon and
229
+ the name readers see. Short names are letters, digits and hyphens, and are permanent.</p>
230
+ <label for="moderator">Your address at this site, to moderate the forum as</label>
231
+ <input type="text" id="moderator" name="moderator" autocomplete="off" spellcheck="false" aria-describedby="moderator-hint"
232
+ placeholder="@you@this-site">
233
+ <p class="hint" id="moderator-hint">Optional. Signing in with the forum's own pod moderates too. More
234
+ moderators can be named on the forum's settings page later.</p>
235
+ </div>
236
+
213
237
  <div role="radiogroup" aria-labelledby="shape-label" id="shape-group">
214
238
  <span id="shape-label" class="grouplabel">Where your address lives</span>
215
239
  <label class="choice"><input type="radio" name="shape" value="pod" checked> On your pod</label>
@@ -221,7 +245,7 @@ a { color: var(--accent); }
221
245
  <span id="addr-label" class="grouplabel">Your Fediverse address</span>
222
246
  <p class="address" id="preview" role="status" aria-live="polite" aria-labelledby="addr-label">@…@…</p>
223
247
  </fieldset>
224
- <p class="hint">The handle and the pod cannot be changed afterwards. Everything else — display
248
+ <p class="hint" id="after-hint">The handle and the pod cannot be changed afterwards. Everything else — display
225
249
  name, bio, pictures — you set later in the client.</p>
226
250
  <p class="err" id="form-error" role="alert"></p>
227
251
  <div id="index-ask" role="alertdialog" aria-labelledby="index-ask-text" hidden>
@@ -246,6 +270,7 @@ a { color: var(--accent); }
246
270
  <h2 id="running-title">Setting up…</h2>
247
271
  <ul class="steps" id="steps" aria-live="polite"></ul>
248
272
  <p class="err" id="run-error" role="alert"></p>
273
+ <p id="run-done" hidden><a id="run-done-link" href="#">Open your forum</a></p>
249
274
  <p id="run-actions" hidden>
250
275
  <button type="button" id="run-back">Start over</button>
251
276
  <button type="button" class="primary" id="run-retry">Try again</button>
@@ -0,0 +1,52 @@
1
+ // signup-forum.mjs — a forum made from the sign-up page. The person made a pod
2
+ // for it at a provider and is signed in as that pod; this writes the forum on
3
+ // the pod, takes its addresses here, and names this site its keeper. The
4
+ // keeper's first run, started by the last switch, mints the forum's keys and
5
+ // publishes its actors. Nothing of the person's own account is touched, and
6
+ // nothing runs on their machine afterwards.
7
+
8
+ import forumSetup from './forum-support.mjs';
9
+ import { BrowserRemotePod } from './pod-remote.mjs';
10
+ import { HttpStorage } from '../../lib/core/storage.mjs';
11
+ import { podBaseOfWebId } from '../../lib/pod/urls.mjs';
12
+ import { resourceExists } from '../../lib/pod/root.mjs';
13
+ import { findAccount } from '../../lib/core/place.mjs';
14
+ import { handleProblem, assertFrontNameFree, readConfigAt } from './signup.mjs';
15
+ import { categoriesFrom, moderatorFrom } from './forum-form.mjs';
16
+
17
+ /** Whether this site makes forums at all (web/app/forum-support.mjs). */
18
+ export const forumsOffered = () => !!forumSetup;
19
+
20
+ export async function signUpForum({ handle, name, categories, moderator = '' }, { session, onStep = () => {}, frontOrigin = null } = {}) {
21
+ if (!forumSetup) throw new Error('This site does not host forums.');
22
+ if (!frontOrigin) throw new Error('A forum lives at a gateway, and this page has none.');
23
+ const bad = handleProblem(handle);
24
+ if (bad) throw new Error(`Forum handle: ${bad}`);
25
+ if (!String(name || '').trim()) throw new Error('A forum name is required.');
26
+ const cats = categoriesFrom(categories);
27
+ const mod = moderatorFrom(moderator, frontOrigin);
28
+ if (!session?.webId || typeof session.fetch !== 'function') throw new Error("sign in at the forum's pod first");
29
+
30
+ const webId = session.webId;
31
+ const pod = podBaseOfWebId(webId);
32
+ const remote = new BrowserRemotePod(session, { webId, role: 'signup', log: () => {} });
33
+
34
+ // A forum needs a pod of its own: not one that hosts an account, not one
35
+ // that holds a forum already; and its names must be free here.
36
+ onStep('pod', 'running');
37
+ if (await findAccount(remote, pod, readConfigAt(remote, pod))) {
38
+ throw new Error('This pod already hosts a FediPod account. A forum needs a pod of its own.');
39
+ }
40
+ if (await resourceExists(session.fetch, pod + forumSetup.ROOT)) {
41
+ throw new Error('This pod already holds a forum.');
42
+ }
43
+ for (const h of [handle, ...cats.map((c) => c.slug)]) await assertFrontNameFree(frontOrigin, h);
44
+ onStep('pod', 'ok');
45
+
46
+ const made = await forumSetup.setUpForumAtGateway({
47
+ remote, storageFor: (base, fetch) => new HttpStorage(base, fetch), fetch: (u, i) => session.fetch(u, i),
48
+ front: frontOrigin, ownerWebId: webId, remotePod: pod, handle, name: String(name).trim(), categories: cats,
49
+ moderators: mod ? [mod] : [], moderatorWebIds: [webId],
50
+ }, { onStep });
51
+ return { handle: made.handle, front: made.front, address: `@${made.handle}@${new URL(made.front).host}` };
52
+ }
@@ -27,7 +27,7 @@ const keysDocFor = (pod, root) => `${pod}${root}ap-state/keys.json`;
27
27
  const stateOf = (pod, root) => `${pod}${root}ap-state/`;
28
28
 
29
29
  /** The account's config as it sits under `root`, read as its owner. */
30
- const readConfigAt = (remote, pod) => (root) => podState.readConfig(remote, { state: stateOf(pod, root) });
30
+ export const readConfigAt = (remote, pod) => (root) => podState.readConfig(remote, { state: stateOf(pod, root) });
31
31
 
32
32
  const HANDLE_RE = /^[a-z0-9-]{2,30}$/;
33
33
 
@@ -49,7 +49,7 @@ const PROGRESS = new Map();
49
49
  const progressKey = (webId, a) => [webId, a.handle, a.shape || 'pod', a.container || ''].join('|');
50
50
 
51
51
  /** A fronted name is one per gateway; a taken one is refused before anything is made. */
52
- async function assertFrontNameFree(frontOrigin, handle) {
52
+ export async function assertFrontNameFree(frontOrigin, handle) {
53
53
  const res = await fetch(`${frontOrigin.replace(/\/$/, '')}/api/handle?handle=${encodeURIComponent(handle)}`,
54
54
  { headers: { accept: 'application/json' } }).catch(() => null);
55
55
  const d = res ? await res.json().catch(() => ({})) : null;
@@ -3,33 +3,25 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>FediPod — access the Fediverse and ATProto from a Solid pod</title>
6
+ <title>FediPod Gateway</title>
7
7
  <link rel="icon" href="/icons/gateway.svg" type="image/svg+xml">
8
8
  <style>
9
- :root { color-scheme: light dark; --line:#d9d4c7; --dim:#45443d; --accent:#6a8a5a; }
9
+ :root { color-scheme: light dark; --dim:#45443d; }
10
10
  @media (prefers-color-scheme: dark) { :root { --dim:#c2c0b4; } }
11
11
  body { font: 20px/1.6 Arial, Helvetica, sans-serif; max-width: 44rem; margin: 2rem auto; padding: 0 1.1rem; }
12
12
  h1 { font-size: 1.7rem; margin: 0 0 .2rem; }
13
- h2 { font-size: 1.2rem; margin: 1.4rem 0 .4rem; }
14
13
  p.lede { color: var(--dim); margin-top: 0; }
15
14
  .hint { color: var(--dim); font-size: .95rem; }
16
- code { background: rgba(120,120,120,.12); padding: .05em .35em; border-radius: 4px; }
17
- pre { background: rgba(120,120,120,.12); padding: .7rem .9rem; border-radius: 8px; overflow-x: auto; }
18
- ol.steps li { margin: .5rem 0; }
15
+ a { text-decoration: underline; }
19
16
  [hidden] { display: none !important; }
20
17
  </style>
21
18
  </head>
22
19
  <body>
23
- <h1>FediPod - join the Fediverse from a Solid pod.</h1>
24
- <p class="lede">Welcome to <b>FediPod</b>! Here, you can get a free pod and Fediverse account or link to your existing pod/account.</p>
25
-
26
- <p class="hint" id="current-version" hidden>Current FediPod: <strong id="current-version-num"></strong><br>
27
- — install or update an existing install with <code id="current-version-cmd"></code></p>
28
-
29
- <p>
30
- FediPod is experimental and free to use. See the <a href="https://github.com/jeff-zucker/FediPod">FediPod repository</a> for details.
31
- </p>
32
-
20
+ <h1>FediPod Gateway</h1>
21
+ <p class="lede">This is a FediPod Gateway: the door other Fediverse servers deliver to, for accounts whose data lives on Solid pods.</p>
22
+ <p>Accounts are made and used in the browser. <a href="/">Open FediPod</a> to create one or sign in. Nothing is installed.</p>
23
+ <p class="hint" id="current-version" hidden>This gateway runs FediPod <strong id="current-version-num"></strong>.</p>
24
+ <p>FediPod is experimental and free to use. See the <a href="https://github.com/jeff-zucker/FediPod">FediPod repository</a> for details.</p>
33
25
  <script src="/new-account.js"></script>
34
26
  </body>
35
27
  </html>
@@ -1,13 +1,9 @@
1
- // new-account.js — the sign-up page's script, in a file of its own.
2
- //
3
- // It lived inline in new-account.html until 2026-09-09. Out here the page can
4
- // be served under `script-src 'self'`, which is what stops a stray bit of
5
- // markup from becoming running code; inline script cannot be told apart from
6
- // injected script by any policy. Served by the front at /new-account.js.
1
+ // new-account.js — the front's root page's script, in a file of its own so
2
+ // the page is served under `script-src 'self'`. Served by the front at
3
+ // /new-account.js. It fills in the version this gateway runs, nothing more.
7
4
  fetch('/api/handle?handle=__probe__').then(r => r.json()).then(d => {
8
5
  if (d.version) {
9
6
  document.getElementById('current-version-num').textContent = d.version;
10
- document.getElementById('current-version-cmd').textContent = `curl -fsSL ${location.origin}/install | sh`;
11
7
  document.getElementById('current-version').hidden = false;
12
8
  }
13
9
  }).catch(() => {});
@@ -1,69 +0,0 @@
1
- #!/bin/sh
2
- # FediPod installer. Usage: curl -fsSL https://fedipod.net/install | sh
3
- # Installs into ~/FediPod (or $FEDIPOD_DIR); re-running updates the install.
4
- #
5
- # A signup page may append parameters (sh -s -- --gateway ... --secret ...);
6
- # they are saved for the first start, whose setup opens pre-filled and
7
- # attaches to the gateway when it completes.
8
- set -eu
9
-
10
- REPO="https://github.com/jeff-zucker/FediPod"
11
- DIR="${FEDIPOD_DIR:-$HOME/FediPod}"
12
-
13
- say() { printf '%s\n' "$*"; }
14
- fail() { printf 'fedipod install: %s\n' "$*" >&2; exit 1; }
15
-
16
- GATEWAY= SECRET= POD= ISSUER= HANDLE= KIND=person FRONTED=false
17
- while [ $# -gt 0 ]; do
18
- case "$1" in
19
- --gateway) GATEWAY=$2; shift 2;;
20
- --secret) SECRET=$2; shift 2;;
21
- --pod) POD=$2; shift 2;;
22
- --issuer) ISSUER=$2; shift 2;;
23
- --handle) HANDLE=$2; shift 2;;
24
- --kind) KIND=$2; shift 2;;
25
- --fronted) FRONTED=true; shift;;
26
- *) fail "unknown option $1";;
27
- esac
28
- done
29
-
30
- command -v git >/dev/null 2>&1 || fail "git is required — install it from https://git-scm.com/ and re-run"
31
- command -v node >/dev/null 2>&1 || fail "node 20 or newer is required — install it from https://nodejs.org/ and re-run"
32
- command -v npm >/dev/null 2>&1 || fail "npm is required (it ships with node) — install node from https://nodejs.org/ and re-run"
33
-
34
- major=$(node -v | sed 's/^v//' | cut -d. -f1)
35
- [ "$major" -ge 20 ] 2>/dev/null || fail "node 20 or newer is required (found $(node -v))"
36
-
37
- if [ -d "$DIR/.git" ]; then
38
- say "updating the existing install in $DIR"
39
- git -C "$DIR" pull --ff-only || fail "could not update $DIR — it has local changes; update it by hand"
40
- elif [ -e "$DIR" ]; then
41
- fail "$DIR exists and is not a FediPod checkout — set FEDIPOD_DIR to another location and re-run"
42
- else
43
- say "installing into $DIR"
44
- git clone --depth 1 "$REPO" "$DIR"
45
- fi
46
-
47
- cd "$DIR"
48
- npm install --omit=dev --no-audit --no-fund
49
-
50
- # Put `fedipod` on PATH, the same command an `npm install -g fedipod` gives.
51
- # Everything printed from here on — the docs, the agent's own advice, the
52
- # command a signup page hands you — names it that way, and a checkout that
53
- # skipped this step could run none of them.
54
- if npm link >/dev/null 2>&1; then
55
- say "linked: the fedipod command is on your PATH"
56
- else
57
- say "note: could not link the fedipod command — run it as: node $DIR/bin/fedipod.mjs"
58
- fi
59
-
60
- if [ -n "$HANDLE" ]; then
61
- printf '{"handle":"%s","kind":"%s","pod":"%s","issuer":"%s","gateway":"%s","secret":"%s","fronted":%s}\n' \
62
- "$HANDLE" "$KIND" "$POD" "$ISSUER" "$GATEWAY" "$SECRET" "$FRONTED" > "$DIR/first-run.json"
63
- chmod 600 "$DIR/first-run.json"
64
- fi
65
-
66
- say ""
67
- say "Installed. To start (first run opens setup in your browser):"
68
- say ""
69
- say " fedipod start"