@fedify/cli 2.4.0-dev.2245 → 2.4.0-dev.2253
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/dist/deno.js +1 -1
- package/dist/diagnostics.js +143 -10
- package/dist/inbox/command.js +6 -2
- package/dist/inbox.js +20 -8
- package/dist/lookup/command.js +5 -0
- package/dist/lookup.js +81 -63
- package/dist/portable.js +136 -0
- package/dist/relay/command.js +2 -2
- package/dist/utils.js +34 -9
- package/dist/webfinger/action.js +107 -21
- package/dist/webfinger/command.js +8 -2
- package/dist/webfinger/error.js +40 -2
- package/package.json +9 -9
package/dist/deno.js
CHANGED
package/dist/diagnostics.js
CHANGED
|
@@ -1,40 +1,173 @@
|
|
|
1
1
|
import "temporal-polyfill";
|
|
2
|
-
import {
|
|
3
|
-
import "@fedify/vocab";
|
|
2
|
+
import { createPortableObjectVerifier, getPortableLookupProblem, isPortableReference } from "./portable.js";
|
|
3
|
+
import { FetchError, UrlError, formatIri } from "@fedify/vocab-runtime";
|
|
4
|
+
import { lookupObject } from "@fedify/vocab";
|
|
4
5
|
//#region src/diagnostics.ts
|
|
5
|
-
/**
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Records loader failures that lookupObject would otherwise swallow or wrap,
|
|
8
|
+
* and portable objects that the verifier rejects.
|
|
9
|
+
* @param documentLoader The document loader to record failures of.
|
|
10
|
+
* @param contextLoader The context loader to record failures of.
|
|
11
|
+
* @param verifierDocumentLoader The document loader for the documents that
|
|
12
|
+
* the [FEP-ef61] portable object verifier
|
|
13
|
+
* discovers. If omitted, the returned
|
|
14
|
+
* `verifyPortableObject` is `undefined`.
|
|
15
|
+
*
|
|
16
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
17
|
+
*/
|
|
18
|
+
function createLookupDiagnostics(documentLoader, contextLoader, verifierDocumentLoader) {
|
|
7
19
|
let objectFailure;
|
|
8
20
|
let contextFailure;
|
|
21
|
+
let portableFailure;
|
|
22
|
+
let verificationFailures;
|
|
9
23
|
const wrap = (loader, source, record) => async (url, options) => {
|
|
10
24
|
try {
|
|
11
25
|
const document = await loader(url, options);
|
|
12
26
|
record(void 0);
|
|
27
|
+
if (verificationFailures != null) verificationFailures.length = 0;
|
|
13
28
|
return document;
|
|
14
29
|
} catch (error) {
|
|
15
|
-
|
|
30
|
+
const failure = {
|
|
16
31
|
error,
|
|
17
32
|
source
|
|
18
|
-
}
|
|
33
|
+
};
|
|
34
|
+
record(failure);
|
|
35
|
+
verificationFailures?.push(failure);
|
|
19
36
|
throw error;
|
|
20
37
|
}
|
|
21
38
|
};
|
|
39
|
+
const recordObjectFailure = (failure) => objectFailure = failure;
|
|
40
|
+
const wrappedContextLoader = wrap(contextLoader, "context", (e) => contextFailure = e);
|
|
41
|
+
const verifyPortableObject = verifierDocumentLoader == null ? void 0 : createPortableObjectVerifier(wrap(verifierDocumentLoader, "object", recordObjectFailure), wrappedContextLoader);
|
|
42
|
+
const verify = verifyPortableObject == null ? void 0 : async (document, options) => {
|
|
43
|
+
const id = getDocumentId(document);
|
|
44
|
+
const failures = [];
|
|
45
|
+
verificationFailures = failures;
|
|
46
|
+
try {
|
|
47
|
+
const result = await verifyPortableObject(document, options);
|
|
48
|
+
if (result.verified) portableFailure = void 0;
|
|
49
|
+
else if (result.reason.type === "collectionOwnerUnavailable" && failures.length > 0) portableFailure = failures[failures.length - 1];
|
|
50
|
+
else portableFailure = {
|
|
51
|
+
error: void 0,
|
|
52
|
+
source: "portable",
|
|
53
|
+
id,
|
|
54
|
+
reason: result.reason
|
|
55
|
+
};
|
|
56
|
+
return result;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
portableFailure = {
|
|
59
|
+
error,
|
|
60
|
+
source: "portable",
|
|
61
|
+
id
|
|
62
|
+
};
|
|
63
|
+
throw error;
|
|
64
|
+
} finally {
|
|
65
|
+
verificationFailures = void 0;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
22
68
|
return {
|
|
23
|
-
documentLoader: wrap(documentLoader, "object",
|
|
24
|
-
contextLoader:
|
|
69
|
+
documentLoader: wrap(documentLoader, "object", recordObjectFailure),
|
|
70
|
+
contextLoader: wrappedContextLoader,
|
|
71
|
+
verifyPortableObject: verify,
|
|
25
72
|
clearFailures() {
|
|
26
73
|
objectFailure = void 0;
|
|
27
74
|
contextFailure = void 0;
|
|
75
|
+
portableFailure = void 0;
|
|
28
76
|
},
|
|
29
77
|
getObjectFailure: () => objectFailure,
|
|
30
|
-
getContextFailure: () => contextFailure
|
|
78
|
+
getContextFailure: () => contextFailure,
|
|
79
|
+
getPortableFailure: () => portableFailure
|
|
31
80
|
};
|
|
32
81
|
}
|
|
82
|
+
function getDocumentId(document) {
|
|
83
|
+
if (document == null || typeof document !== "object") return void 0;
|
|
84
|
+
const record = document;
|
|
85
|
+
const id = record.id ?? record["@id"];
|
|
86
|
+
return typeof id === "string" ? formatIdentifier(id) : void 0;
|
|
87
|
+
}
|
|
88
|
+
/** Returns an object and a snapshot of the evidence for a failed lookup. */
|
|
89
|
+
async function lookupWithDiagnostics(identifier, options, lookup = lookupObject) {
|
|
90
|
+
const { verifierDocumentLoader, ...lookupOptions } = options;
|
|
91
|
+
if (verifierDocumentLoader != null) {
|
|
92
|
+
const problem = getPortableLookupProblem(identifier, options.gateways);
|
|
93
|
+
if (problem != null) return Object.freeze({
|
|
94
|
+
object: null,
|
|
95
|
+
failure: {
|
|
96
|
+
error: void 0,
|
|
97
|
+
source: "portable",
|
|
98
|
+
id: formatIdentifier(identifier),
|
|
99
|
+
problem
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const diagnostics = createLookupDiagnostics(options.documentLoader, options.contextLoader, verifierDocumentLoader);
|
|
104
|
+
try {
|
|
105
|
+
const object = await lookup(identifier, {
|
|
106
|
+
...lookupOptions,
|
|
107
|
+
documentLoader: diagnostics.documentLoader,
|
|
108
|
+
contextLoader: diagnostics.contextLoader,
|
|
109
|
+
...diagnostics.verifyPortableObject == null ? {} : { verifyPortableObject: diagnostics.verifyPortableObject }
|
|
110
|
+
});
|
|
111
|
+
if (object != null) return Object.freeze({ object });
|
|
112
|
+
return Object.freeze({
|
|
113
|
+
object,
|
|
114
|
+
failure: diagnostics.getPortableFailure() ?? diagnostics.getObjectFailure() ?? diagnostics.getContextFailure() ?? (verifierDocumentLoader != null && isPortableReference(identifier) ? {
|
|
115
|
+
error: void 0,
|
|
116
|
+
source: "portable",
|
|
117
|
+
id: formatIdentifier(identifier)
|
|
118
|
+
} : void 0)
|
|
119
|
+
});
|
|
120
|
+
} catch (error) {
|
|
121
|
+
return Object.freeze({
|
|
122
|
+
object: null,
|
|
123
|
+
thrownError: error,
|
|
124
|
+
failure: diagnostics.getContextFailure() ?? diagnostics.getObjectFailure() ?? diagnostics.getPortableFailure() ?? {
|
|
125
|
+
error,
|
|
126
|
+
source: "other"
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function formatIdentifier(identifier) {
|
|
132
|
+
try {
|
|
133
|
+
return formatIri(identifier);
|
|
134
|
+
} catch {
|
|
135
|
+
return typeof identifier === "string" ? identifier : identifier.href;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const PORTABLE_REJECTION_REASONS = {
|
|
139
|
+
notPortableObject: "it has neither a portable ID nor a compatible identifier",
|
|
140
|
+
unsupportedObjectType: "its type cannot be a portable object",
|
|
141
|
+
missingProof: "it has no integrity proof",
|
|
142
|
+
invalidGateways: "its gateways property is missing or invalid",
|
|
143
|
+
invalidProof: "its integrity proof is invalid",
|
|
144
|
+
unsupportedVerificationMethod: "its integrity proof has an unsupported verification method",
|
|
145
|
+
verificationMethodMismatch: "its integrity proof was not created by the DID in its ID",
|
|
146
|
+
unknownCollectionSource: "it is a collection without an integrity proof that was not served by a gateway",
|
|
147
|
+
unknownCollectionOwner: "it is a collection without an integrity proof whose owner is unknown",
|
|
148
|
+
conflictingCollectionOwners: "it is a collection without an integrity proof whose owner is ambiguous",
|
|
149
|
+
collectionPageMismatch: "it is a collection page that does not belong to its collection",
|
|
150
|
+
collectionOwnerUnavailable: "it is a collection without an integrity proof whose owner could not be retrieved",
|
|
151
|
+
collectionNotListedByOwner: "it is a collection without an integrity proof that its owner does not list",
|
|
152
|
+
untrustedGateway: "it is a collection without an integrity proof served by a gateway that its owner does not list"
|
|
153
|
+
};
|
|
154
|
+
function describePortableFailure(failure) {
|
|
155
|
+
const id = failure.id ?? "(unknown)";
|
|
156
|
+
if (failure.problem === "malformed") return `Invalid portable ID: ${id}.`;
|
|
157
|
+
if (failure.problem === "no-gateway") return `The portable ID ${id} has no @gateway location hints. Use the --gateway option to look it up.`;
|
|
158
|
+
if (failure.reason != null) return `Rejected the portable object ${id}, as ${PORTABLE_REJECTION_REASONS[failure.reason.type] ?? failure.reason.type}.`;
|
|
159
|
+
if (failure.error != null) return `Failed to verify the portable object ${id}: ${failure.error instanceof Error ? failure.error.message : String(failure.error)}`;
|
|
160
|
+
return `No gateway returned a valid portable object for ${id}.`;
|
|
161
|
+
}
|
|
33
162
|
function escapeControlCharacters(message) {
|
|
34
163
|
return message.replace(/[\x00-\x1f\x7f-\x9f]/g, (character) => `\\x${character.charCodeAt(0).toString(16).padStart(2, "0")}`);
|
|
35
164
|
}
|
|
36
165
|
/** Describes a failure without recommending signing unless HTTP supports it. */
|
|
37
166
|
function describeLookupFailure(failure, authorizedFetch) {
|
|
167
|
+
if (failure?.source === "portable") return {
|
|
168
|
+
message: escapeControlCharacters(describePortableFailure(failure)),
|
|
169
|
+
suggestsAuthorizedFetch: false
|
|
170
|
+
};
|
|
38
171
|
const error = failure?.error;
|
|
39
172
|
const http = error instanceof FetchError ? /^(.+): HTTP (\d{3}): \1$/s.exec(error.message) : null;
|
|
40
173
|
if (http != null) {
|
|
@@ -67,4 +200,4 @@ function describeLookupFailure(failure, authorizedFetch) {
|
|
|
67
200
|
};
|
|
68
201
|
}
|
|
69
202
|
//#endregion
|
|
70
|
-
export { createLookupDiagnostics, describeLookupFailure };
|
|
203
|
+
export { createLookupDiagnostics, describeLookupFailure, lookupWithDiagnostics };
|
package/dist/inbox/command.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import "temporal-polyfill";
|
|
2
2
|
import { configContext } from "../config.js";
|
|
3
3
|
import { createTunnelOption } from "../options.js";
|
|
4
|
+
import { gatewayUrl } from "../portable.js";
|
|
4
5
|
import { command, constant, group, merge, message, multiple, object, option, string } from "@optique/core";
|
|
5
6
|
import { bindConfig } from "@optique/config";
|
|
6
7
|
//#region src/inbox/command.ts
|
|
@@ -8,12 +9,15 @@ const DEFAULT_EPHEMERAL_INBOX_NAME = "Fedify Ephemeral Inbox";
|
|
|
8
9
|
const DEFAULT_EPHEMERAL_INBOX_SUMMARY = "An ephemeral ActivityPub inbox for testing purposes.";
|
|
9
10
|
const inboxOptions = merge(object("Inbox options", {
|
|
10
11
|
command: constant("inbox"),
|
|
11
|
-
follow: bindConfig(multiple(option("-f", "--follow", string({ metavar: "URI" }), { description: message`Follow the given actor. The argument can be either an actor URI or a handle. Can be specified multiple times.` })), {
|
|
12
|
+
follow: bindConfig(multiple(option("-f", "--follow", string({ metavar: "URI" }), { description: message`Follow the given actor. The argument can be either an actor URI (including an FEP-ef61 portable ID) or a handle. Can be specified multiple times.` })), {
|
|
12
13
|
context: configContext,
|
|
13
14
|
key: (config) => config.inbox?.follow ?? [],
|
|
14
15
|
default: []
|
|
15
16
|
}),
|
|
16
|
-
|
|
17
|
+
gateways: multiple(option("--gateway", gatewayUrl(), { description: message`An FEP-ef61 gateway to look up the portable \
|
|
18
|
+
actors to follow from, instead of their ${"@gateway"} location hints. Can \
|
|
19
|
+
be specified multiple times.` })),
|
|
20
|
+
acceptFollow: bindConfig(multiple(option("-a", "--accept-follow", string({ metavar: "URI" }), { description: message`Accept follow requests from the given actor. The argument can be either an actor URI (including an FEP-ef61 portable ID) or a handle, or a wildcard (${"*"}). Can be specified multiple times. If a wildcard is specified, all follow requests will be accepted.` })), {
|
|
17
21
|
context: configContext,
|
|
18
22
|
key: (config) => config.inbox?.acceptFollow ?? [],
|
|
19
23
|
default: []
|
package/dist/inbox.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { Temporal } from "temporal-polyfill";
|
|
2
|
+
import { getPortableLookupProblem } from "./portable.js";
|
|
2
3
|
import { colors, matchesActor } from "./utils.js";
|
|
3
4
|
import { configureLogging, recordingSink } from "./log.js";
|
|
4
5
|
import { version } from "./deno.js";
|
|
5
|
-
import { getDocumentLoader } from "./docloader.js";
|
|
6
|
+
import { getDocumentLoader as getDocumentLoader$1 } from "./docloader.js";
|
|
6
7
|
import { ActivityEntryPage, ActivityListPage } from "./inbox/view.js";
|
|
7
8
|
import { tableStyle } from "./table.js";
|
|
8
9
|
import { spawnTemporaryServer } from "./tempserver.js";
|
|
9
10
|
import process from "node:process";
|
|
11
|
+
import { formatIri } from "@fedify/vocab-runtime";
|
|
10
12
|
import { MemoryKvStore, createFederation, generateCryptoKeyPair } from "@fedify/fedify";
|
|
11
13
|
import { getLogger } from "@logtape/logtape";
|
|
12
14
|
import ora from "ora";
|
|
13
|
-
import { Accept, Activity, Application, Delete, Endpoints, Follow, Image, PUBLIC_COLLECTION, isActor
|
|
15
|
+
import { Accept, Activity, Application, Delete, Endpoints, Follow, Image, PUBLIC_COLLECTION, isActor } from "@fedify/vocab";
|
|
14
16
|
import Table from "cli-table3";
|
|
15
17
|
import { Hono } from "hono";
|
|
16
18
|
import { jsx } from "hono/jsx/jsx-runtime";
|
|
@@ -32,7 +34,7 @@ async function runInbox(command) {
|
|
|
32
34
|
for (const key of Object.keys(peers)) delete peers[key];
|
|
33
35
|
for (const key of Object.keys(followers)) delete followers[key];
|
|
34
36
|
if (command.debug) await configureLogging();
|
|
35
|
-
const federationDocumentLoader = await getDocumentLoader();
|
|
37
|
+
const federationDocumentLoader = await getDocumentLoader$1();
|
|
36
38
|
const authorizedFetchEnabled = command.authorizedFetch ?? false;
|
|
37
39
|
const authorize = async (ctx) => {
|
|
38
40
|
if (!authorizedFetchEnabled) return true;
|
|
@@ -97,13 +99,13 @@ async function runInbox(command) {
|
|
|
97
99
|
const follower = await activity.getActor();
|
|
98
100
|
if (!isActor(follower)) return;
|
|
99
101
|
if (!await matchesActor(follower, acceptFollows) || activity.id == null) {
|
|
100
|
-
logger.debug("Does not accept follow from {actor}.", { actor: follower.id
|
|
102
|
+
logger.debug("Does not accept follow from {actor}.", { actor: follower.id == null ? null : formatIri(follower.id) });
|
|
101
103
|
return;
|
|
102
104
|
}
|
|
103
|
-
logger.debug("Accepting follow from {actor}.", { actor: follower.id
|
|
105
|
+
logger.debug("Accepting follow from {actor}.", { actor: follower.id == null ? null : formatIri(follower.id) });
|
|
104
106
|
followers[activity.id.href] = follower;
|
|
105
107
|
await ctx.sendActivity({ identifier }, follower, new Accept({
|
|
106
|
-
id: new URL(`#accepts/${follower.id
|
|
108
|
+
id: new URL(`#accepts/${follower.id == null ? "" : formatIri(follower.id)}`, ctx.getActorUri("i")),
|
|
107
109
|
actor: ctx.getActorUri(identifier),
|
|
108
110
|
object: activity.id
|
|
109
111
|
}));
|
|
@@ -181,9 +183,19 @@ async function runInbox(command) {
|
|
|
181
183
|
if (command.follow != null && command.follow.length > 0) {
|
|
182
184
|
spinner.text = "Following actors...";
|
|
183
185
|
const documentLoader = await fedCtx.getDocumentLoader({ identifier: "i" });
|
|
186
|
+
const gateways = command.gateways.length > 0 ? { gateways: command.gateways } : {};
|
|
184
187
|
for (const uri of command.follow) {
|
|
185
188
|
spinner.text = `Following ${colors.green(uri)}...`;
|
|
186
|
-
const
|
|
189
|
+
const problem = getPortableLookupProblem(uri, command.gateways);
|
|
190
|
+
if (problem != null) {
|
|
191
|
+
spinner.fail(problem === "malformed" ? `Invalid portable ID: ${colors.red(uri)}` : `The portable ID ${colors.red(uri)} has no @gateway location hints. Use the --gateway option to follow it.`);
|
|
192
|
+
spinner.start();
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const actor = await fedCtx.lookupObject(uri, {
|
|
196
|
+
documentLoader,
|
|
197
|
+
...gateways
|
|
198
|
+
});
|
|
187
199
|
if (!isActor(actor)) {
|
|
188
200
|
spinner.fail(`Not an actor: ${colors.red(uri)}`);
|
|
189
201
|
spinner.start();
|
|
@@ -191,7 +203,7 @@ async function runInbox(command) {
|
|
|
191
203
|
}
|
|
192
204
|
if (actor.id != null) peers[actor.id?.href] = actor;
|
|
193
205
|
await fedCtx.sendActivity({ identifier: "i" }, actor, new Follow({
|
|
194
|
-
id: new URL(`#follows/${actor.id
|
|
206
|
+
id: new URL(`#follows/${actor.id == null ? "" : formatIri(actor.id)}`, fedCtx.getActorUri("i")),
|
|
195
207
|
actor: fedCtx.getActorUri("i"),
|
|
196
208
|
object: actor.id
|
|
197
209
|
}));
|
package/dist/lookup/command.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import "temporal-polyfill";
|
|
2
2
|
import { configContext } from "../config.js";
|
|
3
3
|
import { createTunnelServiceOption, userAgentOption } from "../options.js";
|
|
4
|
+
import { gatewayUrl } from "../portable.js";
|
|
4
5
|
import { argument, choice, command, constant, flag, float, integer, map, merge, message, multiple, object, option, optionNames, optional, or, string, withDefault } from "@optique/core";
|
|
5
6
|
import { path } from "@optique/run";
|
|
6
7
|
import { bindConfig } from "@optique/config";
|
|
@@ -80,6 +81,10 @@ const lookupModeOption = withDefault(or(object("Recurse options", {
|
|
|
80
81
|
suppressErrors: false
|
|
81
82
|
});
|
|
82
83
|
const lookupOptions = merge(object({ command: constant("lookup") }), lookupModeOption, authorizedFetchOption, merge("Network options", userAgentOption, object({
|
|
84
|
+
gateways: multiple(option("--gateway", gatewayUrl(), { description: message`An FEP-ef61 gateway to look up the given \
|
|
85
|
+
portable objects from, instead of their ${"@gateway"} location hints. Also \
|
|
86
|
+
used for linked portable objects without location hints. Can be specified \
|
|
87
|
+
multiple times.` })),
|
|
83
88
|
allowPrivateAddress: allowPrivateAddressOption,
|
|
84
89
|
timeout: optional(bindConfig(option("-T", "--timeout", float({
|
|
85
90
|
min: 0,
|
package/dist/lookup.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import "temporal-polyfill";
|
|
2
|
+
import { getIriKey, getPortableLookupProblem } from "./portable.js";
|
|
2
3
|
import { FEDIBIRD_QUOTE_IRI, IN_REPLY_TO_IRI, MISSKEY_QUOTE_IRI, QUOTE_IRI, QUOTE_URL_IRI } from "./lookup/command.js";
|
|
3
4
|
import { colorEnabled, colors, describeError, formatObject } from "./utils.js";
|
|
4
5
|
import { configureLogging } from "./log.js";
|
|
5
6
|
import { getContextLoader, getDocumentLoader as getDocumentLoader$1 } from "./docloader.js";
|
|
6
7
|
import { spawnTemporaryServer } from "./tempserver.js";
|
|
7
|
-
import { createLookupDiagnostics, describeLookupFailure } from "./diagnostics.js";
|
|
8
|
+
import { createLookupDiagnostics, describeLookupFailure, lookupWithDiagnostics } from "./diagnostics.js";
|
|
8
9
|
import { renderImages } from "./imagerenderer.js";
|
|
9
10
|
import process from "node:process";
|
|
10
11
|
import { message, optionNames, text } from "@optique/core";
|
|
11
12
|
import { printError } from "@optique/run";
|
|
12
13
|
import { createWriteStream } from "node:fs";
|
|
13
|
-
import { FetchError, UrlError, expandIPv6Address, isValidPublicIPv4Address, isValidPublicIPv6Address } from "@fedify/vocab-runtime";
|
|
14
|
+
import { FetchError, UrlError, expandIPv6Address, formatIri, isValidPublicIPv4Address, isValidPublicIPv6Address } from "@fedify/vocab-runtime";
|
|
14
15
|
import { generateCryptoKeyPair, getAuthenticatedDocumentLoader, respondWithObject } from "@fedify/fedify";
|
|
15
16
|
import { getLogger } from "@logtape/logtape";
|
|
16
17
|
import ora from "ora";
|
|
@@ -44,10 +45,13 @@ const MAX_TIMEOUT_MS = 2147483647;
|
|
|
44
45
|
*/
|
|
45
46
|
var RecursiveLookupError = class extends Error {
|
|
46
47
|
target;
|
|
47
|
-
|
|
48
|
+
/** Why the target could not be fetched, if known. */
|
|
49
|
+
failure;
|
|
50
|
+
constructor(target, failure) {
|
|
48
51
|
super(`Failed to recursively fetch object: ${target}`);
|
|
49
52
|
this.name = "RecursiveLookupError";
|
|
50
53
|
this.target = target;
|
|
54
|
+
this.failure = failure;
|
|
51
55
|
}
|
|
52
56
|
};
|
|
53
57
|
function writeToStream(stream, chunk) {
|
|
@@ -281,12 +285,12 @@ async function collectRecursiveObjects(initialObject, recurseProperty, recurseDe
|
|
|
281
285
|
const visited = options.visited ?? /* @__PURE__ */ new Set();
|
|
282
286
|
const results = [];
|
|
283
287
|
let current = initialObject;
|
|
284
|
-
if (current.id != null) visited.add(current.id
|
|
288
|
+
if (current.id != null) visited.add(getVisitedKey(current.id));
|
|
285
289
|
for (let depth = 0; depth < recurseDepth; depth++) {
|
|
286
290
|
const targetId = getRecursiveTargetId(current, recurseProperty);
|
|
287
291
|
if (targetId == null) break;
|
|
288
|
-
const target = targetId
|
|
289
|
-
if (visited.has(
|
|
292
|
+
const target = formatTarget(targetId);
|
|
293
|
+
if (visited.has(getVisitedKey(targetId))) break;
|
|
290
294
|
let next;
|
|
291
295
|
try {
|
|
292
296
|
next = await lookup(target);
|
|
@@ -308,42 +312,42 @@ async function collectRecursiveObjects(initialObject, recurseProperty, recurseDe
|
|
|
308
312
|
throw new RecursiveLookupError(target);
|
|
309
313
|
}
|
|
310
314
|
results.push(next);
|
|
311
|
-
visited.add(
|
|
312
|
-
if (next.id != null) visited.add(next.id
|
|
315
|
+
visited.add(getVisitedKey(targetId));
|
|
316
|
+
if (next.id != null) visited.add(getVisitedKey(next.id));
|
|
313
317
|
current = next;
|
|
314
318
|
}
|
|
315
319
|
return results;
|
|
316
320
|
}
|
|
321
|
+
/**
|
|
322
|
+
* Gets the key for recognizing an already visited object, which identifies
|
|
323
|
+
* FEP-ef61 portable objects by their canonical IDs.
|
|
324
|
+
* @param iri The object's ID, or the raw string given by the user.
|
|
325
|
+
* @returns The key.
|
|
326
|
+
*/
|
|
327
|
+
function getVisitedKey(iri) {
|
|
328
|
+
return getIriKey(iri) ?? (typeof iri === "string" ? iri : iri.href);
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Formats a recursion target, printing FEP-ef61 portable IDs in their
|
|
332
|
+
* canonical form rather than their percent-encoded `URL` form.
|
|
333
|
+
*/
|
|
334
|
+
function formatTarget(target) {
|
|
335
|
+
try {
|
|
336
|
+
return formatIri(target);
|
|
337
|
+
} catch {
|
|
338
|
+
return target.href;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
317
341
|
async function runLookup(command, deps = {}) {
|
|
318
342
|
const effectiveDeps = {
|
|
319
343
|
lookupObject,
|
|
320
344
|
traverseCollection,
|
|
345
|
+
getDocumentLoader: getDocumentLoader$1,
|
|
321
346
|
exit: (code) => process.exit(code),
|
|
322
347
|
...deps
|
|
323
348
|
};
|
|
324
|
-
const lookupDiagnosed =
|
|
325
|
-
|
|
326
|
-
try {
|
|
327
|
-
const object = await effectiveDeps.lookupObject(identifier, {
|
|
328
|
-
...options,
|
|
329
|
-
...diagnostics
|
|
330
|
-
});
|
|
331
|
-
return {
|
|
332
|
-
object,
|
|
333
|
-
failure: object == null ? diagnostics.getObjectFailure() ?? diagnostics.getContextFailure() : void 0,
|
|
334
|
-
thrownError: void 0
|
|
335
|
-
};
|
|
336
|
-
} catch (error) {
|
|
337
|
-
return {
|
|
338
|
-
object: null,
|
|
339
|
-
thrownError: error,
|
|
340
|
-
failure: diagnostics.getContextFailure() ?? diagnostics.getObjectFailure() ?? {
|
|
341
|
-
error,
|
|
342
|
-
source: "other"
|
|
343
|
-
}
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
};
|
|
349
|
+
const lookupDiagnosed = (identifier, options) => lookupWithDiagnostics(identifier, options, effectiveDeps.lookupObject);
|
|
350
|
+
const gatewayOptions = command.gateways.length > 0 ? { gateways: command.gateways } : {};
|
|
347
351
|
if (command.urls.length < 1) {
|
|
348
352
|
printError(message`At least one URL or actor handle must be provided.`);
|
|
349
353
|
effectiveDeps.exit(1);
|
|
@@ -354,12 +358,12 @@ async function runLookup(command, deps = {}) {
|
|
|
354
358
|
discardStdin: false
|
|
355
359
|
}).start();
|
|
356
360
|
let server = void 0;
|
|
357
|
-
const initialDocumentLoader = wrapDocumentLoaderWithTimeout(await getDocumentLoader
|
|
361
|
+
const initialDocumentLoader = wrapDocumentLoaderWithTimeout(await effectiveDeps.getDocumentLoader({
|
|
358
362
|
userAgent: command.userAgent,
|
|
359
363
|
allowPrivateAddress: true,
|
|
360
364
|
timeout: toDocumentLoaderTimeout(command.timeout)
|
|
361
365
|
}), command.timeout);
|
|
362
|
-
const documentLoader = wrapDocumentLoaderWithTimeout(await getDocumentLoader
|
|
366
|
+
const documentLoader = wrapDocumentLoaderWithTimeout(await effectiveDeps.getDocumentLoader({
|
|
363
367
|
userAgent: command.userAgent,
|
|
364
368
|
allowPrivateAddress: command.allowPrivateAddress,
|
|
365
369
|
timeout: toDocumentLoaderTimeout(command.timeout)
|
|
@@ -475,30 +479,25 @@ async function runLookup(command, deps = {}) {
|
|
|
475
479
|
const visited = /* @__PURE__ */ new Set();
|
|
476
480
|
const url = command.urls[urlIndex];
|
|
477
481
|
if (urlIndex > 0) spinner.text = `Looking up object chain ${urlIndex + 1}/${command.urls.length}...`;
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
482
|
+
const result = await lookupDiagnosed(url, {
|
|
483
|
+
documentLoader: initialLookupDocumentLoader,
|
|
484
|
+
contextLoader,
|
|
485
|
+
verifierDocumentLoader: recursiveLookupDocumentLoader,
|
|
486
|
+
userAgent: command.userAgent,
|
|
487
|
+
...gatewayOptions
|
|
488
|
+
});
|
|
489
|
+
const current = result.object;
|
|
490
|
+
if (current == null) {
|
|
491
|
+
if (result.thrownError instanceof TimeoutError || result.failure?.error instanceof TimeoutError) handleTimeoutError(spinner, command.timeout, url);
|
|
487
492
|
else {
|
|
488
493
|
spinner.fail(`Failed to fetch object: ${colors.red(url)}.`);
|
|
489
|
-
|
|
494
|
+
printError(message`${text(describeLookupFailure(result.failure, authLoader != null).message)}`);
|
|
490
495
|
}
|
|
491
496
|
await finalizeAndExit(1);
|
|
492
497
|
return;
|
|
493
498
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
if (authLoader == null) printError(message`It may be a private object. Try with ${optionNames(["-a", "--authorized-fetch"])}.`);
|
|
497
|
-
await finalizeAndExit(1);
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
visited.add(url);
|
|
501
|
-
if (current.id != null) visited.add(current.id.href);
|
|
499
|
+
visited.add(getVisitedKey(url));
|
|
500
|
+
if (current.id != null) visited.add(getVisitedKey(current.id));
|
|
502
501
|
if (!command.reverse) try {
|
|
503
502
|
if (totalObjects > 0) await writeSeparator(command.separator, getOutputStream());
|
|
504
503
|
await writeObjectToStream(current, command.output, command.format, contextLoader, getOutputStream());
|
|
@@ -511,11 +510,18 @@ async function runLookup(command, deps = {}) {
|
|
|
511
510
|
}
|
|
512
511
|
let chain = [];
|
|
513
512
|
try {
|
|
514
|
-
chain = await collectRecursiveObjects(current, command.recurse, recurseDepth, (target) =>
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
513
|
+
chain = await collectRecursiveObjects(current, command.recurse, recurseDepth, async (target) => {
|
|
514
|
+
const result = await lookupDiagnosed(target, {
|
|
515
|
+
documentLoader: recursiveLookupDocumentLoader,
|
|
516
|
+
contextLoader: recursiveContextLoader,
|
|
517
|
+
verifierDocumentLoader: recursiveLookupDocumentLoader,
|
|
518
|
+
userAgent: command.userAgent,
|
|
519
|
+
...getPortableLookupProblem(target) === "no-gateway" ? gatewayOptions : {}
|
|
520
|
+
});
|
|
521
|
+
if (result.object != null) return result.object;
|
|
522
|
+
if (result.thrownError != null && !(result.failure?.error instanceof TimeoutError)) throw result.thrownError;
|
|
523
|
+
throw new RecursiveLookupError(target, result.failure);
|
|
524
|
+
}, {
|
|
519
525
|
suppressErrors: command.suppressErrors,
|
|
520
526
|
visited
|
|
521
527
|
});
|
|
@@ -532,9 +538,13 @@ async function runLookup(command, deps = {}) {
|
|
|
532
538
|
}
|
|
533
539
|
logger.error("Failed to recursively fetch an object in chain: {error}", { error });
|
|
534
540
|
if (error instanceof TimeoutError) handleTimeoutError(spinner, command.timeout);
|
|
541
|
+
else if (error instanceof RecursiveLookupError && error.failure?.error instanceof TimeoutError) handleTimeoutError(spinner, command.timeout, error.target);
|
|
535
542
|
else if (error instanceof RecursiveLookupError) {
|
|
536
543
|
spinner.fail(`Failed to recursively fetch object: ${colors.red(error.target)}.`);
|
|
544
|
+
const privateContextUrl = error.failure?.source === "context" ? getPrivateContextUrl(error.failure.error) : null;
|
|
537
545
|
if (!command.allowPrivateAddress && isPrivateAddressTarget(error.target)) printRecursivePrivateAddressHint();
|
|
546
|
+
else if (privateContextUrl != null) printRecursivePrivateContextHint(privateContextUrl);
|
|
547
|
+
else if (error.failure != null) printError(message`${text(describeLookupFailure(error.failure, authLoader != null).message)}`);
|
|
538
548
|
else if (authLoader == null) printError(message`It may be a private object. Try with ${optionNames(["-a", "--authorized-fetch"])}.`);
|
|
539
549
|
} else {
|
|
540
550
|
spinner.fail("Failed to recursively fetch object.");
|
|
@@ -604,7 +614,9 @@ async function runLookup(command, deps = {}) {
|
|
|
604
614
|
const result = await lookupDiagnosed(url, {
|
|
605
615
|
documentLoader: initialAuthLoader ?? initialDocumentLoader,
|
|
606
616
|
contextLoader,
|
|
607
|
-
|
|
617
|
+
verifierDocumentLoader: authLoader ?? documentLoader,
|
|
618
|
+
userAgent: command.userAgent,
|
|
619
|
+
...gatewayOptions
|
|
608
620
|
});
|
|
609
621
|
const collection = result.object;
|
|
610
622
|
if (collection == null) {
|
|
@@ -622,13 +634,15 @@ async function runLookup(command, deps = {}) {
|
|
|
622
634
|
return;
|
|
623
635
|
}
|
|
624
636
|
spinner.succeed(`Fetched collection: ${colors.green(url)}.`);
|
|
625
|
-
const diagnostics = createLookupDiagnostics(authLoader ?? documentLoader, contextLoader);
|
|
637
|
+
const diagnostics = createLookupDiagnostics(authLoader ?? documentLoader, contextLoader, authLoader ?? documentLoader);
|
|
626
638
|
try {
|
|
627
639
|
if (command.reverse) {
|
|
628
640
|
const { items: traversedItems, error: traversalError } = await collectAsyncItems(effectiveDeps.traverseCollection(collection, {
|
|
629
641
|
documentLoader: diagnostics.documentLoader,
|
|
630
642
|
contextLoader: diagnostics.contextLoader,
|
|
631
|
-
|
|
643
|
+
verifyPortableObject: diagnostics.verifyPortableObject,
|
|
644
|
+
suppressError: command.suppressErrors,
|
|
645
|
+
...gatewayOptions
|
|
632
646
|
}));
|
|
633
647
|
for (let index = traversedItems.length - 1; index >= 0; index--) {
|
|
634
648
|
const item = traversedItems[index];
|
|
@@ -657,7 +671,9 @@ async function runLookup(command, deps = {}) {
|
|
|
657
671
|
} else for await (const item of effectiveDeps.traverseCollection(collection, {
|
|
658
672
|
documentLoader: diagnostics.documentLoader,
|
|
659
673
|
contextLoader: diagnostics.contextLoader,
|
|
660
|
-
|
|
674
|
+
verifyPortableObject: diagnostics.verifyPortableObject,
|
|
675
|
+
suppressError: command.suppressErrors,
|
|
676
|
+
...gatewayOptions
|
|
661
677
|
})) {
|
|
662
678
|
try {
|
|
663
679
|
if (totalItems > 0) await writeSeparator(command.separator, getOutputStream());
|
|
@@ -686,7 +702,7 @@ async function runLookup(command, deps = {}) {
|
|
|
686
702
|
url,
|
|
687
703
|
error
|
|
688
704
|
});
|
|
689
|
-
const failure = diagnostics.getContextFailure() ?? diagnostics.getObjectFailure() ?? {
|
|
705
|
+
const failure = diagnostics.getContextFailure() ?? diagnostics.getObjectFailure() ?? diagnostics.getPortableFailure() ?? {
|
|
690
706
|
error,
|
|
691
707
|
source: "other"
|
|
692
708
|
};
|
|
@@ -711,7 +727,9 @@ async function runLookup(command, deps = {}) {
|
|
|
711
727
|
const objects = await Promise.all(command.urls.map((url) => lookupDiagnosed(url, {
|
|
712
728
|
documentLoader: initialAuthLoader ?? initialDocumentLoader,
|
|
713
729
|
contextLoader,
|
|
714
|
-
|
|
730
|
+
verifierDocumentLoader: authLoader ?? documentLoader,
|
|
731
|
+
userAgent: command.userAgent,
|
|
732
|
+
...gatewayOptions
|
|
715
733
|
})));
|
|
716
734
|
spinner.stop();
|
|
717
735
|
let success = true;
|
|
@@ -761,4 +779,4 @@ async function runLookup(command, deps = {}) {
|
|
|
761
779
|
if (success && command.output) spinner.succeed(`Successfully wrote output to ${colors.green(command.output)}.`);
|
|
762
780
|
}
|
|
763
781
|
//#endregion
|
|
764
|
-
export { RecursiveLookupError, TimeoutError, collectAsyncItems, collectRecursiveObjects, getLookupFailureHint, getPrivateUrlCandidate, getRecursiveTargetId, isDocumentLoaderTimeoutError, runLookup, shouldPrintLookupFailureHint, shouldSuggestSuppressErrorsForLookupFailure, toDocumentLoaderTimeout, toPresentationOrder, wrapDocumentLoaderWithTimeout, writeObjectToStream, writeSeparator };
|
|
782
|
+
export { RecursiveLookupError, TimeoutError, collectAsyncItems, collectRecursiveObjects, getLookupFailureHint, getPrivateUrlCandidate, getRecursiveTargetId, getVisitedKey, isDocumentLoaderTimeoutError, runLookup, shouldPrintLookupFailureHint, shouldSuggestSuppressErrorsForLookupFailure, toDocumentLoaderTimeout, toPresentationOrder, wrapDocumentLoaderWithTimeout, writeObjectToStream, writeSeparator };
|
package/dist/portable.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import "temporal-polyfill";
|
|
2
|
+
import { message } from "@optique/core";
|
|
3
|
+
import { canonicalizePortableUri, formatIri, fromCompatibleEf61Id, getGatewayHints, isGatewayUrl, parseIri } from "@fedify/vocab-runtime";
|
|
4
|
+
import { verifyPortableObject } from "@fedify/fedify";
|
|
5
|
+
//#region src/portable.ts
|
|
6
|
+
const PORTABLE_IRI_PATTERN = /^ap(?:\+ef61)?:/i;
|
|
7
|
+
/**
|
|
8
|
+
* Checks whether the identifier is an [FEP-ef61] portable `ap:` or
|
|
9
|
+
* `ap+ef61:` ID. It does not check whether the ID is well-formed.
|
|
10
|
+
*
|
|
11
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
12
|
+
*/
|
|
13
|
+
function isPortableIdentifier(identifier) {
|
|
14
|
+
return PORTABLE_IRI_PATTERN.test(typeof identifier === "string" ? identifier : identifier.protocol);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Checks whether the identifier is an [FEP-ef61] portable ID or looks like
|
|
18
|
+
* a compatible identifier, i.e., whether `lookupObject()` looks it up
|
|
19
|
+
* through gateways.
|
|
20
|
+
*
|
|
21
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
22
|
+
*/
|
|
23
|
+
function isPortableReference(identifier) {
|
|
24
|
+
if (isPortableIdentifier(identifier)) return true;
|
|
25
|
+
try {
|
|
26
|
+
return fromCompatibleEf61Id(identifier) != null;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error instanceof TypeError) return true;
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Tells why a portable ID cannot be looked up before asking any server.
|
|
34
|
+
* @param identifier The identifier to look up.
|
|
35
|
+
* @param gateways The gateways given with `--gateway`, if any.
|
|
36
|
+
* @returns `"malformed"` if the identifier is a malformed portable ID,
|
|
37
|
+
* `"no-gateway"` if it is a portable ID without `@gateway` location
|
|
38
|
+
* hints and no gateways are given, or `null` otherwise.
|
|
39
|
+
*/
|
|
40
|
+
function getPortableLookupProblem(identifier, gateways) {
|
|
41
|
+
if (!isPortableIdentifier(identifier)) return null;
|
|
42
|
+
let hints;
|
|
43
|
+
try {
|
|
44
|
+
parseIri(identifier);
|
|
45
|
+
hints = getGatewayHints(identifier);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (error instanceof TypeError) return "malformed";
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
if (hints.length > 0 || gateways != null && gateways.length > 0) return null;
|
|
51
|
+
return "no-gateway";
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Computes a key to compare IRIs with. An [FEP-ef61] portable ID or
|
|
55
|
+
* compatible identifier is turned into its canonical form, which ignores
|
|
56
|
+
* the scheme spelling, the percent-encoding of the DID, the gateway of
|
|
57
|
+
* a compatible identifier, and the query (e.g., `@gateway` location hints).
|
|
58
|
+
* Other IRIs are compared by their normalized URLs.
|
|
59
|
+
*
|
|
60
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
61
|
+
* @param iri The IRI. Pass the raw string as given if available, since
|
|
62
|
+
* parsing it as a `URL` may change the object it identifies.
|
|
63
|
+
* @returns The key, or `null` if the IRI is malformed.
|
|
64
|
+
*/
|
|
65
|
+
function getIriKey(iri) {
|
|
66
|
+
try {
|
|
67
|
+
const raw = typeof iri === "string" ? iri : formatIri(iri);
|
|
68
|
+
if (isPortableIdentifier(raw)) return canonicalizePortableUri(raw);
|
|
69
|
+
const portable = fromCompatibleEf61Id(iri);
|
|
70
|
+
if (portable != null) return canonicalizePortableUri(formatIri(portable));
|
|
71
|
+
return (typeof iri === "string" ? new URL(iri) : iri).href;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error instanceof TypeError) return null;
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Creates the [FEP-ef61] portable object verifier that the CLI uses.
|
|
79
|
+
*
|
|
80
|
+
* The verifier always fetches documents, such as the owner of an unsecured
|
|
81
|
+
* collection, with the given document loader rather than the one passed by
|
|
82
|
+
* the caller, since `lookupObject()` passes its own loader, which may allow
|
|
83
|
+
* private addresses for the objects explicitly given on the command line.
|
|
84
|
+
* @param documentLoader The document loader for the documents that the
|
|
85
|
+
* verifier discovers.
|
|
86
|
+
* @param contextLoader The context loader to use when the caller passes
|
|
87
|
+
* none.
|
|
88
|
+
* @returns The verifier.
|
|
89
|
+
*/
|
|
90
|
+
function createPortableObjectVerifier(documentLoader, contextLoader) {
|
|
91
|
+
return (document, options) => verifyPortableObject(document, {
|
|
92
|
+
...options,
|
|
93
|
+
documentLoader,
|
|
94
|
+
contextLoader: options.contextLoader ?? contextLoader
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* A value parser for the `--gateway` option, which accepts an [FEP-ef61]
|
|
99
|
+
* gateway, i.e., an HTTP(S) origin without credentials, path, query, or
|
|
100
|
+
* fragment.
|
|
101
|
+
*
|
|
102
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
103
|
+
*/
|
|
104
|
+
function gatewayUrl() {
|
|
105
|
+
return {
|
|
106
|
+
mode: "sync",
|
|
107
|
+
metavar: "GATEWAY",
|
|
108
|
+
placeholder: new URL("https://gateway.invalid/"),
|
|
109
|
+
parse(input) {
|
|
110
|
+
let url;
|
|
111
|
+
try {
|
|
112
|
+
url = parseIri(input);
|
|
113
|
+
} catch {
|
|
114
|
+
return {
|
|
115
|
+
success: false,
|
|
116
|
+
error: message`Invalid URL: ${input}.`
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (!isGatewayUrl(url)) return {
|
|
120
|
+
success: false,
|
|
121
|
+
error: message`Invalid gateway: ${input}. A gateway must be \
|
|
122
|
+
an HTTP(S) origin without credentials, path, query, or fragment, \
|
|
123
|
+
e.g., ${"https://example.com"}.`
|
|
124
|
+
};
|
|
125
|
+
return {
|
|
126
|
+
success: true,
|
|
127
|
+
value: url
|
|
128
|
+
};
|
|
129
|
+
},
|
|
130
|
+
format(value) {
|
|
131
|
+
return value.href;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
//#endregion
|
|
136
|
+
export { createPortableObjectVerifier, gatewayUrl, getIriKey, getPortableLookupProblem, isPortableIdentifier, isPortableReference };
|
package/dist/relay/command.js
CHANGED
|
@@ -30,12 +30,12 @@ const relayOptions = merge(object("Relay options", {
|
|
|
30
30
|
key: (config) => config.relay?.name ?? "Fedify Relay",
|
|
31
31
|
default: "Fedify Relay"
|
|
32
32
|
}),
|
|
33
|
-
acceptFollow: bindConfig(multiple(option("-a", "--accept-follow", string({ metavar: "URI" }), { description: message`Accept follow requests from the given actor. The argument can be either an actor URI or a handle, or a wildcard (${"*"}). Can be specified multiple times. If a wildcard is specified, all follow requests will be accepted.` })), {
|
|
33
|
+
acceptFollow: bindConfig(multiple(option("-a", "--accept-follow", string({ metavar: "URI" }), { description: message`Accept follow requests from the given actor. The argument can be either an actor URI (including an FEP-ef61 portable ID) or a handle, or a wildcard (${"*"}). Can be specified multiple times. If a wildcard is specified, all follow requests will be accepted.` })), {
|
|
34
34
|
context: configContext,
|
|
35
35
|
key: (config) => config.relay?.acceptFollow ?? [],
|
|
36
36
|
default: []
|
|
37
37
|
}),
|
|
38
|
-
rejectFollow: bindConfig(multiple(option("-r", "--reject-follow", string({ metavar: "URI" }), { description: message`Reject follow requests from the given actor. The argument can be either an actor URI or a handle, or a wildcard (${"*"}). Can be specified multiple times. If a wildcard is specified, all follow requests will be rejected.` })), {
|
|
38
|
+
rejectFollow: bindConfig(multiple(option("-r", "--reject-follow", string({ metavar: "URI" }), { description: message`Reject follow requests from the given actor. The argument can be either an actor URI (including an FEP-ef61 portable ID) or a handle, or a wildcard (${"*"}). Can be specified multiple times. If a wildcard is specified, all follow requests will be rejected.` })), {
|
|
39
39
|
context: configContext,
|
|
40
40
|
key: (config) => config.relay?.rejectFollow ?? [],
|
|
41
41
|
default: []
|
package/dist/utils.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import "temporal-polyfill";
|
|
2
|
+
import { getIriKey } from "./portable.js";
|
|
2
3
|
import process from "node:process";
|
|
3
4
|
import { message } from "@optique/core";
|
|
4
5
|
import { print, printError } from "@optique/run";
|
|
@@ -20,19 +21,43 @@ function formatObject(obj, colors, json) {
|
|
|
20
21
|
if (enableColors) return highlight(formatted, { language: "json" });
|
|
21
22
|
return formatted;
|
|
22
23
|
}
|
|
23
|
-
|
|
24
|
+
const URI_PATTERN = /^(?:https?|ap|ap\+ef61):/i;
|
|
25
|
+
const HANDLE_PATTERN = /^@?[^@/\s]+@[^@/\s]+$/;
|
|
26
|
+
/**
|
|
27
|
+
* Checks whether the actor matches any of the given actor URIs, handles, or
|
|
28
|
+
* the wildcard (`*`). [FEP-ef61] portable actors match their `ap:` and
|
|
29
|
+
* `ap+ef61:` IDs and compatible identifiers regardless of `@gateway` location
|
|
30
|
+
* hints and the gateway of a compatible identifier.
|
|
31
|
+
*
|
|
32
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
33
|
+
* @param actor The actor to check.
|
|
34
|
+
* @param actorList The actor URIs, handles, or wildcards.
|
|
35
|
+
* @param getHandle The function to get the actor's handle with. It is called
|
|
36
|
+
* at most once, and only if the list has a handle.
|
|
37
|
+
* @returns `true` if the actor matches.
|
|
38
|
+
* @throws If the list has a handle, the actor's handle cannot be determined,
|
|
39
|
+
* and the actor matches nothing else in the list.
|
|
40
|
+
*/
|
|
41
|
+
async function matchesActor(actor, actorList, getHandle = getActorHandle) {
|
|
24
42
|
const actorUri = actor.id;
|
|
25
|
-
let actorHandle = void 0;
|
|
26
43
|
if (actorUri == null) return false;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
44
|
+
const actorKey = getIriKey(actorUri);
|
|
45
|
+
let actorHandle;
|
|
46
|
+
let handleError;
|
|
47
|
+
for (const entry of actorList) {
|
|
48
|
+
if (entry == "*") return true;
|
|
49
|
+
if (URI_PATTERN.test(entry)) {
|
|
50
|
+
const key = getIriKey(entry);
|
|
51
|
+
if (key != null && key === actorKey) return true;
|
|
52
|
+
continue;
|
|
32
53
|
}
|
|
33
|
-
if (
|
|
34
|
-
|
|
54
|
+
if (!HANDLE_PATTERN.test(entry)) continue;
|
|
55
|
+
actorHandle ??= getHandle(actor).then((handle) => ({ handle }), (error) => ({ error }));
|
|
56
|
+
const result = await actorHandle;
|
|
57
|
+
if ("error" in result) handleError = result;
|
|
58
|
+
else if (result.handle === entry) return true;
|
|
35
59
|
}
|
|
60
|
+
if (handleError != null) throw handleError.error;
|
|
36
61
|
return false;
|
|
37
62
|
}
|
|
38
63
|
function describeError(error) {
|
package/dist/webfinger/action.js
CHANGED
|
@@ -1,38 +1,124 @@
|
|
|
1
1
|
import "temporal-polyfill";
|
|
2
|
+
import { createPortableObjectVerifier, getIriKey, getPortableLookupProblem, isPortableReference } from "../portable.js";
|
|
2
3
|
import { formatObject } from "../utils.js";
|
|
3
|
-
import {
|
|
4
|
+
import { getContextLoader, getDocumentLoader as getDocumentLoader$1 } from "../docloader.js";
|
|
5
|
+
import { NotFoundError, PortableActorError, UnlinkedPortableActorError, getErrorMessage } from "./error.js";
|
|
4
6
|
import { convertUrlIfHandle } from "./lib.js";
|
|
5
7
|
import { print } from "@optique/run";
|
|
8
|
+
import { formatIri } from "@fedify/vocab-runtime";
|
|
6
9
|
import ora from "ora";
|
|
10
|
+
import { isActor, lookupObject } from "@fedify/vocab";
|
|
7
11
|
import { formatMessage, message } from "@optique/core/message";
|
|
8
12
|
import { lookupWebFinger } from "@fedify/webfinger";
|
|
9
13
|
//#region src/webfinger/action.ts
|
|
10
|
-
async function runWebFinger({
|
|
11
|
-
await Array.fromAsync(resources
|
|
14
|
+
async function runWebFinger({ resources, userAgent, allowPrivateAddresses, maxRedirection, gateways }) {
|
|
15
|
+
await Array.fromAsync(resources, (resource) => lookupWithSpinner({
|
|
12
16
|
resource,
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
userAgent,
|
|
18
|
+
allowPrivateAddresses,
|
|
19
|
+
maxRedirection,
|
|
20
|
+
gateways
|
|
21
|
+
}));
|
|
15
22
|
}
|
|
16
|
-
|
|
17
|
-
return
|
|
23
|
+
function getLookupWebFingerOptions(options) {
|
|
24
|
+
return {
|
|
25
|
+
userAgent: options.userAgent,
|
|
26
|
+
allowPrivateAddress: options.allowPrivateAddresses,
|
|
27
|
+
maxRedirection: options.maxRedirection
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
async function lookupSingleWebFinger(options) {
|
|
31
|
+
return await lookupWebFinger(convertUrlIfHandle(options.resource), getLookupWebFingerOptions(options)) ?? new NotFoundError(options.resource).throw();
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Looks up the WebFinger address of an [FEP-ef61] portable actor by its ID,
|
|
35
|
+
* i.e., an `ap:` or `ap+ef61:` URI or a compatible identifier.
|
|
36
|
+
*
|
|
37
|
+
* As a portable actor's ID does not belong to any server, its WebFinger
|
|
38
|
+
* address is `acct:` followed by its `preferredUsername`, `@`, and the host
|
|
39
|
+
* of the first gateway in its `gateways`. The actor is looked up and its
|
|
40
|
+
* proof is verified first.
|
|
41
|
+
*
|
|
42
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
43
|
+
* @param options The options.
|
|
44
|
+
* @returns The resource descriptor, which may not link back to the actor;
|
|
45
|
+
* see {@link linksToActor}.
|
|
46
|
+
* @throws {PortableActorError} If the actor cannot be looked up, or does
|
|
47
|
+
* not have a WebFinger address.
|
|
48
|
+
* @throws {NotFoundError} If the WebFinger address is not found.
|
|
49
|
+
*/
|
|
50
|
+
async function lookupPortableWebFinger(options) {
|
|
51
|
+
const { resource, gateways } = options;
|
|
52
|
+
const problem = getPortableLookupProblem(resource, gateways);
|
|
53
|
+
if (problem === "malformed") throw new PortableActorError(resource, "it is not a valid portable ID");
|
|
54
|
+
else if (problem === "no-gateway") throw new PortableActorError(resource, "it has no @gateway location hints; use the --gateway option");
|
|
55
|
+
const documentLoader = await getDocumentLoader$1({
|
|
56
|
+
userAgent: options.userAgent,
|
|
57
|
+
allowPrivateAddress: options.allowPrivateAddresses
|
|
58
|
+
});
|
|
59
|
+
const contextLoader = await getContextLoader({
|
|
60
|
+
userAgent: options.userAgent,
|
|
61
|
+
allowPrivateAddress: options.allowPrivateAddresses
|
|
62
|
+
});
|
|
63
|
+
const actor = await lookupObject(resource, {
|
|
64
|
+
documentLoader,
|
|
65
|
+
contextLoader,
|
|
66
|
+
verifyPortableObject: createPortableObjectVerifier(documentLoader, contextLoader),
|
|
67
|
+
userAgent: options.userAgent,
|
|
68
|
+
...gateways == null || gateways.length < 1 ? {} : { gateways }
|
|
69
|
+
});
|
|
70
|
+
if (actor == null) throw new PortableActorError(resource, "it could not be looked up or verified");
|
|
71
|
+
else if (!isActor(actor) || actor.id == null) throw new PortableActorError(resource, "it is not an actor");
|
|
72
|
+
const gateway = actor.gateway;
|
|
73
|
+
const username = actor.preferredUsername?.toString();
|
|
74
|
+
if (gateway == null || username == null) throw new PortableActorError(resource, "it has no preferredUsername or gateways");
|
|
75
|
+
const address = `acct:${username}@${gateway.host}`;
|
|
76
|
+
return {
|
|
77
|
+
descriptor: await lookupWebFinger(address, getLookupWebFingerOptions(options)) ?? new NotFoundError(address).throw(),
|
|
78
|
+
address,
|
|
79
|
+
actorId: actor.id
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Checks if the first ActivityStreams `self` link of a WebFinger response
|
|
84
|
+
* identifies the given portable actor, either by its portable ID or by
|
|
85
|
+
* a compatible identifier.
|
|
86
|
+
* @param descriptor The WebFinger resource descriptor.
|
|
87
|
+
* @param actorId The portable actor's ID.
|
|
88
|
+
* @returns `true` if the descriptor links back to the actor.
|
|
89
|
+
*/
|
|
90
|
+
function linksToActor(descriptor, actorId) {
|
|
91
|
+
const link = descriptor.links?.find((l) => l.rel === "self" && l.href != null && (l.type === "application/activity+json" || l.type?.match(/application\/ld\+json;\s*profile="https:\/\/www.w3.org\/ns\/activitystreams"/) != null));
|
|
92
|
+
if (link?.href == null) return false;
|
|
93
|
+
const key = getIriKey(link.href);
|
|
94
|
+
return key != null && isPortableReference(link.href) && key === getIriKey(actorId);
|
|
18
95
|
}
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
96
|
+
async function lookupWithSpinner(options) {
|
|
97
|
+
const { resource } = options;
|
|
98
|
+
const spinner = ora({
|
|
99
|
+
text: `Looking up WebFinger for ${resource}`,
|
|
100
|
+
discardStdin: false
|
|
101
|
+
}).start();
|
|
102
|
+
try {
|
|
103
|
+
if (isPortableReference(resource)) {
|
|
104
|
+
const { descriptor, address, actorId } = await lookupPortableWebFinger(options);
|
|
105
|
+
if (linksToActor(descriptor, actorId)) spinner.succeed(formatMessage(message`WebFinger found for ${resource} (${address}):`));
|
|
106
|
+
else spinner.fail(formatMessage(getErrorMessage(resource, new UnlinkedPortableActorError(address, formatIri(actorId)))));
|
|
28
107
|
print([{
|
|
29
108
|
type: "text",
|
|
30
|
-
text: formatObject(
|
|
109
|
+
text: formatObject(descriptor)
|
|
31
110
|
}]);
|
|
32
|
-
|
|
33
|
-
spinner.fail(formatMessage(getErrorMessage(args[0].resource, error)));
|
|
111
|
+
return;
|
|
34
112
|
}
|
|
35
|
-
|
|
113
|
+
const result = await lookupSingleWebFinger(options);
|
|
114
|
+
spinner.succeed(formatMessage(message`WebFinger found for ${resource}:`));
|
|
115
|
+
print([{
|
|
116
|
+
type: "text",
|
|
117
|
+
text: formatObject(result)
|
|
118
|
+
}]);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
spinner.fail(formatMessage(getErrorMessage(resource, error)));
|
|
121
|
+
}
|
|
36
122
|
}
|
|
37
123
|
//#endregion
|
|
38
|
-
export { runWebFinger as default, lookupSingleWebFinger };
|
|
124
|
+
export { runWebFinger as default, linksToActor, lookupPortableWebFinger, lookupSingleWebFinger };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import "temporal-polyfill";
|
|
2
2
|
import { configContext } from "../config.js";
|
|
3
3
|
import { userAgentOption } from "../options.js";
|
|
4
|
+
import { gatewayUrl } from "../portable.js";
|
|
4
5
|
import { argument, command, constant, flag, group, integer, merge, message, multiple, object, option, string } from "@optique/core";
|
|
5
6
|
import { bindConfig } from "@optique/config";
|
|
6
7
|
//#region src/webfinger/command.ts
|
|
@@ -18,13 +19,18 @@ const webFingerOptions = merge("Network options", object({
|
|
|
18
19
|
command: constant("webfinger"),
|
|
19
20
|
resources: group("Arguments", multiple(argument(string({ metavar: "RESOURCE" }), { description: message`WebFinger resource(s) to look up.` }), { min: 1 })),
|
|
20
21
|
allowPrivateAddresses,
|
|
21
|
-
maxRedirection
|
|
22
|
+
maxRedirection,
|
|
23
|
+
gateways: multiple(option("--gateway", gatewayUrl(), { description: message`An FEP-ef61 gateway to look up the given \
|
|
24
|
+
portable actors from, instead of their ${"@gateway"} location hints. Can \
|
|
25
|
+
be specified multiple times.` }))
|
|
22
26
|
}), userAgentOption);
|
|
23
27
|
const webFingerMetadata = {
|
|
24
28
|
brief: message`Look up WebFinger resources.`,
|
|
25
29
|
description: message`Look up WebFinger resources.
|
|
26
30
|
|
|
27
|
-
The argument can be multiple
|
|
31
|
+
The argument can be multiple. For an FEP-ef61 portable actor ID, the actor \
|
|
32
|
+
is looked up first, and the WebFinger address derived from its first gateway \
|
|
33
|
+
is looked up.`
|
|
28
34
|
};
|
|
29
35
|
command("webfinger", webFingerOptions, webFingerMetadata);
|
|
30
36
|
//#endregion
|
package/dist/webfinger/error.js
CHANGED
|
@@ -8,7 +8,7 @@ import { message } from "@optique/core";
|
|
|
8
8
|
* @param {unknown} error The error encountered.
|
|
9
9
|
* @returns {string} A descriptive error message.
|
|
10
10
|
*/
|
|
11
|
-
const getErrorMessage = (resource, error) => error instanceof InvalidHandleError ? message`Invalid handle format: ${error.handle}` : error instanceof NotFoundError ? message`Resource not found: ${error.resource}` : error instanceof Error ? message`Failed to look up WebFinger for ${resource}: ${error.message}` : message`Failed to look up WebFinger for ${resource}: ${String(error)}`;
|
|
11
|
+
const getErrorMessage = (resource, error) => error instanceof InvalidHandleError ? message`Invalid handle format: ${error.handle}` : error instanceof PortableActorError ? message`Failed to look up the portable actor ${error.resource}, as ${error.reason}.` : error instanceof UnlinkedPortableActorError ? message`${error.address} does not link back to the portable actor ${error.actorId}, so it is not verified to be the actor's WebFinger address:` : error instanceof NotFoundError ? message`Resource not found: ${error.resource}` : error instanceof Error ? message`Failed to look up WebFinger for ${resource}: ${error.message}` : message`Failed to look up WebFinger for ${resource}: ${String(error)}`;
|
|
12
12
|
/**
|
|
13
13
|
* Custom error class for invalid handle formats.
|
|
14
14
|
* @param {string} handle The invalid handle that caused the error.
|
|
@@ -41,5 +41,43 @@ var NotFoundError = class extends Error {
|
|
|
41
41
|
throw this;
|
|
42
42
|
}
|
|
43
43
|
};
|
|
44
|
+
/**
|
|
45
|
+
* Custom error class for [FEP-ef61] portable actors whose WebFinger address
|
|
46
|
+
* cannot be determined.
|
|
47
|
+
*
|
|
48
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
49
|
+
* @param resource The portable actor's ID.
|
|
50
|
+
* @param reason Why the WebFinger address cannot be determined.
|
|
51
|
+
* @extends {Error}
|
|
52
|
+
*/
|
|
53
|
+
var PortableActorError = class extends Error {
|
|
54
|
+
resource;
|
|
55
|
+
reason;
|
|
56
|
+
constructor(resource, reason) {
|
|
57
|
+
super(`Failed to look up the portable actor ${resource}, as ${reason}.`);
|
|
58
|
+
this.resource = resource;
|
|
59
|
+
this.reason = reason;
|
|
60
|
+
this.name = "PortableActorError";
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Custom error class for a WebFinger address that does not link back to
|
|
65
|
+
* the [FEP-ef61] portable actor that claims it.
|
|
66
|
+
*
|
|
67
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
68
|
+
* @param address The WebFinger address, e.g., `acct:alice@example.com`.
|
|
69
|
+
* @param actorId The portable actor's ID.
|
|
70
|
+
* @extends {Error}
|
|
71
|
+
*/
|
|
72
|
+
var UnlinkedPortableActorError = class extends Error {
|
|
73
|
+
address;
|
|
74
|
+
actorId;
|
|
75
|
+
constructor(address, actorId) {
|
|
76
|
+
super(`${address} does not link back to the portable actor ${actorId}.`);
|
|
77
|
+
this.address = address;
|
|
78
|
+
this.actorId = actorId;
|
|
79
|
+
this.name = "UnlinkedPortableActorError";
|
|
80
|
+
}
|
|
81
|
+
};
|
|
44
82
|
//#endregion
|
|
45
|
-
export { InvalidHandleError, NotFoundError, getErrorMessage };
|
|
83
|
+
export { InvalidHandleError, NotFoundError, PortableActorError, UnlinkedPortableActorError, getErrorMessage };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fedify/cli",
|
|
3
|
-
"version": "2.4.0-dev.
|
|
3
|
+
"version": "2.4.0-dev.2253+f5212c33",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"README.md",
|
|
@@ -88,14 +88,14 @@
|
|
|
88
88
|
"temporal-polyfill": "^1.0.1",
|
|
89
89
|
"valibot": "^1.4.0",
|
|
90
90
|
"yaml": "^2.9.0",
|
|
91
|
-
"@fedify/
|
|
92
|
-
"@fedify/
|
|
93
|
-
"@fedify/
|
|
94
|
-
"@fedify/
|
|
95
|
-
"@fedify/vocab
|
|
96
|
-
"@fedify/
|
|
97
|
-
"@fedify/vocab-
|
|
98
|
-
"@fedify/webfinger": "2.4.0-dev.
|
|
91
|
+
"@fedify/relay": "2.4.0-dev.2253+f5212c33",
|
|
92
|
+
"@fedify/fedify": "2.4.0-dev.2253+f5212c33",
|
|
93
|
+
"@fedify/init": "2.4.0-dev.2253+f5212c33",
|
|
94
|
+
"@fedify/sqlite": "2.4.0-dev.2253+f5212c33",
|
|
95
|
+
"@fedify/vocab": "2.4.0-dev.2253+f5212c33",
|
|
96
|
+
"@fedify/vocab-tools": "2.4.0-dev.2253+f5212c33",
|
|
97
|
+
"@fedify/vocab-runtime": "2.4.0-dev.2253+f5212c33",
|
|
98
|
+
"@fedify/webfinger": "2.4.0-dev.2253+f5212c33"
|
|
99
99
|
},
|
|
100
100
|
"devDependencies": {
|
|
101
101
|
"@types/bun": "^1.2.23",
|