@fedify/vocab-runtime 2.4.0-pr.936.41 → 2.5.0-dev.2271
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/deno.json +4 -2
- package/dist/{tests/docloader-Ck8bcBir.mjs → contexts-BK1CqBR5.cjs} +164 -275
- package/dist/{tests/docloader-S_gQZ2tt.cjs → contexts-DPuJ4UYL.js} +160 -294
- package/dist/{docloader-C_dir7Xb.d.ts → docloader-CYwqh5Df.d.cts} +66 -2
- package/dist/{docloader-D2DTRiyA.d.cts → docloader-CYwqh5Df.d.ts} +66 -2
- package/dist/internal/jsonld-cache.cjs +70 -1
- package/dist/internal/jsonld-cache.d.cts +35 -2
- package/dist/internal/jsonld-cache.d.ts +35 -2
- package/dist/internal/jsonld-cache.js +67 -2
- package/dist/internal/portable-dereference.cjs +721 -0
- package/dist/internal/portable-dereference.d.cts +349 -0
- package/dist/internal/portable-dereference.d.ts +349 -0
- package/dist/internal/portable-dereference.js +702 -0
- package/dist/internal/signed-representation.cjs +378 -0
- package/dist/internal/signed-representation.d.cts +144 -0
- package/dist/internal/signed-representation.d.ts +144 -0
- package/dist/internal/signed-representation.js +371 -0
- package/dist/jsonld.cjs +2 -2
- package/dist/mod.cjs +711 -4527
- package/dist/mod.d.cts +400 -8
- package/dist/mod.d.ts +400 -9
- package/dist/mod.js +692 -4522
- package/dist/portable-DqtfLy_1.d.cts +160 -0
- package/dist/portable-DtWsu2yU.d.ts +160 -0
- package/dist/tests/body-CVR1bABr.mjs +130 -0
- package/dist/tests/body-T_yQWql3.cjs +153 -0
- package/dist/tests/body.test.cjs +89 -0
- package/dist/tests/body.test.d.cts +1 -0
- package/dist/tests/body.test.d.mts +1 -0
- package/dist/tests/body.test.mjs +90 -0
- package/dist/tests/contexts-CIKsin4e.mjs +4512 -0
- package/dist/tests/contexts-DizzBjz4.cjs +4523 -0
- package/dist/tests/decimal.test.cjs +8 -7
- package/dist/tests/decimal.test.mjs +8 -6
- package/dist/tests/digest-3FeH2Y-Q.cjs +176 -0
- package/dist/tests/digest-COC7xDiQ.mjs +141 -0
- package/dist/tests/digest.test.cjs +102 -0
- package/dist/tests/digest.test.d.cts +1 -0
- package/dist/tests/digest.test.d.mts +1 -0
- package/dist/tests/digest.test.mjs +103 -0
- package/dist/tests/docloader-C6yApEts.mjs +373 -0
- package/dist/tests/docloader-CJK2w3l-.cjs +396 -0
- package/dist/tests/docloader.test.cjs +692 -39
- package/dist/tests/docloader.test.mjs +686 -34
- package/dist/tests/internal/multicodec.test.cjs +2 -3
- package/dist/tests/internal/multicodec.test.mjs +2 -2
- package/dist/tests/internal/portable-dereference.test.cjs +172 -0
- package/dist/tests/internal/portable-dereference.test.d.cts +1 -0
- package/dist/tests/internal/portable-dereference.test.d.mts +1 -0
- package/dist/tests/internal/portable-dereference.test.mjs +173 -0
- package/dist/tests/jsonld-cache-BPQmOZWD.mjs +342 -0
- package/dist/tests/jsonld-cache-C07AyNOY.cjs +397 -0
- package/dist/tests/jsonld-cache.test.cjs +101 -298
- package/dist/tests/jsonld-cache.test.mjs +94 -289
- package/dist/tests/{key-_wXwomh_.cjs → key-C-AYkdJJ.cjs} +11 -4
- package/dist/tests/{key-CDGDH_vC.mjs → key-C2Db_TAJ.mjs} +11 -3
- package/dist/tests/key.test.cjs +6 -5
- package/dist/tests/key.test.mjs +6 -4
- package/dist/tests/langstr.test.cjs +4 -4
- package/dist/tests/langstr.test.mjs +2 -2
- package/dist/tests/link.test.cjs +2 -3
- package/dist/tests/link.test.mjs +2 -2
- package/dist/tests/multibase/multibase.test.cjs +8 -9
- package/dist/tests/multibase/multibase.test.mjs +6 -6
- package/dist/tests/{multibase-Bz_UUDtL.cjs → multibase-B5Mea7Ip.cjs} +19 -2
- package/dist/tests/{multibase-B4bvakyA.mjs → multibase-BPnF_L4e.mjs} +12 -1
- package/dist/tests/portable-dereference-BNXtgg5U.cjs +278 -0
- package/dist/tests/portable-dereference-DRE5bz-l.mjs +249 -0
- package/dist/tests/portable-media-C4LEXJk4.mjs +148 -0
- package/dist/tests/portable-media-D05JJIJg.cjs +153 -0
- package/dist/tests/portable-media.test.cjs +222 -0
- package/dist/tests/portable-media.test.d.cts +1 -0
- package/dist/tests/portable-media.test.d.mts +1 -0
- package/dist/tests/portable-media.test.mjs +223 -0
- package/dist/tests/portable-workers.test.cjs +36 -0
- package/dist/tests/portable-workers.test.d.cts +2 -0
- package/dist/tests/portable-workers.test.d.mts +2 -0
- package/dist/tests/portable-workers.test.mjs +35 -0
- package/dist/tests/{request-uk51rkhO.cjs → request-C-VZ9TLj.cjs} +10 -4
- package/dist/tests/{request-C8CaGwtt.mjs → request-Cf-A3DU-.mjs} +8 -2
- package/dist/tests/request.test.cjs +7 -4
- package/dist/tests/request.test.mjs +5 -2
- package/dist/tests/signed-representation.test.cjs +588 -0
- package/dist/tests/signed-representation.test.d.cts +1 -0
- package/dist/tests/signed-representation.test.d.mts +1 -0
- package/dist/tests/signed-representation.test.mjs +589 -0
- package/dist/tests/temporal.test.cjs +1 -2
- package/dist/tests/temporal.test.mjs +1 -1
- package/dist/tests/url-BNakuZ8k.cjs +998 -0
- package/dist/tests/url-DMxmp7ZG.mjs +859 -0
- package/dist/tests/url.test.cjs +489 -6
- package/dist/tests/url.test.mjs +489 -5
- package/dist/url-DrGTR8yv.cjs +993 -0
- package/dist/url-Dzyp-NsC.js +860 -0
- package/package.json +27 -4
- package/scripts/test-bun.mjs +17 -0
- package/src/body.test.ts +125 -0
- package/src/body.ts +152 -0
- package/src/contexts/cid-v1.json +114 -0
- package/src/contexts/fep-22cd.json +21 -0
- package/src/contexts/miscellany.json +17 -0
- package/src/contexts.ts +39 -1
- package/src/digest.test.ts +220 -0
- package/src/digest.ts +229 -0
- package/src/docloader.test.ts +918 -27
- package/src/docloader.ts +320 -105
- package/src/internal/jsonld-cache.ts +97 -1
- package/src/internal/portable-dereference.test.ts +254 -0
- package/src/internal/portable-dereference.ts +1000 -0
- package/src/internal/signed-representation.ts +565 -0
- package/src/jsonld-cache.test.ts +89 -0
- package/src/key.test.ts +9 -0
- package/src/key.ts +11 -1
- package/src/mod.ts +28 -0
- package/src/multibase/multibase.test.ts +5 -5
- package/src/portable-media.test.ts +293 -0
- package/src/portable-media.ts +264 -0
- package/src/portable-workers.test.ts +81 -0
- package/src/portable.ts +180 -0
- package/src/preprocessor.ts +7 -0
- package/src/request.test.ts +10 -1
- package/src/request.ts +9 -1
- package/src/signed-representation.test.ts +338 -0
- package/src/url.test.ts +823 -3
- package/src/url.ts +700 -23
- package/tsdown.config.ts +2 -0
- package/dist/tests/url-CsOV_B_P.cjs +0 -482
- package/dist/tests/url-Du7RQQgP.mjs +0 -392
- package/dist/url-DD4F0ULf.cjs +0 -483
- package/dist/url-DGVbSVVi.js +0 -393
- /package/dist/{chunk-M78iaK0I.cjs → rolldown-runtime-B7lfambq.cjs} +0 -0
- /package/dist/tests/{langstr-CbAxaeEZ.cjs → langstr-C4Fl80ae.cjs} +0 -0
- /package/dist/tests/{langstr-Di5AvKpB.mjs → langstr-CQ26J_L7.mjs} +0 -0
- /package/dist/tests/{link-NUUWCdnK.mjs → link-Cevmc87v.mjs} +0 -0
- /package/dist/tests/{link-FguCydMA.cjs → link-DlKm8bEr.cjs} +0 -0
- /package/dist/tests/{multicodec-CxGVGa91.cjs → multicodec-CLRPeW4N.cjs} +0 -0
- /package/dist/tests/{multicodec-CyFp54fI.mjs → multicodec-CRIj_05H.mjs} +0 -0
- /package/dist/tests/{chunk-C2EiDwsr.cjs → rolldown-runtime-emK7D4bc.cjs} +0 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { test, testDefinitions } from "@fedify/fixture";
|
|
2
|
+
import { deepStrictEqual, equal } from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
computeDigestMultibase,
|
|
5
|
+
createHashlink,
|
|
6
|
+
verifyHashlink,
|
|
7
|
+
} from "./digest.ts";
|
|
8
|
+
import { exportDidKey, importDidKey } from "./key.ts";
|
|
9
|
+
import { getPortableGatewayCandidates } from "./internal/portable-dereference.ts";
|
|
10
|
+
import {
|
|
11
|
+
canonicalizePortableUri,
|
|
12
|
+
formatIri,
|
|
13
|
+
fromCompatibleEf61Id,
|
|
14
|
+
parseIri,
|
|
15
|
+
toCompatibleEf61Id,
|
|
16
|
+
} from "./url.ts";
|
|
17
|
+
|
|
18
|
+
const portableId = "ap://did:key:z6Mkabc/actor";
|
|
19
|
+
|
|
20
|
+
export { testDefinitions };
|
|
21
|
+
|
|
22
|
+
test("vocab-runtime portable URI formats on Workers", () => {
|
|
23
|
+
equal(formatIri(parseIri(portableId)), "ap+ef61://did:key:z6Mkabc/actor");
|
|
24
|
+
equal(
|
|
25
|
+
canonicalizePortableUri(`${portableId}?@gateway=https%3A%2F%2Fa.example`),
|
|
26
|
+
"ap+ef61://did:key:z6Mkabc/actor",
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("vocab-runtime compatible IDs on Workers", () => {
|
|
31
|
+
const compatible = toCompatibleEf61Id(portableId, "https://gw.example");
|
|
32
|
+
equal(
|
|
33
|
+
compatible.href,
|
|
34
|
+
"https://gw.example/.well-known/apgateway/did:key:z6Mkabc/actor",
|
|
35
|
+
);
|
|
36
|
+
equal(
|
|
37
|
+
formatIri(fromCompatibleEf61Id(compatible)!),
|
|
38
|
+
"ap+ef61://did:key:z6Mkabc/actor",
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("vocab-runtime did:key on Workers", async () => {
|
|
43
|
+
const keys = await crypto.subtle.generateKey("Ed25519", true, [
|
|
44
|
+
"sign",
|
|
45
|
+
"verify",
|
|
46
|
+
]) as CryptoKeyPair;
|
|
47
|
+
const did = await exportDidKey(keys.publicKey);
|
|
48
|
+
const imported = await importDidKey(did);
|
|
49
|
+
deepStrictEqual(
|
|
50
|
+
new Uint8Array(await crypto.subtle.exportKey("raw", imported)),
|
|
51
|
+
new Uint8Array(await crypto.subtle.exportKey("raw", keys.publicKey)),
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("vocab-runtime hashlink on Workers", async () => {
|
|
56
|
+
const bytes = new TextEncoder().encode("portable media");
|
|
57
|
+
const digest = await computeDigestMultibase(bytes);
|
|
58
|
+
equal(await verifyHashlink(bytes, createHashlink(digest)), true);
|
|
59
|
+
equal(
|
|
60
|
+
await verifyHashlink(
|
|
61
|
+
new TextEncoder().encode("tampered"),
|
|
62
|
+
createHashlink(digest),
|
|
63
|
+
),
|
|
64
|
+
false,
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("vocab-runtime gateway candidates on Workers", () => {
|
|
69
|
+
const hinted = parseIri(
|
|
70
|
+
`${portableId}?@gateway=https%3A%2F%2Fhint.example`,
|
|
71
|
+
);
|
|
72
|
+
deepStrictEqual(
|
|
73
|
+
getPortableGatewayCandidates(hinted, ["https://first.example"])
|
|
74
|
+
.map((url) => url.href),
|
|
75
|
+
["https://first.example/"],
|
|
76
|
+
);
|
|
77
|
+
deepStrictEqual(
|
|
78
|
+
getPortableGatewayCandidates(hinted).map((url) => url.href),
|
|
79
|
+
["https://hint.example/"],
|
|
80
|
+
);
|
|
81
|
+
});
|
package/src/portable.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type { TracerProvider } from "@opentelemetry/api";
|
|
2
|
+
import type { DocumentLoader } from "./docloader.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Options passed to a {@link PortableObjectVerifier}.
|
|
6
|
+
* @since 2.4.0
|
|
7
|
+
*/
|
|
8
|
+
export interface PortableObjectVerifierOptions {
|
|
9
|
+
/**
|
|
10
|
+
* The document loader for fetching remote documents, such as verification
|
|
11
|
+
* methods that are not `did:key` URLs.
|
|
12
|
+
*/
|
|
13
|
+
readonly documentLoader?: DocumentLoader;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The document loader for fetching remote JSON-LD contexts. During
|
|
17
|
+
* gateway dereferencing, this loader returns the same context documents
|
|
18
|
+
* that the vocabulary parser sees.
|
|
19
|
+
*/
|
|
20
|
+
readonly contextLoader?: DocumentLoader;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The OpenTelemetry tracer provider.
|
|
24
|
+
*/
|
|
25
|
+
readonly tracerProvider?: TracerProvider;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The URL the document was finally retrieved from, i.e., the `documentUrl`
|
|
29
|
+
* of the document loader's result, which may differ from the requested URL
|
|
30
|
+
* after redirects. Trust decisions such as the [FEP-ef61] gateway trust
|
|
31
|
+
* policy for unsecured collections depend on it, so custom document loaders
|
|
32
|
+
* must report it truthfully.
|
|
33
|
+
*
|
|
34
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
35
|
+
* @since 2.4.0
|
|
36
|
+
*/
|
|
37
|
+
readonly documentUrl?: URL;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The gateways that the caller explicitly passed to the accessor through
|
|
41
|
+
* its `gateways` option, in order. It is `undefined` if the caller did not
|
|
42
|
+
* pass the option, and can be empty.
|
|
43
|
+
* @since 2.4.0
|
|
44
|
+
*/
|
|
45
|
+
readonly gateways?: readonly URL[];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The gateways inferred from the reference, in order, when the caller did
|
|
49
|
+
* not pass the `gateways` option: the `@gateway` location hints of
|
|
50
|
+
* a portable IRI, the gateway named by an FEP-ef61 compatible identifier,
|
|
51
|
+
* or the WebFinger server of a portable actor. They only tell where the
|
|
52
|
+
* document and related objects might be retrieved, and must not be
|
|
53
|
+
* trusted to act for the portable object's DID.
|
|
54
|
+
* @since 2.4.0
|
|
55
|
+
*/
|
|
56
|
+
readonly gatewayHints?: readonly URL[];
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The object whose property referred to the document, if the document was
|
|
60
|
+
* dereferenced through a property accessor.
|
|
61
|
+
* @since 2.4.0
|
|
62
|
+
*/
|
|
63
|
+
readonly referrer?: PortableObjectReferrer;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* An object whose property referred to a portable object being verified.
|
|
68
|
+
* Referrers form a chain: {@link PortableObjectReferrer.referrer} is the
|
|
69
|
+
* object through which this referrer itself was obtained, and so on.
|
|
70
|
+
* @since 2.4.0
|
|
71
|
+
*/
|
|
72
|
+
export interface PortableObjectReferrer {
|
|
73
|
+
/**
|
|
74
|
+
* The referring vocabulary object.
|
|
75
|
+
*/
|
|
76
|
+
readonly object: unknown;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The `@id` of the referring object, if any.
|
|
80
|
+
*/
|
|
81
|
+
readonly id: URL | null;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The expanded URI of the property through which the referring object
|
|
85
|
+
* referred to the next object in the chain, e.g.,
|
|
86
|
+
* `https://www.w3.org/ns/activitystreams#outbox`.
|
|
87
|
+
*/
|
|
88
|
+
readonly property: string;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* How the referring object itself was accepted by a
|
|
92
|
+
* {@link PortableObjectVerifier} when it was dereferenced through gateways:
|
|
93
|
+
*
|
|
94
|
+
* - `"verified"`: accepted as verified, e.g., with a valid integrity proof.
|
|
95
|
+
* - `"unsecured"`: accepted without an integrity proof, e.g., as an
|
|
96
|
+
* unsecured collection served by a trusted gateway.
|
|
97
|
+
*
|
|
98
|
+
* It is `undefined` if the referring object was not dereferenced through
|
|
99
|
+
* gateways, e.g., if it was embedded in another object or constructed by
|
|
100
|
+
* the application.
|
|
101
|
+
*/
|
|
102
|
+
readonly acceptance?: "verified" | "unsecured";
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The opaque collection context that a {@link PortableObjectVerifier}
|
|
106
|
+
* attached to the referring object when it accepted it. See
|
|
107
|
+
* {@link PortableObjectVerification}.
|
|
108
|
+
*/
|
|
109
|
+
readonly collectionContext?: unknown;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The referrer of the referring object, if any.
|
|
113
|
+
*/
|
|
114
|
+
readonly referrer?: PortableObjectReferrer;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Whether the chain was cut off here because it was too long. If `true`,
|
|
118
|
+
* the referring object may have a referrer which is not included.
|
|
119
|
+
*/
|
|
120
|
+
readonly truncated?: boolean;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The result of a {@link PortableObjectVerifier}.
|
|
125
|
+
* @since 2.4.0
|
|
126
|
+
*/
|
|
127
|
+
export type PortableObjectVerification =
|
|
128
|
+
| {
|
|
129
|
+
/** Whether the document is accepted. */
|
|
130
|
+
readonly verified: false;
|
|
131
|
+
}
|
|
132
|
+
| {
|
|
133
|
+
/** Whether the document is accepted. */
|
|
134
|
+
readonly verified: true;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Whether the document was accepted without an integrity proof, e.g., as
|
|
138
|
+
* an unsecured [FEP-ef61] collection served by a gateway that its owner
|
|
139
|
+
* lists. Objects embedded in such a document are not trusted because of
|
|
140
|
+
* their origin; property accessors dereference and verify them one by
|
|
141
|
+
* one instead.
|
|
142
|
+
*
|
|
143
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
144
|
+
*/
|
|
145
|
+
readonly unsecured?: boolean;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* An opaque value that the verifier wants to receive again as
|
|
149
|
+
* {@link PortableObjectReferrer.collectionContext} when objects referred
|
|
150
|
+
* to by this document, such as collection pages, are verified.
|
|
151
|
+
*/
|
|
152
|
+
readonly collectionContext?: unknown;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* A function that applies the [FEP-ef61] trust policy to a portable object
|
|
157
|
+
* fetched through a gateway.
|
|
158
|
+
*
|
|
159
|
+
* It receives the fetched JSON-LD document as is, and resolves to an object
|
|
160
|
+
* whose `verified` property tells whether the document is acceptable, e.g.,
|
|
161
|
+
* whether it carries a valid [FEP-8b32] Object Integrity Proof made by the
|
|
162
|
+
* DID in its portable ID. `verifyPortableObject()` and
|
|
163
|
+
* `verifyPortableObjectProof()` from `@fedify/fedify` satisfy this type, so
|
|
164
|
+
* they can be passed as the `verifyPortableObject` option of generated
|
|
165
|
+
* vocabulary accessors such as `Activity.getObject()`.
|
|
166
|
+
*
|
|
167
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
168
|
+
* [FEP-8b32]: https://w3id.org/fep/8b32
|
|
169
|
+
*
|
|
170
|
+
* @param document The fetched JSON-LD document.
|
|
171
|
+
* @param options Loaders, tracing options, and where the document came from.
|
|
172
|
+
* @returns An object whose `verified` property is `true` if and only if the
|
|
173
|
+
* document satisfies the policy. Other properties, such as
|
|
174
|
+
* a failure reason, are logged when verification fails.
|
|
175
|
+
* @since 2.4.0
|
|
176
|
+
*/
|
|
177
|
+
export type PortableObjectVerifier = (
|
|
178
|
+
document: unknown,
|
|
179
|
+
options: PortableObjectVerifierOptions,
|
|
180
|
+
) => Promise<PortableObjectVerification>;
|
package/src/preprocessor.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DocumentLoader } from "./docloader.ts";
|
|
2
2
|
import type { TracerProvider } from "@opentelemetry/api";
|
|
3
|
+
import type { PortableObjectVerifier } from "./portable.ts";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* JSON value shape passed to property preprocessors.
|
|
@@ -24,6 +25,12 @@ export interface PropertyPreprocessorContext {
|
|
|
24
25
|
contextLoader?: DocumentLoader;
|
|
25
26
|
/** OpenTelemetry tracer provider for instrumentation. */
|
|
26
27
|
tracerProvider?: TracerProvider;
|
|
28
|
+
/**
|
|
29
|
+
* The default FEP-ef61 portable object verifier that objects returned by
|
|
30
|
+
* the preprocessor should use for their property accessors.
|
|
31
|
+
* @since 2.4.0
|
|
32
|
+
*/
|
|
33
|
+
verifyPortableObject?: PortableObjectVerifier;
|
|
27
34
|
/** Base URL for resolving relative references. */
|
|
28
35
|
baseUrl?: URL;
|
|
29
36
|
}
|
package/src/request.test.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { deepStrictEqual } from "node:assert";
|
|
|
2
2
|
import process from "node:process";
|
|
3
3
|
import { test } from "node:test";
|
|
4
4
|
import metadata from "../deno.json" with { type: "json" };
|
|
5
|
-
import { getUserAgent } from "./request.ts";
|
|
5
|
+
import { createActivityPubRequest, getUserAgent } from "./request.ts";
|
|
6
6
|
|
|
7
7
|
test("getUserAgent()", () => {
|
|
8
8
|
if ("Deno" in globalThis) {
|
|
@@ -91,3 +91,12 @@ test("getUserAgent()", () => {
|
|
|
91
91
|
);
|
|
92
92
|
}
|
|
93
93
|
});
|
|
94
|
+
|
|
95
|
+
test("createActivityPubRequest() asks for the ActivityStreams profile", () => {
|
|
96
|
+
const request = createActivityPubRequest("https://example.com/object");
|
|
97
|
+
deepStrictEqual(
|
|
98
|
+
request.headers.get("Accept"),
|
|
99
|
+
"application/activity+json, " +
|
|
100
|
+
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
|
101
|
+
);
|
|
102
|
+
});
|
package/src/request.ts
CHANGED
|
@@ -31,6 +31,14 @@ export class FetchError extends Error {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* The `Accept` header value for fetching ActivityPub objects. ActivityPub
|
|
36
|
+
* and FEP-ef61 gateways require the ActivityStreams profile on the JSON-LD
|
|
37
|
+
* media type.
|
|
38
|
+
*/
|
|
39
|
+
const ACTIVITYPUB_ACCEPT =
|
|
40
|
+
'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"';
|
|
41
|
+
|
|
34
42
|
/**
|
|
35
43
|
* Options for creating a request.
|
|
36
44
|
* @internal
|
|
@@ -52,7 +60,7 @@ export function createActivityPubRequest(
|
|
|
52
60
|
): Request {
|
|
53
61
|
return new Request(url, {
|
|
54
62
|
headers: {
|
|
55
|
-
Accept:
|
|
63
|
+
Accept: ACTIVITYPUB_ACCEPT,
|
|
56
64
|
"User-Agent": typeof options.userAgent === "string"
|
|
57
65
|
? options.userAgent
|
|
58
66
|
: getUserAgent(options.userAgent),
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { deepStrictEqual, ok, strictEqual, throws } from "node:assert";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
enterSignedValueScope,
|
|
5
|
+
getRetainedSignedRepresentation,
|
|
6
|
+
isMarkerSafeContext,
|
|
7
|
+
isPlainJsonTree,
|
|
8
|
+
resolveSignedValues,
|
|
9
|
+
retainedSignedValueRef,
|
|
10
|
+
retainSignedRepresentation,
|
|
11
|
+
type SignedValueScope,
|
|
12
|
+
} from "./internal/signed-representation.ts";
|
|
13
|
+
|
|
14
|
+
const securedDocument = {
|
|
15
|
+
"@context": [
|
|
16
|
+
"https://www.w3.org/ns/activitystreams",
|
|
17
|
+
"https://w3id.org/security/data-integrity/v1",
|
|
18
|
+
],
|
|
19
|
+
id: "ap://did:key:z6Mkabc/objects/1",
|
|
20
|
+
type: "Note",
|
|
21
|
+
content: "Hello",
|
|
22
|
+
proof: {
|
|
23
|
+
"@context": [
|
|
24
|
+
"https://www.w3.org/ns/activitystreams",
|
|
25
|
+
"https://w3id.org/security/data-integrity/v1",
|
|
26
|
+
],
|
|
27
|
+
type: "DataIntegrityProof",
|
|
28
|
+
cryptosuite: "eddsa-jcs-2022",
|
|
29
|
+
created: "2023-02-24T23:36:38Z",
|
|
30
|
+
verificationMethod: "did:key:z6Mkabc#z6Mkabc",
|
|
31
|
+
proofPurpose: "assertionMethod",
|
|
32
|
+
proofValue: "z3FXQ",
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function newScope(options: Record<string, unknown> = {}): SignedValueScope {
|
|
37
|
+
return enterSignedValueScope(options).scope;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function retainedSubject(): object {
|
|
41
|
+
const subject = {};
|
|
42
|
+
retainSignedRepresentation(subject, structuredClone(securedDocument));
|
|
43
|
+
return subject;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
test("isPlainJsonTree() accepts plain JSON and rejects everything else", () => {
|
|
47
|
+
ok(isPlainJsonTree(securedDocument));
|
|
48
|
+
ok(isPlainJsonTree([1, "two", null, { three: true }]));
|
|
49
|
+
ok(isPlainJsonTree(Object.create(null)));
|
|
50
|
+
|
|
51
|
+
ok(!isPlainJsonTree(undefined));
|
|
52
|
+
ok(!isPlainJsonTree(Number.NaN));
|
|
53
|
+
ok(!isPlainJsonTree(Number.POSITIVE_INFINITY));
|
|
54
|
+
ok(!isPlainJsonTree(new Date()));
|
|
55
|
+
ok(!isPlainJsonTree(new URL("https://example.com/")));
|
|
56
|
+
ok(!isPlainJsonTree({ nested: { deeper: undefined } }));
|
|
57
|
+
|
|
58
|
+
const withSymbolKey: Record<string | symbol, unknown> = { a: 1 };
|
|
59
|
+
withSymbolKey[Symbol("b")] = 2;
|
|
60
|
+
ok(!isPlainJsonTree(withSymbolKey));
|
|
61
|
+
|
|
62
|
+
const withAccessor = {};
|
|
63
|
+
Object.defineProperty(withAccessor, "a", { get: () => 1, enumerable: true });
|
|
64
|
+
ok(!isPlainJsonTree(withAccessor));
|
|
65
|
+
|
|
66
|
+
const withHiddenProperty = {};
|
|
67
|
+
Object.defineProperty(withHiddenProperty, "a", {
|
|
68
|
+
value: 1,
|
|
69
|
+
enumerable: false,
|
|
70
|
+
});
|
|
71
|
+
ok(!isPlainJsonTree(withHiddenProperty));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("retainSignedRepresentation() attaches a frozen, hidden snapshot", () => {
|
|
75
|
+
const subject: Record<string, unknown> = {};
|
|
76
|
+
retainSignedRepresentation(subject, structuredClone(securedDocument));
|
|
77
|
+
const retained = getRetainedSignedRepresentation(subject);
|
|
78
|
+
|
|
79
|
+
deepStrictEqual(retained, securedDocument);
|
|
80
|
+
ok(Object.isFrozen(retained));
|
|
81
|
+
ok(Object.isFrozen((retained as Record<string, unknown>).proof));
|
|
82
|
+
// The carrier must not show up in ordinary enumeration or serialization.
|
|
83
|
+
deepStrictEqual(Object.keys(subject), []);
|
|
84
|
+
strictEqual(JSON.stringify(subject), "{}");
|
|
85
|
+
// A fresh object, such as the one `clone()` builds, has no snapshot.
|
|
86
|
+
strictEqual(getRetainedSignedRepresentation({}), undefined);
|
|
87
|
+
strictEqual(getRetainedSignedRepresentation(null), undefined);
|
|
88
|
+
strictEqual(getRetainedSignedRepresentation("string"), undefined);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("retainSignedRepresentation() rejects a value that is not plain JSON", () => {
|
|
92
|
+
throws(
|
|
93
|
+
() =>
|
|
94
|
+
retainSignedRepresentation({}, {
|
|
95
|
+
created: new Date(),
|
|
96
|
+
} as unknown as Record<string, unknown>),
|
|
97
|
+
TypeError,
|
|
98
|
+
);
|
|
99
|
+
throws(
|
|
100
|
+
() =>
|
|
101
|
+
retainSignedRepresentation(
|
|
102
|
+
{},
|
|
103
|
+
[1, 2] as unknown as Record<string, unknown>,
|
|
104
|
+
),
|
|
105
|
+
TypeError,
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("isMarkerSafeContext() accepts contexts Fedify can vet offline", () => {
|
|
110
|
+
ok(isMarkerSafeContext(null));
|
|
111
|
+
ok(isMarkerSafeContext("https://www.w3.org/ns/activitystreams"));
|
|
112
|
+
ok(isMarkerSafeContext([
|
|
113
|
+
"https://www.w3.org/ns/activitystreams",
|
|
114
|
+
"https://w3id.org/security/data-integrity/v1",
|
|
115
|
+
"https://w3id.org/fep/ef61",
|
|
116
|
+
]));
|
|
117
|
+
ok(isMarkerSafeContext([
|
|
118
|
+
"https://www.w3.org/ns/activitystreams",
|
|
119
|
+
{ ex: "https://example.com/ns#" },
|
|
120
|
+
]));
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("isMarkerSafeContext() rejects contexts that could hide a placeholder", () => {
|
|
124
|
+
// A context Fedify cannot resolve offline cannot be vetted at all.
|
|
125
|
+
ok(!isMarkerSafeContext("https://example.com/unknown-context"));
|
|
126
|
+
// An `@id` alias other than ActivityStreams' own `id`.
|
|
127
|
+
ok(!isMarkerSafeContext({ identifier: "@id" }));
|
|
128
|
+
ok(!isMarkerSafeContext({ identifier: { "@id": "@id" } }));
|
|
129
|
+
// An indirect alias: `i` resolves through `id`, which ActivityStreams maps
|
|
130
|
+
// to `@id`. A target with no scheme separator is never an IRI.
|
|
131
|
+
ok(
|
|
132
|
+
!isMarkerSafeContext([
|
|
133
|
+
"https://www.w3.org/ns/activitystreams",
|
|
134
|
+
{ i: "id" },
|
|
135
|
+
]),
|
|
136
|
+
);
|
|
137
|
+
ok(!isMarkerSafeContext({ relative: { "@id": "ns#term" } }));
|
|
138
|
+
// `@nest` moves a property value into a wrapper map.
|
|
139
|
+
ok(!isMarkerSafeContext({ payload: "@nest" }));
|
|
140
|
+
ok(
|
|
141
|
+
!isMarkerSafeContext({
|
|
142
|
+
object: {
|
|
143
|
+
"@id": "https://www.w3.org/ns/activitystreams#object",
|
|
144
|
+
"@nest": "payload",
|
|
145
|
+
},
|
|
146
|
+
}),
|
|
147
|
+
);
|
|
148
|
+
// An id map or type map turns the identifier into a map key.
|
|
149
|
+
ok(
|
|
150
|
+
!isMarkerSafeContext({
|
|
151
|
+
object: {
|
|
152
|
+
"@id": "https://www.w3.org/ns/activitystreams#object",
|
|
153
|
+
"@container": "@id",
|
|
154
|
+
},
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
ok(
|
|
158
|
+
!isMarkerSafeContext({
|
|
159
|
+
object: {
|
|
160
|
+
"@id": "https://www.w3.org/ns/activitystreams#object",
|
|
161
|
+
"@container": ["@set", "@type"],
|
|
162
|
+
},
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
// A prefix, `@vocab` or `@base` covering the placeholder namespace lets
|
|
166
|
+
// compaction emit a shortened or relative marker.
|
|
167
|
+
ok(!isMarkerSafeContext({ urn: "urn:" }));
|
|
168
|
+
// A term named after the placeholder's scheme makes a JSON-LD processor
|
|
169
|
+
// read the marker as a compact IRI, whatever the term points at.
|
|
170
|
+
ok(!isMarkerSafeContext({ urn: "https://example.com/ns#" }));
|
|
171
|
+
ok(!isMarkerSafeContext({ urn: { "@id": "https://example.com/ns#urn" } }));
|
|
172
|
+
ok(!isMarkerSafeContext({ "@vocab": "urn:x-fedify-signed-value:" }));
|
|
173
|
+
ok(!isMarkerSafeContext({ "@base": "urn:" }));
|
|
174
|
+
ok(!isMarkerSafeContext({ everything: "" }));
|
|
175
|
+
// A scoped context is only as safe as its own definitions.
|
|
176
|
+
ok(isMarkerSafeContext({
|
|
177
|
+
Note: {
|
|
178
|
+
"@id": "https://www.w3.org/ns/activitystreams#Note",
|
|
179
|
+
"@context": { ex: "https://example.com/ns#" },
|
|
180
|
+
},
|
|
181
|
+
}));
|
|
182
|
+
ok(
|
|
183
|
+
!isMarkerSafeContext({
|
|
184
|
+
Note: {
|
|
185
|
+
"@id": "https://www.w3.org/ns/activitystreams#Note",
|
|
186
|
+
"@context": { identifier: "@id" },
|
|
187
|
+
},
|
|
188
|
+
}),
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("enterSignedValueScope() gives each call its own scope", () => {
|
|
193
|
+
const callerOptions = { format: "compact" as const };
|
|
194
|
+
const first = { ...callerOptions };
|
|
195
|
+
const second = { ...callerOptions };
|
|
196
|
+
const firstEntry = enterSignedValueScope(first);
|
|
197
|
+
const secondEntry = enterSignedValueScope(second);
|
|
198
|
+
|
|
199
|
+
ok(firstEntry.owner);
|
|
200
|
+
ok(secondEntry.owner);
|
|
201
|
+
ok(firstEntry.scope !== secondEntry.scope);
|
|
202
|
+
// The caller's own object is never touched.
|
|
203
|
+
deepStrictEqual(Object.getOwnPropertySymbols(callerOptions), []);
|
|
204
|
+
|
|
205
|
+
// A nested frame inherits the scope through the generated spread.
|
|
206
|
+
const nested = enterSignedValueScope({ ...first, format: "expand" });
|
|
207
|
+
ok(!nested.owner);
|
|
208
|
+
strictEqual(nested.scope, firstEntry.scope);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("enterSignedValueScope() disables retention where it cannot apply", () => {
|
|
212
|
+
ok(enterSignedValueScope({}).scope.enabled);
|
|
213
|
+
ok(enterSignedValueScope({ format: "compact" }).scope.enabled);
|
|
214
|
+
// An expanded document has no compact representation to preserve.
|
|
215
|
+
ok(!enterSignedValueScope({ format: "expand" }).scope.enabled);
|
|
216
|
+
// A context that could hide a placeholder turns the whole mechanism off
|
|
217
|
+
// rather than risking a corrupted document.
|
|
218
|
+
ok(
|
|
219
|
+
!enterSignedValueScope({
|
|
220
|
+
format: "compact",
|
|
221
|
+
context: { identifier: "@id" } as unknown as Record<string, string>,
|
|
222
|
+
}).scope.enabled,
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("retainedSignedValueRef() mints one marker per instance", () => {
|
|
227
|
+
const scope = newScope();
|
|
228
|
+
const subject = retainedSubject();
|
|
229
|
+
const first = retainedSignedValueRef(subject, scope);
|
|
230
|
+
const second = retainedSignedValueRef(subject, scope);
|
|
231
|
+
|
|
232
|
+
ok(first != null);
|
|
233
|
+
deepStrictEqual(first, second);
|
|
234
|
+
ok(first["@id"].startsWith("urn:x-fedify-signed-value:"));
|
|
235
|
+
strictEqual(
|
|
236
|
+
scope.documents.get(first["@id"]),
|
|
237
|
+
scope.documents.get(second!["@id"]),
|
|
238
|
+
);
|
|
239
|
+
strictEqual(retainedSignedValueRef({}, scope), undefined);
|
|
240
|
+
strictEqual(retainedSignedValueRef(null, scope), undefined);
|
|
241
|
+
strictEqual(
|
|
242
|
+
retainedSignedValueRef(subject, newScope({ format: "expand" })),
|
|
243
|
+
undefined,
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("resolveSignedValues() restores every supported reference shape", () => {
|
|
248
|
+
const scope = newScope();
|
|
249
|
+
const subject = retainedSubject();
|
|
250
|
+
const marker = retainedSignedValueRef(subject, scope)!["@id"];
|
|
251
|
+
retainedSignedValueRef(subject, scope);
|
|
252
|
+
retainedSignedValueRef(subject, scope);
|
|
253
|
+
|
|
254
|
+
const resolved = resolveSignedValues({
|
|
255
|
+
"@context": ["https://www.w3.org/ns/activitystreams"],
|
|
256
|
+
object: marker,
|
|
257
|
+
attachment: [{ "@id": marker }],
|
|
258
|
+
tag: { id: marker },
|
|
259
|
+
}, scope) as Record<string, unknown>;
|
|
260
|
+
|
|
261
|
+
deepStrictEqual(resolved.object, securedDocument);
|
|
262
|
+
deepStrictEqual((resolved.attachment as unknown[])[0], securedDocument);
|
|
263
|
+
deepStrictEqual(resolved.tag, securedDocument);
|
|
264
|
+
// Each site gets its own copy.
|
|
265
|
+
ok(resolved.object !== resolved.tag);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("resolveSignedValues() leaves a document without placeholders alone", () => {
|
|
269
|
+
const scope = newScope();
|
|
270
|
+
const document = { id: "https://example.com/notes/1" };
|
|
271
|
+
strictEqual(resolveSignedValues(document, scope), document);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("resolveSignedValues() does not rewrite a context definition", () => {
|
|
275
|
+
const scope = newScope();
|
|
276
|
+
const subject = retainedSubject();
|
|
277
|
+
const marker = retainedSignedValueRef(subject, scope)!["@id"];
|
|
278
|
+
// A placeholder that only survives inside `@context` was never restored,
|
|
279
|
+
// so the accounting check has to reject the document.
|
|
280
|
+
throws(
|
|
281
|
+
() => resolveSignedValues({ "@context": { marker } }, scope),
|
|
282
|
+
TypeError,
|
|
283
|
+
);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("resolveSignedValues() rejects a document that lost a placeholder", () => {
|
|
287
|
+
const scope = newScope();
|
|
288
|
+
const subject = retainedSubject();
|
|
289
|
+
retainedSignedValueRef(subject, scope);
|
|
290
|
+
|
|
291
|
+
throws(
|
|
292
|
+
() => resolveSignedValues({ id: "https://example.com/notes/1" }, scope),
|
|
293
|
+
TypeError,
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("resolveSignedValues() rejects a document that kept a placeholder trace", () => {
|
|
298
|
+
const scope = newScope();
|
|
299
|
+
const subject = retainedSubject();
|
|
300
|
+
const marker = retainedSignedValueRef(subject, scope)!["@id"];
|
|
301
|
+
const uuid = marker.slice("urn:x-fedify-signed-value:".length);
|
|
302
|
+
|
|
303
|
+
throws(
|
|
304
|
+
() => resolveSignedValues({ object: marker, note: `sv:${uuid}` }, scope),
|
|
305
|
+
TypeError,
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("resolveSignedValues() restores a placeholder only where it is a reference", () => {
|
|
310
|
+
const scope = newScope();
|
|
311
|
+
const subject = retainedSubject();
|
|
312
|
+
const marker = retainedSignedValueRef(subject, scope)!["@id"];
|
|
313
|
+
|
|
314
|
+
// A map with more than the identifier is a node of its own, not a
|
|
315
|
+
// reference, so only the string inside it is replaced.
|
|
316
|
+
const resolved = resolveSignedValues(
|
|
317
|
+
{ object: { "@id": marker, type: "Note" } },
|
|
318
|
+
scope,
|
|
319
|
+
) as Record<string, unknown>;
|
|
320
|
+
const object = resolved.object as Record<string, unknown>;
|
|
321
|
+
deepStrictEqual(object["@id"], securedDocument);
|
|
322
|
+
strictEqual(object.type, "Note");
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("resolveSignedValues() refuses a document too deep to walk", () => {
|
|
326
|
+
const scope = newScope();
|
|
327
|
+
const subject = retainedSubject();
|
|
328
|
+
const marker = retainedSignedValueRef(subject, scope)!["@id"];
|
|
329
|
+
// The shallow occurrence alone must not satisfy the check: a placeholder in
|
|
330
|
+
// an unvisited subtree would otherwise reach the wire.
|
|
331
|
+
let deep: unknown = { "@id": marker };
|
|
332
|
+
for (let i = 0; i < 300; i++) deep = { attachment: deep };
|
|
333
|
+
|
|
334
|
+
throws(
|
|
335
|
+
() => resolveSignedValues({ object: marker, tag: deep }, scope),
|
|
336
|
+
TypeError,
|
|
337
|
+
);
|
|
338
|
+
});
|