@fedify/vocab-runtime 2.4.0-pr.936.41 → 2.4.1
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-B09jb9Pi.cjs} +173 -274
- package/dist/{tests/docloader-S_gQZ2tt.cjs → contexts-DWrj4AGR.js} +169 -293
- package/dist/{docloader-C_dir7Xb.d.ts → docloader-BMHljyYb.d.cts} +68 -2
- package/dist/{docloader-D2DTRiyA.d.cts → docloader-BMHljyYb.d.ts} +68 -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 +762 -4529
- package/dist/mod.d.cts +427 -9
- package/dist/mod.d.ts +427 -10
- package/dist/mod.js +742 -4524
- package/dist/portable-6Aj-7dZJ.d.cts +160 -0
- package/dist/portable-nkTiqH0w.d.ts +160 -0
- package/dist/tests/body-DpC9p-iG.mjs +130 -0
- package/dist/tests/body-cKLBprYm.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-C9Lj11SC.cjs +4533 -0
- package/dist/tests/contexts-duRA_dDC.mjs +4522 -0
- package/dist/tests/decimal.test.cjs +9 -7
- package/dist/tests/decimal.test.mjs +9 -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-DyZ1JGIl.cjs +398 -0
- package/dist/tests/docloader-tUmMeXjU.mjs +375 -0
- package/dist/tests/docloader.test.cjs +816 -39
- package/dist/tests/docloader.test.mjs +810 -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-CmDhHNTx.cjs +12 -0
- package/dist/tests/jsonld-DweIJ07b.mjs +5 -0
- package/dist/tests/jsonld-cache-BEXiiM-V.cjs +386 -0
- package/dist/tests/jsonld-cache-DFnWsLuZ.mjs +339 -0
- package/dist/tests/jsonld-cache.test.cjs +102 -298
- package/dist/tests/jsonld-cache.test.mjs +95 -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-Di5AvKpB.mjs → langstr-DYRmJqFn.mjs} +5 -2
- package/dist/tests/{langstr-CbAxaeEZ.cjs → langstr-_PqsL1ae.cjs} +5 -2
- package/dist/tests/langstr.test.cjs +11 -4
- package/dist/tests/langstr.test.mjs +9 -2
- package/dist/tests/langtag-CUppHs_s.mjs +45 -0
- package/dist/tests/langtag-Czjvxfzx.cjs +50 -0
- package/dist/tests/langtag.test.cjs +45 -0
- package/dist/tests/langtag.test.d.cts +1 -0
- package/dist/tests/langtag.test.d.mts +1 -0
- package/dist/tests/langtag.test.mjs +46 -0
- 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-DmTYDy2p.mjs +250 -0
- package/dist/tests/portable-dereference-qTLMqe2_.cjs +279 -0
- package/dist/tests/portable-media-5p8azal0.cjs +153 -0
- package/dist/tests/portable-media-B_lt9MMA.mjs +148 -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-BgjA9wyl.cjs} +10 -4
- package/dist/tests/{request-C8CaGwtt.mjs → request-DodZNukP.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/fep-6757.json +12 -0
- package/src/contexts/joinmastodon.json +1 -1
- package/src/contexts/miscellany.json +17 -0
- package/src/contexts.ts +47 -1
- package/src/digest.test.ts +220 -0
- package/src/digest.ts +229 -0
- package/src/docloader.test.ts +1066 -27
- package/src/docloader.ts +322 -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/langstr.test.ts +18 -1
- package/src/langstr.ts +6 -2
- package/src/langtag.test.ts +53 -0
- package/src/langtag.ts +76 -0
- package/src/mod.ts +29 -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/{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
package/src/key.ts
CHANGED
|
@@ -174,12 +174,22 @@ export async function importDidKey(did: string | URL): Promise<CryptoKey> {
|
|
|
174
174
|
/**
|
|
175
175
|
* Exports an Ed25519 public key as a `did:key` DID.
|
|
176
176
|
*
|
|
177
|
+
* The DID is encoded in base58-btc with the Ed25519 multicodec prefix, e.g.,
|
|
178
|
+
* `did:key:z6Mk...`, as [FEP-ef61] requires for the DIDs of portable
|
|
179
|
+
* objects.
|
|
180
|
+
*
|
|
181
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
182
|
+
*
|
|
177
183
|
* @param key The Ed25519 public key.
|
|
178
184
|
* @returns The `did:key` DID.
|
|
179
|
-
* @throws {TypeError} If the key is invalid or unsupported
|
|
185
|
+
* @throws {TypeError} If the key is invalid or unsupported, or it is not
|
|
186
|
+
* a public key.
|
|
180
187
|
* @since 2.4.0
|
|
181
188
|
*/
|
|
182
189
|
export async function exportDidKey(key: CryptoKey): Promise<string> {
|
|
190
|
+
if (key.type !== "public") {
|
|
191
|
+
throw new TypeError("The key must be a public key.");
|
|
192
|
+
}
|
|
183
193
|
if (key.algorithm.name !== "Ed25519") {
|
|
184
194
|
throw new TypeError(
|
|
185
195
|
"Unsupported key type: " + JSON.stringify(key.algorithm),
|
package/src/langstr.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { deepStrictEqual } from "node:assert";
|
|
1
|
+
import { deepStrictEqual, throws } from "node:assert";
|
|
2
2
|
import { test } from "node:test";
|
|
3
3
|
import util from "node:util";
|
|
4
4
|
import { LanguageString } from "./langstr.ts";
|
|
@@ -11,6 +11,23 @@ test("new LanguageString()", () => {
|
|
|
11
11
|
deepStrictEqual(new LanguageString("Hello", new Intl.Locale("en")), langStr);
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
+
test("new LanguageString() with extlang tags", () => {
|
|
15
|
+
// Intl.Locale objects have no own enumerable properties, so their base names
|
|
16
|
+
// are compared instead:
|
|
17
|
+
deepStrictEqual(new LanguageString("你好", "zh-YUE").locale.baseName, "yue");
|
|
18
|
+
deepStrictEqual(
|
|
19
|
+
new LanguageString("你好", "zh-yue-HK").locale.baseName,
|
|
20
|
+
"yue-HK",
|
|
21
|
+
);
|
|
22
|
+
// Whether cmn is canonicalized to zh depends on the runtime:
|
|
23
|
+
deepStrictEqual(
|
|
24
|
+
new LanguageString("你好", "zh-cmn-Hant").locale.baseName,
|
|
25
|
+
new Intl.Locale("cmn-Hant").baseName,
|
|
26
|
+
);
|
|
27
|
+
throws(() => new LanguageString("Hello", "en-USA"), RangeError);
|
|
28
|
+
throws(() => new LanguageString("你好", "zh-yue-"), RangeError);
|
|
29
|
+
});
|
|
30
|
+
|
|
14
31
|
test("Deno.inspect(LanguageString)", () => {
|
|
15
32
|
const langStr = new LanguageString("Hello, 'world'", "en");
|
|
16
33
|
deepStrictEqual(
|
package/src/langstr.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { normalizeLanguageTag } from "./langtag.ts";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* A language-tagged string which corresponds to the `rdf:langString` type.
|
|
3
5
|
*/
|
|
@@ -12,12 +14,14 @@ export class LanguageString extends String {
|
|
|
12
14
|
* Constructs a new `LanguageString`.
|
|
13
15
|
* @param value A string value written in the given language.
|
|
14
16
|
* @param language The language of the string. If a string is given, it will
|
|
15
|
-
* be parsed as a `Intl.Locale` object.
|
|
17
|
+
* be parsed as a `Intl.Locale` object. Extended language
|
|
18
|
+
* subtags are normalized first, e.g., `zh-yue` is parsed as
|
|
19
|
+
* `yue`.
|
|
16
20
|
*/
|
|
17
21
|
constructor(value: string, language: Intl.Locale | string) {
|
|
18
22
|
super(value);
|
|
19
23
|
this.locale = typeof language === "string"
|
|
20
|
-
? new Intl.Locale(language)
|
|
24
|
+
? new Intl.Locale(normalizeLanguageTag(language))
|
|
21
25
|
: language;
|
|
22
26
|
}
|
|
23
27
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { deepStrictEqual } from "node:assert";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { normalizeLanguageTag } from "./langtag.ts";
|
|
4
|
+
|
|
5
|
+
test("normalizeLanguageTag() removes extlang subtags", () => {
|
|
6
|
+
const cases: [string, string][] = [
|
|
7
|
+
["zh-yue", "yue"],
|
|
8
|
+
["zh-YUE", "yue"],
|
|
9
|
+
["ZH-Yue-hk", "yue-hk"],
|
|
10
|
+
["zh-yue-HK", "yue-HK"],
|
|
11
|
+
["zh-cmn-Hant", "cmn-Hant"],
|
|
12
|
+
["zh-yue-u-nu-hanidec", "yue-u-nu-hanidec"],
|
|
13
|
+
["sgn-ase", "ase"],
|
|
14
|
+
["ar-arz", "arz"],
|
|
15
|
+
["ms-zlm", "zlm"],
|
|
16
|
+
["kok-gom", "gom"],
|
|
17
|
+
["koK-gom", "gom"],
|
|
18
|
+
["lv-ltg", "ltg"],
|
|
19
|
+
["sw-swc", "swc"],
|
|
20
|
+
["uz-uzs", "uzs"],
|
|
21
|
+
// Valid tags never have more than one extlang, so only the first one is
|
|
22
|
+
// removed and the result is still rejected by Intl.Locale:
|
|
23
|
+
["zh-yue-cmn", "yue-cmn"],
|
|
24
|
+
["zh-yue-", "yue-"],
|
|
25
|
+
];
|
|
26
|
+
for (const [input, expected] of cases) {
|
|
27
|
+
deepStrictEqual(normalizeLanguageTag(input), expected, input);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("normalizeLanguageTag() leaves other tags unchanged", () => {
|
|
32
|
+
const tags = [
|
|
33
|
+
"",
|
|
34
|
+
"en",
|
|
35
|
+
"en-US",
|
|
36
|
+
"en-USA",
|
|
37
|
+
"en-yue",
|
|
38
|
+
"zh",
|
|
39
|
+
"zh-Hant-TW",
|
|
40
|
+
"yue",
|
|
41
|
+
"yue-HK",
|
|
42
|
+
"x-private",
|
|
43
|
+
"i-klingon",
|
|
44
|
+
"zh-min-nan",
|
|
45
|
+
"koK-gom",
|
|
46
|
+
"zh-yuK",
|
|
47
|
+
"constructor-yue",
|
|
48
|
+
"toString-yue",
|
|
49
|
+
];
|
|
50
|
+
for (const tag of tags) {
|
|
51
|
+
deepStrictEqual(normalizeLanguageTag(tag), tag, tag);
|
|
52
|
+
}
|
|
53
|
+
});
|
package/src/langtag.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extended language subtags (extlangs) registered in the IANA Language Subtag
|
|
3
|
+
* Registry, grouped by their prefixes. Every extlang record has exactly one
|
|
4
|
+
* prefix and a `Preferred-Value` equal to the extlang subtag itself, so
|
|
5
|
+
* replacing the prefix and the extlang with the extlang alone yields
|
|
6
|
+
* the canonical form.
|
|
7
|
+
*
|
|
8
|
+
* Generated from the registry with File-Date: 2026-09-17.
|
|
9
|
+
*/
|
|
10
|
+
const EXTLANGS: Record<string, string> = {
|
|
11
|
+
ar: "aao abh abv acm acq acw acx acy adf aeb aec afb ajp apc apd arb arq " +
|
|
12
|
+
"ars ary arz auz avl ayh ayl ayn ayp bbz pga shu ssh",
|
|
13
|
+
kok: "gom knn",
|
|
14
|
+
lv: "ltg lvs",
|
|
15
|
+
ms: "bjn btj bve bvu coa dup hji jak jax kvb kvr kxd lce lcf liw max meo " +
|
|
16
|
+
"mfa mfb min mqg msi mui orn ors pel pse tmw urk vkk vkt xmm zlm zmi " +
|
|
17
|
+
"zsm",
|
|
18
|
+
sgn: "ads aed aen afg ajs ase asf asp asq asw bfi bfk bog bqn bqy bvl bzs " +
|
|
19
|
+
"cds csc csd cse csf csg csl csn csq csr csx doq dse dsl dsz dyl ecs " +
|
|
20
|
+
"ehs esl esn eso eth fcs fse fsl fss gds gse gsg gsm gss gus hab haf " +
|
|
21
|
+
"hds hks hos hps hsh hsl icl iks ils inl ins ise isg isr jcs jhs jks " +
|
|
22
|
+
"jls jos jsl jus kgi kvk lbs lgs lls lsb lsc lsg lsl lsn lso lsp lst " +
|
|
23
|
+
"lsv lsw lsy lws mdl mfs mre msd msr mzc mzg mzy nbs ncs nsi nsl nsp " +
|
|
24
|
+
"nsr nzs okl pgz pks prl prz psc psd psg psl pso psp psr pys rib rms " +
|
|
25
|
+
"rnb rsi rsl rsm rsn sdl sfb sfs sgg sgx slf sls sqk sqs sqx ssp ssr " +
|
|
26
|
+
"svk swl syy szs tse tsm tsq tss tsy tza ugn ugy ukl uks vgt vsi vsl " +
|
|
27
|
+
"vsv wbs xki xml xms yds ygs yhs ysl ysm zhk zib zsl",
|
|
28
|
+
sw: "swc swh",
|
|
29
|
+
uz: "uzn uzs",
|
|
30
|
+
zh: "cdo cjy cmn cnp cpx csp czh czo gan hak hnm hsn luh lzh mnp nan sjc " +
|
|
31
|
+
"wuu yue",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const EXTLANG_PAIRS: ReadonlySet<string> = new Set(
|
|
35
|
+
Object.entries(EXTLANGS).flatMap(([prefix, extlangs]) =>
|
|
36
|
+
extlangs.split(" ").map((extlang) => `${prefix}-${extlang}`)
|
|
37
|
+
),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const PREFIX_PATTERN = /^[A-Za-z]{2,3}$/;
|
|
41
|
+
const EXTLANG_PATTERN = /^[A-Za-z]{3}$/;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Normalizes a BCP 47 language tag that has an extended language subtag
|
|
45
|
+
* (extlang), such as `zh-yue`, into its canonical form, such as `yue`, as
|
|
46
|
+
* described in [RFC 5646, Section 4.5][1]. `Intl.Locale` rejects extlang
|
|
47
|
+
* tags, so tags should be normalized before being passed to it.
|
|
48
|
+
*
|
|
49
|
+
* Only registered extlangs that follow their registered prefix are removed;
|
|
50
|
+
* any other tag is returned unchanged, so the result still needs to be
|
|
51
|
+
* validated, e.g., by `Intl.Locale`.
|
|
52
|
+
*
|
|
53
|
+
* NOTE: This function is marked as internal in the 2.0.x–2.4.x patch
|
|
54
|
+
* releases. When merging it into the *main* branch, remove the `@internal`
|
|
55
|
+
* tag, formalize it as a public API (`@since 2.5.0`), and add it to
|
|
56
|
+
* the changelog fragment.
|
|
57
|
+
*
|
|
58
|
+
* [1]: https://www.rfc-editor.org/rfc/rfc5646.html#section-4.5
|
|
59
|
+
*
|
|
60
|
+
* @param tag The BCP 47 language tag to normalize.
|
|
61
|
+
* @returns The normalized language tag.
|
|
62
|
+
* @internal
|
|
63
|
+
*/
|
|
64
|
+
export function normalizeLanguageTag(tag: string): string {
|
|
65
|
+
const subtags = tag.split("-");
|
|
66
|
+
if (subtags.length < 2) return tag;
|
|
67
|
+
const [prefix, extlang, ...rest] = subtags;
|
|
68
|
+
// Checked before case folding, since `toLowerCase()` maps some non-ASCII
|
|
69
|
+
// characters (e.g., U+212A KELVIN SIGN) to ASCII letters:
|
|
70
|
+
if (!PREFIX_PATTERN.test(prefix) || !EXTLANG_PATTERN.test(extlang)) {
|
|
71
|
+
return tag;
|
|
72
|
+
}
|
|
73
|
+
const language = extlang.toLowerCase();
|
|
74
|
+
if (!EXTLANG_PAIRS.has(`${prefix.toLowerCase()}-${language}`)) return tag;
|
|
75
|
+
return [language, ...rest].join("-");
|
|
76
|
+
}
|
package/src/mod.ts
CHANGED
|
@@ -15,6 +15,8 @@ export {
|
|
|
15
15
|
type GetDocumentLoaderOptions,
|
|
16
16
|
getRemoteDocument,
|
|
17
17
|
type RemoteDocument,
|
|
18
|
+
resolveDocumentLoaderTimeout,
|
|
19
|
+
withDocumentLoaderTimeout,
|
|
18
20
|
} from "./docloader.ts";
|
|
19
21
|
export {
|
|
20
22
|
type DidKeyVerificationMethod,
|
|
@@ -34,7 +36,29 @@ export {
|
|
|
34
36
|
isDecimal,
|
|
35
37
|
parseDecimal,
|
|
36
38
|
} from "./decimal.ts";
|
|
39
|
+
export {
|
|
40
|
+
computeDigestMultibase,
|
|
41
|
+
createHashlink,
|
|
42
|
+
type ParsedDigestMultibase,
|
|
43
|
+
type ParsedHashlink,
|
|
44
|
+
parseDigestMultibase,
|
|
45
|
+
parseHashlink,
|
|
46
|
+
verifyDigestMultibase,
|
|
47
|
+
verifyHashlink,
|
|
48
|
+
} from "./digest.ts";
|
|
37
49
|
export { LanguageString } from "./langstr.ts";
|
|
50
|
+
export { normalizeLanguageTag } from "./langtag.ts";
|
|
51
|
+
export {
|
|
52
|
+
fetchPortableMedia,
|
|
53
|
+
type FetchPortableMediaOptions,
|
|
54
|
+
type PortableMedia,
|
|
55
|
+
} from "./portable-media.ts";
|
|
56
|
+
export type {
|
|
57
|
+
PortableObjectReferrer,
|
|
58
|
+
PortableObjectVerification,
|
|
59
|
+
PortableObjectVerifier,
|
|
60
|
+
PortableObjectVerifierOptions,
|
|
61
|
+
} from "./portable.ts";
|
|
38
62
|
export {
|
|
39
63
|
decodeMultibase,
|
|
40
64
|
encodeMultibase,
|
|
@@ -58,7 +82,9 @@ export {
|
|
|
58
82
|
canonicalizePortableUri,
|
|
59
83
|
expandIPv6Address,
|
|
60
84
|
formatIri,
|
|
85
|
+
fromCompatibleEf61Id,
|
|
61
86
|
getFe34Origin,
|
|
87
|
+
getGatewayHints,
|
|
62
88
|
haveSameFe34Origin,
|
|
63
89
|
haveSameIriOrigin,
|
|
64
90
|
isGatewayUrl,
|
|
@@ -67,6 +93,9 @@ export {
|
|
|
67
93
|
parseGatewayUrl,
|
|
68
94
|
parseIri,
|
|
69
95
|
parseJsonLdId,
|
|
96
|
+
toCompatibleEf61Id,
|
|
70
97
|
UrlError,
|
|
71
98
|
validatePublicUrl,
|
|
99
|
+
withGatewayHints,
|
|
100
|
+
withoutGatewayHints,
|
|
72
101
|
} from "./url.ts";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { deepStrictEqual } from "node:assert";
|
|
2
|
-
import { test } from "
|
|
2
|
+
import { test } from "@fedify/fixture";
|
|
3
3
|
import * as constants from "./constants.ts";
|
|
4
4
|
import { decodeMultibase, encodeMultibase } from "./mod.ts";
|
|
5
5
|
import type { BaseName } from "./types.d.ts";
|
|
@@ -76,7 +76,7 @@ test("multibase.encode and decode", async (t) => {
|
|
|
76
76
|
];
|
|
77
77
|
|
|
78
78
|
for (const [name, input, expectedOutput] of testCases) {
|
|
79
|
-
await t.
|
|
79
|
+
await t.step(`Encoding/Decoding ${name} with ${input}`, () => {
|
|
80
80
|
const encoded = encodeMultibase(name, encodeText(input));
|
|
81
81
|
deepStrictEqual(
|
|
82
82
|
decodeText(encoded),
|
|
@@ -96,7 +96,7 @@ test("multibase.encode and decode", async (t) => {
|
|
|
96
96
|
});
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
await t.
|
|
99
|
+
await t.step("should allow base32pad full alphabet", () => {
|
|
100
100
|
const encodedStr = "ctimaq4ygg2iegci7";
|
|
101
101
|
const decoded = decodeMultibase(encodedStr);
|
|
102
102
|
const encoded = encodeMultibase("c", decoded);
|
|
@@ -105,12 +105,12 @@ test("multibase.encode and decode", async (t) => {
|
|
|
105
105
|
});
|
|
106
106
|
|
|
107
107
|
test("constants", async (t) => {
|
|
108
|
-
await t.
|
|
108
|
+
await t.step("constants indexed by name", () => {
|
|
109
109
|
const names = constants.names;
|
|
110
110
|
deepStrictEqual(Object.keys(names).length, 23);
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
-
await t.
|
|
113
|
+
await t.step("constants indexed by code", () => {
|
|
114
114
|
const codes = constants.codes;
|
|
115
115
|
deepStrictEqual(Object.keys(codes).length, 23);
|
|
116
116
|
});
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { deepStrictEqual, equal, rejects } from "node:assert/strict";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { test } from "node:test";
|
|
4
|
+
import type { Document, Image, Link } from "@fedify/vocab";
|
|
5
|
+
import { computeDigestMultibase, createHashlink } from "./digest.ts";
|
|
6
|
+
import { fetchPortableMedia, type PortableMedia } from "./portable-media.ts";
|
|
7
|
+
|
|
8
|
+
const bytes = new TextEncoder().encode("portable image");
|
|
9
|
+
const digest = "zQmU2Jds4B2J83K7CrYC34CEJdB6vCLyqkM2cgBxYeaNWTA";
|
|
10
|
+
const hashlink = createHashlink(digest);
|
|
11
|
+
|
|
12
|
+
const networkTest = (name: string, run: () => Promise<void>) =>
|
|
13
|
+
test(name, async (t) => {
|
|
14
|
+
if (
|
|
15
|
+
"Deno" in globalThis &&
|
|
16
|
+
(await Deno.permissions.query({ name: "net" })).state !== "granted"
|
|
17
|
+
) {
|
|
18
|
+
t.skip("Requires network permission for a local test server.");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
await run();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
type Handler = (path: string) => Response | undefined;
|
|
25
|
+
|
|
26
|
+
async function withServers(
|
|
27
|
+
handlers: Handler[],
|
|
28
|
+
run: (origins: string[]) => Promise<void>,
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
const servers = handlers.map((handler) =>
|
|
31
|
+
createServer(async (request, reply) => {
|
|
32
|
+
const result = handler(request.url ?? "/");
|
|
33
|
+
if (result == null) return;
|
|
34
|
+
reply.writeHead(result.status, Object.fromEntries(result.headers));
|
|
35
|
+
reply.end(new Uint8Array(await result.arrayBuffer()));
|
|
36
|
+
})
|
|
37
|
+
);
|
|
38
|
+
try {
|
|
39
|
+
const origins: string[] = [];
|
|
40
|
+
for (const server of servers) {
|
|
41
|
+
await new Promise<void>((resolve, reject) => {
|
|
42
|
+
server.once("error", reject);
|
|
43
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
44
|
+
});
|
|
45
|
+
const address = server.address();
|
|
46
|
+
if (address == null || typeof address === "string") {
|
|
47
|
+
throw new Error("Server did not bind a TCP port.");
|
|
48
|
+
}
|
|
49
|
+
origins.push(`http://127.0.0.1:${address.port}`);
|
|
50
|
+
}
|
|
51
|
+
await run(origins);
|
|
52
|
+
} finally {
|
|
53
|
+
await Promise.all(servers.map(async (server) => {
|
|
54
|
+
server.closeAllConnections();
|
|
55
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
networkTest(
|
|
61
|
+
"fetchPortableMedia() accepts Link, Image, and Document",
|
|
62
|
+
async () => {
|
|
63
|
+
const accepts = <T extends PortableMedia>() => {};
|
|
64
|
+
accepts<Link>();
|
|
65
|
+
accepts<Image>();
|
|
66
|
+
accepts<Document>();
|
|
67
|
+
const link: PortableMedia = {
|
|
68
|
+
href: new URL(hashlink),
|
|
69
|
+
digestMultibase: digest,
|
|
70
|
+
};
|
|
71
|
+
const image: PortableMedia = {
|
|
72
|
+
url: new URL(hashlink),
|
|
73
|
+
digestMultibase: digest,
|
|
74
|
+
};
|
|
75
|
+
const document: PortableMedia = {
|
|
76
|
+
url: { href: new URL(hashlink), digestMultibase: digest },
|
|
77
|
+
};
|
|
78
|
+
const requested: string[] = [];
|
|
79
|
+
await withServers([(path) => {
|
|
80
|
+
requested.push(path);
|
|
81
|
+
return new Response(bytes);
|
|
82
|
+
}], async ([gateway]) => {
|
|
83
|
+
for (const media of [link, image, document]) {
|
|
84
|
+
equal(
|
|
85
|
+
await (await fetchPortableMedia(media, {
|
|
86
|
+
gateways: [gateway],
|
|
87
|
+
allowPrivateAddress: true,
|
|
88
|
+
})).text(),
|
|
89
|
+
"portable image",
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
deepStrictEqual(
|
|
94
|
+
requested,
|
|
95
|
+
Array(3).fill(
|
|
96
|
+
`/.well-known/apgateway/${hashlink}`,
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
networkTest(
|
|
103
|
+
"fetchPortableMedia() falls back on status and digest mismatch",
|
|
104
|
+
async () => {
|
|
105
|
+
const requested: string[] = [];
|
|
106
|
+
await withServers([
|
|
107
|
+
(path) => {
|
|
108
|
+
requested.push(`first:${path}`);
|
|
109
|
+
return new Response(null, { status: 503 });
|
|
110
|
+
},
|
|
111
|
+
(path) => {
|
|
112
|
+
requested.push(`second:${path}`);
|
|
113
|
+
return new Response("wrong bytes");
|
|
114
|
+
},
|
|
115
|
+
(path) => {
|
|
116
|
+
requested.push(`third:${path}`);
|
|
117
|
+
return new Response(bytes, {
|
|
118
|
+
headers: { "Content-Type": "image/png", "Set-Cookie": "secret=1" },
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
], async (gateways) => {
|
|
122
|
+
const result = await fetchPortableMedia(
|
|
123
|
+
{ url: new URL(hashlink), digestMultibase: digest },
|
|
124
|
+
{ gateways, allowPrivateAddress: true },
|
|
125
|
+
);
|
|
126
|
+
equal(await result.text(), "portable image");
|
|
127
|
+
equal(result.headers.get("Content-Type"), "image/png");
|
|
128
|
+
equal(result.headers.get("Set-Cookie"), null);
|
|
129
|
+
});
|
|
130
|
+
deepStrictEqual(requested.map((r) => r.split(":")[0]), [
|
|
131
|
+
"first",
|
|
132
|
+
"second",
|
|
133
|
+
"third",
|
|
134
|
+
]);
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
networkTest("fetchPortableMedia() verifies direct HTTP media", async () => {
|
|
139
|
+
const wrongDigest = await computeDigestMultibase(new Uint8Array());
|
|
140
|
+
await withServers([() => new Response(bytes)], async ([origin]) => {
|
|
141
|
+
const result = await fetchPortableMedia(`${origin}/a`, {
|
|
142
|
+
digestMultibase: digest,
|
|
143
|
+
allowPrivateAddress: true,
|
|
144
|
+
});
|
|
145
|
+
equal(await result.text(), "portable image");
|
|
146
|
+
await rejects(() =>
|
|
147
|
+
fetchPortableMedia(`${origin}/a`, {
|
|
148
|
+
digestMultibase: wrongDigest,
|
|
149
|
+
allowPrivateAddress: true,
|
|
150
|
+
}), /digest does not match/);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("fetchPortableMedia() rejects invalid references before fetching", async () => {
|
|
155
|
+
const wrongDigest = await computeDigestMultibase(new Uint8Array());
|
|
156
|
+
await rejects(
|
|
157
|
+
() => fetchPortableMedia({ url: new URL(hashlink) }),
|
|
158
|
+
/requires digestMultibase/,
|
|
159
|
+
);
|
|
160
|
+
await rejects(() =>
|
|
161
|
+
fetchPortableMedia({
|
|
162
|
+
url: new URL(hashlink),
|
|
163
|
+
digestMultibase: digest,
|
|
164
|
+
}), /requires at least one gateway/);
|
|
165
|
+
await rejects(() =>
|
|
166
|
+
fetchPortableMedia({
|
|
167
|
+
url: new URL(hashlink),
|
|
168
|
+
digestMultibase: wrongDigest,
|
|
169
|
+
}, { gateways: ["https://example.com"] }), /disagree/);
|
|
170
|
+
await rejects(() =>
|
|
171
|
+
fetchPortableMedia({
|
|
172
|
+
url: new URL("hl:invalid"),
|
|
173
|
+
digestMultibase: digest,
|
|
174
|
+
}, { gateways: ["https://example.com"] }), TypeError);
|
|
175
|
+
await rejects(() =>
|
|
176
|
+
fetchPortableMedia({
|
|
177
|
+
url: { href: new URL(hashlink), digestMultibase: wrongDigest },
|
|
178
|
+
digestMultibase: digest,
|
|
179
|
+
}, { gateways: ["https://example.com"] }), /digests disagree/);
|
|
180
|
+
await rejects(() =>
|
|
181
|
+
fetchPortableMedia("file:///tmp/media", {
|
|
182
|
+
digestMultibase: digest,
|
|
183
|
+
}), TypeError);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
networkTest("fetchPortableMedia() bounds response bytes", async () => {
|
|
187
|
+
await withServers([() => new Response(bytes)], async ([origin]) => {
|
|
188
|
+
await rejects(() =>
|
|
189
|
+
fetchPortableMedia(`${origin}/media`, {
|
|
190
|
+
digestMultibase: digest,
|
|
191
|
+
maxBytes: bytes.length - 1,
|
|
192
|
+
allowPrivateAddress: true,
|
|
193
|
+
}), /exceeds the limit/);
|
|
194
|
+
});
|
|
195
|
+
await rejects(() =>
|
|
196
|
+
fetchPortableMedia("https://example.com/media", {
|
|
197
|
+
digestMultibase: digest,
|
|
198
|
+
maxBytes: 0,
|
|
199
|
+
}), RangeError);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("fetchPortableMedia() rejects private media and gateways", async () => {
|
|
203
|
+
await rejects(() =>
|
|
204
|
+
fetchPortableMedia("http://127.0.0.1/media", {
|
|
205
|
+
digestMultibase: digest,
|
|
206
|
+
}), /private address/);
|
|
207
|
+
await rejects(
|
|
208
|
+
() =>
|
|
209
|
+
fetchPortableMedia({
|
|
210
|
+
url: new URL(hashlink),
|
|
211
|
+
digestMultibase: digest,
|
|
212
|
+
}, { gateways: ["http://127.0.0.1"] }),
|
|
213
|
+
/No gateway returned verified portable media/,
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("fetchPortableMedia() propagates cancellation", async () => {
|
|
218
|
+
const controller = new AbortController();
|
|
219
|
+
const reason = new Error("cancelled");
|
|
220
|
+
controller.abort(reason);
|
|
221
|
+
await rejects(() =>
|
|
222
|
+
fetchPortableMedia({
|
|
223
|
+
url: new URL(hashlink),
|
|
224
|
+
digestMultibase: digest,
|
|
225
|
+
}, {
|
|
226
|
+
gateways: ["https://example.com"],
|
|
227
|
+
signal: controller.signal,
|
|
228
|
+
}), (error) => error === reason);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
networkTest(
|
|
232
|
+
"fetchPortableMedia() falls back after a gateway timeout",
|
|
233
|
+
async () => {
|
|
234
|
+
await withServers([
|
|
235
|
+
() => undefined,
|
|
236
|
+
() => new Response(bytes),
|
|
237
|
+
], async (gateways) => {
|
|
238
|
+
const response = await fetchPortableMedia({
|
|
239
|
+
url: new URL(hashlink),
|
|
240
|
+
digestMultibase: digest,
|
|
241
|
+
}, {
|
|
242
|
+
gateways,
|
|
243
|
+
allowPrivateAddress: true,
|
|
244
|
+
gatewayTimeout: 500,
|
|
245
|
+
timeout: 5_000,
|
|
246
|
+
});
|
|
247
|
+
equal(await response.text(), "portable image");
|
|
248
|
+
});
|
|
249
|
+
},
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
networkTest("fetchPortableMedia() applies one overall timeout", async () => {
|
|
253
|
+
await withServers(
|
|
254
|
+
[() => undefined, () => new Response(bytes)],
|
|
255
|
+
async (gateways) => {
|
|
256
|
+
await rejects(
|
|
257
|
+
() =>
|
|
258
|
+
fetchPortableMedia({
|
|
259
|
+
url: new URL(hashlink),
|
|
260
|
+
digestMultibase: digest,
|
|
261
|
+
}, {
|
|
262
|
+
gateways,
|
|
263
|
+
allowPrivateAddress: true,
|
|
264
|
+
gatewayTimeout: 100,
|
|
265
|
+
timeout: 20,
|
|
266
|
+
}),
|
|
267
|
+
(error) =>
|
|
268
|
+
error instanceof DOMException && error.name === "TimeoutError",
|
|
269
|
+
);
|
|
270
|
+
},
|
|
271
|
+
);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
networkTest("fetchPortableMedia() follows a safe redirect", async () => {
|
|
275
|
+
const requested: string[] = [];
|
|
276
|
+
await withServers([(path) => {
|
|
277
|
+
requested.push(path);
|
|
278
|
+
return path === "/media" ? new Response(bytes) : new Response(null, {
|
|
279
|
+
status: 302,
|
|
280
|
+
headers: { Location: "/media" },
|
|
281
|
+
});
|
|
282
|
+
}], async ([gateway]) => {
|
|
283
|
+
const response = await fetchPortableMedia({
|
|
284
|
+
url: new URL(hashlink),
|
|
285
|
+
digestMultibase: digest,
|
|
286
|
+
}, { gateways: [gateway], allowPrivateAddress: true });
|
|
287
|
+
equal(await response.text(), "portable image");
|
|
288
|
+
});
|
|
289
|
+
deepStrictEqual(requested, [
|
|
290
|
+
`/.well-known/apgateway/${hashlink}`,
|
|
291
|
+
"/media",
|
|
292
|
+
]);
|
|
293
|
+
});
|