fedipod-server 0.11.0 → 0.13.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 +22 -6
- package/dist/claims.d.ts +8 -0
- package/dist/claims.js +10 -0
- package/dist/handler.d.ts +13 -0
- package/dist/handler.js +56 -11
- package/dist/handler.jsonld +8 -0
- package/dist/store-pod.js +18 -4
- package/lib/{c2s.mjs → client/c2s.mjs} +10 -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 +170 -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/graphview.mjs +269 -0
- 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 +142 -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 +199 -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/server/embed.mjs +405 -0
- package/lib/{links.mjs → shared/links.mjs} +1 -1
- package/lib/{ua.mjs → shared/ua.mjs} +1 -1
- package/package.json +1 -1
- package/run-agent.mjs +33 -25
- 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 +14 -16
- package/web/app/atproto-browser.mjs +1 -1
- package/web/app/boot.mjs +2 -3
- package/web/app/deliver-relay.mjs +1 -1
- package/web/app/dist/boot.js +22 -3
- package/web/app/dist/boot.js.map +2 -2
- package/web/app/dist/sw.js +21913 -5446
- package/web/app/dist/sw.js.map +4 -4
- package/web/app/fediacct-browser.mjs +1 -1
- package/web/app/keys-browser.mjs +27 -4
- package/web/app/shims/shapes-text.mjs +8 -0
- package/web/app/signup.mjs +2 -3
- 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/boot.js +22 -3
- package/web/app/site/sw.js +21913 -5446
- package/web/app/sw-src.mjs +17 -2
- package/lib/admin.mjs +0 -1913
- package/lib/embed.mjs +0 -220
- 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,129 @@
|
|
|
1
|
+
# activitystreams.ttl — shapes for the ActivityStreams documents an identity
|
|
2
|
+
# reads from strangers.
|
|
3
|
+
#
|
|
4
|
+
# Shapes are identified by fragments of this document, so nothing here claims a
|
|
5
|
+
# namespace of its own.
|
|
6
|
+
#
|
|
7
|
+
# These are deliberately thin. They state what the ActivityPub specification
|
|
8
|
+
# requires and nothing more, because every fediverse implementation writes
|
|
9
|
+
# slightly different documents and a constraint beyond the spec is a constraint
|
|
10
|
+
# that fires on somebody's real traffic. Nothing here rejects anything: a
|
|
11
|
+
# failure is recorded and the activity is handled. A shape earns its way to
|
|
12
|
+
# being enforced by not firing.
|
|
13
|
+
|
|
14
|
+
@prefix sh: <http://www.w3.org/ns/shacl#> .
|
|
15
|
+
@prefix as: <https://www.w3.org/ns/activitystreams#> .
|
|
16
|
+
@prefix ldp: <http://www.w3.org/ns/ldp#> .
|
|
17
|
+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
|
18
|
+
|
|
19
|
+
# --- the actor document ------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
<#Actor>
|
|
22
|
+
a sh:NodeShape ;
|
|
23
|
+
sh:targetClass as:Person, as:Service, as:Group, as:Organization, as:Application ;
|
|
24
|
+
sh:name "Actor" ;
|
|
25
|
+
sh:description "Someone or something that acts: it has an address activities are sent to, and a name it is addressed by." ;
|
|
26
|
+
sh:property [
|
|
27
|
+
sh:path ldp:inbox ;
|
|
28
|
+
sh:name "inbox" ;
|
|
29
|
+
sh:description "The collection an actor receives activities in." ;
|
|
30
|
+
sh:nodeKind sh:IRI ;
|
|
31
|
+
sh:minCount 1 ;
|
|
32
|
+
sh:maxCount 1 ;
|
|
33
|
+
] ;
|
|
34
|
+
sh:property [
|
|
35
|
+
sh:path as:preferredUsername ;
|
|
36
|
+
sh:name "preferred username" ;
|
|
37
|
+
sh:description "The short name an actor is addressed by, the local part of its @handle@host address." ;
|
|
38
|
+
sh:datatype xsd:string ;
|
|
39
|
+
sh:maxCount 1 ;
|
|
40
|
+
] .
|
|
41
|
+
|
|
42
|
+
# --- the activities an identity acts on --------------------------------------
|
|
43
|
+
#
|
|
44
|
+
# Each states the two things ActivityPub requires of an activity: who performed
|
|
45
|
+
# it, and what it is about.
|
|
46
|
+
|
|
47
|
+
<#Create>
|
|
48
|
+
a sh:NodeShape ;
|
|
49
|
+
sh:targetClass as:Create ;
|
|
50
|
+
sh:name "Create" ;
|
|
51
|
+
sh:description "The bringing into existence of the object it names." ;
|
|
52
|
+
sh:property [
|
|
53
|
+
sh:path as:actor ;
|
|
54
|
+
sh:name "actor" ;
|
|
55
|
+
sh:description "The one that performed the activity." ;
|
|
56
|
+
sh:nodeKind sh:BlankNodeOrIRI ;
|
|
57
|
+
sh:minCount 1 ;
|
|
58
|
+
sh:maxCount 1 ;
|
|
59
|
+
] ;
|
|
60
|
+
sh:property [
|
|
61
|
+
sh:path as:object ;
|
|
62
|
+
sh:name "object" ;
|
|
63
|
+
sh:description "The thing the activity is about." ;
|
|
64
|
+
sh:minCount 1 ;
|
|
65
|
+
sh:maxCount 1 ;
|
|
66
|
+
] .
|
|
67
|
+
|
|
68
|
+
<#Follow>
|
|
69
|
+
a sh:NodeShape ;
|
|
70
|
+
sh:targetClass as:Follow ;
|
|
71
|
+
sh:name "Follow" ;
|
|
72
|
+
sh:description "A request to receive what the object it names publishes." ;
|
|
73
|
+
sh:property [
|
|
74
|
+
sh:path as:actor ;
|
|
75
|
+
sh:name "actor" ;
|
|
76
|
+
sh:description "The one that performed the activity." ;
|
|
77
|
+
sh:nodeKind sh:BlankNodeOrIRI ;
|
|
78
|
+
sh:minCount 1 ;
|
|
79
|
+
sh:maxCount 1 ;
|
|
80
|
+
] ;
|
|
81
|
+
sh:property [
|
|
82
|
+
sh:path as:object ;
|
|
83
|
+
sh:name "object" ;
|
|
84
|
+
sh:description "The thing the activity is about." ;
|
|
85
|
+
sh:minCount 1 ;
|
|
86
|
+
sh:maxCount 1 ;
|
|
87
|
+
] .
|
|
88
|
+
|
|
89
|
+
<#Accept>
|
|
90
|
+
a sh:NodeShape ;
|
|
91
|
+
sh:targetClass as:Accept ;
|
|
92
|
+
sh:name "Accept" ;
|
|
93
|
+
sh:description "Assent to the activity it names." ;
|
|
94
|
+
sh:property [
|
|
95
|
+
sh:path as:actor ;
|
|
96
|
+
sh:name "actor" ;
|
|
97
|
+
sh:description "The one that performed the activity." ;
|
|
98
|
+
sh:nodeKind sh:BlankNodeOrIRI ;
|
|
99
|
+
sh:minCount 1 ;
|
|
100
|
+
sh:maxCount 1 ;
|
|
101
|
+
] ;
|
|
102
|
+
sh:property [
|
|
103
|
+
sh:path as:object ;
|
|
104
|
+
sh:name "object" ;
|
|
105
|
+
sh:description "The thing the activity is about." ;
|
|
106
|
+
sh:minCount 1 ;
|
|
107
|
+
sh:maxCount 1 ;
|
|
108
|
+
] .
|
|
109
|
+
|
|
110
|
+
<#Undo>
|
|
111
|
+
a sh:NodeShape ;
|
|
112
|
+
sh:targetClass as:Undo ;
|
|
113
|
+
sh:name "Undo" ;
|
|
114
|
+
sh:description "The retraction of the activity it names, by the one that performed it." ;
|
|
115
|
+
sh:property [
|
|
116
|
+
sh:path as:actor ;
|
|
117
|
+
sh:name "actor" ;
|
|
118
|
+
sh:description "The one that performed the activity." ;
|
|
119
|
+
sh:nodeKind sh:BlankNodeOrIRI ;
|
|
120
|
+
sh:minCount 1 ;
|
|
121
|
+
sh:maxCount 1 ;
|
|
122
|
+
] ;
|
|
123
|
+
sh:property [
|
|
124
|
+
sh:path as:object ;
|
|
125
|
+
sh:name "object" ;
|
|
126
|
+
sh:description "The thing the activity is about." ;
|
|
127
|
+
sh:minCount 1 ;
|
|
128
|
+
sh:maxCount 1 ;
|
|
129
|
+
] .
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// shapes/index.mjs — validating an ActivityStreams document against the shapes
|
|
2
|
+
// beside this file.
|
|
3
|
+
//
|
|
4
|
+
// A shape says what a kind of document IS. Checking one against a document we
|
|
5
|
+
// were sent is a different question from whether we trust it: a forged actor,
|
|
6
|
+
// a receipt that does not bind, a reply naming a note we never wrote are each
|
|
7
|
+
// guarded elsewhere, and none of those guards is replaced here.
|
|
8
|
+
//
|
|
9
|
+
// NOTHING HERE REJECTS ANYTHING. A failure is recorded and the activity is
|
|
10
|
+
// handled exactly as it would have been. Every fediverse implementation writes
|
|
11
|
+
// slightly different documents, and a shape strict enough to catch something
|
|
12
|
+
// real is strict enough to reject somebody's quirk; which one cannot be known
|
|
13
|
+
// in advance, so it is learned from the record rather than guessed at. A shape
|
|
14
|
+
// earns enforcement by not firing.
|
|
15
|
+
|
|
16
|
+
import { Parser } from 'n3';
|
|
17
|
+
import Environment from '@rdfjs/environment';
|
|
18
|
+
import DataFactory from '@rdfjs/data-model/Factory.js';
|
|
19
|
+
import DatasetFactory from '@rdfjs/dataset/Factory.js';
|
|
20
|
+
import NamespaceFactory from '@rdfjs/namespace/Factory.js';
|
|
21
|
+
import TermSetFactory from '@rdfjs/term-set/Factory.js';
|
|
22
|
+
import TermMapFactory from '@rdfjs/term-map/Factory.js';
|
|
23
|
+
import ClownfaceFactory from 'clownface/Factory.js';
|
|
24
|
+
import SHACLValidator from 'rdf-validate-shacl';
|
|
25
|
+
import { SHAPES_TTL } from './shapes-text.mjs';
|
|
26
|
+
|
|
27
|
+
// The validator needs a full RDF/JS environment, not a bare factory. These
|
|
28
|
+
// pieces are what rdf-validate-shacl itself depends on, composed here rather
|
|
29
|
+
// than pulling in a packaged environment that reads files — this has to run in
|
|
30
|
+
// a service worker too.
|
|
31
|
+
const rdf = new Environment([DataFactory, DatasetFactory, NamespaceFactory, TermSetFactory, TermMapFactory, ClownfaceFactory]);
|
|
32
|
+
|
|
33
|
+
// The shapes document's own URL is the base, so `<#Create>` resolves to a
|
|
34
|
+
// fragment of it and no namespace is claimed.
|
|
35
|
+
const SHAPES_BASE = 'https://fedipod.invalid/shapes';
|
|
36
|
+
|
|
37
|
+
let validator;
|
|
38
|
+
let brokenReported = false;
|
|
39
|
+
|
|
40
|
+
/** The validator, built once. Reading and parsing the shapes is not per-document work. */
|
|
41
|
+
function shacl() {
|
|
42
|
+
if (!validator) {
|
|
43
|
+
const quads = new Parser({ baseIRI: SHAPES_BASE, format: 'text/turtle' }).parse(SHAPES_TTL);
|
|
44
|
+
validator = new SHACLValidator(rdf.dataset(quads.map(asTerm)), { factory: rdf });
|
|
45
|
+
}
|
|
46
|
+
return validator;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** jsonld and n3 each mint their own terms; the validator's environment must mint the ones it sees. */
|
|
50
|
+
function asTerm(q) {
|
|
51
|
+
const term = (t) => {
|
|
52
|
+
if (t.termType === 'NamedNode') return rdf.namedNode(t.value);
|
|
53
|
+
if (t.termType === 'BlankNode') return rdf.blankNode(String(t.value).replace(/^_:/u, ''));
|
|
54
|
+
if (t.termType === 'Literal') {
|
|
55
|
+
return rdf.literal(t.value, t.language || (t.datatype ? rdf.namedNode(t.datatype.value) : undefined));
|
|
56
|
+
}
|
|
57
|
+
return rdf.defaultGraph();
|
|
58
|
+
};
|
|
59
|
+
return rdf.quad(term(q.subject), term(q.predicate), term(q.object));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check a document's graph against the shapes.
|
|
64
|
+
*
|
|
65
|
+
* `quads` is what `parseAS2` produced. Returns null when the document
|
|
66
|
+
* conforms or when there was no graph to check, and otherwise a short record
|
|
67
|
+
* of what did not fit — the failing node, the property, and the shape's own
|
|
68
|
+
* words for it.
|
|
69
|
+
*
|
|
70
|
+
* Never throws. A validator that cannot run is not a reason to drop mail — but
|
|
71
|
+
* it is said once, because a checker that silently passes everything is the
|
|
72
|
+
* same silence this change exists to remove.
|
|
73
|
+
*/
|
|
74
|
+
export async function checkShapes(quads) {
|
|
75
|
+
if (!Array.isArray(quads) || quads.length === 0) return null;
|
|
76
|
+
let report;
|
|
77
|
+
try {
|
|
78
|
+
report = await shacl().validate(rdf.dataset(quads.map(asTerm)));
|
|
79
|
+
} catch (e) {
|
|
80
|
+
// A validator that cannot run must not read as "everything conforms" —
|
|
81
|
+
// that is the same silence this whole change exists to remove. Said once,
|
|
82
|
+
// then quiet, because a broken validator would otherwise say it per item.
|
|
83
|
+
if (!brokenReported) {
|
|
84
|
+
brokenReported = true;
|
|
85
|
+
return { conforms: false, broken: true, results: [{ focus: null, path: null, message: `shape validation is not running: ${e.message}`, shape: null }] };
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
if (report.conforms) return null;
|
|
90
|
+
return {
|
|
91
|
+
conforms: false,
|
|
92
|
+
results: report.results.slice(0, 10).map((r) => ({
|
|
93
|
+
focus: r.focusNode?.value ?? null,
|
|
94
|
+
path: r.path?.value ?? null,
|
|
95
|
+
message: r.message?.[0]?.value ?? String(r.sourceConstraintComponent?.value ?? 'does not fit the shape'),
|
|
96
|
+
shape: r.sourceShape?.value ?? null,
|
|
97
|
+
})),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The failure as one line, for a log or a dead-letter record. */
|
|
102
|
+
export function describeShapeFailure(failure) {
|
|
103
|
+
if (!failure) return null;
|
|
104
|
+
return failure.results
|
|
105
|
+
.map((r) => `${r.path ? r.path.replace(/^.*[#/]/u, '') : 'node'}: ${r.message}`)
|
|
106
|
+
.join('; ');
|
|
107
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// shapes-text.mjs — the shapes document, as text.
|
|
2
|
+
//
|
|
3
|
+
// The shapes are authored as Turtle in activitystreams.ttl, which is the one
|
|
4
|
+
// source of truth and opens in any RDF tool. Reading it differs by where this
|
|
5
|
+
// runs: Node reads the file, and the browser bundle swaps this module for a
|
|
6
|
+
// shim that imports the same .ttl through esbuild's text loader.
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
export const SHAPES_TTL = fs.readFileSync(
|
|
12
|
+
path.join(path.dirname(fileURLToPath(import.meta.url)), 'activitystreams.ttl'), 'utf8',
|
|
13
|
+
);
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import crypto from 'node:crypto';
|
|
7
7
|
import * as wire from './wire.mjs';
|
|
8
|
-
import * as podNotes from '
|
|
8
|
+
import * as podNotes from '../pod/notes.mjs';
|
|
9
9
|
import { dropFollower } from './store.mjs';
|
|
10
10
|
|
|
11
11
|
// WebFinger, ours rather than fedify's — v2 dropped lookupWebFinger.
|
|
@@ -21,7 +21,7 @@ export async function lookupWebFinger(acct) {
|
|
|
21
21
|
if (at < 1) return null;
|
|
22
22
|
const [user, host] = [clean.slice(0, at), clean.slice(at + 1)];
|
|
23
23
|
if (!host || /[/\\?#]/.test(host)) return null;
|
|
24
|
-
const { safeFetch, readCapped } = await import('
|
|
24
|
+
const { safeFetch, readCapped } = await import('../shared/safefetch.mjs');
|
|
25
25
|
const url = `https://${host}/.well-known/webfinger?resource=${encodeURIComponent(`acct:${clean}`)}`;
|
|
26
26
|
const res = await safeFetch(url, {
|
|
27
27
|
headers: { accept: 'application/jrd+json, application/json' },
|
|
@@ -152,6 +152,10 @@ export class PodStore {
|
|
|
152
152
|
|
|
153
153
|
has(name) { return this.cache.has(name); }
|
|
154
154
|
|
|
155
|
+
// The documents held here. Callers that keep one document per thing — a
|
|
156
|
+
// connected account, say — list them with this rather than a fixed name.
|
|
157
|
+
names() { return [...this.cache.keys()]; }
|
|
158
|
+
|
|
155
159
|
read(name, fallback) {
|
|
156
160
|
return this.cache.has(name) ? structuredClone(this.cache.get(name)) : fallback;
|
|
157
161
|
}
|
|
@@ -11,9 +11,9 @@ export const PUBLIC = 'https://www.w3.org/ns/activitystreams#Public';
|
|
|
11
11
|
// the container root to be stated rather than defaulted. What stays here is
|
|
12
12
|
// FediPod's own answer to "which root", and the handle rule, both of which read
|
|
13
13
|
// this application's config shape.
|
|
14
|
-
import { apUrls as podUrls } from '
|
|
14
|
+
import { apUrls as podUrls } from '../pod/urls.mjs';
|
|
15
15
|
|
|
16
|
-
export { webfingerHost } from '
|
|
16
|
+
export { webfingerHost } from '../pod/urls.mjs';
|
|
17
17
|
|
|
18
18
|
// The container the Node agent has always used. Older than the rename to
|
|
19
19
|
// FediPod and kept because it is what every existing Node-side pod actually
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// admin.mjs — loopback HTTP front: admin API + Mastodon client-API facade +
|
|
2
|
+
// the bundled Phanpy UI served same-origin (no CORS, no mixed content — the
|
|
3
|
+
// role data-kitchen's router plays for the in-app pane). Loopback-bound; the
|
|
4
|
+
// gate (vendor/gate.cjs) engages only when a token is configured, exactly
|
|
5
|
+
// its standalone behavior.
|
|
6
|
+
//
|
|
7
|
+
// The surface is surface.mjs, its routes are the modules under routes/, the
|
|
8
|
+
// static serving is static.mjs, the listener is server.mjs. This file is the
|
|
9
|
+
// door the rest of the agent imports through.
|
|
10
|
+
|
|
11
|
+
export { buildAdminSurface } from './surface.mjs';
|
|
12
|
+
export { startAdmin } from './server.mjs';
|
|
13
|
+
export { namedOrigin, secureOrigin, wsOrigins } from './origins.mjs';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// origins.mjs — the addresses an identity answers on, as a browser must be
|
|
2
|
+
// sent to them.
|
|
3
|
+
|
|
4
|
+
import { allowedAuthorities, hostLabel } from '../../shared/guard.mjs';
|
|
5
|
+
|
|
6
|
+
// The origin a browser should be sent to for an identity. A browser keys
|
|
7
|
+
// storage per ORIGIN, so linking every identity at localhost:<port> files them
|
|
8
|
+
// all in one bucket — which is how a client ends up holding one actor's login
|
|
9
|
+
// and showing it on another's page. Named whenever the handle is a legal host
|
|
10
|
+
// label; bare loopback when it is not, because a mangled name would be an
|
|
11
|
+
// origin the other agent's own guard refuses.
|
|
12
|
+
export function namedOrigin(handle, port) {
|
|
13
|
+
const label = hostLabel(handle);
|
|
14
|
+
return `http://${label ? label + '.' : ''}localhost:${port}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// The https origin for an identity, on the one port it was given.
|
|
18
|
+
// This is the advertised default — what gets printed, opened and linked; the
|
|
19
|
+
// http listener stays served beside it for anything that needs cleartext.
|
|
20
|
+
export function secureOrigin(handle, httpsPort) {
|
|
21
|
+
const label = hostLabel(handle);
|
|
22
|
+
return `https://${label ? label + '.' : ''}localhost:${httpsPort}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function wsOrigins(port, labels = []) {
|
|
26
|
+
const out = new Set();
|
|
27
|
+
for (const a of allowedAuthorities(port, labels)) {
|
|
28
|
+
// No IPv6 at all. Bare `::1` cannot carry a port, and Chrome rejects the
|
|
29
|
+
// bracketed form inside a CSP source expression — one invalid source makes
|
|
30
|
+
// it drop the whole directive, which is worse than not listing the socket.
|
|
31
|
+
if (a.startsWith('::') || a.startsWith('[')) continue;
|
|
32
|
+
out.add(`wss://${/:\d+$/.test(a) ? a : `${a}:${port}`}`);
|
|
33
|
+
}
|
|
34
|
+
return [...out];
|
|
35
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// connections.mjs — the accounts held elsewhere: a Bluesky account and
|
|
2
|
+
// accounts on other fediverse servers, connected, adjusted, disconnected;
|
|
3
|
+
// and the page the other server's sign-in comes back to.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
// The page the other server's redirect lands on. Self-contained on purpose:
|
|
8
|
+
// the browser arrives here from somewhere else, and nothing may load from
|
|
9
|
+
// that somewhere.
|
|
10
|
+
function callbackPage(ok, msg) {
|
|
11
|
+
const esc = (s) => String(s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
|
12
|
+
const head = ok ? 'Account connected' : 'Not connected';
|
|
13
|
+
return `<!doctype html>
|
|
14
|
+
<html lang="en"><head><meta charset="utf-8">
|
|
15
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
16
|
+
<title>${head}</title>
|
|
17
|
+
<style>
|
|
18
|
+
:root { color-scheme: light dark; }
|
|
19
|
+
body { font: 16px/1.7 system-ui, -apple-system, sans-serif; margin: 0;
|
|
20
|
+
padding: 2.5rem 2rem; background: #ffffff; color: #1a1a1a; }
|
|
21
|
+
main { max-width: 32rem; margin: 0 auto; }
|
|
22
|
+
h1 { font-size: 1.375rem; font-weight: 500; margin: 0 0 1rem; }
|
|
23
|
+
p { margin: 0 0 1rem; }
|
|
24
|
+
@media (prefers-color-scheme: dark) {
|
|
25
|
+
body { background: #16161a; color: #ececf0; }
|
|
26
|
+
}
|
|
27
|
+
</style></head>
|
|
28
|
+
<body><main>
|
|
29
|
+
<h1>${head}</h1>
|
|
30
|
+
<p>${esc(msg)}</p>
|
|
31
|
+
<p>You can close this tab and go back to FediPod.</p>
|
|
32
|
+
</main></body></html>
|
|
33
|
+
`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function get(p, ctx, req, res, url) { // eslint-disable-line no-unused-vars
|
|
37
|
+
const { agent, log, allowed, embedded, port, publicOrigin, versionOnDisk, isGroup } = ctx; // eslint-disable-line no-unused-vars
|
|
38
|
+
const json = (res, status, obj) => { ctx.json(res, status, obj); return true; };
|
|
39
|
+
|
|
40
|
+
if (req.method === 'GET' && p === '/fediacct') {
|
|
41
|
+
return json(res, 200, { accounts: agent.fediaccts?.status() || [] });
|
|
42
|
+
}
|
|
43
|
+
// Where the other server sends the browser back. A GET, so it gets none
|
|
44
|
+
// of the POST branch's local-only protection for free and asks for its
|
|
45
|
+
// own — a code arriving from anywhere else is not the owner's.
|
|
46
|
+
if (req.method === 'GET' && p === '/fediacct/callback') {
|
|
47
|
+
if (!embedded && !allowed.isLocalRequest(req)) {
|
|
48
|
+
return json(res, 403, { error: 'connecting an account is available on this machine only' });
|
|
49
|
+
}
|
|
50
|
+
const q = new URL(req.url, 'https://x.invalid').searchParams;
|
|
51
|
+
const done = (ok, msg) => {
|
|
52
|
+
res.writeHead(ok ? 200 : 400, { 'content-type': 'text/html; charset=utf-8' });
|
|
53
|
+
res.end(callbackPage(ok, msg));
|
|
54
|
+
return true;
|
|
55
|
+
};
|
|
56
|
+
if (q.get('error')) return done(false, q.get('error_description') || q.get('error'));
|
|
57
|
+
if (!q.get('code') || !q.get('state')) return done(false, 'that sign-in came back incomplete');
|
|
58
|
+
try {
|
|
59
|
+
const row = await agent.fediaccts.complete({ state: q.get('state'), code: q.get('code') });
|
|
60
|
+
const cfg = agent.store.getConfig();
|
|
61
|
+
agent.store.setConfig({ ...cfg, fediAccounts: agent.fediaccts.roster() });
|
|
62
|
+
await agent.store.flush();
|
|
63
|
+
agent.restartAccts?.();
|
|
64
|
+
return done(true, `${row.handle} is connected.`);
|
|
65
|
+
} catch (e) { return done(false, e.message); }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function post(p, body, ctx, req, res) { // eslint-disable-line no-unused-vars
|
|
72
|
+
const { agent, log, allowed, embedded, port, publicOrigin, versionOnDisk, isGroup } = ctx; // eslint-disable-line no-unused-vars
|
|
73
|
+
const json = (res, status, obj) => { ctx.json(res, status, obj); return true; };
|
|
74
|
+
switch (p) {
|
|
75
|
+
case '/atproto/connect': {
|
|
76
|
+
// The one endpoint that ever sees the app password; loopback-only.
|
|
77
|
+
if (!body.identifier || !body.appPassword) {
|
|
78
|
+
return json(res, 400, { error: 'identifier and appPassword required' });
|
|
79
|
+
}
|
|
80
|
+
const conn = await agent.atproto.connect({
|
|
81
|
+
service: body.service, identifier: body.identifier, appPassword: body.appPassword,
|
|
82
|
+
});
|
|
83
|
+
const cfg = agent.store.getConfig();
|
|
84
|
+
agent.store.setConfig({
|
|
85
|
+
...cfg,
|
|
86
|
+
atproto: { ...conn, crossPost: cfg.atproto?.crossPost ?? true },
|
|
87
|
+
});
|
|
88
|
+
await agent.store.flush();
|
|
89
|
+
agent.startBsky?.();
|
|
90
|
+
return json(res, 200, { ok: true, ...agent.atproto.status() });
|
|
91
|
+
}
|
|
92
|
+
case '/atproto/disconnect': {
|
|
93
|
+
agent.stopBsky?.();
|
|
94
|
+
await agent.atproto.disconnect();
|
|
95
|
+
const { atproto, ...rest } = agent.store.getConfig();
|
|
96
|
+
agent.store.setConfig(rest);
|
|
97
|
+
await agent.store.flush();
|
|
98
|
+
return json(res, 200, { ok: true });
|
|
99
|
+
}
|
|
100
|
+
case '/atproto': {
|
|
101
|
+
// Non-secret settings only — today that is the cross-post toggle.
|
|
102
|
+
const cfg = agent.store.getConfig();
|
|
103
|
+
if (!cfg.atproto) return json(res, 400, { error: 'no bluesky account connected' });
|
|
104
|
+
agent.store.setConfig({
|
|
105
|
+
...cfg, atproto: { ...cfg.atproto, crossPost: !!body.crossPost },
|
|
106
|
+
});
|
|
107
|
+
await agent.store.flush();
|
|
108
|
+
return json(res, 200, { ok: true, atproto: { ...cfg.atproto, crossPost: !!body.crossPost } });
|
|
109
|
+
}
|
|
110
|
+
case '/fediacct/connect': {
|
|
111
|
+
// Starts the sign-in at the other server. Loopback-only, and the
|
|
112
|
+
// redirect returns to the origin the owner is actually using, not a
|
|
113
|
+
// guess — a client reaches this agent by several names.
|
|
114
|
+
if (!body.host) return json(res, 400, { error: 'the address of the server is required' });
|
|
115
|
+
try {
|
|
116
|
+
const { url } = await agent.fediaccts.begin({
|
|
117
|
+
host: body.host, redirectUri: `https://${req.headers.host}/fediacct/callback`,
|
|
118
|
+
});
|
|
119
|
+
return json(res, 200, { ok: true, authorize: url });
|
|
120
|
+
} catch (e) { return json(res, 400, { error: e.message }); }
|
|
121
|
+
}
|
|
122
|
+
case '/fediacct/disconnect': {
|
|
123
|
+
if (!body.id) return json(res, 400, { error: 'id required' });
|
|
124
|
+
if (!await agent.fediaccts.remove(body.id)) return json(res, 404, { error: 'no such account' });
|
|
125
|
+
const cfg = agent.store.getConfig();
|
|
126
|
+
agent.store.setConfig({ ...cfg, fediAccounts: agent.fediaccts.roster() });
|
|
127
|
+
await agent.store.flush();
|
|
128
|
+
agent.restartAccts?.();
|
|
129
|
+
return json(res, 200, { ok: true });
|
|
130
|
+
}
|
|
131
|
+
case '/fediacct': {
|
|
132
|
+
// Non-secret settings only — today that is whether it is polled.
|
|
133
|
+
if (!body.id) return json(res, 400, { error: 'id required' });
|
|
134
|
+
const row = agent.fediaccts.setEnabled(body.id, !!body.enabled);
|
|
135
|
+
if (!row) return json(res, 404, { error: 'no such account' });
|
|
136
|
+
const cfg = agent.store.getConfig();
|
|
137
|
+
agent.store.setConfig({ ...cfg, fediAccounts: agent.fediaccts.roster() });
|
|
138
|
+
await agent.store.flush();
|
|
139
|
+
agent.restartAccts?.();
|
|
140
|
+
return json(res, 200, { ok: true, account: row });
|
|
141
|
+
}
|
|
142
|
+
default: return false;
|
|
143
|
+
}
|
|
144
|
+
}
|