fedipod 1.55.2 → 1.56.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,59 @@
|
|
|
1
|
+
// forum-pages.mjs — a forum's page at its own host (bb.<gateway>/<forum>/…).
|
|
2
|
+
// Every address under a forum is handed to the forum's own code
|
|
3
|
+
// (ctx.forum.renderPage, netlify/forum-support.mjs), which draws the ones it
|
|
4
|
+
// draws; for the rest, or when drawing fails, the page as the site serves it,
|
|
5
|
+
// which the reader's browser builds. Which addresses are drawn is the forum's
|
|
6
|
+
// business, not this file's.
|
|
7
|
+
|
|
8
|
+
// A forum's path, and anything under it.
|
|
9
|
+
const VIEW = /^\/([^/]+)\//u;
|
|
10
|
+
// First path segments the bb host gives to something other than a forum.
|
|
11
|
+
const NOT_FORUMS = new Set(['api', 'bb', 'icons', 'u', '.well-known']);
|
|
12
|
+
// How long the edge keeps a drawn page. The forum's documents are dropped
|
|
13
|
+
// from the edge when a post lands and when the forum runs, under the labels
|
|
14
|
+
// the page carries too, so an hour only bounds a change nothing announced.
|
|
15
|
+
// Kept per region, not durably: a page names the scripts of the deploy that
|
|
16
|
+
// drew it.
|
|
17
|
+
const PAGE_EDGE_SECONDS = 3600;
|
|
18
|
+
const MISSING_EDGE_SECONDS = 120;
|
|
19
|
+
|
|
20
|
+
// The page as the site serves it, read once per running instance: it changes
|
|
21
|
+
// only with a deploy, which starts new ones.
|
|
22
|
+
const shells = new Map();
|
|
23
|
+
async function shellOf(origin, f) {
|
|
24
|
+
if (shells.has(origin)) return shells.get(origin);
|
|
25
|
+
const r = await f(`${origin}/bb/index.html`).catch(() => null);
|
|
26
|
+
const text = r?.ok ? await r.text().catch(() => null) : null;
|
|
27
|
+
if (text) shells.set(origin, text);
|
|
28
|
+
return text;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The answer for a forum's page at its own host, or null for any other request. */
|
|
32
|
+
export async function routeForumPage(request, ctx) {
|
|
33
|
+
const url = new URL(request.url);
|
|
34
|
+
if (!/^bb\./u.test(url.hostname)) return null;
|
|
35
|
+
const m = VIEW.exec(url.pathname);
|
|
36
|
+
if (!m || NOT_FORUMS.has(m[1])) return null;
|
|
37
|
+
if (request.method !== 'GET' && request.method !== 'HEAD') return { status: 405, headers: { allow: 'GET, HEAD' }, body: '' };
|
|
38
|
+
const shell = await shellOf(url.origin, ctx.siteFetch || globalThis.fetch.bind(globalThis));
|
|
39
|
+
if (!shell) return { status: 503, headers: { 'content-type': 'text/plain', 'cache-control': 'no-store', 'retry-after': '30' }, body: 'the forum\'s page is not available right now\n' };
|
|
40
|
+
const page = (status, body, headers) => ({ status, headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-cache', ...headers }, body });
|
|
41
|
+
// An address with a query is a sign-in coming back, or nothing a reader
|
|
42
|
+
// follows: the page builds it.
|
|
43
|
+
const draws = !!ctx.forum?.renderPage && !url.search;
|
|
44
|
+
// A drawing not done in time goes on after the page has gone, where the
|
|
45
|
+
// host lets work outlive its answer (ctx.waitUntil): the next reader's
|
|
46
|
+
// drawing then finds the forum's documents at hand.
|
|
47
|
+
const drawn = draws ? await ctx.forum.renderPage({ url: url.href, shell, keepGoing: ctx.waitUntil || null }).catch(() => null) : null;
|
|
48
|
+
if (drawn) {
|
|
49
|
+
const hold = drawn.status === 404 ? MISSING_EDGE_SECONDS : PAGE_EDGE_SECONDS;
|
|
50
|
+
return page(drawn.status, drawn.html, {
|
|
51
|
+
'netlify-cdn-cache-control': `public, s-maxage=${hold}, stale-while-revalidate=${hold * 4}`,
|
|
52
|
+
'netlify-cache-tag': drawn.tags.join(','),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
// Not drawn. Where drawing was tried, this answer is not kept, so the next
|
|
56
|
+
// reader gets a drawn page; where nothing here draws, it is the same page
|
|
57
|
+
// for everyone and is kept like one.
|
|
58
|
+
return page(200, shell, draws || url.search ? {} : { 'netlify-cdn-cache-control': `public, s-maxage=${PAGE_EDGE_SECONDS}` });
|
|
59
|
+
}
|
|
@@ -26,6 +26,7 @@ import { routeQuietApi, noteOpened, noteReceived, closedState, accountState, clo
|
|
|
26
26
|
import { holdsMail, mayBeAway, isPresent, holdingPut, afterHeld, routeHereApi, routeKeeperApi, routePushApi, routeAlertApi } from './held-mail.mjs';
|
|
27
27
|
import { routeCheckApi, missingLetter } from './check-letter.mjs';
|
|
28
28
|
import { routeNoticesApi } from './notices.mjs';
|
|
29
|
+
import { routeForumPage } from './forum-pages.mjs';
|
|
29
30
|
import { withSecurityHeaders } from './headers.mjs';
|
|
30
31
|
import { claimedWebId, verifyPodToken } from './token-claims.mjs';
|
|
31
32
|
|
|
@@ -307,7 +308,7 @@ function identFor(rec, policy = null) {
|
|
|
307
308
|
// an adapter that reads a store directly and so
|
|
308
309
|
// must confine the read itself.
|
|
309
310
|
export async function routeFront(request, ctx) {
|
|
310
|
-
const out = await route(request, ctx);
|
|
311
|
+
const out = (await routeForumPage(request, ctx)) || await route(request, ctx);
|
|
311
312
|
return { ...out, headers: withSecurityHeaders(out.headers, out.body) };
|
|
312
313
|
}
|
|
313
314
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fedipod",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.56.0",
|
|
4
4
|
"description": "Standalone single-actor ActivityPub agent whose wire face, RDF truth and state all live on a Solid pod (CSS). Bundles a Phanpy UI and a Mastodon client-API facade.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"install-service": "node bin/fedipod.mjs install-service",
|
|
56
56
|
"uninstall-service": "node bin/fedipod.mjs uninstall-service",
|
|
57
57
|
"prepublishOnly": "npm test",
|
|
58
|
-
"test": "node claude/smoke-tests/agent-smoke.mjs && node claude/smoke-tests/front-hard-test.mjs && node claude/smoke-tests/copy-test.mjs && node claude/smoke-tests/copy-mode-test.mjs && node claude/smoke-tests/hold-test.mjs && node claude/smoke-tests/masto-gateway-test.mjs && node claude/smoke-tests/admin-facade-smoke.mjs && node scripts/check-pod-calls.mjs && node scripts/verify-ui.mjs && node --test claude/smoke-tests/forum-signup-form.test.mjs claude/smoke-tests/door-verify.test.mjs claude/smoke-tests/oauth-signin.test.mjs claude/smoke-tests/app-registration.test.mjs claude/smoke-tests/push-signout.test.mjs claude/smoke-tests/token-fetches.test.mjs claude/smoke-tests/signin-hardening.test.mjs claude/smoke-tests/load-fixes.test.mjs claude/smoke-tests/unread-docs.test.mjs claude/smoke-tests/post-text-once.test.mjs claude/smoke-tests/connected-pod-only.test.mjs claude/smoke-tests/check-letter.test.mjs claude/smoke-tests/gone-through-relay.test.mjs claude/smoke-tests/connected-feeds.test.mjs && node scripts/check-sizes.mjs && node scripts/test-dependents.mjs && node claude/validation/browser-agent/all.mjs",
|
|
58
|
+
"test": "node claude/smoke-tests/agent-smoke.mjs && node claude/smoke-tests/front-hard-test.mjs && node claude/smoke-tests/copy-test.mjs && node claude/smoke-tests/copy-mode-test.mjs && node claude/smoke-tests/hold-test.mjs && node claude/smoke-tests/masto-gateway-test.mjs && node claude/smoke-tests/admin-facade-smoke.mjs && node scripts/check-pod-calls.mjs && node scripts/verify-ui.mjs && node --test claude/smoke-tests/forum-signup-form.test.mjs claude/smoke-tests/door-verify.test.mjs claude/smoke-tests/oauth-signin.test.mjs claude/smoke-tests/app-registration.test.mjs claude/smoke-tests/push-signout.test.mjs claude/smoke-tests/token-fetches.test.mjs claude/smoke-tests/signin-hardening.test.mjs claude/smoke-tests/load-fixes.test.mjs claude/smoke-tests/unread-docs.test.mjs claude/smoke-tests/post-text-once.test.mjs claude/smoke-tests/connected-pod-only.test.mjs claude/smoke-tests/check-letter.test.mjs claude/smoke-tests/gone-through-relay.test.mjs claude/smoke-tests/connected-feeds.test.mjs claude/smoke-tests/forum-pages.test.mjs && node scripts/check-sizes.mjs && node scripts/test-dependents.mjs && node claude/validation/browser-agent/all.mjs",
|
|
59
59
|
"verify-ui": "node scripts/verify-ui.mjs",
|
|
60
60
|
"build:sw": "node scripts/build-app.mjs --entry web/app/sw-src.mjs --out web/app/dist/sw.js",
|
|
61
61
|
"build:boot": "node scripts/build-app.mjs --entry web/app/boot.mjs --out web/app/dist/boot.js",
|
|
@@ -97,7 +97,6 @@
|
|
|
97
97
|
"package-lock.json": "npm:empty-npm-package@1.0.0"
|
|
98
98
|
},
|
|
99
99
|
"devDependencies": {
|
|
100
|
-
"esbuild": "^0.28.2"
|
|
101
|
-
"fedipod-bb": "^0.4.4"
|
|
100
|
+
"esbuild": "^0.28.2"
|
|
102
101
|
}
|
|
103
102
|
}
|
package/scripts/build-app.mjs
CHANGED
|
@@ -67,6 +67,9 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|
|
67
67
|
const entry = args[args.indexOf('--entry') + 1] || path.join(root, 'web/app/agent.mjs');
|
|
68
68
|
const out = args[args.indexOf('--out') + 1] || path.join(root, 'web/app/dist/agent.js');
|
|
69
69
|
const format = args.includes('--format') ? args[args.indexOf('--format') + 1] : 'esm';
|
|
70
|
+
// The app's "make a forum" is built from the forum's own folder, as it is now.
|
|
71
|
+
const { bringForum } = await import('./forum-package.mjs');
|
|
72
|
+
bringForum();
|
|
70
73
|
const { bytes } = await buildApp({ entry, out, format });
|
|
71
74
|
console.log(`built ${path.relative(root, out)} — ${Math.round(bytes / 1024)} KB`);
|
|
72
75
|
}
|
|
@@ -18,6 +18,9 @@ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'fn-bundles-'));
|
|
|
18
18
|
let fails = 0;
|
|
19
19
|
const check = (ok, msg) => { console.log(`${ok ? 'PASS' : 'FAIL'} ${msg}`); if (!ok) fails++; };
|
|
20
20
|
try {
|
|
21
|
+
// The functions carry the forum as it is in its own folder now.
|
|
22
|
+
const { bringForum } = await import('./forum-package.mjs');
|
|
23
|
+
bringForum();
|
|
21
24
|
execFileSync('netlify', ['functions:build', '--src', path.join(root, 'netlify/functions'), '--functions', path.join(tmp, 'zips')],
|
|
22
25
|
{ cwd: root, stdio: 'ignore' });
|
|
23
26
|
const manifest = JSON.parse(fs.readFileSync(path.join(tmp, 'zips/manifest.json'), 'utf8'));
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// forum-package.mjs — the forum (fedipod-bb) that fedipod.net runs and
|
|
2
|
+
// serves, taken from the forum's own folder every time, not from a version
|
|
3
|
+
// FediPod pins: the checkout beside FediPod, or a clone of the forum's GitHub
|
|
4
|
+
// main where there is none (scripts/dependents.mjs). It is packed as npm would
|
|
5
|
+
// publish it and put in node_modules without touching FediPod's package list,
|
|
6
|
+
// so nothing FediPod records names a forum version. Staging the site, the
|
|
7
|
+
// bundle check and the app build each bring it first.
|
|
8
|
+
import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path';
|
|
9
|
+
import { execFileSync } from 'node:child_process';
|
|
10
|
+
import { fetchDependent, root } from './dependents.mjs';
|
|
11
|
+
|
|
12
|
+
/** The forum from its folder, installed in node_modules. Returns its folder and version. */
|
|
13
|
+
export function bringForum({ log = console.log } = {}) {
|
|
14
|
+
const dir = fetchDependent('fedipod-bb', { log });
|
|
15
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'forum-pack-'));
|
|
16
|
+
try {
|
|
17
|
+
const packed = JSON.parse(execFileSync('npm', ['pack', dir, '--pack-destination', tmp, '--ignore-scripts', '--json'],
|
|
18
|
+
{ cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }))[0];
|
|
19
|
+
execFileSync('npm', ['install', '--no-save', '--no-audit', '--no-fund', '--ignore-scripts', path.join(tmp, packed.filename)],
|
|
20
|
+
{ cwd: root, stdio: 'ignore' });
|
|
21
|
+
log(`forum: fedipod-bb ${packed.version} from ${path.relative(root, dir) || dir}`);
|
|
22
|
+
return { dir, version: packed.version };
|
|
23
|
+
} finally { fs.rmSync(tmp, { recursive: true, force: true }); }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (import.meta.url === `file://${process.argv[1]}`) bringForum();
|
package/scripts/stage-site.mjs
CHANGED
|
@@ -14,8 +14,11 @@
|
|
|
14
14
|
import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url';
|
|
15
15
|
import { createHash } from 'node:crypto';
|
|
16
16
|
import { readyDependent } from './dependents.mjs';
|
|
17
|
+
import { bringForum } from './forum-package.mjs';
|
|
17
18
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
18
19
|
const site = path.join(root, 'web/app/site');
|
|
20
|
+
// The forum's page is copied from the forum's own folder, as it is now.
|
|
21
|
+
bringForum({ log: () => {} });
|
|
19
22
|
fs.rmSync(site, { recursive: true, force: true }); fs.mkdirSync(path.join(site, 'app'), { recursive: true });
|
|
20
23
|
const cp = (from, to) => fs.copyFileSync(path.resolve(root, from), path.join(site, to));
|
|
21
24
|
cp('web/app/index.html', 'index.html');
|
|
@@ -219,6 +222,12 @@ fs.writeFileSync(path.join(site, '_redirects'), [
|
|
|
219
222
|
`https://${h}/update.js /update.js 200!`,
|
|
220
223
|
`https://${h}/build.json /build.json 200!`,
|
|
221
224
|
`https://${h}/api/* /.netlify/functions/front 200!`,
|
|
225
|
+
// The forum's host answers /robots.txt with the forum's own.
|
|
226
|
+
`https://${h}/robots.txt /bb/robots.txt 200!`,
|
|
227
|
+
// Every address under a forum goes to the forum's code, which draws the
|
|
228
|
+
// ones it draws (lib/gateway/forum-pages.mjs); the rest get the page.
|
|
229
|
+
`https://${h}/:forum/ /.netlify/functions/front 200!`,
|
|
230
|
+
`https://${h}/:forum/* /.netlify/functions/front 200!`,
|
|
222
231
|
`https://${h}/* /bb/index.html 200!`,
|
|
223
232
|
]),
|
|
224
233
|
'# the in-browser app, served static (these win over the project catch-all)',
|
|
@@ -267,7 +276,13 @@ fs.writeFileSync(path.join(site, '_redirects'), [
|
|
|
267
276
|
// own /u/<handle>/ or a pod named outright. Its script is a module file of
|
|
268
277
|
// its own, so it is served under the same `script-src 'self'` as the app.
|
|
269
278
|
fs.mkdirSync(path.join(site, 'bb'), { recursive: true });
|
|
270
|
-
|
|
279
|
+
// Every file the forum's page ships, whatever the forum names them (its
|
|
280
|
+
// robots.txt among them): the forum decides what its page is made of.
|
|
281
|
+
// Editor leftovers and hidden files are not part of it.
|
|
282
|
+
for (const f of fs.readdirSync(path.join(root, 'node_modules/fedipod-bb/site'))) {
|
|
283
|
+
if (/^[.#]|~$/u.test(f)) continue;
|
|
284
|
+
cp(`node_modules/fedipod-bb/site/${f}`, `bb/${f}`);
|
|
285
|
+
}
|
|
271
286
|
cp('web/admin/tokens.css', 'bb/tokens.css'); // the site's shared palette, size and family
|
|
272
287
|
cp('node_modules/fediverse-session/oidc-session.mjs', 'bb/solid-oidc-session.mjs'); // signing in to a pod: the library the forum's binding names
|
|
273
288
|
// The account library's demo page, with the library beside it, under /demo/:
|
|
@@ -292,15 +307,22 @@ cp(path.join(sessionRepo, 'demo.html'), 'demo/index.html');
|
|
|
292
307
|
const sub = (n, pairs) => { let t = fs.readFileSync(bb(n), 'utf8');
|
|
293
308
|
for (const [a, b] of pairs) t = t.split(a).join(b); fs.writeFileSync(bb(n), t); };
|
|
294
309
|
sub('oidc-session.mjs', [["'fediverse-session/oidc-session.mjs'", `'./solid-oidc-session.mjs?v=${stamp('solid-oidc-session.mjs')}'`]]);
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
310
|
+
// Every module names the others it loads by their stamped address, and each
|
|
311
|
+
// is stamped only after the ones it names, so one module is one address
|
|
312
|
+
// wherever it is loaded from.
|
|
313
|
+
const modules = fs.readdirSync(path.join(site, 'bb')).filter((n) => /\.m?js$/u.test(n));
|
|
314
|
+
const loads = (n) => [...fs.readFileSync(bb(n), 'utf8').matchAll(/from '\.\/([^'?]+)'/gu)].map((m) => m[1]).filter((m) => modules.includes(m));
|
|
315
|
+
const done = new Set();
|
|
316
|
+
while (done.size < modules.length) {
|
|
317
|
+
const ready = modules.filter((n) => !done.has(n) && loads(n).every((m) => done.has(m)));
|
|
318
|
+
if (!ready.length) throw new Error(`stage-site: the forum page's modules load each other in a circle (${modules.filter((n) => !done.has(n)).join(', ')})`);
|
|
319
|
+
for (const n of ready) {
|
|
320
|
+
sub(n, loads(n).map((m) => [`'./${m}'`, `'./${m}?v=${stamp(m)}'`]));
|
|
321
|
+
done.add(n);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
sub('index.html', [...fs.readFileSync(bb('index.html'), 'utf8').matchAll(/"\/bb\/([^"?]+)"/gu)].map((m) => m[1])
|
|
325
|
+
.filter((n) => fs.existsSync(bb(n))).map((n) => [`"/bb/${n}"`, `"/bb/${n}?v=${stamp(n)}"`]));
|
|
304
326
|
}
|
|
305
327
|
injectUpdate(path.join(site, 'bb/index.html'));
|
|
306
328
|
injectUpdate(path.join(site, 'index.html'));
|