identifier-js 0.0.12 → 0.0.14
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.js +26 -8
- package/normalization.md +299 -0
- package/package.json +41 -41
- package/readme.md +449 -110
- package/tests/resolve.test.js +21 -0
- package/tests/to-relative.test.js +23 -0
- package/tests/validation-ip-port.test.js +32 -0
package/index.js
CHANGED
|
@@ -140,10 +140,10 @@ const validate = (string, rule) => {
|
|
|
140
140
|
function compose(parts = {}) {
|
|
141
141
|
let result = '';
|
|
142
142
|
if (parts.scheme) result += parts.scheme + ':';
|
|
143
|
-
if (parts.authority) result += '//' + parts.authority;
|
|
144
|
-
result += parts.path
|
|
145
|
-
if (parts.query) result += '?' + parts.query;
|
|
146
|
-
if (parts.fragment) result += '#' + parts.fragment;
|
|
143
|
+
if (parts.authority !== undefined && parts.authority !== null) result += '//' + parts.authority;
|
|
144
|
+
result += parts.path ?? '';
|
|
145
|
+
if (parts.query !== undefined && parts.query !== null) result += '?' + parts.query;
|
|
146
|
+
if (parts.fragment !== undefined && parts.fragment !== null) result += '#' + parts.fragment;
|
|
147
147
|
return result;
|
|
148
148
|
}
|
|
149
149
|
// remove dot segments algorithm per RFC 3986 Section 5.2.4 (loop and replace)
|
|
@@ -251,21 +251,40 @@ const toRelativeReference = (target, base) => {
|
|
|
251
251
|
if (T.scheme !== B.scheme || T.authority !== B.authority) return target;
|
|
252
252
|
let result;
|
|
253
253
|
if (B.path === T.path) {
|
|
254
|
-
|
|
254
|
+
if (T.query === undefined && B.query !== undefined) {
|
|
255
|
+
// Use an explicit path to prevent the base query from being inherited.
|
|
256
|
+
if (T.path.startsWith('/')) result = T.path;
|
|
257
|
+
else if (T.path) {
|
|
258
|
+
const segment = T.path.slice(T.path.lastIndexOf('/') + 1);
|
|
259
|
+
result = segment && !segment.includes(':') ? segment : `./${segment}`;
|
|
260
|
+
} else if (T.authority !== undefined) result = `//${T.authority}`;
|
|
261
|
+
else return target;
|
|
262
|
+
} else result = '';
|
|
263
|
+
} else if (!T.path) {
|
|
264
|
+
// A network-path reference is required to represent an empty path without inheritance.
|
|
265
|
+
if (T.authority !== undefined) result = `//${T.authority}`;
|
|
266
|
+
else return target;
|
|
255
267
|
} else {
|
|
256
268
|
const baseSegments = B.path.split('/');
|
|
257
269
|
const targetSegments = T.path.split('/');
|
|
258
270
|
let position = 0;
|
|
271
|
+
// Find the common path prefix before constructing the relative traversal.
|
|
259
272
|
while (baseSegments[position] === targetSegments[position] && position < baseSegments.length - 1 && position < targetSegments.length - 1) {
|
|
260
273
|
position++;
|
|
261
274
|
}
|
|
262
275
|
const segments = [];
|
|
276
|
+
// Backtrack from the base resource to the common path prefix.
|
|
263
277
|
for (let index = position + 1; index < baseSegments.length; index++) segments.push('..');
|
|
278
|
+
// Append the target path after the common prefix.
|
|
264
279
|
for (let index = position; index < targetSegments.length; index++) segments.push(targetSegments[index]);
|
|
265
280
|
result = segments.join('/');
|
|
281
|
+
if (!result) result = T.path.startsWith('/') ? T.path : './';
|
|
282
|
+
else if (/^[^/]*:/.test(result)) result = './' + result;
|
|
266
283
|
}
|
|
267
284
|
if (T.query !== undefined) result += `?${T.query}`;
|
|
268
285
|
if (T.fragment !== undefined) result += `#${T.fragment}`;
|
|
286
|
+
// Parent traversal would convert a rootless path into an absolute path during resolution.
|
|
287
|
+
if (T.authority === undefined && !T.path.startsWith('/') && result.startsWith('..')) return target;
|
|
269
288
|
return result;
|
|
270
289
|
};
|
|
271
290
|
// export
|
|
@@ -285,8 +304,7 @@ module.exports = {
|
|
|
285
304
|
parseIriReference: (string) => parse(string, 'IRI_reference'),
|
|
286
305
|
parseAbsoluteIri: (string) => parse(string, 'absolute_IRI'),
|
|
287
306
|
resolveReference,
|
|
288
|
-
normalizeReference: (string) => string, // not done yet
|
|
289
|
-
toAbsoluteReference: (string) => resolveReference('', string),
|
|
290
307
|
toRelativeReference,
|
|
308
|
+
toAbsoluteReference: (string) => resolveReference('', string),
|
|
309
|
+
normalizeReference: (string) => string, // not done yet
|
|
291
310
|
};
|
|
292
|
-
|
package/normalization.md
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# Identifier normalization research notes
|
|
2
|
+
|
|
3
|
+
These notes summarize the standards and design choices that should be resolved before implementing `normalizeReference`. They are research material, not the current API contract.
|
|
4
|
+
|
|
5
|
+
## Central constraint
|
|
6
|
+
|
|
7
|
+
There is no universal canonical form for every URI or IRI. RFC 3986 defines comparison in levels because a transformation that is safe for one scheme or application can merge distinct identifiers in another.
|
|
8
|
+
|
|
9
|
+
A normalizer should minimize false negatives without creating false positives:
|
|
10
|
+
|
|
11
|
+
1. **Simple comparison** — compare characters exactly.
|
|
12
|
+
2. **Syntax-based normalization** — apply transformations licensed by generic URI syntax.
|
|
13
|
+
3. **Scheme-based normalization** — apply additional equivalences defined by a scheme.
|
|
14
|
+
4. **Protocol-based normalization** — use equivalences established by observed protocol behavior.
|
|
15
|
+
|
|
16
|
+
`normalizeReference` should implement only an explicitly selected level. Protocol-derived normalization does not belong in a deterministic identifier library.
|
|
17
|
+
|
|
18
|
+
Reference: [RFC 3986 §6](https://www.rfc-editor.org/rfc/rfc3986#section-6).
|
|
19
|
+
|
|
20
|
+
## Generic syntax-based normalization
|
|
21
|
+
|
|
22
|
+
The following transformations are suitable candidates for a generic URI profile.
|
|
23
|
+
|
|
24
|
+
### Scheme and host case
|
|
25
|
+
|
|
26
|
+
- Lowercase the scheme.
|
|
27
|
+
- Lowercase the host.
|
|
28
|
+
- Do not lowercase userinfo, path, query, or fragment.
|
|
29
|
+
- Preserve Unicode component case unless a scheme or external policy defines equivalence.
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
HTTP://WWW.EXAMPLE.COM/ → http://www.example.com/
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Reference: RFC 3986 §6.2.2.1.
|
|
38
|
+
|
|
39
|
+
### Percent-triplet case
|
|
40
|
+
|
|
41
|
+
Uppercase hexadecimal letters in every valid percent triplet:
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
%3a → %3A
|
|
45
|
+
%2f → %2F
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
This changes presentation, not the represented octet.
|
|
49
|
+
|
|
50
|
+
Reference: RFC 3986 §§2.1 and 6.2.2.1.
|
|
51
|
+
|
|
52
|
+
### Decode percent-encoded unreserved characters
|
|
53
|
+
|
|
54
|
+
Decode a percent triplet only when it represents an ASCII unreserved character:
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
ALPHA / DIGIT / "-" / "." / "_" / "~"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Examples:
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
%63 → c
|
|
64
|
+
%7E → ~
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Do not generically decode reserved characters. `%2F` and `/`, for example, can have different structural meanings.
|
|
68
|
+
|
|
69
|
+
Parse components before decoding so an encoded delimiter cannot be mistaken for syntax. Never decode the same data twice.
|
|
70
|
+
|
|
71
|
+
References: RFC 3986 §§2.2–2.4 and 6.2.2.2.
|
|
72
|
+
|
|
73
|
+
### Remove dot segments
|
|
74
|
+
|
|
75
|
+
Apply the RFC 3986 §5.2.4 algorithm to the parsed path:
|
|
76
|
+
|
|
77
|
+
```text
|
|
78
|
+
/a/b/c/./../../g → /a/g
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Only complete `.` and `..` path segments are special. Do not process similar text in a query or fragment:
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
g?y/../x
|
|
85
|
+
g#s/../x
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
References: RFC 3986 §§5.2.4 and 6.2.2.3.
|
|
89
|
+
|
|
90
|
+
### Preserve component presence
|
|
91
|
+
|
|
92
|
+
Recomposition must distinguish an absent component from a present but empty component:
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
https://example.com/path
|
|
96
|
+
https://example.com/path?
|
|
97
|
+
https://example.com/path#
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The same applies to an empty authority. Component delimiters must be emitted based on definedness, not truthiness.
|
|
101
|
+
|
|
102
|
+
Reference: RFC 3986 §5.3.
|
|
103
|
+
|
|
104
|
+
## Scheme-based profiles
|
|
105
|
+
|
|
106
|
+
Scheme-specific transformations should not run unless the scheme profile is selected or automatic scheme handling is part of the documented contract.
|
|
107
|
+
|
|
108
|
+
### HTTP and HTTPS
|
|
109
|
+
|
|
110
|
+
RFC 9110 permits these normal forms:
|
|
111
|
+
|
|
112
|
+
- lowercase scheme and host;
|
|
113
|
+
- remove port `80` from `http`;
|
|
114
|
+
- remove port `443` from `https`;
|
|
115
|
+
- remove an explicitly empty port;
|
|
116
|
+
- use `/` when authority is present and path is empty;
|
|
117
|
+
- decode percent-encoded unreserved characters;
|
|
118
|
+
- preserve all other component case.
|
|
119
|
+
|
|
120
|
+
Examples:
|
|
121
|
+
|
|
122
|
+
```text
|
|
123
|
+
HTTP://Example.COM:80 → http://example.com/
|
|
124
|
+
https://example.com:443/a → https://example.com/a
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
An HTTP request target does not include a fragment, but identifier normalization should not silently discard a fragment unless the selected operation specifically produces a request target.
|
|
128
|
+
|
|
129
|
+
Userinfo in HTTP and HTTPS targets is deprecated and should be rejected or reported by an HTTP policy rather than silently normalized away.
|
|
130
|
+
|
|
131
|
+
Reference: [RFC 9110 §§4.2.1–4.2.5](https://www.rfc-editor.org/rfc/rfc9110#section-4.2).
|
|
132
|
+
|
|
133
|
+
### WS and WSS
|
|
134
|
+
|
|
135
|
+
Potential scheme-specific rules include:
|
|
136
|
+
|
|
137
|
+
- default port `80` for `ws`;
|
|
138
|
+
- default port `443` for `wss`;
|
|
139
|
+
- `/` as the resource path when the path is empty;
|
|
140
|
+
- no fragment identifiers;
|
|
141
|
+
- IDN-to-ASCII handling under an explicitly selected hostname policy.
|
|
142
|
+
|
|
143
|
+
Fragment rejection is validation, not normalization. A normalizer should not repair a WebSocket URI by silently deleting its fragment.
|
|
144
|
+
|
|
145
|
+
Reference: [RFC 6455 §3](https://www.rfc-editor.org/rfc/rfc6455#section-3).
|
|
146
|
+
|
|
147
|
+
### File
|
|
148
|
+
|
|
149
|
+
A generic `file` normalizer is not recommended. Behavior varies by platform and filesystem:
|
|
150
|
+
|
|
151
|
+
- local empty authority versus `localhost`;
|
|
152
|
+
- POSIX roots;
|
|
153
|
+
- Windows drive-letter case and UNC paths;
|
|
154
|
+
- path case sensitivity;
|
|
155
|
+
- backslash handling;
|
|
156
|
+
- platform-specific Unicode normalization;
|
|
157
|
+
- reserved device names and namespace paths.
|
|
158
|
+
|
|
159
|
+
Require an explicit platform profile before applying these transformations.
|
|
160
|
+
|
|
161
|
+
Reference: [RFC 8089](https://www.rfc-editor.org/rfc/rfc8089).
|
|
162
|
+
|
|
163
|
+
### Other schemes
|
|
164
|
+
|
|
165
|
+
Apply only generic syntax normalization unless the scheme's authoritative specification defines additional equivalences. Do not infer default ports, path case rules, or authority behavior from a similar scheme.
|
|
166
|
+
|
|
167
|
+
## IRI and Unicode decisions
|
|
168
|
+
|
|
169
|
+
RFC 3987 recommends that creators produce IRIs in NFC, but comparison code must not arbitrarily normalize an existing Unicode IRI. Normalizing third-party text can merge identifiers that were intentionally distinct.
|
|
170
|
+
|
|
171
|
+
Recommended policy:
|
|
172
|
+
|
|
173
|
+
- do not apply Unicode normalization by default;
|
|
174
|
+
- offer NFC only as an explicit creation or application-policy option;
|
|
175
|
+
- do not offer NFKC as a generic identifier transformation;
|
|
176
|
+
- retain the original IRI when a normalized form is generated only as a comparison key.
|
|
177
|
+
|
|
178
|
+
### IRI-to-URI mapping is a separate operation
|
|
179
|
+
|
|
180
|
+
Mapping an IRI to a URI is not merely normalization:
|
|
181
|
+
|
|
182
|
+
1. encode non-ASCII `ucschar` and `iprivate` characters as UTF-8;
|
|
183
|
+
2. percent-encode each UTF-8 octet as `%HH`;
|
|
184
|
+
3. preserve existing valid percent triplets and URI-allowed characters;
|
|
185
|
+
4. apply an explicit IDNA policy to internationalized hostnames when required.
|
|
186
|
+
|
|
187
|
+
The reverse operation must decode only valid UTF-8 and must preserve encoded reserved characters. It must not guess legacy encodings.
|
|
188
|
+
|
|
189
|
+
Reference: [RFC 3987 §§3 and 5](https://www.rfc-editor.org/rfc/rfc3987).
|
|
190
|
+
|
|
191
|
+
## Transformations excluded by default
|
|
192
|
+
|
|
193
|
+
A generic normalizer should not:
|
|
194
|
+
|
|
195
|
+
- decode percent-encoded reserved characters;
|
|
196
|
+
- lowercase userinfo, paths, queries, or fragments;
|
|
197
|
+
- sort, deduplicate, or reinterpret query parameters;
|
|
198
|
+
- remove empty query or fragment delimiters;
|
|
199
|
+
- add or remove trailing slashes without scheme authority;
|
|
200
|
+
- apply Unicode NFC or NFKC automatically;
|
|
201
|
+
- convert an internationalized hostname without a defined IDNA version and policy;
|
|
202
|
+
- remove userinfo rather than reporting it;
|
|
203
|
+
- infer filesystem semantics for `file`;
|
|
204
|
+
- infer equivalence from redirects or successful retrievals;
|
|
205
|
+
- convert a relative reference without a supplied base.
|
|
206
|
+
|
|
207
|
+
## Suggested API design
|
|
208
|
+
|
|
209
|
+
Avoid a single aggressive operation. Two viable designs are:
|
|
210
|
+
|
|
211
|
+
### Explicit profiles
|
|
212
|
+
|
|
213
|
+
```js
|
|
214
|
+
normalizeReference(reference, {
|
|
215
|
+
profile: 'generic', // generic | http | https | ws | wss
|
|
216
|
+
unicodeNormalization: false,
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
A `file` profile should additionally require a platform policy.
|
|
221
|
+
|
|
222
|
+
### Separate operations
|
|
223
|
+
|
|
224
|
+
```js
|
|
225
|
+
normalizeReference(reference); // generic syntax only
|
|
226
|
+
normalizeHttpReference(reference); // HTTP/HTTPS scheme rules
|
|
227
|
+
normalizeWebSocketReference(reference); // WS/WSS scheme rules
|
|
228
|
+
iriToUri(reference, options); // explicit representation mapping
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Separate operations are harder to misuse and make compatibility changes more visible. A profile-based API is easier to extend. The final choice should follow the expected consumers.
|
|
232
|
+
|
|
233
|
+
## Suggested generic processing order
|
|
234
|
+
|
|
235
|
+
1. Parse and retain whether authority, query, and fragment are absent or empty.
|
|
236
|
+
2. Lowercase scheme and host where generic syntax permits it.
|
|
237
|
+
3. Normalize percent-triplet hexadecimal case.
|
|
238
|
+
4. Decode percent-encoded ASCII unreserved characters component by component.
|
|
239
|
+
5. Remove dot segments from the path.
|
|
240
|
+
6. Apply an explicitly selected scheme profile.
|
|
241
|
+
7. Apply Unicode normalization only when explicitly requested.
|
|
242
|
+
8. Recompose while preserving empty components.
|
|
243
|
+
9. Validate the normalized output under the same identifier and scheme policy.
|
|
244
|
+
10. Optionally verify idempotence:
|
|
245
|
+
|
|
246
|
+
```js
|
|
247
|
+
normalizeReference(normalizeReference(value)) === normalizeReference(value)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Acceptance scenarios
|
|
251
|
+
|
|
252
|
+
A generic profile should include at least these cases:
|
|
253
|
+
|
|
254
|
+
```text
|
|
255
|
+
HTTP://Example.COM/%7euser → http://example.com/~user
|
|
256
|
+
http://example.com/a/./b/../c → http://example.com/a/c
|
|
257
|
+
http://example.com/path? → http://example.com/path?
|
|
258
|
+
http://example.com/path# → http://example.com/path#
|
|
259
|
+
http://example.com/%2F → http://example.com/%2F
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
An HTTP profile can additionally include:
|
|
263
|
+
|
|
264
|
+
```text
|
|
265
|
+
http://example.com:80 → http://example.com/
|
|
266
|
+
https://example.com:443/a → https://example.com/a
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Cases that must remain distinct under generic normalization include:
|
|
270
|
+
|
|
271
|
+
```text
|
|
272
|
+
http://example.com/a ≠ http://example.com/a/
|
|
273
|
+
http://example.com/path ≠ http://example.com/path?
|
|
274
|
+
http://example.com/%2F ≠ http://example.com//
|
|
275
|
+
http://example.com/A ≠ http://example.com/a
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Testing should cover URI and IRI forms, empty components, rootless paths, percent triplets, Unicode supplementary characters, idempotence, and normalization followed by parsing/recomposition.
|
|
279
|
+
|
|
280
|
+
## Open decisions
|
|
281
|
+
|
|
282
|
+
Before implementation, decide:
|
|
283
|
+
|
|
284
|
+
1. Whether `normalizeReference` is generic-only or selects profiles automatically by scheme.
|
|
285
|
+
2. Whether output preserves the input category: URI versus IRI.
|
|
286
|
+
3. Whether relative references are normalized in place or require a base and become absolute.
|
|
287
|
+
4. Whether Unicode NFC is offered, and for which creation contexts.
|
|
288
|
+
5. Whether IDNA conversion belongs here or in a dedicated hostname dependency.
|
|
289
|
+
6. Whether comparison keys and display/transport identifiers use separate APIs.
|
|
290
|
+
7. Whether unsupported scheme profiles throw, fall back to generic rules, or require an explicit option.
|
|
291
|
+
8. Whether current callers can rely on `normalizeReference` remaining an identity function until a major release.
|
|
292
|
+
|
|
293
|
+
## Authoritative references
|
|
294
|
+
|
|
295
|
+
- [RFC 3986 — Uniform Resource Identifier: Generic Syntax](https://www.rfc-editor.org/rfc/rfc3986)
|
|
296
|
+
- [RFC 3987 — Internationalized Resource Identifiers](https://www.rfc-editor.org/rfc/rfc3987)
|
|
297
|
+
- [RFC 9110 — HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110)
|
|
298
|
+
- [RFC 6455 — The WebSocket Protocol](https://www.rfc-editor.org/rfc/rfc6455)
|
|
299
|
+
- [RFC 8089 — The `file` URI Scheme](https://www.rfc-editor.org/rfc/rfc8089)
|
package/package.json
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "identifier-js",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "A RFC3986 / RFC3987 compliant fast parser/validator/resolver/composer for NodeJS and browser.",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"IRI",
|
|
7
|
-
"URI",
|
|
8
|
-
"IRI-reference",
|
|
9
|
-
"URI-reference",
|
|
10
|
-
"ipv4",
|
|
11
|
-
"ipv6",
|
|
12
|
-
"uuid",
|
|
13
|
-
"parser",
|
|
14
|
-
"validator",
|
|
15
|
-
"RFC3986",
|
|
16
|
-
"RFC3987"
|
|
17
|
-
],
|
|
18
|
-
"homepage": "https://github.com/SorinGFS/identifier-js#readme",
|
|
19
|
-
"bugs": {
|
|
20
|
-
"url": "https://github.com/SorinGFS/identifier-js/issues"
|
|
21
|
-
},
|
|
22
|
-
"repository": {
|
|
23
|
-
"type": "git",
|
|
24
|
-
"url": "git+https://github.com/SorinGFS/identifier-js.git"
|
|
25
|
-
},
|
|
26
|
-
"license": "MIT",
|
|
27
|
-
"author": "SorinGFS",
|
|
28
|
-
"type": "commonjs",
|
|
29
|
-
"main": "index.js",
|
|
30
|
-
"scripts": {
|
|
31
|
-
"update-deps": "npx npm-check-updates -u && npm install",
|
|
32
|
-
"test": "node tests/vitest-setup && npm test"
|
|
33
|
-
},
|
|
34
|
-
"dependencies": {
|
|
35
|
-
"url-templates": "^1.0.
|
|
36
|
-
},
|
|
37
|
-
"engines": {
|
|
38
|
-
"node": ">=18.0.0"
|
|
39
|
-
},
|
|
40
|
-
"engineStrict": true
|
|
41
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "identifier-js",
|
|
3
|
+
"version": "0.0.14",
|
|
4
|
+
"description": "A RFC3986 / RFC3987 compliant fast parser/validator/resolver/composer for NodeJS and browser.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"IRI",
|
|
7
|
+
"URI",
|
|
8
|
+
"IRI-reference",
|
|
9
|
+
"URI-reference",
|
|
10
|
+
"ipv4",
|
|
11
|
+
"ipv6",
|
|
12
|
+
"uuid",
|
|
13
|
+
"parser",
|
|
14
|
+
"validator",
|
|
15
|
+
"RFC3986",
|
|
16
|
+
"RFC3987"
|
|
17
|
+
],
|
|
18
|
+
"homepage": "https://github.com/SorinGFS/identifier-js#readme",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/SorinGFS/identifier-js/issues"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/SorinGFS/identifier-js.git"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"author": "SorinGFS",
|
|
28
|
+
"type": "commonjs",
|
|
29
|
+
"main": "index.js",
|
|
30
|
+
"scripts": {
|
|
31
|
+
"update-deps": "npx npm-check-updates -u && npm install",
|
|
32
|
+
"test": "node tests/vitest-setup && npm test"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"url-templates": "^1.0.5"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18.0.0"
|
|
39
|
+
},
|
|
40
|
+
"engineStrict": true
|
|
41
|
+
}
|
package/readme.md
CHANGED
|
@@ -1,110 +1,449 @@
|
|
|
1
|
-
---
|
|
2
|
-
|
|
3
|
-
title: Identifier JS
|
|
4
|
-
|
|
5
|
-
description:
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
1
|
+
---
|
|
2
|
+
|
|
3
|
+
title: Identifier JS
|
|
4
|
+
|
|
5
|
+
description: An RFC 3986 and RFC 3987 parser, validator, and reference resolver for Node.js and browser bundles.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Identifier JS
|
|
10
|
+
|
|
11
|
+
`identifier-js` is a fully RFC [3986](https://www.rfc-editor.org/rfc/rfc3986) and RFC [3987](https://www.rfc-editor.org/rfc/rfc3987) compliant URI/IRI parser, validator, resolver, and composer. It provides:
|
|
12
|
+
|
|
13
|
+
- URI and IRI validation;
|
|
14
|
+
- parsed identifier components;
|
|
15
|
+
- RFC 3986 reference resolution and dot-segment removal;
|
|
16
|
+
- relative-reference generation with round-trip guarantees for supported forms;
|
|
17
|
+
- UUID and UUIDv4 lexical validation;
|
|
18
|
+
- lazily compiled and cached regular expressions.
|
|
19
|
+
|
|
20
|
+
The package is synchronous, CommonJS, and supports Node.js 18 or newer. Browser use requires a bundler or runtime that supports CommonJS dependencies and Unicode regular expressions.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash title="console"
|
|
25
|
+
npm install identifier-js
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## API
|
|
29
|
+
|
|
30
|
+
Every validator returns `true` or throws at the first detected violation. Parsing and reference operations also throw when their input does not satisfy the required grammar.
|
|
31
|
+
|
|
32
|
+
### Validate URI syntax
|
|
33
|
+
|
|
34
|
+
Validate a complete URI, a URI reference, or an absolute URI without a fragment.
|
|
35
|
+
|
|
36
|
+
<details>
|
|
37
|
+
<summary><strong>API and examples</strong></summary>
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
isUri(value: string): true
|
|
41
|
+
isUriReference(value: string): true
|
|
42
|
+
isAbsoluteUri(value: string): true
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
const { isUri, isUriReference, isAbsoluteUri } = require('identifier-js');
|
|
47
|
+
|
|
48
|
+
console.log(isUri('https://example.com/path?query#fragment')); // true
|
|
49
|
+
console.log(isUriReference('../asset?version=2')); // true
|
|
50
|
+
console.log(isAbsoluteUri('https://example.com/path?query')); // true
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
An `absolute-URI` is the fragment-free grammar defined by RFC 3986. Use `isUri` when a fragment is allowed.
|
|
54
|
+
|
|
55
|
+
</details>
|
|
56
|
+
|
|
57
|
+
### Parse URI components
|
|
58
|
+
|
|
59
|
+
Parse URI syntax into scheme, authority, userinfo, host, port, path, query, and fragment components.
|
|
60
|
+
|
|
61
|
+
<details>
|
|
62
|
+
<summary><strong>API and examples</strong></summary>
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
parseUri(value: string): IdentifierComponents
|
|
66
|
+
parseUriReference(value: string): RelativeIdentifierComponents
|
|
67
|
+
parseAbsoluteUri(value: string): AbsoluteIdentifierComponents
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
const { parseUri } = require('identifier-js');
|
|
72
|
+
|
|
73
|
+
console.log(parseUri('https://user@example.com:8443/a?b#c'));
|
|
74
|
+
// {
|
|
75
|
+
// scheme: 'https',
|
|
76
|
+
// authority: 'user@example.com:8443',
|
|
77
|
+
// userinfo: 'user',
|
|
78
|
+
// host: 'example.com',
|
|
79
|
+
// port: '8443',
|
|
80
|
+
// path: '/a',
|
|
81
|
+
// query: 'b',
|
|
82
|
+
// fragment: 'c'
|
|
83
|
+
// }
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Absent optional components are returned as `undefined`. Present but empty query and fragment components are returned as empty strings.
|
|
87
|
+
|
|
88
|
+
</details>
|
|
89
|
+
|
|
90
|
+
### Validate IRI syntax
|
|
91
|
+
|
|
92
|
+
Validate Unicode-capable identifiers using RFC 3987 grammar.
|
|
93
|
+
|
|
94
|
+
<details>
|
|
95
|
+
<summary><strong>API and examples</strong></summary>
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
isIri(value: string): true
|
|
99
|
+
isIriReference(value: string): true
|
|
100
|
+
isAbsoluteIri(value: string): true
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
const { isIri, isIriReference } = require('identifier-js');
|
|
105
|
+
|
|
106
|
+
console.log(isIri('https://例え.テスト/資料?項目=値#概要')); // true
|
|
107
|
+
console.log(isIriReference('../résumé')); // true
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
IRI support permits RFC 3987 Unicode ranges in applicable components. Complete IDNA processing and conversion to a transport URI are separate responsibilities.
|
|
111
|
+
|
|
112
|
+
</details>
|
|
113
|
+
|
|
114
|
+
### Parse IRI components
|
|
115
|
+
|
|
116
|
+
Parse an IRI while preserving its Unicode component values.
|
|
117
|
+
|
|
118
|
+
<details>
|
|
119
|
+
<summary><strong>API and examples</strong></summary>
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
parseIri(value: string): IdentifierComponents
|
|
123
|
+
parseIriReference(value: string): RelativeIdentifierComponents
|
|
124
|
+
parseAbsoluteIri(value: string): AbsoluteIdentifierComponents
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
```js
|
|
128
|
+
const { parseIri } = require('identifier-js');
|
|
129
|
+
|
|
130
|
+
console.log(parseIri('https://usér@例え.テスト:8443/résumé?lang=fr#profil'));
|
|
131
|
+
// {
|
|
132
|
+
// scheme: 'https',
|
|
133
|
+
// authority: 'usér@例え.テスト:8443',
|
|
134
|
+
// userinfo: 'usér',
|
|
135
|
+
// host: '例え.テスト',
|
|
136
|
+
// port: '8443',
|
|
137
|
+
// path: '/résumé',
|
|
138
|
+
// query: 'lang=fr',
|
|
139
|
+
// fragment: 'profil'
|
|
140
|
+
// }
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
</details>
|
|
144
|
+
|
|
145
|
+
### Resolve a reference
|
|
146
|
+
|
|
147
|
+
Resolve a URI or IRI reference against an absolute base using RFC 3986 §5.
|
|
148
|
+
|
|
149
|
+
<details>
|
|
150
|
+
<summary><strong>API and examples</strong></summary>
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
resolveReference(
|
|
154
|
+
reference: string,
|
|
155
|
+
base: string,
|
|
156
|
+
strict?: boolean,
|
|
157
|
+
returnParts?: boolean
|
|
158
|
+
): string | Record<string, string | undefined>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
```js
|
|
162
|
+
const { resolveReference } = require('identifier-js');
|
|
163
|
+
|
|
164
|
+
console.log(resolveReference('../images/logo.svg', 'https://example.com/docs/api/page'));
|
|
165
|
+
// https://example.com/docs/images/logo.svg
|
|
166
|
+
|
|
167
|
+
console.log(resolveReference('?page=2', 'https://example.com/items?page=1#current'));
|
|
168
|
+
// https://example.com/items?page=2
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`strict` defaults to `true`. In strict mode, a reference containing a scheme replaces the base identifier even when both schemes are equal. `returnParts` defaults to `false`; when enabled at runtime, the function returns the resolved component object.
|
|
172
|
+
|
|
173
|
+
Empty authorities, queries, and fragments are preserved during recomposition.
|
|
174
|
+
|
|
175
|
+
</details>
|
|
176
|
+
|
|
177
|
+
### Produce absolute and relative forms
|
|
178
|
+
|
|
179
|
+
Remove a base fragment or derive a relative reference that resolves back to a target.
|
|
180
|
+
|
|
181
|
+
<details>
|
|
182
|
+
<summary><strong>API and examples</strong></summary>
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
toAbsoluteReference(reference: string): string
|
|
186
|
+
toRelativeReference(target: string, base: string): string
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
```js
|
|
190
|
+
const { toAbsoluteReference, toRelativeReference } = require('identifier-js');
|
|
191
|
+
|
|
192
|
+
console.log(toAbsoluteReference('https://example.com/a/../b#section'));
|
|
193
|
+
// https://example.com/b
|
|
194
|
+
|
|
195
|
+
const target = 'https://example.com/docs/images/logo.svg';
|
|
196
|
+
const base = 'https://example.com/docs/api/page';
|
|
197
|
+
const relative = toRelativeReference(target, base);
|
|
198
|
+
console.log(relative); // ../images/logo.svg
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
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.
|
|
202
|
+
|
|
203
|
+
</details>
|
|
204
|
+
|
|
205
|
+
### Normalization status
|
|
206
|
+
|
|
207
|
+
`normalizeReference` is reserved for future normalization policy and currently returns its input unchanged.
|
|
208
|
+
|
|
209
|
+
<details>
|
|
210
|
+
<summary><strong>Current behavior and research</strong></summary>
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
normalizeReference(reference: string): string
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
```js
|
|
217
|
+
const { normalizeReference } = require('identifier-js');
|
|
218
|
+
|
|
219
|
+
console.log(normalizeReference('HTTP://Example.COM/a/../b'));
|
|
220
|
+
// HTTP://Example.COM/a/../b
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
URI/IRI normalization has multiple standards-defined levels and scheme-specific tradeoffs. See [`normalization.md`](normalization.md) for implementation options, unsafe transformations, suggested profiles, and acceptance scenarios.
|
|
224
|
+
|
|
225
|
+
</details>
|
|
226
|
+
|
|
227
|
+
### Validate UUID text
|
|
228
|
+
|
|
229
|
+
Validate the canonical UUID text shape or the stricter UUIDv4 version and variant fields.
|
|
230
|
+
|
|
231
|
+
<details>
|
|
232
|
+
<summary><strong>API and examples</strong></summary>
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
isUUID(value: string): true
|
|
236
|
+
isUUIDv4(value: string): true
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
```js
|
|
240
|
+
const { isUUID, isUUIDv4 } = require('identifier-js');
|
|
241
|
+
|
|
242
|
+
console.log(isUUID('99c17cbb-656f-564a-940f-1a4568f03487')); // true
|
|
243
|
+
console.log(isUUIDv4('123e4567-e89b-42d3-9456-426614174000')); // true
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
`isUUID` validates the `8-4-4-4-12` hexadecimal layout without restricting the version or variant fields. `isUUIDv4` requires version `4` and the RFC variant nibble `8`, `9`, `a`, or `b`.
|
|
247
|
+
|
|
248
|
+
</details>
|
|
249
|
+
|
|
250
|
+
## Processing model
|
|
251
|
+
|
|
252
|
+
The parser builds its validation logic from declarative RFC grammar fragments:
|
|
253
|
+
|
|
254
|
+
1. Select generic URI/IRI rules or the package's scheme-specific hostname policy.
|
|
255
|
+
2. Recursively expand grammar references through `url-templates`.
|
|
256
|
+
3. Add named capture groups when parsing is requested.
|
|
257
|
+
4. Compile the complete expression with Unicode support.
|
|
258
|
+
5. Cache the expression by operation, grammar rule, and scheme-policy class.
|
|
259
|
+
6. Validate with `RegExp.test()` or parse with `RegExp.exec()`.
|
|
260
|
+
7. Resolve references by component inheritance, path merging, dot-segment removal, and definedness-preserving recomposition.
|
|
261
|
+
|
|
262
|
+
<details>
|
|
263
|
+
<summary><strong>Lazy compilation and cache behavior</strong></summary>
|
|
264
|
+
|
|
265
|
+
Parsing and validation use separate cached expressions because parsing requires named groups and validation does not. Generic and scheme-specific identifiers also use separate entries.
|
|
266
|
+
|
|
267
|
+
The first call for an operation/rule/policy combination includes recursive grammar expansion and regular-expression compilation. Later calls reuse the cached expression and are considerably faster. No regular expressions are generated during package import.
|
|
268
|
+
|
|
269
|
+
</details>
|
|
270
|
+
|
|
271
|
+
<details>
|
|
272
|
+
<summary><strong>Scheme-specific hostname policy</strong></summary>
|
|
273
|
+
|
|
274
|
+
The following schemes trigger DNS-style ASCII or Unicode label rules instead of the fully generic `reg-name` grammar:
|
|
275
|
+
|
|
276
|
+
- `http`
|
|
277
|
+
- `https`
|
|
278
|
+
- `ws`
|
|
279
|
+
- `wss`
|
|
280
|
+
- `file`
|
|
281
|
+
|
|
282
|
+
Matching is case-insensitive. Other valid schemes use generic RFC 3986/3987 registered-name syntax.
|
|
283
|
+
|
|
284
|
+
This policy validates label shape and selected Unicode character classes. It is not complete IDNA processing and does not replace normalization, contextual, bidi, registry, or Punycode validation.
|
|
285
|
+
|
|
286
|
+
</details>
|
|
287
|
+
|
|
288
|
+
## Real-world use cases
|
|
289
|
+
|
|
290
|
+
### Follow HTTP redirects and resource locations
|
|
291
|
+
|
|
292
|
+
HTTP `Location` values are URI references and can be relative to the original target URI.
|
|
293
|
+
|
|
294
|
+
<details>
|
|
295
|
+
<summary><strong>Example and context</strong></summary>
|
|
296
|
+
|
|
297
|
+
```js
|
|
298
|
+
const { resolveReference } = require('identifier-js');
|
|
299
|
+
|
|
300
|
+
const requestUrl = 'https://example.com/account/profile';
|
|
301
|
+
const location = '../login?return=profile';
|
|
302
|
+
console.log(resolveReference(location, requestUrl));
|
|
303
|
+
// https://example.com/login?return=profile
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
RFC 9110 uses URI references in `Location`, `Content-Location`, and `Referer`. Correct resolution requires component parsing, path merging, query inheritance rules, and dot-segment removal.
|
|
307
|
+
|
|
308
|
+
</details>
|
|
309
|
+
|
|
310
|
+
### Keep document trees portable
|
|
311
|
+
|
|
312
|
+
Relative references let documents and assets move together without rewriting every internal link.
|
|
313
|
+
|
|
314
|
+
<details>
|
|
315
|
+
<summary><strong>Example and context</strong></summary>
|
|
316
|
+
|
|
317
|
+
```js
|
|
318
|
+
const { resolveReference, toRelativeReference } = require('identifier-js');
|
|
319
|
+
|
|
320
|
+
const documentUrl = 'https://example.com/manual/chapters/intro.html';
|
|
321
|
+
const imageUrl = 'https://example.com/manual/images/diagram.svg';
|
|
322
|
+
const relative = toRelativeReference(imageUrl, documentUrl);
|
|
323
|
+
console.log(relative); // ../images/diagram.svg
|
|
324
|
+
console.log(resolveReference(relative, documentUrl)); // original image URL
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
RFC 3986 identifies portable hypertext document trees as a central use for relative references.
|
|
328
|
+
|
|
329
|
+
</details>
|
|
330
|
+
|
|
331
|
+
### Process internationalized identifiers
|
|
332
|
+
|
|
333
|
+
IRI parsing preserves native-script host, path, query, and fragment text for interfaces and internationalized content.
|
|
334
|
+
|
|
335
|
+
<details>
|
|
336
|
+
<summary><strong>Example and context</strong></summary>
|
|
337
|
+
|
|
338
|
+
```js
|
|
339
|
+
const { parseIri } = require('identifier-js');
|
|
340
|
+
|
|
341
|
+
const parts = parseIri('https://例え.テスト/検索?q=資料#結果');
|
|
342
|
+
console.log(parts.host); // 例え.テスト
|
|
343
|
+
console.log(parts.path); // /検索
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
RFC 3987 uses IRIs for internationalized identification while requiring mapping to URIs when a protocol accepts only URI syntax. Cache lookup, browser history, XML identifiers, and indexing are cited comparison contexts.
|
|
347
|
+
|
|
348
|
+
</details>
|
|
349
|
+
|
|
350
|
+
### Validate and route protocol identifiers
|
|
351
|
+
|
|
352
|
+
Component parsing supports policy decisions without unsafe string splitting.
|
|
353
|
+
|
|
354
|
+
<details>
|
|
355
|
+
<summary><strong>Example and context</strong></summary>
|
|
356
|
+
|
|
357
|
+
```js
|
|
358
|
+
const { parseUri } = require('identifier-js');
|
|
359
|
+
|
|
360
|
+
const parts = parseUri('wss://example.com:8443/events?channel=updates');
|
|
361
|
+
console.log(parts.scheme); // wss
|
|
362
|
+
console.log(parts.host); // example.com
|
|
363
|
+
console.log(parts.port); // 8443
|
|
364
|
+
console.log(parts.path); // /events
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Applications can inspect scheme, authority, path, and query before selecting a connector, enforcing an allowlist, constructing an HTTP request target, or routing to a service. Protocol-specific security and semantic validation remains the application's responsibility.
|
|
368
|
+
|
|
369
|
+
</details>
|
|
370
|
+
|
|
371
|
+
### Validate externally supplied UUID text
|
|
372
|
+
|
|
373
|
+
Lexical UUID checks are useful at API, configuration, and storage boundaries.
|
|
374
|
+
|
|
375
|
+
<details>
|
|
376
|
+
<summary><strong>Example and context</strong></summary>
|
|
377
|
+
|
|
378
|
+
```js
|
|
379
|
+
const { isUUIDv4 } = require('identifier-js');
|
|
380
|
+
|
|
381
|
+
try {
|
|
382
|
+
isUUIDv4('123e4567-e89b-42d3-9456-426614174000');
|
|
383
|
+
console.log('valid UUIDv4');
|
|
384
|
+
} catch (error) {
|
|
385
|
+
console.error(error.message);
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
RFC 9562 lists database keys, filenames, system identifiers, and transaction identifiers among common UUID uses. UUID validation does not establish authorization, unpredictability, uniqueness, or safe use as a capability token.
|
|
390
|
+
|
|
391
|
+
</details>
|
|
392
|
+
|
|
393
|
+
## Intentional behavior and limitations
|
|
394
|
+
|
|
395
|
+
<details>
|
|
396
|
+
<summary><strong>Validation and parsing boundaries</strong></summary>
|
|
397
|
+
|
|
398
|
+
- Validators return `true` or throw; they do not return `false`.
|
|
399
|
+
- URI functions reject non-ASCII characters where RFC 3986 permits only URI syntax. Use the IRI functions for RFC 3987 Unicode ranges.
|
|
400
|
+
- `absolute-URI` and `absolute-IRI` exclude fragments by definition. The complete `URI` and `IRI` functions permit fragments.
|
|
401
|
+
- Scheme-specific processing currently specializes hostname syntax; it does not implement every protocol rule for HTTP, WebSocket, or `file` identifiers.
|
|
402
|
+
- Scheme-specific Unicode labels are not complete IDNA validation.
|
|
403
|
+
- Ports are restricted to an empty value or the numeric range 0–65535. Generic RFC 3986 syntax itself permits any sequence of digits.
|
|
404
|
+
- Generic registered names may contain syntax that DNS-style hostnames reject.
|
|
405
|
+
- Parsing separates components before any application-level percent decoding.
|
|
406
|
+
- The library does not perform network, DNS, filesystem, registry, or authorization checks.
|
|
407
|
+
|
|
408
|
+
</details>
|
|
409
|
+
|
|
410
|
+
<details>
|
|
411
|
+
<summary><strong>Resolution and conversion boundaries</strong></summary>
|
|
412
|
+
|
|
413
|
+
- Resolution uses IRI grammar, so Unicode references and bases are accepted.
|
|
414
|
+
- Empty authorities, queries, and fragments remain distinct from absent components.
|
|
415
|
+
- `strict = false` enables RFC 3986 backward-compatible handling when a reference repeats the base scheme.
|
|
416
|
+
- `toAbsoluteReference` requires an identifier containing a scheme and removes its fragment through empty-reference resolution.
|
|
417
|
+
- `toRelativeReference` compares scheme and authority text exactly; it does not normalize them first.
|
|
418
|
+
- `toRelativeReference` may return an absolute target when a rootless relative path cannot preserve identity.
|
|
419
|
+
- `normalizeReference` is intentionally an identity function until a normalization contract is selected.
|
|
420
|
+
|
|
421
|
+
</details>
|
|
422
|
+
|
|
423
|
+
<details>
|
|
424
|
+
<summary><strong>UUID boundaries</strong></summary>
|
|
425
|
+
|
|
426
|
+
- `isUUID` validates canonical hexadecimal layout only; it does not enforce a known version or the RFC variant.
|
|
427
|
+
- `isUUIDv4` validates the version and variant fields but does not assess random-number quality.
|
|
428
|
+
- A syntactically valid UUID is not proof of uniqueness, integrity, authenticity, or authorization.
|
|
429
|
+
|
|
430
|
+
</details>
|
|
431
|
+
|
|
432
|
+
## Qualification
|
|
433
|
+
|
|
434
|
+
The current suite contains 418 active tests covering URI/IRI validation and parsing, scheme-specific hosts, IPv4, IPv6, ports, UUIDs, RFC 3986 resolution examples, empty components, absolute conversion, and relative-reference round trips.
|
|
435
|
+
|
|
436
|
+
The reference-conversion changes were additionally checked against 2,646 combinations of paths, absent/empty/non-empty queries, and absent/empty/non-empty fragments.
|
|
437
|
+
|
|
438
|
+
## Authoritative references
|
|
439
|
+
|
|
440
|
+
- [RFC 3986 — Uniform Resource Identifier: Generic Syntax](https://www.rfc-editor.org/rfc/rfc3986)
|
|
441
|
+
- [RFC 3987 — Internationalized Resource Identifiers](https://www.rfc-editor.org/rfc/rfc3987)
|
|
442
|
+
- [RFC 9110 — HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110)
|
|
443
|
+
- [RFC 6455 — The WebSocket Protocol](https://www.rfc-editor.org/rfc/rfc6455)
|
|
444
|
+
- [RFC 8089 — The `file` URI Scheme](https://www.rfc-editor.org/rfc/rfc8089)
|
|
445
|
+
- [RFC 9562 — Universally Unique IDentifiers](https://www.rfc-editor.org/rfc/rfc9562)
|
|
446
|
+
|
|
447
|
+
## Disclaimer
|
|
448
|
+
|
|
449
|
+
Validation establishes conformance with this implementation's syntax and policy. It does not establish that an identifier is registered, reachable, trustworthy, safe to dereference, or appropriate for a particular protocol operation.
|
package/tests/resolve.test.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from 'vitest';
|
|
2
2
|
import id from '../index.js';
|
|
3
3
|
|
|
4
|
+
// Verify reference resolution behavior beyond the RFC's published examples.
|
|
4
5
|
const resolveTests = [
|
|
5
6
|
['urn:some:ip:prop', 'urn:some:ip:prop', 'urn:some:ip:prop'],
|
|
6
7
|
['urn:some:ip:prop', 'urn:some:other:prop', 'urn:some:ip:prop'],
|
|
@@ -13,4 +14,24 @@ describe('resolveReference', () => {
|
|
|
13
14
|
expect(subject).to.equal(expected);
|
|
14
15
|
});
|
|
15
16
|
});
|
|
17
|
+
|
|
18
|
+
// Preserve an explicitly empty query while replacing the base query.
|
|
19
|
+
test('Preserves an empty query component', () => {
|
|
20
|
+
expect(id.resolveReference('?', 'https://example.com/path?old#fragment')).to.equal('https://example.com/path?');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Preserve an explicitly empty fragment after inheriting the base query.
|
|
24
|
+
test('Preserves an empty fragment component', () => {
|
|
25
|
+
expect(id.resolveReference('#', 'https://example.com/path?query#old')).to.equal('https://example.com/path?query#');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Preserve an empty query inherited by an empty reference.
|
|
29
|
+
test('Preserves an inherited empty query component', () => {
|
|
30
|
+
expect(id.resolveReference('', 'https://example.com/path?#fragment')).to.equal('https://example.com/path?');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Preserve the delimiter that distinguishes an empty authority from no authority.
|
|
34
|
+
test('Preserves an empty authority component', () => {
|
|
35
|
+
expect(id.resolveReference('uri:///target', 'uri:/base')).to.equal('uri:///target');
|
|
36
|
+
});
|
|
16
37
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from 'vitest';
|
|
2
2
|
import id from '../index.js';
|
|
3
3
|
|
|
4
|
+
// Verify relative-reference generation and round trips through reference resolution.
|
|
4
5
|
describe('toRelativeReference IRI', () => {
|
|
5
6
|
test.each([
|
|
6
7
|
['https://examplé.com/var/lib', 'https://examplé.com', '/var/lib'],
|
|
@@ -48,3 +49,25 @@ describe('toRelativeReference URI', () => {
|
|
|
48
49
|
expect(id.resolveReference(relative, base)).to.equal(target); // sanity check
|
|
49
50
|
});
|
|
50
51
|
});
|
|
52
|
+
|
|
53
|
+
// Verify empty components and path forms that require explicit inheritance control.
|
|
54
|
+
describe('toRelativeReference component presence', () => {
|
|
55
|
+
// Require every generated reference to have the expected form and resolve back to its target.
|
|
56
|
+
test.each([
|
|
57
|
+
['clears a query on the same absolute path', 'https://example.com/a/item', 'https://example.com/a/item?old', '/a/item'],
|
|
58
|
+
['preserves an empty target query', 'https://example.com/a/item?', 'https://example.com/a/item?old', '?'],
|
|
59
|
+
['preserves an empty target fragment', 'https://example.com/a/item#', 'https://example.com/a/item', '#'],
|
|
60
|
+
['clears a query on an empty path', 'https://example.com', 'https://example.com?old', '//example.com'],
|
|
61
|
+
['clears a query on a colon-containing rootless path', 'urn:a:b', 'urn:a:b?old', './a:b'],
|
|
62
|
+
['produces an empty path from a non-empty base path', 'https://example.com', 'https://example.com/a', '//example.com'],
|
|
63
|
+
['produces a root path from a single-segment base path', 'https://example.com/', 'https://example.com/a', '/'],
|
|
64
|
+
['preserves a trailing slash on a rootless directory', 'urn:a/', 'urn:a/b', './'],
|
|
65
|
+
['protects a colon-containing first path segment', 'urn:a:b', 'urn:c', './a:b'],
|
|
66
|
+
['falls back for an empty rootless target path', 'urn:', 'urn:a', 'urn:'],
|
|
67
|
+
['falls back when rootless parent traversal changes path form', 'urn:a', 'urn:a/', 'urn:a'],
|
|
68
|
+
])('%s', (description, target, base, expected) => {
|
|
69
|
+
const relative = id.toRelativeReference(target, base);
|
|
70
|
+
expect(relative).to.equal(expected);
|
|
71
|
+
expect(id.resolveReference(relative, base)).to.equal(target);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -101,6 +101,14 @@ describe('isUri – IPv6 host validation', () => {
|
|
|
101
101
|
test('Invalid IPv6 – empty address literal', () => {
|
|
102
102
|
expect(() => id.isUri('https://[]')).to.throw(Error, 'Invalid URI: https://[]');
|
|
103
103
|
});
|
|
104
|
+
|
|
105
|
+
test('Invalid IPv6 – address literal with missing opening bracket', () => {
|
|
106
|
+
expect(() => id.isUri('https://::1]')).to.throw(Error, 'Invalid URI: https://::1]');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('Invalid IPv6 – address literal with missing closing bracket', () => {
|
|
110
|
+
expect(() => id.isUri('https://[::1')).to.throw(Error, 'Invalid URI: https://[::1');
|
|
111
|
+
});
|
|
104
112
|
});
|
|
105
113
|
|
|
106
114
|
describe('isIri – IPv6 host validation', () => {
|
|
@@ -183,6 +191,14 @@ describe('isIri – IPv6 host validation', () => {
|
|
|
183
191
|
test('Invalid IPv6 – empty address literal', () => {
|
|
184
192
|
expect(() => id.isIri('https://[]')).to.throw(Error, 'Invalid IRI: https://[]');
|
|
185
193
|
});
|
|
194
|
+
|
|
195
|
+
test('Invalid IPv6 – address literal with missing opening bracket', () => {
|
|
196
|
+
expect(() => id.isIri('https://::1]')).to.throw(Error, 'Invalid IRI: https://::1]');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('Invalid IPv6 – address literal with missing closing bracket', () => {
|
|
200
|
+
expect(() => id.isIri('https://[::1')).to.throw(Error, 'Invalid IRI: https://[::1');
|
|
201
|
+
});
|
|
186
202
|
});
|
|
187
203
|
|
|
188
204
|
describe('isUri – port validation', () => {
|
|
@@ -206,6 +222,14 @@ describe('isUri – port validation', () => {
|
|
|
206
222
|
expect(() => id.isUri('https://example.com: /')).to.throw(Error, 'Invalid URI: https://example.com: /');
|
|
207
223
|
});
|
|
208
224
|
|
|
225
|
+
test('Invalid "space" before port', () => {
|
|
226
|
+
expect(() => id.isUri('https://example.com: 80/')).to.throw(Error, 'Invalid URI: https://example.com: 80/');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('Invalid "space" after port', () => {
|
|
230
|
+
expect(() => id.isUri('https://example.com:80 /')).to.throw(Error, 'Invalid URI: https://example.com:80 /');
|
|
231
|
+
});
|
|
232
|
+
|
|
209
233
|
test('Invalid port char', () => {
|
|
210
234
|
expect(() => id.isUri('https://example.com:ff')).to.throw(Error, 'Invalid URI: https://example.com:ff');
|
|
211
235
|
});
|
|
@@ -236,6 +260,14 @@ describe('isIri – port validation', () => {
|
|
|
236
260
|
expect(() => id.isIri('https://example.com: /')).to.throw(Error, 'Invalid IRI: https://example.com: /');
|
|
237
261
|
});
|
|
238
262
|
|
|
263
|
+
test('Invalid "space" before port', () => {
|
|
264
|
+
expect(() => id.isIri('https://example.com: 80/')).to.throw(Error, 'Invalid IRI: https://example.com: 80/');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test('Invalid "space" after port', () => {
|
|
268
|
+
expect(() => id.isIri('https://example.com:80 /')).to.throw(Error, 'Invalid IRI: https://example.com:80 /');
|
|
269
|
+
});
|
|
270
|
+
|
|
239
271
|
test('Invalid port char', () => {
|
|
240
272
|
expect(() => id.isIri('https://example.com:ff')).to.throw(Error, 'Invalid IRI: https://example.com:ff');
|
|
241
273
|
});
|