fedipod 1.0.0 → 1.2.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.
- package/README.md +4 -4
- package/bin/fedipod.mjs +45 -2253
- package/gateway.md +1 -1
- package/lib/{c2s.mjs → client/c2s.mjs} +8 -3
- package/lib/{localapi.mjs → client/localapi.mjs} +2 -2
- package/lib/client/masto/accounts.mjs +264 -0
- package/lib/client/masto/body.mjs +69 -0
- package/lib/client/masto/index.mjs +183 -0
- package/lib/client/masto/instance.mjs +104 -0
- package/lib/client/masto/media.mjs +133 -0
- package/lib/client/masto/oauth.mjs +599 -0
- package/lib/client/masto/render.mjs +459 -0
- package/lib/client/masto/statuses.mjs +331 -0
- package/lib/client/masto/timelines.mjs +316 -0
- package/lib/{streaming.mjs → client/streaming.mjs} +1 -1
- package/lib/{acctfeed.mjs → connections/acctfeed.mjs} +1 -1
- package/lib/{atproto.mjs → connections/atproto.mjs} +15 -16
- package/lib/{bskygroup.mjs → connections/bskygroup.mjs} +1 -1
- package/lib/{fediacct.mjs → connections/fediacct.mjs} +31 -35
- package/lib/{import.mjs → connections/import.mjs} +1 -1
- package/lib/{tagfeed.mjs → connections/tagfeed.mjs} +3 -3
- package/lib/connections/vault.mjs +114 -0
- package/lib/core/as2.mjs +124 -0
- package/lib/core/contexts/activitystreams.json +379 -0
- package/lib/core/contexts/did-v1.json +57 -0
- package/lib/core/contexts/fep-5711.json +36 -0
- package/lib/core/contexts/gotosocial.json +86 -0
- package/lib/core/contexts/identity-v1.json +152 -0
- package/lib/core/contexts/index.mjs +45 -0
- package/lib/core/contexts/join-lemmy.json +33 -0
- package/lib/core/contexts/joinmastodon.json +28 -0
- package/lib/core/contexts/map.json +16 -0
- package/lib/core/contexts/miscellany.json +19 -0
- package/lib/core/contexts/schemaorg.json +8845 -0
- package/lib/core/contexts/security-data-integrity-v1.json +78 -0
- package/lib/core/contexts/security-data-integrity-v2.json +81 -0
- package/lib/core/contexts/security-multikey-v1.json +35 -0
- package/lib/core/contexts/security-v1.json +74 -0
- package/lib/core/contexts/webfinger.json +10 -0
- package/lib/{deliver.mjs → core/deliver.mjs} +2 -2
- package/lib/core/intake/activities.mjs +437 -0
- package/lib/core/intake/activity.mjs +240 -0
- package/lib/core/intake/channel.mjs +144 -0
- package/lib/core/intake/group.mjs +222 -0
- package/lib/core/intake/index.mjs +629 -0
- package/lib/core/intake/notes.mjs +288 -0
- package/lib/core/intake/verify.mjs +141 -0
- package/lib/{keys.mjs → core/keys.mjs} +1 -1
- package/lib/core/publisher/collections.mjs +229 -0
- package/lib/core/publisher/index.mjs +421 -0
- package/lib/core/publisher/notes.mjs +188 -0
- package/lib/core/publisher/questions.mjs +233 -0
- package/lib/core/publisher/restore.mjs +196 -0
- package/lib/core/shapes/activitystreams.ttl +129 -0
- package/lib/core/shapes/index.mjs +107 -0
- package/lib/core/shapes/shapes-text.mjs +13 -0
- package/lib/{social.mjs → core/social.mjs} +2 -2
- package/lib/{store.mjs → core/store.mjs} +4 -0
- package/lib/{wire.mjs → core/wire.mjs} +2 -2
- package/lib/device/admin/index.mjs +13 -0
- package/lib/device/admin/origins.mjs +35 -0
- package/lib/device/admin/routes/connections.mjs +144 -0
- package/lib/device/admin/routes/gateway.mjs +199 -0
- package/lib/device/admin/routes/lifecycle.mjs +191 -0
- package/lib/device/admin/routes/owner.mjs +322 -0
- package/lib/device/admin/routes/setup.mjs +393 -0
- package/lib/device/admin/routes/social.mjs +188 -0
- package/lib/device/admin/server.mjs +95 -0
- package/lib/device/admin/static.mjs +244 -0
- package/lib/device/admin/surface.mjs +274 -0
- package/lib/device/cli/commands/account.mjs +586 -0
- package/lib/device/cli/commands/run.mjs +278 -0
- package/lib/device/cli/commands/service.mjs +221 -0
- package/lib/device/cli/commands/setup.mjs +410 -0
- package/lib/device/cli/commands/state.mjs +559 -0
- package/lib/device/cli/context.mjs +288 -0
- package/lib/{migrate.mjs → device/migrate.mjs} +1 -1
- package/lib/{remote.mjs → device/remote.mjs} +3 -3
- package/lib/{setup.mjs → device/setup.mjs} +3 -3
- package/lib/{update.mjs → device/update.mjs} +1 -1
- package/lib/{directory.mjs → gateway/directory.mjs} +1 -1
- package/lib/{front-core.mjs → gateway/front-core.mjs} +3 -3
- package/lib/{gateway-core.mjs → gateway/gateway-core.mjs} +1 -1
- package/lib/{httpsig.mjs → gateway/httpsig.mjs} +1 -1
- package/lib/{embed.mjs → server/embed.mjs} +131 -22
- package/lib/{links.mjs → shared/links.mjs} +1 -1
- package/lib/{ua.mjs → shared/ua.mjs} +1 -1
- package/package.json +11 -1
- package/run-agent.mjs +33 -25
- package/scripts/build-app.mjs +4 -0
- package/scripts/check-pod-calls.mjs +8 -2
- package/scripts/refresh-contexts.mjs +39 -0
- package/web/admin/actors.js +145 -0
- package/web/admin/common.js +23 -0
- package/web/admin/connections.js +112 -0
- package/web/admin/gateway.js +111 -0
- package/web/admin/group.js +258 -0
- package/web/admin/index.html +7 -1
- package/web/admin/record.js +378 -0
- package/web/admin/setup/index.html +1 -0
- package/web/admin/setup/setup.js +2 -13
- package/web/admin/upkeep.js +170 -0
- package/web/app/README.md +6 -6
- package/web/app/admin-facade.mjs +3 -3
- package/web/app/agent.mjs +12 -12
- package/web/app/atproto-browser.mjs +1 -1
- package/web/app/deliver-relay.mjs +1 -1
- package/web/app/dist/sw.js +21684 -5415
- package/web/app/dist/sw.js.map +4 -4
- package/web/app/fediacct-browser.mjs +1 -1
- package/web/app/shims/shapes-text.mjs +8 -0
- package/web/app/site/admin/actors.js +145 -0
- package/web/app/site/admin/common.js +23 -0
- package/web/app/site/admin/connections.js +112 -0
- package/web/app/site/admin/gateway.js +111 -0
- package/web/app/site/admin/group.js +258 -0
- package/web/app/site/admin/index.html +7 -1
- package/web/app/site/admin/record.js +378 -0
- package/web/app/site/admin/setup/index.html +1 -0
- package/web/app/site/admin/setup/setup.js +2 -13
- package/web/app/site/admin/upkeep.js +170 -0
- package/web/app/site/sw.js +21684 -5415
- package/web/app/sw-src.mjs +17 -2
- package/lib/admin.mjs +0 -1913
- package/lib/intake.mjs +0 -1981
- package/lib/mastoapi.mjs +0 -2284
- package/lib/publisher.mjs +0 -1192
- package/web/admin/admin.js +0 -1181
- package/web/app/site/admin/admin.js +0 -1181
- /package/lib/{oidc-auth.mjs → client/oidc-auth.mjs} +0 -0
- /package/lib/{webpush.mjs → client/webpush.mjs} +0 -0
- /package/lib/{bskyfeed.mjs → connections/bskyfeed.mjs} +0 -0
- /package/lib/{lease.mjs → core/lease.mjs} +0 -0
- /package/lib/{polls.mjs → core/polls.mjs} +0 -0
- /package/lib/{proof.mjs → core/proof.mjs} +0 -0
- /package/lib/{storage.mjs → core/storage.mjs} +0 -0
- /package/lib/{account.mjs → device/account.mjs} +0 -0
- /package/lib/{certs.mjs → device/certs.mjs} +0 -0
- /package/lib/{export-collections.mjs → device/export-collections.mjs} +0 -0
- /package/lib/{home.mjs → device/home.mjs} +0 -0
- /package/lib/{ports.mjs → device/ports.mjs} +0 -0
- /package/lib/{guard.mjs → shared/guard.mjs} +0 -0
- /package/lib/{safefetch.mjs → shared/safefetch.mjs} +0 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// context.mjs — what every command starts from: the arguments, which
|
|
2
|
+
// identity (HOME) and port (PORT) a command means, and the helpers that ask,
|
|
3
|
+
// probe and finish. HOME, PORT and DEFAULT_ISSUE are live bindings: useProfile()
|
|
4
|
+
// reassigns them and every command module sees the new value.
|
|
5
|
+
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import { localFetch } from '../../../lib/client/localapi.mjs';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import net from 'node:net';
|
|
11
|
+
import readline from 'node:readline';
|
|
12
|
+
import { spawn } from 'node:child_process';
|
|
13
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
14
|
+
import { apRoot, profilesDir, identityHomes, isLegacyRoot, CURRENT_ROOT, tildify, rootOf,
|
|
15
|
+
readRoot, writeRoot, defaultProfile, profileHome, rootHoldsIdentity, ROOT_FILE,
|
|
16
|
+
recordLastUsed, writeJsonAtomic } from '../../../lib/device/home.mjs';
|
|
17
|
+
import { insecureUrlReason } from '../../../lib/shared/safefetch.mjs';
|
|
18
|
+
import { portFree, freePortFrom } from '../../../lib/device/ports.mjs';
|
|
19
|
+
|
|
20
|
+
export const args = process.argv.slice(2);
|
|
21
|
+
export const cmd = args[0];
|
|
22
|
+
export const flag = (name, dflt) => {
|
|
23
|
+
const i = args.indexOf('--' + name);
|
|
24
|
+
return i >= 0 ? args[i + 1] : dflt;
|
|
25
|
+
};
|
|
26
|
+
export const has = (name) => args.includes('--' + name);
|
|
27
|
+
// The port can also be given bare — `npm start 8081` reaches us as `up 8081`,
|
|
28
|
+
// because npm passes positionals through but eats `--port`. Bare form only for
|
|
29
|
+
// the start-style commands, where a lone number cannot mean anything else.
|
|
30
|
+
export const barePort = () => {
|
|
31
|
+
if (!['up', 'start', 'run'].includes(cmd)) return null;
|
|
32
|
+
for (let i = 1; i < args.length; i++) {
|
|
33
|
+
if (/^\d+$/.test(args[i]) && !args[i - 1].startsWith('--')) return args[i];
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
};
|
|
37
|
+
export const portFlag = () => flag('port', null) || barePort();
|
|
38
|
+
// One identity per home, and EVERY identity is `<root>/profiles/<name>/`. There
|
|
39
|
+
// is no privileged unnamed one: `root.json` names which you get when you do not
|
|
40
|
+
// say, and that is a pointer you can change rather than a directory you have to
|
|
41
|
+
// move a private key out of. lib/home.mjs decides the root.
|
|
42
|
+
//
|
|
43
|
+
// An explicit --home / AP_HOME still wins and is taken literally: you named a
|
|
44
|
+
// directory, so that directory is the identity, root.json unread. That is what
|
|
45
|
+
// `rootOf` has always documented for a custom home.
|
|
46
|
+
export const PROFILE = flag('profile', process.env.AP_PROFILE || null);
|
|
47
|
+
export const AP_ROOT = apRoot();
|
|
48
|
+
export const PROFILES_DIR = profilesDir(AP_ROOT);
|
|
49
|
+
|
|
50
|
+
// `let`, because setup does not know which identity it is until it has asked for
|
|
51
|
+
// the handle — the home is named after it. Everything below reads HOME at call
|
|
52
|
+
// time, so reassigning it once, early, is enough; PORT is the exception and is
|
|
53
|
+
// recomputed with it.
|
|
54
|
+
export let DEFAULT_ISSUE = null; // set when the pointer is unusable
|
|
55
|
+
export let HOME = flag('home', process.env.AP_HOME || (() => {
|
|
56
|
+
if (PROFILE) return profileHome(AP_ROOT, PROFILE);
|
|
57
|
+
const d = defaultProfile(AP_ROOT);
|
|
58
|
+
if (typeof d === 'string') return profileHome(AP_ROOT, d);
|
|
59
|
+
DEFAULT_ISSUE = d?.missing
|
|
60
|
+
? `${ROOT_FILE} names "${d.missing}", which is not an identity here`
|
|
61
|
+
: 'there is more than one identity here and none is the default';
|
|
62
|
+
return profileHome(AP_ROOT, d?.missing || '');
|
|
63
|
+
})());
|
|
64
|
+
|
|
65
|
+
// The port chosen at setup is remembered, so `start`/`stop`/`status` need no
|
|
66
|
+
// flags afterwards. Precedence: --port (or a bare port number, `npm start
|
|
67
|
+
// 8081`) > AP_PORT > the recorded choice > 8030.
|
|
68
|
+
// The handle is remembered alongside it, for the named origin: the agent also
|
|
69
|
+
// answers at <handle>.localhost:<port>, and that has to work from the first
|
|
70
|
+
// request, before pod state has been read.
|
|
71
|
+
export function recordedAgent() {
|
|
72
|
+
try { return JSON.parse(fs.readFileSync(path.join(HOME, 'agent.json'), 'utf8')) || {}; }
|
|
73
|
+
catch { return {}; }
|
|
74
|
+
}
|
|
75
|
+
export function recordedPort() { return Number(recordedAgent().port) || null; }
|
|
76
|
+
export function recordAgent(fields) {
|
|
77
|
+
try {
|
|
78
|
+
fs.mkdirSync(HOME, { recursive: true, mode: 0o700 });
|
|
79
|
+
const rec = { ...recordedAgent(), ...fields };
|
|
80
|
+
writeJsonAtomic(path.join(HOME, 'agent.json'), rec, { mode: 0o644 });
|
|
81
|
+
} catch { /* the flag still works, it just isn't remembered */ }
|
|
82
|
+
}
|
|
83
|
+
export let PORT = Number(portFlag() || process.env.AP_PORT || recordedPort() || 8030);
|
|
84
|
+
|
|
85
|
+
// Setup is the one command that cannot know its home in advance: the identity is
|
|
86
|
+
// named after the handle, and the handle is the first thing it asks. Everything
|
|
87
|
+
// that reads HOME does so at call time, so pointing it at the right directory as
|
|
88
|
+
// soon as the name exists is enough — PORT is recomputed because it was read
|
|
89
|
+
// from the old home's agent.json.
|
|
90
|
+
export function useProfile(name) {
|
|
91
|
+
HOME = flag('home', process.env.AP_HOME || profileHome(AP_ROOT, name));
|
|
92
|
+
PORT = Number(portFlag() || process.env.AP_PORT || recordedPort() || 8030);
|
|
93
|
+
DEFAULT_ISSUE = null;
|
|
94
|
+
return HOME;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// A handle becomes a directory name, so it is checked before it is one. Same
|
|
98
|
+
// rule the admin API applies before creating an actor (admin.mjs) — without it
|
|
99
|
+
// a handle containing a slash or `..` climbs out of profiles/.
|
|
100
|
+
const HANDLE_RE = /^[a-z0-9][a-z0-9_-]{0,30}$/;
|
|
101
|
+
export function requireHandle(handle) {
|
|
102
|
+
if (HANDLE_RE.test(handle)) return handle;
|
|
103
|
+
console.error(`"${handle}" cannot be a handle: letters, digits, hyphens and underscores,`);
|
|
104
|
+
console.error('starting with a letter or digit, at most 31 characters.');
|
|
105
|
+
process.exit(2);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Commands that act on an identity need one decided. Says which of the three
|
|
109
|
+
// ways it failed, because "no identity" and "which identity" are different
|
|
110
|
+
// problems with different fixes.
|
|
111
|
+
export function requireIdentity() {
|
|
112
|
+
// An explicit AP_HOME / --home is an explicit identity directory. Nothing
|
|
113
|
+
// about the machine's root applies to it — including whether that root has
|
|
114
|
+
// been restructured, which is somebody else's install's problem.
|
|
115
|
+
if (process.env.AP_HOME || flag('home')) return;
|
|
116
|
+
if (rootHoldsIdentity(AP_ROOT)) {
|
|
117
|
+
console.error(`${tildify(AP_ROOT)} still keeps an identity at its top level.`);
|
|
118
|
+
console.error('Every identity lives in profiles/<name>/ now. Move this one down with:\n');
|
|
119
|
+
console.error(` ${process.argv[1]} home --restructure\n`);
|
|
120
|
+
process.exit(2);
|
|
121
|
+
}
|
|
122
|
+
if (!DEFAULT_ISSUE) return;
|
|
123
|
+
const homes = identityHomes(AP_ROOT).filter(h => fs.existsSync(path.join(h.dir, 'credential.json')));
|
|
124
|
+
console.error(DEFAULT_ISSUE + '.');
|
|
125
|
+
if (homes.length) {
|
|
126
|
+
console.error(`\n ${process.argv[1]} --profile <name> start\n`);
|
|
127
|
+
console.error('Whichever you start is remembered, so plain commands mean that one afterwards.');
|
|
128
|
+
console.error(`here: ${homes.map(h => h.name).join(', ')}`);
|
|
129
|
+
} else {
|
|
130
|
+
console.error(`\nThere are no identities yet — ${process.argv[1]} setup`);
|
|
131
|
+
}
|
|
132
|
+
process.exit(2);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function askHidden(prompt) {
|
|
136
|
+
return new Promise((resolve) => {
|
|
137
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
138
|
+
const onData = (c) => { if (String(c) !== '\n' && String(c) !== '\r') readline.moveCursor(process.stdout, -1, 0), process.stdout.write('*'); };
|
|
139
|
+
process.stdin.on('data', onData);
|
|
140
|
+
rl.question(prompt, (answer) => { process.stdin.off('data', onData); rl.close(); process.stdout.write('\n'); resolve(answer); });
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Plain prompt with a default: Enter accepts it. Non-interactive runs
|
|
145
|
+
// (scripts, CI) take the default silently, so flags remain sufficient.
|
|
146
|
+
let sharedRl = null; // one interface: a new one per
|
|
147
|
+
export function ask(prompt, dflt = '') { // question would drop buffered input
|
|
148
|
+
if (!process.stdin.isTTY) return Promise.resolve(dflt);
|
|
149
|
+
sharedRl ||= readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
150
|
+
return new Promise((resolve) => {
|
|
151
|
+
sharedRl.question(dflt ? `${prompt} [${dflt}]: ` : `${prompt}: `, (answer) => {
|
|
152
|
+
resolve(String(answer).trim() || dflt);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
export function endAsking() { sharedRl?.close(); sharedRl = null; }
|
|
157
|
+
|
|
158
|
+
// "Occupied" means "cannot be bound", not "does not answer HTTP": something
|
|
159
|
+
// holding a port without speaking HTTP reads as free to a GET, and then the
|
|
160
|
+
// agent dies on EADDRINUSE.
|
|
161
|
+
|
|
162
|
+
// The first port from `first` upward that binds. Walking always ends in one,
|
|
163
|
+
// so this is a step rather than a condition.
|
|
164
|
+
|
|
165
|
+
// Is anything at all on this port? For the operations that MOVE data, "it did
|
|
166
|
+
// not answer as one of ours" is not the same as "nothing is there": an agent
|
|
167
|
+
// started with AP_GATE_TOKEN answers 401 to an un-tokened /status, so agentOn
|
|
168
|
+
// reads a perfectly live agent as stopped — and a sweep that trusted it would
|
|
169
|
+
// copy the state out from under one, which the next write then overwrites.
|
|
170
|
+
// Bind to find out, and fail closed.
|
|
171
|
+
export async function somethingOn(port) {
|
|
172
|
+
const mine = await agentOn(port);
|
|
173
|
+
if (mine) return 'running';
|
|
174
|
+
return (await portFree(port)) ? null : 'something is on the port and did not answer as ours';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Whatever is on the port — is it one of ours?
|
|
178
|
+
export async function agentOn(port) {
|
|
179
|
+
try {
|
|
180
|
+
const res = await localFetch(HOME, port, `/status`, { signal: AbortSignal.timeout(2000) });
|
|
181
|
+
const body = await res.json();
|
|
182
|
+
return typeof body?.configured === 'boolean' ? body : null;
|
|
183
|
+
} catch { return null; }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export const isInside = (root, p) => {
|
|
187
|
+
const rel = path.relative(root, p);
|
|
188
|
+
return !!rel && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export function openBrowser(url) {
|
|
192
|
+
try {
|
|
193
|
+
const win = process.platform === 'win32';
|
|
194
|
+
const cmd = win ? 'cmd' : process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
195
|
+
const child = spawn(cmd, win ? ['/c', 'start', '', url] : [url], { detached: true, stdio: 'ignore' });
|
|
196
|
+
// A box with no opener at all (a server, a bare container) emits this
|
|
197
|
+
// asynchronously, where the try/catch cannot reach it — and an unhandled
|
|
198
|
+
// 'error' event on a child process ends the agent.
|
|
199
|
+
child.on('error', () => {});
|
|
200
|
+
child.unref();
|
|
201
|
+
} catch { /* best-effort — the URL is printed anyway */ }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// A one-shot command holds the lease only for its own duration: exiting without
|
|
205
|
+
// releasing it leaves the next `start` as a read-only viewer until the lease
|
|
206
|
+
// expires, which is 5 minutes of doing nothing.
|
|
207
|
+
export async function finish(agent, code = 0) {
|
|
208
|
+
await agent.lease?.release().catch(() => {});
|
|
209
|
+
await agent.store?.flush?.().catch(() => {});
|
|
210
|
+
process.exit(code);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Anything that decides the identity. Given even one of these, setup stays on
|
|
214
|
+
// the command line exactly as it always did — scripts, CI and the tarball's
|
|
215
|
+
// unpack-and-go line depend on that. At a terminal with none of them, setup
|
|
216
|
+
// asks the two things it needs to open a browser and asks the rest there.
|
|
217
|
+
export const IDENTITY_FLAGS = ['new-account', 'pod', 'issuer', 'email', 'name', 'pod-name',
|
|
218
|
+
'group', 'approve-joins', 'summary', 'icon', 'root', 'keys', 'rotate-key'];
|
|
219
|
+
|
|
220
|
+
// Refuse before anything is asked, let alone typed: setup used to overwrite
|
|
221
|
+
// credential.json in place, and a minted credential is only shown once — so
|
|
222
|
+
// the identity it belonged to could not be recovered afterwards.
|
|
223
|
+
export function refuseExistingIdentity() {
|
|
224
|
+
const credPath = path.join(HOME, 'credential.json');
|
|
225
|
+
if (!fs.existsSync(credPath) || has('force')) return;
|
|
226
|
+
let held = '(unreadable)';
|
|
227
|
+
try { held = JSON.parse(fs.readFileSync(credPath, 'utf8')).remotePod; } catch {}
|
|
228
|
+
console.error(`${HOME} already holds an identity: ${held}`);
|
|
229
|
+
console.error('For another identity: fedipod setup --profile <name>');
|
|
230
|
+
console.error('To list what exists: fedipod profiles');
|
|
231
|
+
console.error('To replace this one: add --force (the old credential is lost)');
|
|
232
|
+
console.error('');
|
|
233
|
+
console.error('If a setup died half-way, do NOT re-run it — the credential it already');
|
|
234
|
+
console.error('minted cannot be minted twice. Run `fedipod start` and');
|
|
235
|
+
console.error('finish at /admin/setup/ in the browser.');
|
|
236
|
+
process.exit(2);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Ask the handle (permanent, and it names the origin) and the port, start
|
|
240
|
+
// serving, and hand over to the page at /admin/setup/. Nothing is created here: the
|
|
241
|
+
// agent's own POST /setup does all of it, so a closed tab cannot lose a
|
|
242
|
+
// credential that only exists in an HTTP response.
|
|
243
|
+
export async function runBrowserSetup() {
|
|
244
|
+
const handle = flag('handle') || await ask('handle (the name in your address; permanent)');
|
|
245
|
+
if (!handle) { console.error('a handle is required'); process.exit(2); }
|
|
246
|
+
// The handle names the home, so nothing can be decided before it — including
|
|
247
|
+
// which identity would be overwritten, and which port was remembered.
|
|
248
|
+
requireHandle(handle);
|
|
249
|
+
useProfile(PROFILE || handle);
|
|
250
|
+
refuseExistingIdentity();
|
|
251
|
+
const port = portFlag() ? PORT : (Number(await ask('port', String(PORT))) || PORT);
|
|
252
|
+
endAsking();
|
|
253
|
+
|
|
254
|
+
// Recorded before the server starts, so `stop`/`status` work while the
|
|
255
|
+
// browser flow is still open — it used to be written only after the mint.
|
|
256
|
+
recordAgent({ port, handle });
|
|
257
|
+
|
|
258
|
+
const { Agent } = await import(new URL('../../../run-agent.mjs', import.meta.url));
|
|
259
|
+
const { startAdmin } = await import(new URL('../../../lib/device/admin/index.mjs', import.meta.url));
|
|
260
|
+
const { hostLabel } = await import(new URL('../../../lib/shared/guard.mjs', import.meta.url));
|
|
261
|
+
const agent = new Agent({ home: HOME, log: (...a) => console.log('[ap]', ...a) });
|
|
262
|
+
startAdmin({
|
|
263
|
+
port, handle, agent,
|
|
264
|
+
gateToken: process.env.AP_GATE_TOKEN || '',
|
|
265
|
+
log: (...a) => console.log('[ap]', ...a),
|
|
266
|
+
});
|
|
267
|
+
const shutdown = () => {
|
|
268
|
+
setTimeout(() => process.exit(0), 1500).unref();
|
|
269
|
+
try { fs.rmSync(path.join(HOME, 'agent.pid'), { force: true }); } catch {}
|
|
270
|
+
Promise.allSettled([agent.store.flush(), agent.lease?.release()]).finally(() => process.exit(0));
|
|
271
|
+
};
|
|
272
|
+
process.on('SIGINT', shutdown);
|
|
273
|
+
process.on('SIGTERM', shutdown);
|
|
274
|
+
|
|
275
|
+
const label = hostLabel(handle);
|
|
276
|
+
const named = label ? `https://${label}.localhost:${port}/` : null;
|
|
277
|
+
const plain = `https://localhost:${port}/`;
|
|
278
|
+
const pad = Math.max(named?.length || 0, plain.length);
|
|
279
|
+
console.log('');
|
|
280
|
+
if (named) {
|
|
281
|
+
console.log(` ${named.padEnd(pad)} <- opening this`);
|
|
282
|
+
console.log(` ${plain.padEnd(pad)} <- the same agent, if your browser cannot find that name`);
|
|
283
|
+
} else {
|
|
284
|
+
console.log(` ${plain}`);
|
|
285
|
+
}
|
|
286
|
+
console.log('\nsetup continues in the browser — Ctrl-C to stop\n');
|
|
287
|
+
openBrowser(named || plain);
|
|
288
|
+
}
|
|
@@ -99,7 +99,7 @@ export function classifyRemoteState(urls, base) {
|
|
|
99
99
|
// a failure part-way leaves the source authoritative.
|
|
100
100
|
// Used by both `state --to` and the record page's Move private data.
|
|
101
101
|
export async function copyPrivateHalf({ from, to, log = () => {} }) {
|
|
102
|
-
const { PodStore } = await import('
|
|
102
|
+
const { PodStore } = await import('../core/store.mjs');
|
|
103
103
|
const src = new PodStore({ log: () => {} });
|
|
104
104
|
src.attach(from.state);
|
|
105
105
|
await src.load();
|
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
import { createRequire } from 'node:module';
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
|
-
import { PodTransport } from '
|
|
15
|
+
import { PodTransport } from '../pod/transport.mjs';
|
|
16
16
|
|
|
17
17
|
const require = createRequire(import.meta.url);
|
|
18
|
-
const vendorDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '
|
|
18
|
+
const vendorDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../vendor');
|
|
19
19
|
const { mintCredential, discoverTokenEndpoint, createGrantSession, revokeCredentialViaAccount } =
|
|
20
20
|
require(path.join(vendorDir, 'idp-grant.cjs'));
|
|
21
21
|
|
|
@@ -23,7 +23,7 @@ export { mintCredential, discoverTokenEndpoint, revokeCredentialViaAccount };
|
|
|
23
23
|
|
|
24
24
|
// Re-exported so the callers that guard their own deletions — run-agent, the
|
|
25
25
|
// server package's live test, the smoke suite — keep importing it from here.
|
|
26
|
-
export { protectedFromDeletion } from '
|
|
26
|
+
export { protectedFromDeletion } from '../pod/transport.mjs';
|
|
27
27
|
|
|
28
28
|
export class RemotePod extends PodTransport {
|
|
29
29
|
constructor(credential, { log = () => {}, home = null, session = null, role = 'agent' } = {}) {
|
|
@@ -15,10 +15,10 @@ import * as $rdf from 'rdflib';
|
|
|
15
15
|
|
|
16
16
|
import { createAccountWithPod as realCreateAccount } from './account.mjs';
|
|
17
17
|
import { mintCredential as realMint } from './remote.mjs';
|
|
18
|
-
import { hashPassword } from '
|
|
19
|
-
import { webfingerHost } from '
|
|
18
|
+
import { hashPassword } from '../client/masto/index.mjs';
|
|
19
|
+
import { webfingerHost } from '../core/wire.mjs';
|
|
20
20
|
import { rootOf, recordLastUsed, writeJsonAtomic } from './home.mjs';
|
|
21
|
-
import { insecureUrlReason } from '
|
|
21
|
+
import { insecureUrlReason } from '../shared/safefetch.mjs';
|
|
22
22
|
import { CURRENT_LAYOUT, isCurrent } from './migrate.mjs';
|
|
23
23
|
|
|
24
24
|
const SOLID = $rdf.Namespace('http://www.w3.org/ns/solid/terms#');
|
|
@@ -13,7 +13,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
13
13
|
|
|
14
14
|
export const LATEST_URL = 'https://raw.githubusercontent.com/jeff-zucker/FediPod/main/package.json';
|
|
15
15
|
|
|
16
|
-
export const repoRoot = () => path.resolve(path.dirname(fileURLToPath(import.meta.url)), '
|
|
16
|
+
export const repoRoot = () => path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
|
17
17
|
|
|
18
18
|
export const localVersion = (root = repoRoot()) => {
|
|
19
19
|
try { return JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).version || null; }
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// whole mechanism (the test suite sets it, so throwaway agents do not fight
|
|
14
14
|
// over a real machine's door).
|
|
15
15
|
import https from 'node:https';
|
|
16
|
-
import { localFetch } from '
|
|
16
|
+
import { localFetch } from '../client/localapi.mjs';
|
|
17
17
|
|
|
18
18
|
export const DIRECTORY_PORT = 8030;
|
|
19
19
|
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
|
|
18
18
|
import crypto from 'node:crypto';
|
|
19
19
|
import { handleDelivery } from './gateway-core.mjs';
|
|
20
|
-
import { readCapped, safeFetch, isLoopbackHost } from '
|
|
21
|
-
import * as podRoot from '
|
|
22
|
-
import * as podPolicy from '
|
|
20
|
+
import { readCapped, safeFetch, isLoopbackHost } from '../shared/safefetch.mjs';
|
|
21
|
+
import * as podRoot from '../pod/root.mjs';
|
|
22
|
+
import * as podPolicy from '../pod/policy.mjs';
|
|
23
23
|
|
|
24
24
|
// The one WebFinger document, spelled out here rather than imported from
|
|
25
25
|
// wire.mjs: wire drags the agent's whole HTML pipeline (sanitize-html and
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import crypto from 'node:crypto';
|
|
16
16
|
import { verifyHttpSignature, makeSafeLoader, makeReceipt, signReceipt } from './httpsig.mjs';
|
|
17
|
-
import * as inbox from '
|
|
17
|
+
import * as inbox from '../pod/inbox.mjs';
|
|
18
18
|
|
|
19
19
|
const DEFAULT_MAX_BYTES = 512 * 1024; // mirror intake.mjs MAX_ITEM_BYTES
|
|
20
20
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import crypto from 'node:crypto';
|
|
13
13
|
import { verifyRequestDetailed } from '@fedify/fedify/sig';
|
|
14
|
-
import { assertPublicUrl, safeFetch, readCapped } from '
|
|
14
|
+
import { assertPublicUrl, safeFetch, readCapped } from '../shared/safefetch.mjs';
|
|
15
15
|
|
|
16
16
|
// An SSRF-safe JSON-LD document loader for Fedify's key fetch. The keyId in a
|
|
17
17
|
// delivery is chosen by the sender, so every deref it triggers is untrusted
|
|
@@ -16,15 +16,16 @@ import path from 'node:path';
|
|
|
16
16
|
|
|
17
17
|
import { createRequire } from 'node:module';
|
|
18
18
|
|
|
19
|
-
import { Agent } from '
|
|
20
|
-
import { RemotePod } from '
|
|
21
|
-
import { apUrls } from '
|
|
22
|
-
import { writeJsonAtomic } from '
|
|
23
|
-
import { buildAdminSurface } from '
|
|
24
|
-
import { FixedAuthorities } from '
|
|
19
|
+
import { Agent } from '../../run-agent.mjs';
|
|
20
|
+
import { RemotePod } from '../device/remote.mjs';
|
|
21
|
+
import { apUrls, DEFAULT_ROOT } from '../core/wire.mjs';
|
|
22
|
+
import { writeJsonAtomic } from '../device/home.mjs';
|
|
23
|
+
import { buildAdminSurface } from '../device/admin/index.mjs';
|
|
24
|
+
import { FixedAuthorities } from '../shared/guard.mjs';
|
|
25
|
+
import { CONNECTION_PREFIX, fileVault, moveVault, podVault } from '../connections/vault.mjs';
|
|
25
26
|
|
|
26
27
|
const require = createRequire(import.meta.url);
|
|
27
|
-
const { makeGate } = require('
|
|
28
|
+
const { makeGate } = require('../../vendor/gate.cjs');
|
|
28
29
|
|
|
29
30
|
// How long to gather store events before draining, so a delivery of several
|
|
30
31
|
// items costs one sweep rather than one each.
|
|
@@ -34,23 +35,57 @@ const DRAIN_COALESCE_MS = 250;
|
|
|
34
35
|
const mintSecret = () => crypto.randomBytes(32).toString('base64');
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
|
-
* The secret guarding one identity's owner door, kept
|
|
38
|
-
* Minted when absent; `rotate` re-mints over an
|
|
39
|
-
* lost secret is recovered, by proving pod
|
|
38
|
+
* The secret guarding one identity's owner door, kept in its pod's state
|
|
39
|
+
* beside its signing key. Minted when absent; `rotate` re-mints over an
|
|
40
|
+
* existing one — which is how a lost secret is recovered, by proving pod
|
|
41
|
+
* control again. Never logged.
|
|
42
|
+
*
|
|
43
|
+
* `dataDir`, when given, is where an identity set up before this kept it: the
|
|
44
|
+
* secret is moved into the pod and the host's copy removed, so the owner's
|
|
45
|
+
* existing door link goes on working.
|
|
40
46
|
*/
|
|
41
|
-
export function ensureDoorSecret(
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
const
|
|
47
|
+
export async function ensureDoorSecret(session, podBase, { rotate = false, dataDir = null, handle = null, log = () => {} } = {}) {
|
|
48
|
+
const base = podBase.endsWith('/') ? podBase : podBase + '/';
|
|
49
|
+
const url = apUrls(base, DEFAULT_ROOT).state + 'door-secret.json';
|
|
50
|
+
const onHost = dataDir && handle ? path.join(dataDir, handle, 'door-secret.json') : null;
|
|
51
|
+
|
|
45
52
|
if (!rotate) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
const res = await session.fetch(url, { headers: { accept: 'application/json' } });
|
|
54
|
+
if (res.ok) {
|
|
55
|
+
try {
|
|
56
|
+
const rec = JSON.parse(await res.text());
|
|
57
|
+
if (rec?.secret) {
|
|
58
|
+
if (onHost) fs.rmSync(onHost, { force: true });
|
|
59
|
+
return { secret: rec.secret, url, rotated: false };
|
|
60
|
+
}
|
|
61
|
+
} catch { /* unreadable: mint below */ }
|
|
62
|
+
}
|
|
63
|
+
// Set up before the secret lived on the pod: keep the one the owner has.
|
|
64
|
+
if (onHost && fs.existsSync(onHost)) {
|
|
65
|
+
try {
|
|
66
|
+
const rec = JSON.parse(fs.readFileSync(onHost, 'utf8'));
|
|
67
|
+
if (rec?.secret && await putSecret(session, url, rec)) {
|
|
68
|
+
fs.rmSync(onHost, { force: true });
|
|
69
|
+
log('door secret moved into the pod');
|
|
70
|
+
return { secret: rec.secret, url, rotated: false };
|
|
71
|
+
}
|
|
72
|
+
} catch { /* unreadable: mint below */ }
|
|
73
|
+
}
|
|
50
74
|
}
|
|
75
|
+
|
|
51
76
|
const rec = { secret: mintSecret(), mintedAt: new Date().toISOString() };
|
|
52
|
-
|
|
53
|
-
|
|
77
|
+
if (!await putSecret(session, url, rec)) throw new Error(`could not write the door secret to ${url}`);
|
|
78
|
+
if (onHost) fs.rmSync(onHost, { force: true });
|
|
79
|
+
return { secret: rec.secret, url, rotated: rotate };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function putSecret(session, url, rec) {
|
|
83
|
+
const res = await session.fetch(url, {
|
|
84
|
+
method: 'PUT',
|
|
85
|
+
headers: { 'content-type': 'application/json' },
|
|
86
|
+
body: JSON.stringify(rec, null, 2) + '\n',
|
|
87
|
+
});
|
|
88
|
+
return res.ok;
|
|
54
89
|
}
|
|
55
90
|
|
|
56
91
|
/** The identity's name: a subdomain pod is its label, a path pod its last segment. */
|
|
@@ -64,20 +99,90 @@ export function handleFor(podBase) {
|
|
|
64
99
|
* The credential file an embedded identity runs on. It names the pod and
|
|
65
100
|
* nothing else: there is no client id and no secret, because there is nobody
|
|
66
101
|
* to authenticate to. Absent `privateRoot`, so the state tree stays on the pod.
|
|
102
|
+
*
|
|
103
|
+
* `keysMode: 'pod'` because here the pod's server and the agent are the same
|
|
104
|
+
* process: keeping the signing key on the host's disk puts it somewhere the
|
|
105
|
+
* pod does not travel to, so handing the pod over hands over an identity that
|
|
106
|
+
* cannot sign. An identity set up before this gets the mode added.
|
|
67
107
|
*/
|
|
68
108
|
function ensureCredential(home, { podBase, webId }) {
|
|
69
109
|
const file = path.join(home, 'credential.json');
|
|
70
|
-
try {
|
|
71
|
-
|
|
110
|
+
try {
|
|
111
|
+
const rec = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
112
|
+
if (rec.keysMode === 'pod') return rec;
|
|
113
|
+
const updated = { ...rec, keysMode: 'pod' };
|
|
114
|
+
writeJsonAtomic(file, updated, { mode: 0o600 });
|
|
115
|
+
return updated;
|
|
116
|
+
} catch { /* first run for this identity */ }
|
|
72
117
|
const rec = {
|
|
73
118
|
webId,
|
|
74
119
|
remotePod: podBase.endsWith('/') ? podBase : podBase + '/',
|
|
75
120
|
root: 'activitypods-js/',
|
|
121
|
+
keysMode: 'pod',
|
|
76
122
|
};
|
|
77
123
|
writeJsonAtomic(file, rec, { mode: 0o600 });
|
|
78
124
|
return rec;
|
|
79
125
|
}
|
|
80
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Move an identity's signing key off the host's disk and into its pod.
|
|
129
|
+
*
|
|
130
|
+
* Only after the pod has it: the private key is the one thing here that
|
|
131
|
+
* nothing can rebuild, and remote servers have its public half cached, so a
|
|
132
|
+
* copy lost between the two places is an identity that can never sign again
|
|
133
|
+
* under the name it already published.
|
|
134
|
+
*/
|
|
135
|
+
async function keyIntoPod(agent, home, log) {
|
|
136
|
+
const local = path.join(home, 'keys.json');
|
|
137
|
+
if (!fs.existsSync(local)) return;
|
|
138
|
+
if (agent.store.read('keys.json', null)) {
|
|
139
|
+
fs.rmSync(local, { force: true });
|
|
140
|
+
log('this identity signs with the key in its pod; removed the copy on this host');
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
let rec;
|
|
144
|
+
try {
|
|
145
|
+
rec = JSON.parse(fs.readFileSync(local, 'utf8'));
|
|
146
|
+
} catch (e) {
|
|
147
|
+
log(`the local signing key could not be read (${e.message}) — leaving it where it is`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
agent.store.write('keys.json', rec);
|
|
151
|
+
if (!await agent.store.commit()) {
|
|
152
|
+
log('could not write the signing key to the pod — it stays on this host for now');
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
fs.rmSync(local, { force: true });
|
|
156
|
+
log('signing key moved into the pod');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Move an identity's credentials for accounts it holds on OTHER servers off
|
|
161
|
+
* the host's disk and into its pod. A token here is full access to somebody's
|
|
162
|
+
* account on a server this project does not run, so the host's copy goes only
|
|
163
|
+
* after the pod has taken it.
|
|
164
|
+
*/
|
|
165
|
+
async function connectionsIntoPod(agent, home, log) {
|
|
166
|
+
const moves = [
|
|
167
|
+
[fileVault(path.join(home, 'fediaccts')), podVault(agent.store, `${CONNECTION_PREFIX}fedi-`)],
|
|
168
|
+
[fileVault(path.join(home, 'fediaccts', '_apps')), podVault(agent.store, `${CONNECTION_PREFIX}fediapp-`)],
|
|
169
|
+
[fileVault(home), podVault(agent.store, CONNECTION_PREFIX)],
|
|
170
|
+
];
|
|
171
|
+
// The last pair is the whole home directory, where only the Bluesky
|
|
172
|
+
// credential is ours to move.
|
|
173
|
+
for (const [from, to] of moves.slice(0, 2)) await moveVault(from, to, log);
|
|
174
|
+
const bluesky = moves[2][0].read('atproto');
|
|
175
|
+
if (bluesky && !moves[2][1].read('bluesky')) {
|
|
176
|
+
moves[2][1].write('bluesky', bluesky);
|
|
177
|
+
if (await moves[2][1].commit()) {
|
|
178
|
+
await moves[2][0].remove('atproto');
|
|
179
|
+
log('the Bluesky connection moved into the pod');
|
|
180
|
+
} else {
|
|
181
|
+
log('could not write the Bluesky connection to the pod — it stays on this host for now');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
81
186
|
/**
|
|
82
187
|
* Bring one identity up inside the server.
|
|
83
188
|
*
|
|
@@ -151,6 +256,10 @@ export async function startEmbeddedAgent({
|
|
|
151
256
|
agent.stateLoaded = true;
|
|
152
257
|
}
|
|
153
258
|
|
|
259
|
+
// Before connect(), which is what looks the key and the connections up.
|
|
260
|
+
await keyIntoPod(agent, home, log);
|
|
261
|
+
await connectionsIntoPod(agent, home, log);
|
|
262
|
+
|
|
154
263
|
await agent.connect();
|
|
155
264
|
|
|
156
265
|
// The client surfaces, on the pod's own origin: the Mastodon API a phone app
|
|
@@ -16,7 +16,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
16
16
|
let version = '0';
|
|
17
17
|
try {
|
|
18
18
|
({ version } = JSON.parse(fs.readFileSync(
|
|
19
|
-
path.join(path.dirname(fileURLToPath(import.meta.url)), '
|
|
19
|
+
path.join(path.dirname(fileURLToPath(import.meta.url)), '../../package.json'), 'utf8')));
|
|
20
20
|
} catch {
|
|
21
21
|
try {
|
|
22
22
|
({ version } = JSON.parse(fs.readFileSync(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fedipod",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.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",
|
|
@@ -49,10 +49,20 @@
|
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@fedify/fedify": "^2.3.4",
|
|
51
51
|
"@netlify/blobs": "^10.7.13",
|
|
52
|
+
"@rdfjs/data-model": "^2.1.2",
|
|
53
|
+
"@rdfjs/dataset": "^2.0.3",
|
|
54
|
+
"@rdfjs/environment": "^1.0.0",
|
|
55
|
+
"@rdfjs/namespace": "^2.0.1",
|
|
56
|
+
"@rdfjs/term-map": "^2.0.2",
|
|
57
|
+
"@rdfjs/term-set": "^2.0.3",
|
|
52
58
|
"@solid/access-token-verifier": "^2.1.1",
|
|
59
|
+
"clownface": "^2.0.3",
|
|
53
60
|
"jose": "^6.2.8",
|
|
54
61
|
"json-canon": "^1.0.1",
|
|
62
|
+
"jsonld": "^9.0.0",
|
|
63
|
+
"n3": "^2.1.1",
|
|
55
64
|
"node-forge": "^1.4.0",
|
|
65
|
+
"rdf-validate-shacl": "^0.6.5",
|
|
56
66
|
"rdflib": "^2.4.0",
|
|
57
67
|
"sanitize-html": "^2.17.6",
|
|
58
68
|
"undici": "^6.28.0",
|