@fedify/vocab-runtime 2.4.0-dev.1618 → 2.4.0-dev.1655
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 +1 -1
- package/dist/{docloader-DnUMWHaJ.d.cts → docloader-C_dir7Xb.d.ts} +2 -0
- package/dist/{docloader-xRGn1azD.d.ts → docloader-D2DTRiyA.d.cts} +2 -0
- package/dist/internal/jsonld-cache.cjs +1 -1
- package/dist/internal/jsonld-cache.d.cts +1 -1
- package/dist/internal/jsonld-cache.d.ts +1 -1
- package/dist/internal/jsonld-cache.js +1 -1
- package/dist/mod.cjs +159 -8
- package/dist/mod.d.cts +91 -2
- package/dist/mod.d.ts +91 -2
- package/dist/mod.js +152 -9
- package/dist/tests/decimal.test.cjs +6 -5
- package/dist/tests/decimal.test.mjs +6 -5
- package/dist/tests/digest-BJlumPNJ.mjs +140 -0
- package/dist/tests/digest-DLwUyHCK.cjs +175 -0
- package/dist/tests/digest.test.cjs +103 -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-QmR6VOqT.cjs → docloader-DVVWmXLI.cjs} +12 -6
- package/dist/tests/{docloader-CBVde8Va.mjs → docloader-HbEvzi-t.mjs} +12 -6
- package/dist/tests/docloader.test.cjs +3 -3
- package/dist/tests/docloader.test.mjs +3 -3
- package/dist/tests/jsonld-cache.test.cjs +1 -1
- package/dist/tests/jsonld-cache.test.mjs +1 -1
- package/dist/tests/{key-_wXwomh_.cjs → key-BcOWDLMm.cjs} +1 -1
- package/dist/tests/{key-CDGDH_vC.mjs → key-Drbw_2tw.mjs} +1 -1
- package/dist/tests/key.test.cjs +2 -2
- package/dist/tests/key.test.mjs +2 -2
- package/dist/tests/langstr.test.cjs +1 -1
- package/dist/tests/langstr.test.mjs +1 -1
- package/dist/tests/multibase/multibase.test.cjs +1 -1
- package/dist/tests/multibase/multibase.test.mjs +1 -1
- package/dist/tests/{multibase-B4bvakyA.mjs → multibase-Cluwl8OM.mjs} +12 -1
- package/dist/tests/{multibase-Bz_UUDtL.cjs → multibase-DcDQDNnI.cjs} +17 -0
- package/dist/tests/{request-BHx0fCb5.mjs → request-B6s_F7bi.mjs} +1 -1
- package/dist/tests/{request-DxSceLvB.cjs → request-DvEyU5Zf.cjs} +1 -1
- package/dist/tests/request.test.cjs +1 -1
- package/dist/tests/request.test.mjs +1 -1
- package/dist/tests/{url-BvjYQdxL.cjs → url-CsOV_B_P.cjs} +26 -0
- package/dist/tests/{url-a2D8NAgh.mjs → url-Du7RQQgP.mjs} +15 -1
- package/dist/tests/url.test.cjs +18 -1
- package/dist/tests/url.test.mjs +18 -1
- package/dist/{url-Ck3dGEwH.cjs → url-DD4F0ULf.cjs} +26 -0
- package/dist/{url-m1YxGNZ0.js → url-DGVbSVVi.js} +15 -1
- package/package.json +1 -1
- package/src/contexts/fep-ef61.json +10 -0
- package/src/contexts/security-data-integrity-v1.json +0 -4
- package/src/contexts.ts +2 -0
- package/src/digest.test.ts +220 -0
- package/src/digest.ts +229 -0
- package/src/docloader.ts +2 -0
- package/src/mod.ts +12 -0
- package/src/url.test.ts +23 -0
- package/src/url.ts +23 -0
- /package/dist/tests/{langstr-CbAxaeEZ.cjs → langstr-BOQHfJDr.cjs} +0 -0
- /package/dist/tests/{langstr-Di5AvKpB.mjs → langstr-DDLc4833.mjs} +0 -0
|
@@ -3,10 +3,6 @@
|
|
|
3
3
|
"id": "@id",
|
|
4
4
|
"type": "@type",
|
|
5
5
|
"@protected": true,
|
|
6
|
-
"digestMultibase": {
|
|
7
|
-
"@id": "https://w3id.org/security#digestMultibase",
|
|
8
|
-
"@type": "https://w3id.org/security#multibase"
|
|
9
|
-
},
|
|
10
6
|
"proof": {
|
|
11
7
|
"@id": "https://w3id.org/security#proof",
|
|
12
8
|
"@type": "@id",
|
package/src/contexts.ts
CHANGED
|
@@ -8,6 +8,7 @@ import activitystreams from "./contexts/activitystreams.json" with {
|
|
|
8
8
|
import didV1 from "./contexts/did-v1.json" with { type: "json" };
|
|
9
9
|
import fep5711 from "./contexts/fep-5711.json" with { type: "json" };
|
|
10
10
|
import fep7aa9 from "./contexts/fep-7aa9.json" with { type: "json" };
|
|
11
|
+
import fepEf61 from "./contexts/fep-ef61.json" with { type: "json" };
|
|
11
12
|
import gotosocial from "./contexts/gotosocial.json" with { type: "json" };
|
|
12
13
|
import identityV1 from "./contexts/identity-v1.json" with { type: "json" };
|
|
13
14
|
import joinLemmyContext from "./contexts/join-lemmy.json" with { type: "json" };
|
|
@@ -37,6 +38,7 @@ const preloadedContexts: Record<string, unknown> = {
|
|
|
37
38
|
"https://gotosocial.org/ns": gotosocial,
|
|
38
39
|
"https://w3id.org/fep/5711": fep5711,
|
|
39
40
|
"https://w3id.org/fep/7aa9": fep7aa9,
|
|
41
|
+
"https://w3id.org/fep/ef61": fepEf61,
|
|
40
42
|
|
|
41
43
|
// Lemmy's context document is served as application/json without the JSON-LD
|
|
42
44
|
// context Link header. The default document loader treats that as a regular
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { deepStrictEqual, equal, rejects, throws } from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { addMulticodecPrefix } from "./internal/multicodec.ts";
|
|
4
|
+
import { encodeMultibase } from "./multibase/mod.ts";
|
|
5
|
+
import {
|
|
6
|
+
computeDigestMultibase,
|
|
7
|
+
createHashlink,
|
|
8
|
+
parseDigestMultibase,
|
|
9
|
+
parseHashlink,
|
|
10
|
+
verifyDigestMultibase,
|
|
11
|
+
verifyHashlink,
|
|
12
|
+
} from "./digest.ts";
|
|
13
|
+
|
|
14
|
+
const encoder = new TextEncoder();
|
|
15
|
+
const decoder = new TextDecoder();
|
|
16
|
+
const bytes = encoder.encode("Hello World!");
|
|
17
|
+
// Test vector from draft-sporny-hashlink-07, appendix B.1.
|
|
18
|
+
const digestMultibase = "zQmWvQxTqbG2Z9HPJgG57jjwR154cKhbtJenbyYTWkjgF3e";
|
|
19
|
+
const hashlink = `hl:${digestMultibase}`;
|
|
20
|
+
|
|
21
|
+
test("computeDigestMultibase() computes a SHA-256 multihash", async () => {
|
|
22
|
+
equal(await computeDigestMultibase(bytes), digestMultibase);
|
|
23
|
+
const parsed = parseDigestMultibase(digestMultibase);
|
|
24
|
+
equal(parsed.algorithm, "sha2-256");
|
|
25
|
+
deepStrictEqual(
|
|
26
|
+
parsed.digest,
|
|
27
|
+
new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)),
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("digest helpers accept SharedArrayBuffer-backed bytes", async () => {
|
|
32
|
+
const sharedBytes = new Uint8Array(new SharedArrayBuffer(bytes.length));
|
|
33
|
+
sharedBytes.set(bytes);
|
|
34
|
+
Object.defineProperty(sharedBytes, "slice", {
|
|
35
|
+
value: () => {
|
|
36
|
+
throw new Error("Shared input must not be copied with slice().");
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
equal(await computeDigestMultibase(sharedBytes), digestMultibase);
|
|
40
|
+
equal(await verifyDigestMultibase(sharedBytes, digestMultibase), true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("digest helpers ignore spoofed shared buffer tags", async () => {
|
|
44
|
+
const taggedBytes = bytes.slice();
|
|
45
|
+
Object.defineProperty(taggedBytes.buffer, Symbol.toStringTag, {
|
|
46
|
+
value: "SharedArrayBuffer",
|
|
47
|
+
});
|
|
48
|
+
Object.defineProperty(taggedBytes, "slice", {
|
|
49
|
+
value: () => {
|
|
50
|
+
throw new Error("ArrayBuffer input must not be copied.");
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
equal(await computeDigestMultibase(taggedBytes), digestMultibase);
|
|
54
|
+
equal(await verifyDigestMultibase(taggedBytes, digestMultibase), true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("createHashlink() and parseHashlink() round-trip simple hashlinks", () => {
|
|
58
|
+
equal(createHashlink(digestMultibase), hashlink);
|
|
59
|
+
deepStrictEqual(parseHashlink(hashlink), { digestMultibase });
|
|
60
|
+
deepStrictEqual(parseHashlink(new URL(hashlink)), { digestMultibase });
|
|
61
|
+
|
|
62
|
+
const base64DigestMultibase = decoder.decode(
|
|
63
|
+
encodeMultibase(
|
|
64
|
+
"base64",
|
|
65
|
+
addMulticodecPrefix(
|
|
66
|
+
0x12,
|
|
67
|
+
addMulticodecPrefix(32, new Uint8Array(32).fill(0xff)),
|
|
68
|
+
),
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
const base64Hashlink = createHashlink(base64DigestMultibase);
|
|
72
|
+
equal(
|
|
73
|
+
base64Hashlink,
|
|
74
|
+
"hl:mEiD//////////////////////////////////////////w",
|
|
75
|
+
);
|
|
76
|
+
deepStrictEqual(parseHashlink(base64Hashlink), {
|
|
77
|
+
digestMultibase: base64DigestMultibase,
|
|
78
|
+
});
|
|
79
|
+
deepStrictEqual(parseHashlink(new URL(base64Hashlink)), {
|
|
80
|
+
digestMultibase: base64DigestMultibase,
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("digest and hashlink verification accepts matching bytes", async () => {
|
|
85
|
+
equal(await verifyDigestMultibase(bytes, digestMultibase), true);
|
|
86
|
+
equal(await verifyHashlink(bytes, hashlink), true);
|
|
87
|
+
equal(await verifyHashlink(bytes, new URL(hashlink)), true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("digest and hashlink verification rejects non-matching bytes", async () => {
|
|
91
|
+
const different = encoder.encode("Hello World?");
|
|
92
|
+
equal(await verifyDigestMultibase(different, digestMultibase), false);
|
|
93
|
+
equal(await verifyHashlink(different, hashlink), false);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("parseDigestMultibase() rejects unsupported algorithms", () => {
|
|
97
|
+
const sha1Multihash = addMulticodecPrefix(
|
|
98
|
+
0x11,
|
|
99
|
+
addMulticodecPrefix(20, new Uint8Array(20)),
|
|
100
|
+
);
|
|
101
|
+
const value = decoder.decode(encodeMultibase("base58btc", sha1Multihash));
|
|
102
|
+
throws(
|
|
103
|
+
() => parseDigestMultibase(value),
|
|
104
|
+
new TypeError("Unsupported digest algorithm: 0x11"),
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("parseDigestMultibase() rejects malformed values", async () => {
|
|
109
|
+
throws(
|
|
110
|
+
() => parseDigestMultibase("not-multibase"),
|
|
111
|
+
new TypeError("Invalid digestMultibase encoding."),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const missingLength = decoder.decode(
|
|
115
|
+
encodeMultibase("base58btc", Uint8Array.of(0x12)),
|
|
116
|
+
);
|
|
117
|
+
throws(
|
|
118
|
+
() => parseDigestMultibase(missingLength),
|
|
119
|
+
new TypeError("Invalid digestMultibase multihash."),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const multihash = addMulticodecPrefix(
|
|
123
|
+
0x12,
|
|
124
|
+
addMulticodecPrefix(32, new Uint8Array(32)),
|
|
125
|
+
);
|
|
126
|
+
const base2 = decoder.decode(encodeMultibase("base2", multihash));
|
|
127
|
+
equal(base2.length, 273);
|
|
128
|
+
deepStrictEqual(parseDigestMultibase(base2).digest, new Uint8Array(32));
|
|
129
|
+
throws(
|
|
130
|
+
() => parseDigestMultibase(`${base2}0`),
|
|
131
|
+
new TypeError("Invalid digestMultibase encoding."),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const padded = decoder.decode(encodeMultibase("base64pad", multihash));
|
|
135
|
+
equal(padded.endsWith("=="), true);
|
|
136
|
+
deepStrictEqual(parseDigestMultibase(padded).digest, new Uint8Array(32));
|
|
137
|
+
throws(
|
|
138
|
+
() => parseDigestMultibase(padded.slice(0, -1)),
|
|
139
|
+
new TypeError("Invalid digestMultibase encoding."),
|
|
140
|
+
);
|
|
141
|
+
throws(
|
|
142
|
+
() => createHashlink(`${padded}=`),
|
|
143
|
+
new TypeError("Invalid digestMultibase encoding."),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const overlongAlgorithm = decoder.decode(
|
|
147
|
+
encodeMultibase(
|
|
148
|
+
"base58btc",
|
|
149
|
+
Uint8Array.of(0x92, 0x00, 0x20, ...new Uint8Array(32)),
|
|
150
|
+
),
|
|
151
|
+
);
|
|
152
|
+
throws(
|
|
153
|
+
() => parseDigestMultibase(overlongAlgorithm),
|
|
154
|
+
new TypeError("Invalid digestMultibase multihash."),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const overlongLength = decoder.decode(
|
|
158
|
+
encodeMultibase(
|
|
159
|
+
"base58btc",
|
|
160
|
+
Uint8Array.of(0x12, 0xa0, 0x00, ...new Uint8Array(32)),
|
|
161
|
+
),
|
|
162
|
+
);
|
|
163
|
+
throws(
|
|
164
|
+
() => parseDigestMultibase(overlongLength),
|
|
165
|
+
new TypeError("Invalid digestMultibase multihash."),
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const shortDigest = decoder.decode(
|
|
169
|
+
encodeMultibase(
|
|
170
|
+
"base58btc",
|
|
171
|
+
addMulticodecPrefix(0x12, addMulticodecPrefix(31, new Uint8Array(31))),
|
|
172
|
+
),
|
|
173
|
+
);
|
|
174
|
+
throws(
|
|
175
|
+
() => parseDigestMultibase(shortDigest),
|
|
176
|
+
new TypeError("Invalid SHA-256 digest length."),
|
|
177
|
+
);
|
|
178
|
+
await rejects(
|
|
179
|
+
() => verifyDigestMultibase(bytes, shortDigest),
|
|
180
|
+
new TypeError("Invalid SHA-256 digest length."),
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("simple hashlink helpers reject metadata and malformed forms", async () => {
|
|
185
|
+
for (const terminator of ["\n", "\r", "\r\n", "\u2028", "\u2029"]) {
|
|
186
|
+
const malformedHashlink = `${hashlink}${terminator}`;
|
|
187
|
+
throws(
|
|
188
|
+
() => parseHashlink(malformedHashlink),
|
|
189
|
+
new TypeError("Invalid simple hashlink."),
|
|
190
|
+
);
|
|
191
|
+
await rejects(
|
|
192
|
+
() => verifyHashlink(bytes, malformedHashlink),
|
|
193
|
+
new TypeError("Invalid simple hashlink."),
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
throws(
|
|
197
|
+
() => parseHashlink(`${hashlink}:zmetadata`),
|
|
198
|
+
new TypeError("Invalid simple hashlink."),
|
|
199
|
+
);
|
|
200
|
+
await rejects(
|
|
201
|
+
() => verifyHashlink(bytes, `${hashlink}:zmetadata`),
|
|
202
|
+
new TypeError("Invalid simple hashlink."),
|
|
203
|
+
);
|
|
204
|
+
throws(
|
|
205
|
+
() => parseHashlink(`https://example.com/file?hl=${digestMultibase}`),
|
|
206
|
+
new TypeError("Invalid simple hashlink."),
|
|
207
|
+
);
|
|
208
|
+
throws(
|
|
209
|
+
() => parseHashlink("hl:not-multibase"),
|
|
210
|
+
new TypeError("Invalid digestMultibase encoding."),
|
|
211
|
+
);
|
|
212
|
+
await rejects(
|
|
213
|
+
() => verifyHashlink(bytes, "hl:not-multibase"),
|
|
214
|
+
new TypeError("Invalid digestMultibase encoding."),
|
|
215
|
+
);
|
|
216
|
+
throws(
|
|
217
|
+
() => createHashlink("not-multibase"),
|
|
218
|
+
new TypeError("Invalid digestMultibase encoding."),
|
|
219
|
+
);
|
|
220
|
+
});
|
package/src/digest.ts
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addMulticodecPrefix,
|
|
3
|
+
getMulticodecPrefix,
|
|
4
|
+
} from "./internal/multicodec.ts";
|
|
5
|
+
import {
|
|
6
|
+
decodeMultibase,
|
|
7
|
+
encodeMultibase,
|
|
8
|
+
encodingFromBaseData,
|
|
9
|
+
} from "./multibase/mod.ts";
|
|
10
|
+
|
|
11
|
+
const SHA2_256_MULTIHASH_CODE = 0x12;
|
|
12
|
+
const SHA2_256_DIGEST_LENGTH = 32;
|
|
13
|
+
const MAX_DIGEST_MULTIBASE_LENGTH = 1 + (SHA2_256_DIGEST_LENGTH + 2) * 8;
|
|
14
|
+
const textDecoder = new TextDecoder();
|
|
15
|
+
const getArrayBufferByteLength = Object.getOwnPropertyDescriptor(
|
|
16
|
+
ArrayBuffer.prototype,
|
|
17
|
+
"byteLength",
|
|
18
|
+
)?.get;
|
|
19
|
+
|
|
20
|
+
function isCanonicalVarintPrefix(
|
|
21
|
+
data: Uint8Array,
|
|
22
|
+
prefix: ReturnType<typeof getMulticodecPrefix>,
|
|
23
|
+
): boolean {
|
|
24
|
+
const canonical = addMulticodecPrefix(prefix.code, new Uint8Array());
|
|
25
|
+
if (canonical.length !== prefix.prefixLength) return false;
|
|
26
|
+
for (let i = 0; i < canonical.length; i++) {
|
|
27
|
+
if (data[i] !== canonical[i]) return false;
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function toWebCryptoBytes(bytes: Uint8Array): Uint8Array<ArrayBuffer> {
|
|
33
|
+
if (getArrayBufferByteLength != null) {
|
|
34
|
+
try {
|
|
35
|
+
getArrayBufferByteLength.call(bytes.buffer);
|
|
36
|
+
return bytes as Uint8Array<ArrayBuffer>;
|
|
37
|
+
} catch {
|
|
38
|
+
// SharedArrayBuffer input needs to be copied for Web Crypto.
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return new Uint8Array(bytes);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A parsed SHA-256 resource digest.
|
|
46
|
+
*
|
|
47
|
+
* @since 2.4.0
|
|
48
|
+
*/
|
|
49
|
+
export interface ParsedDigestMultibase {
|
|
50
|
+
/** The multihash algorithm represented by the digest. */
|
|
51
|
+
readonly algorithm: "sha2-256";
|
|
52
|
+
|
|
53
|
+
/** The raw 32-byte SHA-256 digest. */
|
|
54
|
+
readonly digest: Uint8Array;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A parsed simple hashlink.
|
|
59
|
+
*
|
|
60
|
+
* @since 2.4.0
|
|
61
|
+
*/
|
|
62
|
+
export interface ParsedHashlink {
|
|
63
|
+
/** The multibase-encoded multihash carried by the hashlink. */
|
|
64
|
+
readonly digestMultibase: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Computes the SHA-256 digest of a byte sequence and encodes it as a
|
|
69
|
+
* base58-btc multibase multihash suitable for FEP-ef61's
|
|
70
|
+
* `digestMultibase` property.
|
|
71
|
+
*
|
|
72
|
+
* @param bytes The bytes to digest.
|
|
73
|
+
* @returns The base58-btc multibase-encoded SHA-256 multihash.
|
|
74
|
+
* @since 2.4.0
|
|
75
|
+
*/
|
|
76
|
+
export async function computeDigestMultibase(
|
|
77
|
+
bytes: Uint8Array,
|
|
78
|
+
): Promise<string> {
|
|
79
|
+
const digest = new Uint8Array(
|
|
80
|
+
await crypto.subtle.digest("SHA-256", toWebCryptoBytes(bytes)),
|
|
81
|
+
);
|
|
82
|
+
const lengthAndDigest = addMulticodecPrefix(digest.length, digest);
|
|
83
|
+
const multihash = addMulticodecPrefix(
|
|
84
|
+
SHA2_256_MULTIHASH_CODE,
|
|
85
|
+
lengthAndDigest,
|
|
86
|
+
);
|
|
87
|
+
return textDecoder.decode(encodeMultibase("base58btc", multihash));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Parses and validates a SHA-256 `digestMultibase` value.
|
|
92
|
+
*
|
|
93
|
+
* @param value The multibase-encoded multihash to parse.
|
|
94
|
+
* @returns The digest algorithm and raw digest bytes.
|
|
95
|
+
* @throws {TypeError} If the value is malformed or uses an unsupported hash
|
|
96
|
+
* algorithm.
|
|
97
|
+
* @since 2.4.0
|
|
98
|
+
*/
|
|
99
|
+
export function parseDigestMultibase(value: string): ParsedDigestMultibase {
|
|
100
|
+
if (value.length > MAX_DIGEST_MULTIBASE_LENGTH) {
|
|
101
|
+
throw new TypeError("Invalid digestMultibase encoding.");
|
|
102
|
+
}
|
|
103
|
+
let multihash: Uint8Array;
|
|
104
|
+
try {
|
|
105
|
+
multihash = decodeMultibase(value);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
throw new TypeError("Invalid digestMultibase encoding.", { cause: error });
|
|
108
|
+
}
|
|
109
|
+
const encoding = encodingFromBaseData(value);
|
|
110
|
+
const canonicalValue = textDecoder.decode(
|
|
111
|
+
encodeMultibase(encoding.name, multihash),
|
|
112
|
+
);
|
|
113
|
+
if (value !== canonicalValue) {
|
|
114
|
+
throw new TypeError("Invalid digestMultibase encoding.");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let algorithmPrefix: ReturnType<typeof getMulticodecPrefix>;
|
|
118
|
+
try {
|
|
119
|
+
algorithmPrefix = getMulticodecPrefix(multihash);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
throw new TypeError("Invalid digestMultibase multihash.", { cause: error });
|
|
122
|
+
}
|
|
123
|
+
if (!isCanonicalVarintPrefix(multihash, algorithmPrefix)) {
|
|
124
|
+
throw new TypeError("Invalid digestMultibase multihash.");
|
|
125
|
+
}
|
|
126
|
+
if (algorithmPrefix.code !== SHA2_256_MULTIHASH_CODE) {
|
|
127
|
+
throw new TypeError(
|
|
128
|
+
`Unsupported digest algorithm: 0x${algorithmPrefix.code.toString(16)}`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const lengthAndDigest = multihash.subarray(algorithmPrefix.prefixLength);
|
|
133
|
+
let lengthPrefix: ReturnType<typeof getMulticodecPrefix>;
|
|
134
|
+
try {
|
|
135
|
+
lengthPrefix = getMulticodecPrefix(lengthAndDigest);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
throw new TypeError("Invalid digestMultibase multihash.", { cause: error });
|
|
138
|
+
}
|
|
139
|
+
if (!isCanonicalVarintPrefix(lengthAndDigest, lengthPrefix)) {
|
|
140
|
+
throw new TypeError("Invalid digestMultibase multihash.");
|
|
141
|
+
}
|
|
142
|
+
const digest = lengthAndDigest.slice(lengthPrefix.prefixLength);
|
|
143
|
+
if (
|
|
144
|
+
lengthPrefix.code !== SHA2_256_DIGEST_LENGTH ||
|
|
145
|
+
digest.length !== SHA2_256_DIGEST_LENGTH
|
|
146
|
+
) {
|
|
147
|
+
throw new TypeError("Invalid SHA-256 digest length.");
|
|
148
|
+
}
|
|
149
|
+
return { algorithm: "sha2-256", digest };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function extractDigestMultibase(value: string | URL): string {
|
|
153
|
+
const hashlink = typeof value === "string" ? value : value.href;
|
|
154
|
+
const match = /^hl:([^:\r\n\u2028\u2029]+)$/i.exec(hashlink);
|
|
155
|
+
if (match == null || match[0].length !== hashlink.length) {
|
|
156
|
+
throw new TypeError("Invalid simple hashlink.");
|
|
157
|
+
}
|
|
158
|
+
return match[1];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Parses a metadata-free `hl:` hashlink and validates its resource digest.
|
|
163
|
+
*
|
|
164
|
+
* @param value The simple hashlink to parse.
|
|
165
|
+
* @returns The `digestMultibase` value carried by the hashlink.
|
|
166
|
+
* @throws {TypeError} If the hashlink is malformed, contains metadata, or
|
|
167
|
+
* carries an invalid or unsupported digest.
|
|
168
|
+
* @since 2.4.0
|
|
169
|
+
*/
|
|
170
|
+
export function parseHashlink(value: string | URL): ParsedHashlink {
|
|
171
|
+
const digestMultibase = extractDigestMultibase(value);
|
|
172
|
+
parseDigestMultibase(digestMultibase);
|
|
173
|
+
return { digestMultibase };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Creates a metadata-free `hl:` hashlink from a `digestMultibase` value.
|
|
178
|
+
*
|
|
179
|
+
* @param digestMultibase The SHA-256 multibase multihash to embed.
|
|
180
|
+
* @returns The simple hashlink.
|
|
181
|
+
* @throws {TypeError} If the digest is malformed or unsupported.
|
|
182
|
+
* @since 2.4.0
|
|
183
|
+
*/
|
|
184
|
+
export function createHashlink(digestMultibase: string): string {
|
|
185
|
+
parseDigestMultibase(digestMultibase);
|
|
186
|
+
return `hl:${digestMultibase}`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Verifies that bytes match a SHA-256 `digestMultibase` value.
|
|
191
|
+
*
|
|
192
|
+
* @param bytes The bytes to verify.
|
|
193
|
+
* @param digestMultibase The expected digest.
|
|
194
|
+
* @returns Whether the bytes match the digest.
|
|
195
|
+
* @throws {TypeError} If the digest is malformed or unsupported.
|
|
196
|
+
* @since 2.4.0
|
|
197
|
+
*/
|
|
198
|
+
export async function verifyDigestMultibase(
|
|
199
|
+
bytes: Uint8Array,
|
|
200
|
+
digestMultibase: string,
|
|
201
|
+
): Promise<boolean> {
|
|
202
|
+
const expected = parseDigestMultibase(digestMultibase).digest;
|
|
203
|
+
const actual = new Uint8Array(
|
|
204
|
+
await crypto.subtle.digest("SHA-256", toWebCryptoBytes(bytes)),
|
|
205
|
+
);
|
|
206
|
+
if (expected.length !== actual.length) return false;
|
|
207
|
+
let difference = 0;
|
|
208
|
+
for (let i = 0; i < expected.length; i++) {
|
|
209
|
+
difference |= expected[i] ^ actual[i];
|
|
210
|
+
}
|
|
211
|
+
return difference === 0;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Verifies that bytes match the resource digest in a simple hashlink.
|
|
216
|
+
*
|
|
217
|
+
* @param bytes The bytes to verify.
|
|
218
|
+
* @param hashlink The simple hashlink containing the expected digest.
|
|
219
|
+
* @returns Whether the bytes match the hashlink digest.
|
|
220
|
+
* @throws {TypeError} If the hashlink or digest is malformed or unsupported.
|
|
221
|
+
* @since 2.4.0
|
|
222
|
+
*/
|
|
223
|
+
export async function verifyHashlink(
|
|
224
|
+
bytes: Uint8Array,
|
|
225
|
+
hashlink: string | URL,
|
|
226
|
+
): Promise<boolean> {
|
|
227
|
+
const digestMultibase = extractDigestMultibase(hashlink);
|
|
228
|
+
return await verifyDigestMultibase(bytes, digestMultibase);
|
|
229
|
+
}
|
package/src/docloader.ts
CHANGED
|
@@ -351,8 +351,10 @@ export interface GetDocumentLoaderOptions extends DocumentLoaderFactoryOptions {
|
|
|
351
351
|
* - <https://www.w3.org/ns/activitystreams>
|
|
352
352
|
* - <https://w3id.org/security/v1>
|
|
353
353
|
* - <https://w3id.org/security/data-integrity/v1>
|
|
354
|
+
* - <https://w3id.org/security/data-integrity/v2>
|
|
354
355
|
* - <https://www.w3.org/ns/did/v1>
|
|
355
356
|
* - <https://w3id.org/security/multikey/v1>
|
|
357
|
+
* - <https://w3id.org/fep/ef61>
|
|
356
358
|
* - <https://purl.archive.org/socialweb/webfinger>
|
|
357
359
|
* - <http://schema.org/>
|
|
358
360
|
* @param options Options for the document loader.
|
package/src/mod.ts
CHANGED
|
@@ -34,6 +34,16 @@ export {
|
|
|
34
34
|
isDecimal,
|
|
35
35
|
parseDecimal,
|
|
36
36
|
} from "./decimal.ts";
|
|
37
|
+
export {
|
|
38
|
+
computeDigestMultibase,
|
|
39
|
+
createHashlink,
|
|
40
|
+
type ParsedDigestMultibase,
|
|
41
|
+
type ParsedHashlink,
|
|
42
|
+
parseDigestMultibase,
|
|
43
|
+
parseHashlink,
|
|
44
|
+
verifyDigestMultibase,
|
|
45
|
+
verifyHashlink,
|
|
46
|
+
} from "./digest.ts";
|
|
37
47
|
export { LanguageString } from "./langstr.ts";
|
|
38
48
|
export {
|
|
39
49
|
decodeMultibase,
|
|
@@ -61,8 +71,10 @@ export {
|
|
|
61
71
|
getFe34Origin,
|
|
62
72
|
haveSameFe34Origin,
|
|
63
73
|
haveSameIriOrigin,
|
|
74
|
+
isGatewayUrl,
|
|
64
75
|
isValidPublicIPv4Address,
|
|
65
76
|
isValidPublicIPv6Address,
|
|
77
|
+
parseGatewayUrl,
|
|
66
78
|
parseIri,
|
|
67
79
|
parseJsonLdId,
|
|
68
80
|
UrlError,
|
package/src/url.test.ts
CHANGED
|
@@ -8,8 +8,10 @@ import {
|
|
|
8
8
|
getFe34Origin,
|
|
9
9
|
haveSameFe34Origin,
|
|
10
10
|
haveSameIriOrigin,
|
|
11
|
+
isGatewayUrl,
|
|
11
12
|
isValidPublicIPv4Address,
|
|
12
13
|
isValidPublicIPv6Address,
|
|
14
|
+
parseGatewayUrl,
|
|
13
15
|
parseIri,
|
|
14
16
|
parseJsonLdId,
|
|
15
17
|
UrlError,
|
|
@@ -638,6 +640,27 @@ test("parseIri() preserves encoded percent signs while decoding delimiters", ()
|
|
|
638
640
|
);
|
|
639
641
|
});
|
|
640
642
|
|
|
643
|
+
test("parseGatewayUrl() accepts only HTTP(S) base URIs", () => {
|
|
644
|
+
for (const url of ["https://server.example/", "http://server.example/"]) {
|
|
645
|
+
deepStrictEqual(parseGatewayUrl(url), new URL(url));
|
|
646
|
+
ok(isGatewayUrl(new URL(url)));
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
for (
|
|
650
|
+
const url of [
|
|
651
|
+
"ftp://server.example/",
|
|
652
|
+
"https://user:pass@server.example/",
|
|
653
|
+
"https://user@server.example/",
|
|
654
|
+
"https://server.example/path",
|
|
655
|
+
"https://server.example/?x=1",
|
|
656
|
+
"https://server.example/#fragment",
|
|
657
|
+
]
|
|
658
|
+
) {
|
|
659
|
+
throws(() => parseGatewayUrl(url), TypeError);
|
|
660
|
+
ok(!isGatewayUrl(new URL(url)));
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
|
|
641
664
|
test("validatePublicUrl()", async () => {
|
|
642
665
|
await rejects(() => validatePublicUrl("ftp://localhost"), UrlError);
|
|
643
666
|
await rejects(
|
package/src/url.ts
CHANGED
|
@@ -323,6 +323,29 @@ function parseAtUri(uri: string): URL {
|
|
|
323
323
|
return new URL("at://" + encodeURIComponent(authority) + path);
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
+
/**
|
|
327
|
+
* Checks whether the URL is an FEP-ef61 gateway base URI.
|
|
328
|
+
*/
|
|
329
|
+
export function isGatewayUrl(url: URL): boolean {
|
|
330
|
+
return (url.protocol === "http:" || url.protocol === "https:") &&
|
|
331
|
+
url.username === "" && url.password === "" &&
|
|
332
|
+
url.pathname === "/" && url.search === "" && url.hash === "";
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Parses and validates an FEP-ef61 gateway base URI.
|
|
337
|
+
*/
|
|
338
|
+
export function parseGatewayUrl(url: string): URL {
|
|
339
|
+
const parsed = parseIri(url);
|
|
340
|
+
if (!isGatewayUrl(parsed)) {
|
|
341
|
+
throw new TypeError(
|
|
342
|
+
"FEP-ef61 gateways must be HTTP(S) base URIs with no credentials, " +
|
|
343
|
+
"path, query, or fragment.",
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
return parsed;
|
|
347
|
+
}
|
|
348
|
+
|
|
326
349
|
/**
|
|
327
350
|
* Validates a URL to prevent SSRF attacks.
|
|
328
351
|
*/
|
|
File without changes
|
|
File without changes
|