identifier-js 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +27 -8
- package/index.js +110 -16
- package/normalization.md +47 -10
- package/package.json +5 -3
- package/readme.md +67 -16
package/index.d.ts
CHANGED
|
@@ -31,25 +31,32 @@ export const parseIri: (iri: string) => ParsedIdentifierComponents;
|
|
|
31
31
|
export const parseIriReference: (iriReference: string) => ParsedRelativeIdentifierComponents;
|
|
32
32
|
/** @throws {Error} If the absolute-IRI is invalid. */
|
|
33
33
|
export const parseAbsoluteIri: (iri: string) => ParsedAbsoluteIdentifierComponents;
|
|
34
|
-
/**
|
|
34
|
+
/** Resolve with generic RFC 3986 semantics; this does not invoke a URN resolution service.
|
|
35
|
+
* @throws {Error} If the base or the reference is invalid.
|
|
36
|
+
*/
|
|
35
37
|
export function resolveReference(reference: string, base: string, strict?: boolean, returnParts?: false): string;
|
|
36
38
|
export function resolveReference(reference: string, base: string, strict: boolean | undefined, returnParts: true): IdentifierComponents;
|
|
37
39
|
export function resolveReference(reference: string, base: string, strict: boolean | undefined, returnParts: boolean | undefined): string | IdentifierComponents;
|
|
38
40
|
/** @throws {Error} If the reference is invalid. */
|
|
39
41
|
export const toAbsoluteReference: (reference: string) => string;
|
|
40
|
-
/**
|
|
42
|
+
/** Derive a generic URI reference without scheme-specific relative-URN semantics.
|
|
43
|
+
* @throws {Error} If the base or the reference is invalid.
|
|
44
|
+
*/
|
|
41
45
|
export const toRelativeReference: (target: string, base: string) => string;
|
|
42
46
|
|
|
43
47
|
/** Map a parsed non-empty registered-name host to caller-owned text. */
|
|
44
48
|
export type RegNameMapper = (regName: string) => string;
|
|
45
49
|
|
|
46
|
-
/** Select
|
|
50
|
+
/** Select a target representation after parsed-result normalization. */
|
|
51
|
+
export type NormalizeTransform = 'URI' | 'IRI';
|
|
52
|
+
|
|
53
|
+
/** Select optional representation transformation and registered-name mapping. */
|
|
47
54
|
export type NormalizeOptions = {
|
|
48
|
-
|
|
55
|
+
transform?: NormalizeTransform;
|
|
49
56
|
mapRegName?: RegNameMapper;
|
|
50
57
|
};
|
|
51
58
|
|
|
52
|
-
/** Provide lazy RFC normalization and optional IRI
|
|
59
|
+
/** Provide lazy RFC normalization and optional URI/IRI transformation on a parsed result. */
|
|
53
60
|
export type NormalizableReference = {
|
|
54
61
|
normalize(options?: NormalizeOptions): string;
|
|
55
62
|
};
|
|
@@ -87,6 +94,18 @@ export type AbsoluteIdentifierComponents = {
|
|
|
87
94
|
query?: string;
|
|
88
95
|
};
|
|
89
96
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
97
|
+
// Describe the scheme-specific captures returned by each complete or fragment-free URN grammar.
|
|
98
|
+
type UrnIdentifierComponents = {
|
|
99
|
+
scheme: string;
|
|
100
|
+
nid: string;
|
|
101
|
+
nss: string;
|
|
102
|
+
rComponent?: string;
|
|
103
|
+
qComponent?: string;
|
|
104
|
+
fragment?: string;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
type AbsoluteUrnIdentifierComponents = Omit<UrnIdentifierComponents, 'fragment'>;
|
|
108
|
+
|
|
109
|
+
export type ParsedIdentifierComponents = (IdentifierComponents | UrnIdentifierComponents) & NormalizableReference;
|
|
110
|
+
export type ParsedRelativeIdentifierComponents = (RelativeIdentifierComponents | UrnIdentifierComponents) & NormalizableReference;
|
|
111
|
+
export type ParsedAbsoluteIdentifierComponents = (AbsoluteIdentifierComponents | AbsoluteUrnIdentifierComponents) & NormalizableReference;
|
package/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
// Parse, validate, normalize, resolve, and convert RFC 3986 URI
|
|
3
|
-
//
|
|
2
|
+
// Parse, validate, normalize, resolve, and convert RFC 3986 URI, RFC 3987 IRI, and RFC 8141 URN references.
|
|
3
|
+
// A valid URI is always a valid IRI, subject to every implemented scheme's more specific grammar.
|
|
4
4
|
const { recursiveCompile } = require('url-templates');
|
|
5
5
|
const patterns = new Map();
|
|
6
|
-
const implemented_schemes = '(?:[hH][tT][tT][pP][sS]?|[wW][sS][sS]?|[fF][iI][lL][eE])';
|
|
6
|
+
const implemented_schemes = '(?:[hH][tT][tT][pP][sS]?|[wW][sS][sS]?|[fF][iI][lL][eE]|[uU][rR][nN])';
|
|
7
7
|
// RFC3986/RFC3987 common rules + https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2:~:text=DNS%29%2E-,A,of%20%5BRFC1123%5D%2E
|
|
8
8
|
const commonRules = {
|
|
9
9
|
implemented_schemes,
|
|
@@ -79,6 +79,31 @@ const iriRules = {
|
|
|
79
79
|
iprivate: '[\\uE000-\\uF8FF\\u{F0000}-\\u{FFFFD}\\u{100000}-\\u{10FFFD}]',
|
|
80
80
|
ucschar: '[\\xA0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF\\u{10000}-\\u{1FFFD}\\u{20000}-\\u{2FFFD}\\u{30000}-\\u{3FFFD}\\u{40000}-\\u{4FFFD}\\u{50000}-\\u{5FFFD}\\u{60000}-\\u{6FFFD}\\u{70000}-\\u{7FFFD}\\u{80000}-\\u{8FFFD}\\u{90000}-\\u{9FFFD}\\u{A0000}-\\u{AFFFD}\\u{B0000}-\\u{BFFFD}\\u{C0000}-\\u{CFFFD}\\u{D0000}-\\u{DFFFD}\\u{E1000}-\\u{EFFFD}]',
|
|
81
81
|
};
|
|
82
|
+
// Define RFC 8141 productions and URI/IRI root overrides for the conditional URN profile.
|
|
83
|
+
const urnRules = {
|
|
84
|
+
scheme: implemented_schemes,
|
|
85
|
+
URI_reference: '{URI}',
|
|
86
|
+
URI: '{namestring}',
|
|
87
|
+
absolute_URI: '{assigned_name}(?:{rq_components})?',
|
|
88
|
+
IRI_reference: '{IRI}',
|
|
89
|
+
IRI: '{URI}',
|
|
90
|
+
absolute_IRI: '{absolute_URI}',
|
|
91
|
+
namestring: '{assigned_name}(?:{rq_components})?(?:#{f_component})?',
|
|
92
|
+
assigned_name: '{scheme}:{NID}:{NSS}',
|
|
93
|
+
NID: '{alpha_digit}{ldh}{0,30}{alpha_digit}',
|
|
94
|
+
ldh: '(?:{alpha_digit}|-)',
|
|
95
|
+
NSS: '{pchar}(?:{pchar}|/)*',
|
|
96
|
+
rq_components: '(?:[?][+]{r_component})?(?:[?]={q_component})?',
|
|
97
|
+
r_component: '{pchar}(?:{pchar}|/|[?](?!=))*',
|
|
98
|
+
q_component: '{pchar}(?:{pchar}|/|[?])*',
|
|
99
|
+
f_component: '{fragment}',
|
|
100
|
+
};
|
|
101
|
+
// Reuse the grammar repertoires when selecting URI octets safe for IRI output.
|
|
102
|
+
const uriUnreservedPattern = new RegExp(`^${commonRules.unreserved}$`);
|
|
103
|
+
const iriUcscharPattern = new RegExp(`^${iriRules.ucschar}$`, 'u');
|
|
104
|
+
const iriPrivatePattern = new RegExp(`^${iriRules.iprivate}$`, 'u');
|
|
105
|
+
// Apply the additional RFC 3987 Section 4.1 prose restriction outside the ABNF repertoire.
|
|
106
|
+
const forbiddenIriFormattingPattern = /^[\u200E\u200F\u202A-\u202E]$/u;
|
|
82
107
|
// scheme specific URI reg_name and IRI ireg_name
|
|
83
108
|
const schemeSpecificRules = {
|
|
84
109
|
scheme: implemented_schemes,
|
|
@@ -119,11 +144,16 @@ const groupNames = {
|
|
|
119
144
|
ipath_noscheme: 'path',
|
|
120
145
|
ipath_rootless: 'path',
|
|
121
146
|
ipath_empty: 'path',
|
|
147
|
+
NID: 'nid',
|
|
148
|
+
NSS: 'nss',
|
|
149
|
+
r_component: 'rComponent',
|
|
150
|
+
q_component: 'qComponent',
|
|
122
151
|
};
|
|
123
|
-
//
|
|
152
|
+
// Detect schemes for which the package implements grammar beyond generic URI/IRI syntax.
|
|
124
153
|
const isSpecificScheme = (string) => new RegExp('^' + implemented_schemes + ':').test(string);
|
|
125
|
-
|
|
126
|
-
const
|
|
154
|
+
// Select and merge generic, DNS-host, empty-file-host, or URN grammar profiles.
|
|
155
|
+
const schemeProfile = (string) => (string.slice(0, 4).toLowerCase() === 'urn:' ? 'u' : string.slice(0, 8).toLowerCase() === 'file:///' ? 'f' : isSpecificScheme(string) ? 's' : '');
|
|
156
|
+
const rules = (profile) => Object.assign({}, commonRules, uriRules, iriRules, profile === 'u' ? urnRules : profile === 'f' ? emptyFileHostRules : profile ? schemeSpecificRules : {});
|
|
127
157
|
// parse (slower, it uses regex.exec and includes named capture groups)
|
|
128
158
|
const parse = (string, rule) => {
|
|
129
159
|
if (typeof string !== 'string') throw new TypeError(`Invalid ${rule.replace('_', '-')} type: must be a string.`);
|
|
@@ -321,6 +351,10 @@ function isIPv4Address(host) {
|
|
|
321
351
|
}
|
|
322
352
|
return true;
|
|
323
353
|
}
|
|
354
|
+
// Identify ASCII octets through the URI grammar's canonical unreserved repertoire.
|
|
355
|
+
function isAsciiUnreservedOctet(octet) {
|
|
356
|
+
return uriUnreservedPattern.test(String.fromCharCode(octet));
|
|
357
|
+
}
|
|
324
358
|
// Normalize percent triplets while optionally decoding only ASCII unreserved octets.
|
|
325
359
|
function normalizePercentEncoding(value, decodeUnreserved = true) {
|
|
326
360
|
let result = '';
|
|
@@ -332,8 +366,7 @@ function normalizePercentEncoding(value, decodeUnreserved = true) {
|
|
|
332
366
|
}
|
|
333
367
|
const hexadecimal = value.slice(index + 1, index + 3);
|
|
334
368
|
const octet = Number.parseInt(hexadecimal, 16);
|
|
335
|
-
|
|
336
|
-
result += decodeUnreserved && unreserved ? String.fromCharCode(octet) : `%${hexadecimal.toUpperCase()}`;
|
|
369
|
+
result += decodeUnreserved && isAsciiUnreservedOctet(octet) ? String.fromCharCode(octet) : `%${hexadecimal.toUpperCase()}`;
|
|
337
370
|
index += 2;
|
|
338
371
|
}
|
|
339
372
|
return result;
|
|
@@ -459,6 +492,52 @@ function encodeIriComponent(component) {
|
|
|
459
492
|
// Process complete code points so supplementary characters produce one UTF-8 sequence.
|
|
460
493
|
return component.replace(/[^\x00-\x7F]/gu, (character) => encodeURIComponent(character).toUpperCase());
|
|
461
494
|
}
|
|
495
|
+
// Decode the maximal RFC 3987 URI octet repertoire allowed by one IRI component.
|
|
496
|
+
function decodeUriComponentToIri(component, allowPrivate = false) {
|
|
497
|
+
let result = '';
|
|
498
|
+
// Inspect each normalized percent triplet as either ASCII or the lead of one strict UTF-8 scalar.
|
|
499
|
+
for (let index = 0; index < component.length; index++) {
|
|
500
|
+
if (component[index] !== '%' || !/^[0-9A-F]{2}$/.test(component.slice(index + 1, index + 3))) {
|
|
501
|
+
result += component[index];
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
const hexadecimal = component.slice(index + 1, index + 3);
|
|
505
|
+
const octet = Number.parseInt(hexadecimal, 16);
|
|
506
|
+
if (octet <= 0x7F) {
|
|
507
|
+
result += isAsciiUnreservedOctet(octet) ? String.fromCharCode(octet) : `%${hexadecimal}`;
|
|
508
|
+
index += 2;
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
const sequenceLength = octet >= 0xC2 && octet <= 0xDF ? 2 : octet >= 0xE0 && octet <= 0xEF ? 3 : octet >= 0xF0 && octet <= 0xF4 ? 4 : 0;
|
|
512
|
+
let encoded = '';
|
|
513
|
+
// Collect exactly one candidate scalar without consuming malformed trailing input.
|
|
514
|
+
for (let sequenceIndex = 0; sequenceIndex < sequenceLength; sequenceIndex++) {
|
|
515
|
+
const position = index + sequenceIndex * 3;
|
|
516
|
+
if (component[position] !== '%' || !/^[0-9A-F]{2}$/.test(component.slice(position + 1, position + 3))) {
|
|
517
|
+
encoded = '';
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
520
|
+
encoded += component.slice(position, position + 3);
|
|
521
|
+
}
|
|
522
|
+
let character;
|
|
523
|
+
if (encoded) {
|
|
524
|
+
try {
|
|
525
|
+
character = decodeURIComponent(encoded);
|
|
526
|
+
} catch {
|
|
527
|
+
character = undefined;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
const allowed = character !== undefined && !forbiddenIriFormattingPattern.test(character) && (iriUcscharPattern.test(character) || (allowPrivate && iriPrivatePattern.test(character)));
|
|
531
|
+
if (allowed) {
|
|
532
|
+
result += character;
|
|
533
|
+
index += encoded.length - 1;
|
|
534
|
+
} else {
|
|
535
|
+
result += `%${hexadecimal}`;
|
|
536
|
+
index += 2;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return result;
|
|
540
|
+
}
|
|
462
541
|
// Rebuild authority from normalized values while preserving other empty component delimiters.
|
|
463
542
|
function normalizeAuthority(parts, scheme, mapRegName) {
|
|
464
543
|
if (parts.authority === undefined) return undefined;
|
|
@@ -475,9 +554,16 @@ function normalizeAuthority(parts, scheme, mapRegName) {
|
|
|
475
554
|
function normalizeParsedReference(parts, options = {}) {
|
|
476
555
|
// Validate the optional API settings before they select normalization behavior.
|
|
477
556
|
if (options === null || typeof options !== 'object' || Array.isArray(options)) throw new TypeError('Invalid normalization argument type: must be an options object.');
|
|
478
|
-
const {
|
|
479
|
-
if (
|
|
557
|
+
const { transform, mapRegName } = options;
|
|
558
|
+
if (transform !== undefined && transform !== 'URI' && transform !== 'IRI') throw new TypeError('Invalid transform option: must be "URI" or "IRI".');
|
|
480
559
|
if (mapRegName !== undefined && typeof mapRegName !== 'function') throw new TypeError('Invalid registered-name mapper type: must be a function.');
|
|
560
|
+
// Normalize captured URN fields without applying generic path or representation processing.
|
|
561
|
+
if (parts.nid !== undefined) {
|
|
562
|
+
const rComponent = parts.rComponent === undefined ? undefined : normalizePercentEncoding(parts.rComponent, false);
|
|
563
|
+
const qComponent = parts.qComponent === undefined ? undefined : normalizePercentEncoding(parts.qComponent, false);
|
|
564
|
+
const query = rComponent !== undefined ? `+${rComponent}${qComponent === undefined ? '' : `?=${qComponent}`}` : qComponent === undefined ? undefined : `=${qComponent}`;
|
|
565
|
+
return compose({ scheme: parts.scheme.toLowerCase(), path: `${parts.nid.toLowerCase()}:${normalizePercentEncoding(parts.nss, false)}`, query, fragment: parts.fragment === undefined ? undefined : normalizePercentEncoding(parts.fragment, false) });
|
|
566
|
+
}
|
|
481
567
|
// Normalize each component independently so encoded delimiters cannot become structure.
|
|
482
568
|
const scheme = parts.scheme === undefined ? undefined : parts.scheme.toLowerCase();
|
|
483
569
|
const normalized = {
|
|
@@ -496,12 +582,20 @@ function normalizeParsedReference(parts, options = {}) {
|
|
|
496
582
|
// Preserve a no-authority path when reduction would reparse it as an authority.
|
|
497
583
|
if (normalized.authority !== undefined || !reducedPath.startsWith('//')) normalized.path = reducedPath;
|
|
498
584
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
585
|
+
// Select an explicit target representation only after syntax and scheme normalization is complete.
|
|
586
|
+
if (transform === 'URI') {
|
|
587
|
+
// Map every non-ASCII authority, path, query, and fragment scalar under RFC 3987 URI output.
|
|
588
|
+
if (normalized.authority !== undefined) normalized.authority = encodeIriComponent(normalized.authority);
|
|
589
|
+
normalized.path = encodeIriComponent(normalized.path);
|
|
590
|
+
if (normalized.query !== undefined) normalized.query = encodeIriComponent(normalized.query);
|
|
591
|
+
if (normalized.fragment !== undefined) normalized.fragment = encodeIriComponent(normalized.fragment);
|
|
592
|
+
} else if (transform === 'IRI') {
|
|
593
|
+
// Decode valid UTF-8 percent sequences only where the destination component permits their scalar.
|
|
594
|
+
if (normalized.authority !== undefined) normalized.authority = decodeUriComponentToIri(normalized.authority);
|
|
595
|
+
normalized.path = decodeUriComponentToIri(normalized.path);
|
|
596
|
+
if (normalized.query !== undefined) normalized.query = decodeUriComponentToIri(normalized.query, true);
|
|
597
|
+
if (normalized.fragment !== undefined) normalized.fragment = decodeUriComponentToIri(normalized.fragment);
|
|
598
|
+
}
|
|
505
599
|
return compose(normalized);
|
|
506
600
|
}
|
|
507
601
|
// export
|
package/normalization.md
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
# URI and IRI normalization
|
|
2
2
|
|
|
3
|
-
Parsed URI and IRI results expose `normalize()` for syntax-based normalization, the implemented HTTP, HTTPS, WS, and
|
|
3
|
+
Parsed URI and IRI results expose `normalize()` for syntax-based normalization, the implemented HTTP, HTTPS, WS, WSS, and URN scheme rules, and optional RFC 3987 URI/IRI representation transformation. The method returns a string and leaves the parsed components unchanged.
|
|
4
4
|
|
|
5
5
|
## API
|
|
6
6
|
|
|
7
7
|
```ts
|
|
8
8
|
type RegNameMapper = (regName: string) => string
|
|
9
9
|
|
|
10
|
+
type NormalizeTransform = 'URI' | 'IRI'
|
|
11
|
+
|
|
10
12
|
type NormalizeOptions = {
|
|
11
|
-
|
|
13
|
+
transform?: NormalizeTransform
|
|
12
14
|
mapRegName?: RegNameMapper
|
|
13
15
|
}
|
|
14
16
|
|
|
@@ -47,14 +49,39 @@ parsed.path;
|
|
|
47
49
|
| Behavior | Implementation | Source |
|
|
48
50
|
| --- | --- | --- |
|
|
49
51
|
| Case normalization | Lowercase the scheme and an ASCII-only host. Uppercase hexadecimal letters in percent triplets. | [RFC 3986 §6.2.2.1](https://www.rfc-editor.org/rfc/rfc3986#section-6.2.2.1), [RFC 3987 §5.3.2.1](https://www.rfc-editor.org/rfc/rfc3987#section-5.3.2.1) |
|
|
50
|
-
| Percent-encoded unreserved characters |
|
|
51
|
-
| Path segments | Apply the RFC dot-segment algorithm where
|
|
52
|
+
| Percent-encoded unreserved characters | For generic URI/IRI components, decode percent triplets representing ASCII letters, digits, `-`, `.`, `_`, or `~`. Retain percent encoding for reserved octets. URNs use the non-decoding rules below. | [RFC 3986 §§2.2–2.4 and 6.2.2.2](https://www.rfc-editor.org/rfc/rfc3986#section-6.2.2.2), [RFC 3987 §5.3.2.3](https://www.rfc-editor.org/rfc/rfc3987#section-5.3.2.3) |
|
|
53
|
+
| Path segments | Apply the RFC dot-segment algorithm where a generic parsed reference can be normalized independently. Preserve unresolved rootless-relative path semantics and every URN NSS segment. | [RFC 3986 §§5.2.4 and 6.2.2.3](https://www.rfc-editor.org/rfc/rfc3986#section-5.2.4), [RFC 3987 §5.3.2.4](https://www.rfc-editor.org/rfc/rfc3987#section-5.3.2.4), [RFC 8141 §§2.2 and 3.1](https://www.rfc-editor.org/rfc/rfc8141#section-3.1) |
|
|
52
54
|
| Component recomposition | Emit authority, query, and fragment delimiters from component presence, including present-empty components. | [RFC 3986 §5.3](https://www.rfc-editor.org/rfc/rfc3986#section-5.3) |
|
|
53
55
|
| IPv6 text | Suppress leading zeroes, compress the longest zero run with first-run tie breaking, and use lowercase hexadecimal. Known embedded-IPv4 forms use mixed notation. | [RFC 5952 §§4–5](https://www.rfc-editor.org/rfc/rfc5952#section-4) |
|
|
54
|
-
| IRI-to-URI output | With `
|
|
56
|
+
| IRI-to-URI output | With `transform: 'URI'`, encode non-ASCII authority, path, query, and fragment characters as uppercase UTF-8 percent triplets. | [RFC 3987 §3.1](https://www.rfc-editor.org/rfc/rfc3987#section-3.1) |
|
|
57
|
+
| URI-to-IRI output | With `transform: 'IRI'`, decode percent-encoded ASCII unreserved characters and strictly legal UTF-8 sequences permitted in each destination component. Retain reserved, malformed, disallowed, and non-UTF-8 octets. | [RFC 3987 §3.2](https://www.rfc-editor.org/rfc/rfc3987#section-3.2) |
|
|
55
58
|
|
|
56
59
|
Without a mapper, normalization retains the parser's host classification as an IP literal, IPv4 address, or registered name. IPvFuture literals use generic host case normalization. Existing non-ASCII IRI host text is retained unless the registered-name mapper supplies another value.
|
|
57
60
|
|
|
61
|
+
## URNs
|
|
62
|
+
|
|
63
|
+
A parsed value under the case-insensitive `urn` scheme takes a separate RFC 8141 normalization path using its captured `scheme`, `nid`, `nss`, `rComponent`, `qComponent`, and `fragment` properties.
|
|
64
|
+
|
|
65
|
+
| Input component | Output | Source |
|
|
66
|
+
| --- | --- | --- |
|
|
67
|
+
| Scheme | Convert `urn` to lowercase. | [RFC 8141 §3.1](https://www.rfc-editor.org/rfc/rfc8141#section-3.1) |
|
|
68
|
+
| NID | Convert ASCII letters to lowercase. | [RFC 8141 §§2.1 and 3.1](https://www.rfc-editor.org/rfc/rfc8141#section-3.1) |
|
|
69
|
+
| NSS | Uppercase hexadecimal letters in percent triplets without decoding any octet. Preserve literal case, slash structure, and dot segments. | [RFC 8141 §§2.2 and 3.1](https://www.rfc-editor.org/rfc/rfc8141#section-3.1) |
|
|
70
|
+
| r-, q-, and f-components | Retain the components and their delimiters, uppercasing hexadecimal letters in percent triplets without decoding. | [RFC 8141 §2.3](https://www.rfc-editor.org/rfc/rfc8141#section-2.3), [RFC 3986 §6.2.2.1](https://www.rfc-editor.org/rfc/rfc3986#section-6.2.2.1) |
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
const { parseUri } = require('identifier-js');
|
|
74
|
+
|
|
75
|
+
parseUri('URN:EXAMPLE:a%62/./b/../C?+r%2f?=q%2f#f%2f').normalize();
|
|
76
|
+
// urn:example:a%62/./b/../C?+r%2F?=q%2F#f%2F
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
RFC 8141 URNs remain ASCII, including when parsed through an IRI operation. Consequently, `transform: 'URI'` and `transform: 'IRI'` produce the same URN representation, and `mapRegName` is not called because a URN has no authority or registered-name host.
|
|
80
|
+
|
|
81
|
+
For a parsed URN, the current scheme-specific fields are the normalization input. The NSS and optional-component values stay opaque except for percent-triplet letter case. The method leaves every property unchanged.
|
|
82
|
+
|
|
83
|
+
Normalization is not a URN-equivalence API. RFC 8141 equivalence compares the normalized assigned-name and ignores r-, q-, and f-components; namespace definitions can add further equivalence rules. This method instead retains those optional components in its returned string. The package does not implement generic or namespace-specific URN-equivalence comparison.
|
|
84
|
+
|
|
58
85
|
## HTTP and HTTPS
|
|
59
86
|
|
|
60
87
|
For `http` and `https`, normalization applies the generic rules and these scheme rules:
|
|
@@ -91,7 +118,7 @@ wss://example.com:00443/chat → wss://example.com/chat
|
|
|
91
118
|
ws://example.com?channel=updates → ws://example.com/?channel=updates
|
|
92
119
|
```
|
|
93
120
|
|
|
94
|
-
## Registered-name mapping and
|
|
121
|
+
## Registered-name mapping and representation transformation
|
|
95
122
|
|
|
96
123
|
For a non-empty registered-name host, `options.mapRegName` is called once with the current host spelling before built-in normalization. IP literals, IPv4 addresses, absent hosts, and empty hosts bypass the mapper.
|
|
97
124
|
|
|
@@ -108,18 +135,27 @@ mapped;
|
|
|
108
135
|
|
|
109
136
|
The example deliberately produces text that is not a valid URI or IRI; validating or selecting mapper output belongs to the application.
|
|
110
137
|
|
|
111
|
-
With `
|
|
138
|
+
With `transform: 'URI'`, retained reserved and non-ASCII percent triplets remain encoded, and literal non-ASCII userinfo, mapper output, path, query, and fragment text becomes uppercase UTF-8 percent triplets. A mapper can supply an ASCII hostname when its consuming scheme requires one; this package does not validate mapper output against that scheme.
|
|
112
139
|
|
|
113
140
|
```js
|
|
114
|
-
const { parseIri } = require('identifier-js');
|
|
141
|
+
const { parseIri, parseUri } = require('identifier-js');
|
|
115
142
|
|
|
116
|
-
parseIri('x:/café?q=資料#résultat').normalize({
|
|
143
|
+
parseIri('x:/café?q=資料#résultat').normalize({ transform: 'URI' });
|
|
117
144
|
// x:/caf%C3%A9?q=%E8%B3%87%E6%96%99#r%C3%A9sultat
|
|
145
|
+
|
|
146
|
+
parseUri('x:/caf%C3%A9?q=%E8%B3%87%E6%96%99#r%C3%A9sultat').normalize({ transform: 'IRI' });
|
|
147
|
+
// x:/café?q=資料#résultat
|
|
118
148
|
```
|
|
119
149
|
|
|
150
|
+
With `transform: 'IRI'`, conversion uses UTF-8 exclusively and decodes as many eligible percent-encoded characters as possible. Encoded reserved characters, `%25`, malformed or incomplete UTF-8, legacy character encodings, Unicode outside the RFC 3987 component repertoire, and forbidden bidirectional formatting characters remain percent encoded. Private-use characters are decoded only in the query component. The hexadecimal letters of retained triplets are uppercase.
|
|
151
|
+
|
|
152
|
+
The IRI transformation decodes percent-encoded ASCII unreserved characters even when this changes a registered name into IPv4-looking text. Without an explicit transformation, normalization preserves that registered-name host classification.
|
|
153
|
+
|
|
154
|
+
ACE-to-Unicode and Unicode-to-ACE registered-name conversion remain application policy. `mapRegName` runs before the selected representation transformation, so applications can provide the appropriate mapping in either direction.
|
|
155
|
+
|
|
120
156
|
## Verification
|
|
121
157
|
|
|
122
|
-
The normalization suite covers URI and IRI parser results, component presence, percent triplets, dot segments, host kinds, RFC 5952 output, HTTP and WebSocket scheme rules, registered-name mapping,
|
|
158
|
+
The normalization suite covers URI and IRI parser results, component presence, percent triplets, dot segments, host kinds, RFC 5952 output, HTTP and WebSocket scheme rules, registered-name mapping, both RFC representation transformations, malformed UTF-8 retention, component-specific Unicode repertoires, component non-mutation, round trips, and idempotence.
|
|
123
159
|
|
|
124
160
|
```sh
|
|
125
161
|
npm test
|
|
@@ -129,6 +165,7 @@ npm test
|
|
|
129
165
|
|
|
130
166
|
- [RFC 3986 — Uniform Resource Identifier: Generic Syntax](https://www.rfc-editor.org/rfc/rfc3986)
|
|
131
167
|
- [RFC 3987 — Internationalized Resource Identifiers](https://www.rfc-editor.org/rfc/rfc3987)
|
|
168
|
+
- [RFC 8141 — Uniform Resource Names](https://www.rfc-editor.org/rfc/rfc8141)
|
|
132
169
|
- [RFC 5952 — A Recommendation for IPv6 Address Text Representation](https://www.rfc-editor.org/rfc/rfc5952)
|
|
133
170
|
- [RFC 6455 — The WebSocket Protocol](https://www.rfc-editor.org/rfc/rfc6455)
|
|
134
171
|
- [RFC 9110 — HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110)
|
package/package.json
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "identifier-js",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "A fast URI/IRI parser, validator, normalizer, resolver, and composer
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "A fast RFC 3986/3987 URI/IRI parser, validator, normalizer, resolver, and composer with RFC 8141 URN syntax support.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"IRI",
|
|
7
7
|
"URI",
|
|
8
8
|
"IRI-reference",
|
|
9
9
|
"URI-reference",
|
|
10
|
+
"URN",
|
|
10
11
|
"ipv4",
|
|
11
12
|
"ipv6",
|
|
12
13
|
"uuid",
|
|
13
14
|
"parser",
|
|
14
15
|
"validator",
|
|
15
16
|
"RFC3986",
|
|
16
|
-
"RFC3987"
|
|
17
|
+
"RFC3987",
|
|
18
|
+
"RFC8141"
|
|
17
19
|
],
|
|
18
20
|
"homepage": "https://github.com/SorinGFS/identifier-js#readme",
|
|
19
21
|
"bugs": {
|
package/readme.md
CHANGED
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
title: Identifier JS
|
|
4
4
|
|
|
5
|
-
description:
|
|
5
|
+
description: RFC 3986/3987 URI and IRI tools with scheme-specific RFC 8141 URN syntax and normalization support.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Identifier JS
|
|
10
10
|
|
|
11
|
-
`identifier-js` is a URI/IRI parser, validator, normalizer, resolver, and composer based on RFC [3986](https://www.rfc-editor.org/rfc/rfc3986) and RFC [3987](https://www.rfc-editor.org/rfc/rfc3987). Its recognized HTTP, WebSocket, and `file` schemes retain the documented hostname-policy restrictions below. It provides:
|
|
11
|
+
`identifier-js` is a URI/IRI parser, validator, normalizer, resolver, and composer based on RFC [3986](https://www.rfc-editor.org/rfc/rfc3986) and RFC [3987](https://www.rfc-editor.org/rfc/rfc3987), with scheme-specific RFC [8141](https://www.rfc-editor.org/rfc/rfc8141) URN support. Its recognized HTTP, WebSocket, and `file` schemes retain the documented hostname-policy restrictions below. It provides:
|
|
12
12
|
|
|
13
|
-
- URI and IRI validation;
|
|
14
|
-
- parsed
|
|
15
|
-
- conservative syntax normalization, recognized-scheme
|
|
13
|
+
- URI and IRI validation, including RFC 8141 URN namestring syntax;
|
|
14
|
+
- parsed generic URI/IRI components and scheme-specific URN components;
|
|
15
|
+
- conservative syntax normalization, recognized-scheme forms, and a registered-name extension point;
|
|
16
16
|
- RFC 3986 reference resolution and dot-segment removal;
|
|
17
17
|
- relative-reference generation with resolution round-trip guarantees for supported forms;
|
|
18
18
|
- UUID and UUIDv4 lexical validation;
|
|
@@ -143,6 +143,45 @@ console.log(parseIri('https://usér@例え.テスト:8443/résumé?lang=fr#profi
|
|
|
143
143
|
|
|
144
144
|
</details>
|
|
145
145
|
|
|
146
|
+
### Validate and parse RFC 8141 URNs
|
|
147
|
+
|
|
148
|
+
URNs use the existing URI and IRI operations because a URN is a URI under the `urn` scheme. Values with a case-insensitive `urn:` prefix are validated against RFC 8141 namestring syntax; no separate `isUrn` or `parseUrn` API is exported.
|
|
149
|
+
|
|
150
|
+
```text
|
|
151
|
+
urn:NID:NSS[?+r-component][?=q-component][#f-component]
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The NID contains 2–32 ASCII characters, starts and ends with a letter or digit, and permits letters, digits, or hyphens internally. The NSS begins with an RFC 3986 `pchar` and then permits `pchar` or `/`. The ordered r- and q-components also begin with `pchar` and then permit `pchar`, `/`, or `?`, while an f-component can be empty. The first `?=` sequence after an r-component starts the q-component, and any other question mark outside an optional component is rejected.
|
|
155
|
+
|
|
156
|
+
<details>
|
|
157
|
+
<summary><strong>API behavior and examples</strong></summary>
|
|
158
|
+
|
|
159
|
+
```js
|
|
160
|
+
const { isUri, isIri, parseUri } = require('identifier-js');
|
|
161
|
+
|
|
162
|
+
const value = 'URN:Example:a%2f/../B?+service?x?=key=value#part';
|
|
163
|
+
console.log(isUri(value)); // true
|
|
164
|
+
console.log(isIri(value)); // true
|
|
165
|
+
|
|
166
|
+
const parsed = parseUri(value);
|
|
167
|
+
console.log(parsed.scheme); // URN
|
|
168
|
+
console.log(parsed.nid); // Example
|
|
169
|
+
console.log(parsed.nss); // a%2f/../B
|
|
170
|
+
console.log(parsed.rComponent); // service?x
|
|
171
|
+
console.log(parsed.qComponent); // key=value
|
|
172
|
+
console.log(parsed.fragment); // part
|
|
173
|
+
console.log(parsed.normalize());
|
|
174
|
+
// urn:example:a%2F/../B?+service?x?=key=value#part
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
URN parse results expose `nid`, `nss`, `rComponent`, and `qComponent`, while the RFC-defined f-component is exposed as `fragment`. They do not expose generic `path` or `query` aliases. To require a URN after parsing a value accepted as a general URI, check `parsed.scheme.toLowerCase() === 'urn'`.
|
|
178
|
+
|
|
179
|
+
URNs remain ASCII even through the IRI operations. Callers representing non-ASCII names must first encode them as UTF-8 and then percent-encode the resulting octets; lexical validation does not decode or verify those octet sequences.
|
|
180
|
+
|
|
181
|
+
Validation is deliberately lexical and namespace-independent. Success does not prove that an NID is registered or otherwise legitimate, that an NSS obeys a namespace's additional syntax and canonicalization rules, or that the name was legitimately assigned.
|
|
182
|
+
|
|
183
|
+
</details>
|
|
184
|
+
|
|
146
185
|
### Resolve a reference
|
|
147
186
|
|
|
148
187
|
Resolve a URI or IRI reference against an absolute base using RFC 3986 §5.
|
|
@@ -173,6 +212,8 @@ console.log(resolveReference('?page=2', 'https://example.com/items?page=1#curren
|
|
|
173
212
|
|
|
174
213
|
Empty authorities, queries, and fragments are preserved during recomposition.
|
|
175
214
|
|
|
215
|
+
This function performs generic RFC 3986 reference resolution only. It does not invoke a URN resolution service or implement scheme-specific URN resolution semantics.
|
|
216
|
+
|
|
176
217
|
</details>
|
|
177
218
|
|
|
178
219
|
### Produce absolute and relative forms
|
|
@@ -201,6 +242,8 @@ console.log(relative); // ../images/logo.svg
|
|
|
201
242
|
|
|
202
243
|
When no safe rootless relative form can round-trip to the target, `toRelativeReference` returns the absolute target. Different schemes or authorities also return the target unchanged. Complete dot segments in either path also trigger this fallback because RFC resolution removes them. For those inputs, resolving the result produces the same identifier as resolving the target directly; lexical dot-segment spelling is not preserved.
|
|
203
244
|
|
|
245
|
+
These conversion functions retain their generic URI-reference behavior. They do not construct, resolve, or interpret scheme-specific relative URNs.
|
|
246
|
+
|
|
204
247
|
</details>
|
|
205
248
|
|
|
206
249
|
### Normalize parsed URI and IRI references
|
|
@@ -213,8 +256,10 @@ Every URI and IRI parse result provides an optional, non-enumerable `normalize()
|
|
|
213
256
|
```ts
|
|
214
257
|
type RegNameMapper = (regName: string) => string
|
|
215
258
|
|
|
259
|
+
type NormalizeTransform = 'URI' | 'IRI'
|
|
260
|
+
|
|
216
261
|
type NormalizeOptions = {
|
|
217
|
-
|
|
262
|
+
transform?: NormalizeTransform
|
|
218
263
|
mapRegName?: RegNameMapper
|
|
219
264
|
}
|
|
220
265
|
|
|
@@ -237,9 +282,11 @@ The method is available from `parseUri`, `parseUriReference`, `parseAbsoluteUri`
|
|
|
237
282
|
|
|
238
283
|
Normalization implements RFC 3986 and RFC 3987 syntax normalization for scheme and host case, percent triplets, ASCII unreserved characters, path dot segments, and component recomposition. IPv6 literals use RFC 5952 text. HTTP(S) default ports and empty paths follow RFC 9110; WS(S) defaults and resource-name paths follow RFC 6455.
|
|
239
284
|
|
|
285
|
+
URN normalization lowercases the scheme and NID, uppercases percent-triplet hexadecimal letters without decoding, and preserves NSS case, slashes, and dot segments. The r-, q-, and f-components are retained, so normalized-string equality is not the RFC 8141 URN-equivalence procedure. Namespace-specific equivalence and URN resolution are outside this package's scope. URI/IRI transformation and registered-name mapping options do not alter authority-free, ASCII-only URNs.
|
|
286
|
+
|
|
240
287
|
For a non-empty registered-name host, `mapRegName` receives the current host spelling before built-in normalization. The mapper exclusively owns validation, representation, and host-kind policy for its returned string. Apart from enforcing the declared string return type, this package does not check whether mapper output is non-empty, remains a registered name, introduces delimiters, resembles an IP address, or satisfies a scheme-specific hostname grammar.
|
|
241
288
|
|
|
242
|
-
With `
|
|
289
|
+
With `transform: 'URI'`, non-ASCII userinfo, mapper output, path, query, and fragment text becomes uppercase UTF-8 percent triplets under RFC 3987 §3.1. With `transform: 'IRI'`, eligible percent-encoded ASCII unreserved characters and strictly legal UTF-8 sequences become IRI characters under RFC 3987 §3.2; reserved, malformed, disallowed, and non-UTF-8 octets remain encoded. Private-use characters are decoded only in queries, and forbidden bidirectional formatting characters remain encoded. A mapper can supply the desired Unicode or ASCII hostname representation; this package does not enforce that policy or validate the complete normalized result.
|
|
243
290
|
|
|
244
291
|
See [`normalization.md`](normalization.md) for the exact RFC section mapping and examples.
|
|
245
292
|
|
|
@@ -272,11 +319,11 @@ console.log(isUUIDv4('123e4567-e89b-42d3-9456-426614174000')); // true
|
|
|
272
319
|
|
|
273
320
|
The parser builds its validation logic from declarative RFC grammar fragments:
|
|
274
321
|
|
|
275
|
-
1. Select
|
|
322
|
+
1. Select the applicable generic or scheme-specific URI/IRI syntax.
|
|
276
323
|
2. Recursively expand grammar references through `url-templates`.
|
|
277
|
-
3. Add named
|
|
324
|
+
3. Add named captures for the components exposed by the selected syntax.
|
|
278
325
|
4. Compile the complete expression with Unicode support.
|
|
279
|
-
5. Cache
|
|
326
|
+
5. Cache expressions by operation, grammar rule, and scheme class.
|
|
280
327
|
6. Validate with `RegExp.test()` or parse with `RegExp.exec()`.
|
|
281
328
|
7. Resolve references by component inheritance, path merging, dot-segment removal, and definedness-preserving recomposition.
|
|
282
329
|
|
|
@@ -300,7 +347,7 @@ The following schemes trigger DNS-style ASCII or Unicode label rules instead of
|
|
|
300
347
|
- `wss`
|
|
301
348
|
- `file`
|
|
302
349
|
|
|
303
|
-
Matching is case-insensitive. Other valid schemes use generic RFC 3986/3987 registered-name syntax. RFC 8089's empty `file` authority is accepted when followed by an absolute path, as in `file:///path`; empty hosts remain rejected for HTTP and WebSocket schemes.
|
|
350
|
+
Matching is case-insensitive. Other valid schemes use generic RFC 3986/3987 registered-name syntax; URNs instead follow RFC 8141 namestring syntax. RFC 8089's empty `file` authority is accepted when followed by an absolute path, as in `file:///path`; empty hosts remain rejected for HTTP and WebSocket schemes.
|
|
304
351
|
|
|
305
352
|
Parsing validates DNS-style label shape and the selected RFC 3987 Unicode character classes. A registered-name mapper runs later during optional normalization, and its returned string is not submitted to this hostname policy again.
|
|
306
353
|
|
|
@@ -416,8 +463,10 @@ RFC 9562 lists database keys, filenames, system identifiers, and transaction ide
|
|
|
416
463
|
<details>
|
|
417
464
|
<summary><strong>Validation and parsing</strong></summary>
|
|
418
465
|
|
|
419
|
-
- Generic URI syntax follows RFC 3986 character and component grammar;
|
|
420
|
-
- Generic IRI syntax follows the RFC 3987 Unicode extensions to URI grammar;
|
|
466
|
+
- Generic URI syntax follows RFC 3986 character and component grammar; HTTP, WebSocket, and `file` schemes apply the documented hostname restrictions.
|
|
467
|
+
- Generic IRI syntax follows the RFC 3987 Unicode extensions to URI grammar; HTTP, WebSocket, and `file` schemes apply the documented hostname restrictions.
|
|
468
|
+
- Values with the case-insensitive `urn` scheme follow RFC 8141 namestring syntax and expose NID, NSS, r-component, q-component, and fragment fields through the URI and IRI parsers.
|
|
469
|
+
- URN validation establishes generic lexical syntax only, not namespace registration, namespace-specific syntax, assignment, resolution, or equivalence.
|
|
421
470
|
- Validators return `true` or throw at the first grammar violation.
|
|
422
471
|
- `absolute-URI` and `absolute-IRI` use the fragment-free grammar defined by their RFCs; complete URI and IRI operations accept fragments.
|
|
423
472
|
- Port syntax follows RFC 3986 `port = *DIGIT`, including empty and leading-zero values.
|
|
@@ -433,7 +482,8 @@ RFC 9562 lists database keys, filenames, system identifiers, and transaction ide
|
|
|
433
482
|
- `strict = false` implements RFC 3986 §5.2.2 backward-compatible same-scheme handling.
|
|
434
483
|
- `toAbsoluteReference` removes the fragment from an identifier containing a scheme.
|
|
435
484
|
- `toRelativeReference` generates a reference whose RFC resolution equals the target resolution for supported forms.
|
|
436
|
-
- `normalize()` implements the applicable case, percent-encoding, and path-segment rules from RFC 3986 §§6.2.2.1–6.2.2.3 and RFC 3987 §§5.3.2.1, 5.3.2.3–5.3.2.4, RFC 3987
|
|
485
|
+
- `normalize()` implements the applicable case, percent-encoding, and path-segment rules from RFC 3986 §§6.2.2.1–6.2.2.3 and RFC 3987 §§5.3.2.1, 5.3.2.3–5.3.2.4, RFC 3987 §§3.1–3.2 URI/IRI representation transformation, RFC 5952 IPv6 text, RFC 9110 HTTP(S) port/path forms, RFC 6455 WS(S) port/resource-name forms, and RFC 8141 scheme/NID/percent-triplet normalization without NSS decoding or path reduction.
|
|
486
|
+
- RFC 3986 reference resolution and relative-reference generation receive no URN-specific semantics; RFC 8141 URN resolution services and URN-equivalence APIs are not implemented.
|
|
437
487
|
|
|
438
488
|
</details>
|
|
439
489
|
|
|
@@ -473,7 +523,7 @@ Run `gh workspace-data load` again to refresh materialized data after public-dat
|
|
|
473
523
|
|
|
474
524
|
### Tests
|
|
475
525
|
|
|
476
|
-
The active suite contains 3,
|
|
526
|
+
The active suite contains 3,157 tests covering URI/IRI validation and parsing, RFC 8141 URN syntax and normalization, generic normalization, bidirectional URI/IRI representation transformation, scheme-specific hosts, IPv4, IPv6, IPvFuture, ports, UUIDs, RFC 3986 resolution examples, empty components, absolute conversion, and relative-reference round trips, including 2,646 generated combinations of target/base paths, query-presence states, and target-fragment states across equivalent URI and IRI families.
|
|
477
527
|
|
|
478
528
|
<details>
|
|
479
529
|
<summary><strong>Test details</strong></summary>
|
|
@@ -493,7 +543,7 @@ The suite uses the `node:test` module built into Node.js and requires no separat
|
|
|
493
543
|
|
|
494
544
|
### Benchmarks
|
|
495
545
|
|
|
496
|
-
The materialized benchmark suite provides portable, version-aware measurements for every exported function
|
|
546
|
+
The 26-scenario materialized benchmark suite provides portable, version-aware measurements for every exported function, isolated package loading, and RFC 8141 URN validation, parsing, and normalization. It reports initial-call behavior, warmed latency statistics, integer throughput, workload counts, representative inputs, and environment metadata.
|
|
497
547
|
|
|
498
548
|
<details>
|
|
499
549
|
<summary><strong>Benchmark details</strong></summary>
|
|
@@ -524,6 +574,7 @@ The generic coordinator delegates version-layer selection and ordered concern di
|
|
|
524
574
|
|
|
525
575
|
- [RFC 3986 — Uniform Resource Identifier: Generic Syntax](https://www.rfc-editor.org/rfc/rfc3986)
|
|
526
576
|
- [RFC 3987 — Internationalized Resource Identifiers](https://www.rfc-editor.org/rfc/rfc3987)
|
|
577
|
+
- [RFC 8141 — Uniform Resource Names](https://www.rfc-editor.org/rfc/rfc8141)
|
|
527
578
|
- [RFC 9110 — HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110)
|
|
528
579
|
- [RFC 6455 — The WebSocket Protocol](https://www.rfc-editor.org/rfc/rfc6455)
|
|
529
580
|
- [RFC 8089 — The `file` URI Scheme](https://www.rfc-editor.org/rfc/rfc8089)
|