identifier-js 0.0.13 → 0.0.15

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 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
- result = '';
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
@@ -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.13",
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.4"
36
- },
37
- "engines": {
38
- "node": ">=18.0.0"
39
- },
40
- "engineStrict": true
41
- }
1
+ {
2
+ "name": "identifier-js",
3
+ "version": "0.0.15",
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 \"#/public/tests/vitest-setup.js\" && npm test"
33
+ },
34
+ "dependencies": {
35
+ "url-templates": "^1.0.7"
36
+ },
37
+ "engines": {
38
+ "node": ">=18.0.0"
39
+ },
40
+ "engineStrict": true
41
+ }