@openclaw/lobster 2026.9.5 → 2026.9.7
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/assets/icon.png +0 -0
- package/dist/index.js +8 -294
- package/node_modules/@clawdbot/lobster/README.md +20 -133
- package/node_modules/@clawdbot/lobster/VISION.md +15 -238
- package/node_modules/@clawdbot/lobster/bin/clawd.invoke.js +2 -21
- package/node_modules/@clawdbot/lobster/bin/invoke.js +19 -0
- package/node_modules/@clawdbot/lobster/bin/lobster.js +2 -21
- package/node_modules/@clawdbot/lobster/bin/openclaw.invoke.js +2 -24
- package/node_modules/@clawdbot/lobster/dist/src/cli.js +32 -48
- package/node_modules/@clawdbot/lobster/dist/src/cli.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/command_tokens.js +81 -0
- package/node_modules/@clawdbot/lobster/dist/src/command_tokens.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/dedupe.js +1 -12
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/dedupe.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/group_by.js +2 -14
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/group_by.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/llm_invoke.js +1 -337
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/llm_invoke.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/map.js +2 -13
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/map.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/sort.js +4 -21
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/sort.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/state.js +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/state.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/template.js +2 -13
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/template.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/workflows/workflows_run.js +0 -18
- package/node_modules/@clawdbot/lobster/dist/src/commands/workflows/workflows_run.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/core/llm_accounting.js +304 -0
- package/node_modules/@clawdbot/lobster/dist/src/core/llm_accounting.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/core/value_path.js +11 -0
- package/node_modules/@clawdbot/lobster/dist/src/core/value_path.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/parser.js +2 -67
- package/node_modules/@clawdbot/lobster/dist/src/parser.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/recipes/github/index.js +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/recipes/github/pr-monitor.js +5 -64
- package/node_modules/@clawdbot/lobster/dist/src/recipes/github/pr-monitor.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/recipes/github/read_pr.js +45 -0
- package/node_modules/@clawdbot/lobster/dist/src/recipes/github/read_pr.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/recipes/github/snapshot.js +45 -0
- package/node_modules/@clawdbot/lobster/dist/src/recipes/github/snapshot.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/recipes/github/stages/pr-view.js +9 -47
- package/node_modules/@clawdbot/lobster/dist/src/recipes/github/stages/pr-view.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/sdk/Lobster.js +18 -135
- package/node_modules/@clawdbot/lobster/dist/src/sdk/Lobster.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/sdk/index.js +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/sdk/primitives/approve.js +1 -2
- package/node_modules/@clawdbot/lobster/dist/src/sdk/primitives/approve.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/sdk/primitives/diff.js +2 -8
- package/node_modules/@clawdbot/lobster/dist/src/sdk/primitives/diff.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/sdk/primitives/exec.js +15 -81
- package/node_modules/@clawdbot/lobster/dist/src/sdk/primitives/exec.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/sdk/primitives/state.js +3 -9
- package/node_modules/@clawdbot/lobster/dist/src/sdk/primitives/state.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/sdk/runtime.js +0 -12
- package/node_modules/@clawdbot/lobster/dist/src/sdk/runtime.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/sdk/state_env.js +6 -0
- package/node_modules/@clawdbot/lobster/dist/src/sdk/state_env.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/state/atomic_file.js +192 -0
- package/node_modules/@clawdbot/lobster/dist/src/state/atomic_file.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/state/store.js +2 -188
- package/node_modules/@clawdbot/lobster/dist/src/state/store.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/workflows/dry_run.js +262 -0
- package/node_modules/@clawdbot/lobster/dist/src/workflows/dry_run.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/workflows/expressions.js +345 -0
- package/node_modules/@clawdbot/lobster/dist/src/workflows/expressions.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/workflows/file.js +21 -1036
- package/node_modules/@clawdbot/lobster/dist/src/workflows/file.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/workflows/github_pr_monitor.js +8 -77
- package/node_modules/@clawdbot/lobster/dist/src/workflows/github_pr_monitor.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/workflows/graph.js +1 -7
- package/node_modules/@clawdbot/lobster/dist/src/workflows/graph.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/workflows/load.js +347 -0
- package/node_modules/@clawdbot/lobster/dist/src/workflows/load.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/workflows/types.js +2 -0
- package/node_modules/@clawdbot/lobster/dist/src/workflows/types.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/workflows/values.js +93 -0
- package/node_modules/@clawdbot/lobster/dist/src/workflows/values.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/cancellation.test.js +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/cancellation.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/cli_run_errors.test.js +53 -0
- package/node_modules/@clawdbot/lobster/dist/test/cli_run_errors.test.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/command_arguments.test.js +82 -0
- package/node_modules/@clawdbot/lobster/dist/test/command_arguments.test.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/cost_tracker.test.js +49 -19
- package/node_modules/@clawdbot/lobster/dist/test/cost_tracker.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/helpers/node_shell_fixture.js +17 -0
- package/node_modules/@clawdbot/lobster/dist/test/helpers/node_shell_fixture.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/llm_invoke.test.js +2 -0
- package/node_modules/@clawdbot/lobster/dist/test/llm_invoke.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/pipeline_resume_state.test.js +65 -0
- package/node_modules/@clawdbot/lobster/dist/test/pipeline_resume_state.test.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/process_output_limit.test.js +5 -3
- package/node_modules/@clawdbot/lobster/dist/test/process_output_limit.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/sdk_exec.test.js +72 -25
- package/node_modules/@clawdbot/lobster/dist/test/sdk_exec.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/workflow_graph.test.js +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/workflow_graph.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/README.md +1 -0
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/SECURITY.md +62 -13
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/index.js +40 -9
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/lib/schemes.js +76 -16
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/lib/utils.js +54 -10
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/package.json +1 -1
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/fixtures/uri-js-parse.json +1 -0
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/ipv6-validation.test.js +130 -0
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/mailto-authority-security.test.js +125 -0
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/mailto-field-name-security.test.js +151 -0
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/security-normalization.test.js +38 -0
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/types/index.d.ts +16 -0
- package/node_modules/@clawdbot/lobster/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +19 -18
- package/node_modules/@clawdbot/lobster/node_modules/yaml/browser/dist/nodes/Alias.js +25 -23
- package/node_modules/@clawdbot/lobster/node_modules/yaml/dist/compose/resolve-flow-scalar.js +19 -18
- package/node_modules/@clawdbot/lobster/node_modules/yaml/dist/nodes/Alias.js +25 -23
- package/node_modules/@clawdbot/lobster/node_modules/yaml/package.json +1 -1
- package/node_modules/@clawdbot/lobster/package.json +6 -8
- package/node_modules/typebox/build/compile/code.mjs +1 -2
- package/node_modules/typebox/build/compile/validator.mjs +5 -1
- package/node_modules/typebox/build/format/iri.mjs +16 -9
- package/node_modules/typebox/build/guard/emit.mjs +2 -2
- package/node_modules/typebox/build/guard/guard.d.mts +7 -5
- package/node_modules/typebox/build/guard/guard.mjs +14 -9
- package/node_modules/typebox/build/guard/index.mjs +6 -0
- package/node_modules/typebox/build/guard/unicode/unicode.d.mts +6 -0
- package/node_modules/typebox/build/guard/unicode/unicode.mjs +35 -0
- package/node_modules/typebox/build/guard/{string.mjs → unicode/unicode_segment.mjs} +16 -19
- package/node_modules/typebox/build/schema/build.mjs +2 -3
- package/node_modules/typebox/build/schema/engine/_pathing.d.mts +1 -0
- package/node_modules/typebox/build/schema/engine/_pathing.mjs +11 -0
- package/node_modules/typebox/build/schema/engine/_refine.mjs +13 -5
- package/node_modules/typebox/build/schema/engine/additionalProperties.mjs +2 -1
- package/node_modules/typebox/build/schema/engine/dependencies.mjs +2 -1
- package/node_modules/typebox/build/schema/engine/dependentSchemas.mjs +2 -1
- package/node_modules/typebox/build/schema/engine/dynamicRef.d.mts +1 -1
- package/node_modules/typebox/build/schema/engine/dynamicRef.mjs +2 -2
- package/node_modules/typebox/build/schema/engine/format.mjs +9 -2
- package/node_modules/typebox/build/schema/engine/patternProperties.mjs +3 -2
- package/node_modules/typebox/build/schema/engine/properties.mjs +3 -2
- package/node_modules/typebox/build/schema/engine/propertyNames.mjs +2 -1
- package/node_modules/typebox/build/schema/engine/recursiveRef.d.mts +1 -1
- package/node_modules/typebox/build/schema/engine/recursiveRef.mjs +2 -2
- package/node_modules/typebox/build/schema/engine/ref.d.mts +1 -1
- package/node_modules/typebox/build/schema/engine/ref.mjs +2 -2
- package/node_modules/typebox/build/schema/engine/schema.mjs +24 -24
- package/node_modules/typebox/build/schema/parse.mjs +3 -0
- package/node_modules/typebox/build/schema/pointer/pointer.d.mts +1 -6
- package/node_modules/typebox/build/schema/pointer/pointer.mjs +47 -64
- package/node_modules/typebox/build/system/settings/internal.d.mts +2 -0
- package/node_modules/typebox/build/system/settings/internal.mjs +20 -0
- package/node_modules/typebox/build/system/settings/settings.d.mts +11 -0
- package/node_modules/typebox/build/system/settings/settings.mjs +14 -0
- package/node_modules/typebox/build/type/extends/inference.d.mts +1 -1
- package/node_modules/typebox/build/type/extends/inference.mjs +15 -2
- package/node_modules/typebox/build/type/extends/tuple.d.mts +2 -1
- package/node_modules/typebox/build/type/extends/tuple.mjs +21 -1
- package/node_modules/typebox/build/value/parse/parse.mjs +10 -4
- package/node_modules/typebox/package.json +1 -1
- package/node_modules/typebox/readme.md +3 -1
- package/package.json +6 -6
- package/dist/runtime-api.js +0 -3
- package/node_modules/@clawdbot/lobster/dist/src/sdk/token.js +0 -9
- package/node_modules/@clawdbot/lobster/dist/src/sdk/token.js.map +0 -1
- /package/node_modules/typebox/build/guard/{string.d.mts → unicode/unicode_segment.d.mts} +0 -0
|
@@ -15,38 +15,87 @@ Reports based solely on comparing or mixing parsers that follow these different
|
|
|
15
15
|
standards are out of scope. Applications must use the same parsing and
|
|
16
16
|
normalization rules for both security decisions and subsequent URI use.
|
|
17
17
|
|
|
18
|
+
A WHATWG comparison may be useful supporting evidence, but a parser difference
|
|
19
|
+
is not by itself a vulnerability. A report remains in scope when it independently
|
|
20
|
+
demonstrates a defect in `fast-uri`'s RFC or documented behavior for a supported
|
|
21
|
+
scheme and a concrete security impact when the application uses `fast-uri`
|
|
22
|
+
consistently. An exploit that requires validating with `fast-uri` and then
|
|
23
|
+
passing the original, unmodified input to a parser with different rules is an
|
|
24
|
+
application parser-confusion issue.
|
|
25
|
+
|
|
18
26
|
Reports are assessed based on reproducibility, documented supported usage, and
|
|
19
27
|
concrete security impact—not merely on whether they mention WHATWG URL
|
|
20
|
-
behavior.
|
|
21
|
-
scheme remain eligible for evaluation.
|
|
28
|
+
behavior.
|
|
22
29
|
|
|
23
30
|
## Threat model
|
|
24
31
|
|
|
25
32
|
`fast-uri`'s threat model extends the
|
|
26
33
|
[Node.js threat model](https://github.com/nodejs/node/blob/main/SECURITY.md#the-nodejs-threat-model).
|
|
27
34
|
|
|
28
|
-
**Trusted:** Application code,
|
|
29
|
-
|
|
35
|
+
**Trusted:** Application code, structured component objects constructed or
|
|
36
|
+
modified by application code and passed directly to `serialize()`, parser
|
|
37
|
+
options, configuration, and the runtime environment.
|
|
30
38
|
|
|
31
39
|
**Untrusted:** URI and IRI strings passed to the package's public APIs.
|
|
32
40
|
|
|
41
|
+
## Vulnerability classification
|
|
42
|
+
|
|
43
|
+
A report is treated as a vulnerability when it demonstrates all of the
|
|
44
|
+
following:
|
|
45
|
+
|
|
46
|
+
1. Attacker-controlled input reaches a public API through documented,
|
|
47
|
+
supported usage.
|
|
48
|
+
2. `fast-uri` violates an applicable RFC or its documented API contract.
|
|
49
|
+
3. The defect creates a concrete confidentiality, integrity, or availability
|
|
50
|
+
impact in a realistic supported use case.
|
|
51
|
+
4. The impact does not depend solely on application misuse, an unsupported
|
|
52
|
+
scheme, a different parser interpreting the original input, or another
|
|
53
|
+
package's behavior.
|
|
54
|
+
|
|
55
|
+
A reproducible bug can still merit a normal issue and fix without meeting the
|
|
56
|
+
security-vulnerability threshold.
|
|
57
|
+
|
|
33
58
|
### Examples of vulnerabilities
|
|
34
59
|
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
|
|
60
|
+
- `parse()`, `normalize()`, or `resolve()` changes an attacker-controlled
|
|
61
|
+
authority within `fast-uri`, and the report demonstrates a host security
|
|
62
|
+
control being bypassed using that output
|
|
63
|
+
- `equal()` aliases distinct URIs under the rules of a supported scheme, and
|
|
64
|
+
the report demonstrates an authorization, signature, or cache boundary being
|
|
65
|
+
bypassed
|
|
66
|
+
- One public API call on a reasonably sized malformed URI causes
|
|
67
|
+
disproportionate CPU or memory consumption, with reproducible availability
|
|
68
|
+
impact
|
|
69
|
+
- Parsing or normalization turns component data into a structural delimiter
|
|
70
|
+
and the report demonstrates a security-control bypass in documented usage
|
|
71
|
+
|
|
72
|
+
### Denial-of-service reports
|
|
73
|
+
|
|
74
|
+
A synchronous exception, parse error, or non-idempotent result is not by itself
|
|
75
|
+
a denial-of-service vulnerability. A report must demonstrate disproportionate
|
|
76
|
+
resource consumption in a public API call, or loss of availability in a
|
|
77
|
+
supported deployment despite normal exception handling. An application that
|
|
78
|
+
terminates on an uncaught exception, or repeatedly normalizes until reaching a
|
|
79
|
+
fixpoint, does not establish a package vulnerability by itself.
|
|
38
80
|
|
|
39
81
|
### Examples of non-vulnerabilities
|
|
40
82
|
|
|
41
83
|
The following are **not** considered vulnerabilities in `fast-uri`:
|
|
42
84
|
|
|
43
|
-
- **Different URL standards:**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
- **Unsupported scheme semantics:**
|
|
47
|
-
`fast-uri` does not document as supported
|
|
85
|
+
- **Different URL standards:** `fast-uri` returns one host, but an application
|
|
86
|
+
passes the original input to WHATWG `URL`, `fetch`, or another parser that
|
|
87
|
+
applies different recovery, whitespace, or legacy-IP rules
|
|
88
|
+
- **Unsupported scheme semantics:** Behavior specific to schemes such as
|
|
89
|
+
`ftp` or `file`, which `fast-uri` does not document as supported
|
|
90
|
+
- **Correctness without security impact:** Non-idempotent normalization,
|
|
91
|
+
surprising equality, truncation, re-encoding, or malformed-input handling
|
|
92
|
+
without a demonstrated security boundary or availability impact
|
|
93
|
+
- **Caller-created resource use:** An application repeatedly parses or
|
|
94
|
+
normalizes a value, or fails to catch a synchronous exception
|
|
95
|
+
- **Trusted structured input:** Boundary characters supplied in a component
|
|
96
|
+
object that application code constructs and passes to `serialize()`
|
|
48
97
|
- **Application code vulnerabilities:** Security flaws in code that consumes
|
|
49
|
-
`fast-uri` output
|
|
98
|
+
`fast-uri` output contrary to its documented contract
|
|
50
99
|
- **Configuration mistakes:** Security issues caused by incorrect parser
|
|
51
100
|
options or application configuration
|
|
52
101
|
- **Missing security features:** Application-level protections that are not
|
|
@@ -89,6 +89,22 @@ function resolve (baseURI, relativeURI, options) {
|
|
|
89
89
|
return serialize(resolved, schemelessOptions)
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Copies a host component together with its parse-time zone identifier (if
|
|
94
|
+
* any), so downstream recomposition does not re-derive an IPv6 zone from the
|
|
95
|
+
* ambiguous single-"%" component form. The zone is validated on use by
|
|
96
|
+
* `recomposeAuthority` rather than blindly trusted.
|
|
97
|
+
*
|
|
98
|
+
* @param {import('./types/index').URIComponent} target
|
|
99
|
+
* @param {import('./types/index').URIComponent} source
|
|
100
|
+
*/
|
|
101
|
+
function copyHost (target, source) {
|
|
102
|
+
target.host = source.host
|
|
103
|
+
if (source.ipv6Zone !== undefined) {
|
|
104
|
+
target.ipv6Zone = source.ipv6Zone
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
92
108
|
/**
|
|
93
109
|
* @param {import ('./types/index').URIComponent} base
|
|
94
110
|
* @param {import ('./types/index').URIComponent} relative
|
|
@@ -109,7 +125,7 @@ function resolveComponent (base, relative, options, skipNormalization) {
|
|
|
109
125
|
target.scheme = relative.scheme
|
|
110
126
|
// target.authority = relative.authority;
|
|
111
127
|
target.userinfo = relative.userinfo
|
|
112
|
-
target
|
|
128
|
+
copyHost(target, relative)
|
|
113
129
|
target.port = relative.port
|
|
114
130
|
target.path = removeDotSegments(relative.path || '')
|
|
115
131
|
target.query = relative.query
|
|
@@ -117,7 +133,7 @@ function resolveComponent (base, relative, options, skipNormalization) {
|
|
|
117
133
|
if (relative.userinfo !== undefined || relative.host !== undefined || relative.port !== undefined) {
|
|
118
134
|
// target.authority = relative.authority;
|
|
119
135
|
target.userinfo = relative.userinfo
|
|
120
|
-
target
|
|
136
|
+
copyHost(target, relative)
|
|
121
137
|
target.port = relative.port
|
|
122
138
|
target.path = removeDotSegments(relative.path || '')
|
|
123
139
|
target.query = relative.query
|
|
@@ -146,7 +162,7 @@ function resolveComponent (base, relative, options, skipNormalization) {
|
|
|
146
162
|
}
|
|
147
163
|
// target.authority = base.authority;
|
|
148
164
|
target.userinfo = base.userinfo
|
|
149
|
-
target
|
|
165
|
+
copyHost(target, base)
|
|
150
166
|
target.port = base.port
|
|
151
167
|
}
|
|
152
168
|
target.scheme = base.scheme
|
|
@@ -178,6 +194,7 @@ function equal (uriA, uriB, options) {
|
|
|
178
194
|
function serialize (cmpts, opts) {
|
|
179
195
|
const component = {
|
|
180
196
|
host: cmpts.host,
|
|
197
|
+
ipv6Zone: cmpts.ipv6Zone,
|
|
181
198
|
scheme: cmpts.scheme,
|
|
182
199
|
userinfo: cmpts.userinfo,
|
|
183
200
|
port: cmpts.port,
|
|
@@ -506,6 +523,16 @@ function parseWithStatus (uri, opts) {
|
|
|
506
523
|
malformedIPLiteral = hasIPLiteralBracket && (!bracketedIPLiteral || ipv6result.error === true)
|
|
507
524
|
parsed.host = isIP ? ipv6result.host : ipv6result.host.toLowerCase()
|
|
508
525
|
|
|
526
|
+
if (isIP && ipv6result.isIPV6 === true && parsed.host.indexOf('%') !== -1) {
|
|
527
|
+
// Persist the zone identifier so recomposition never has to re-derive
|
|
528
|
+
// it from the ambiguous single-"%" component form, which misreads a
|
|
529
|
+
// zone that begins with "25" as a %25 separator. It is validated on
|
|
530
|
+
// use during recomposition, so a host reassigned after parse or an
|
|
531
|
+
// input zone that does not describe the host is ignored. Only zoned
|
|
532
|
+
// literals are ambiguous; plain IPv6 addresses round-trip without it.
|
|
533
|
+
parsed.ipv6Zone = parsed.host.slice(parsed.host.indexOf('%') + 1)
|
|
534
|
+
}
|
|
535
|
+
|
|
509
536
|
if (malformedIPLiteral) {
|
|
510
537
|
parsed.error = parsed.error || 'URI host is malformed.'
|
|
511
538
|
malformedAuthorityOrPort = true
|
|
@@ -537,13 +564,17 @@ function parseWithStatus (uri, opts) {
|
|
|
537
564
|
malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP)
|
|
538
565
|
}
|
|
539
566
|
|
|
540
|
-
if (
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
567
|
+
if (uri.indexOf('%') !== -1 && parsed.host !== undefined && !malformedIPLiteral) {
|
|
568
|
+
let host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true)
|
|
569
|
+
if (!isIP) {
|
|
570
|
+
// Fold reg-name case after decoding unreserved octets. The second
|
|
571
|
+
// pass only restores uppercase hex in escapes that remain encoded.
|
|
572
|
+
host = normalizePercentEncoding(host.toLowerCase())
|
|
546
573
|
}
|
|
574
|
+
parsed.host = reescapeHostDelimiters(host, isIP)
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (!schemeHandler || (schemeHandler && !schemeHandler.skipNormalize)) {
|
|
547
578
|
if (parsed.path) {
|
|
548
579
|
parsed.path = normalizePathEncoding(parsed.path)
|
|
549
580
|
}
|
|
@@ -290,6 +290,7 @@ const DTEXT_IRI = allowSet(DTEXT_ALLOWED, true)
|
|
|
290
290
|
const HEX_PAIR = /^[\da-f]{2}$/iu
|
|
291
291
|
const MAILTO_DOMAIN_LITERAL = /^\[[\x21-\x5A\x5E-\x7E]*\]$/u
|
|
292
292
|
const MAILTO_DOMAIN_ERROR = 'URI mailto has an invalid recipient domain.'
|
|
293
|
+
const MAILTO_AUTHORITY_ERROR = 'URI mailto must not have an authority component.'
|
|
293
294
|
const HAS_SURROGATE = /[\uD800-\uDFFF]/u
|
|
294
295
|
|
|
295
296
|
/**
|
|
@@ -495,29 +496,51 @@ function mailtoParse (component, options) {
|
|
|
495
496
|
mailtoComponent.error = mailtoComponent.error || 'URI mailto has malformed header fields.'
|
|
496
497
|
continue
|
|
497
498
|
}
|
|
498
|
-
const name = eqIdx === -1 ? token : token.slice(0, eqIdx)
|
|
499
|
+
const name = decodeHex(eqIdx === -1 ? token : token.slice(0, eqIdx))
|
|
500
|
+
const normalizedName = name.toLowerCase()
|
|
499
501
|
const value = eqIdx === -1 ? '' : token.slice(eqIdx + 1)
|
|
500
|
-
if (
|
|
502
|
+
if (normalizedName === 'to') {
|
|
501
503
|
const addrs = value.split(',')
|
|
502
504
|
for (let j = 0; j < addrs.length; j++) to.push(addrs[j])
|
|
503
505
|
continue
|
|
504
506
|
}
|
|
505
|
-
if (
|
|
507
|
+
if (normalizedName === 'subject') {
|
|
506
508
|
mailtoComponent.subject = decodeHex(value)
|
|
507
509
|
continue
|
|
508
510
|
}
|
|
509
|
-
if (
|
|
511
|
+
if (normalizedName === 'body') {
|
|
510
512
|
mailtoComponent.body = decodeHex(value)
|
|
511
513
|
continue
|
|
512
514
|
}
|
|
513
515
|
if (headers === null) headers = /** @type {Record<string,string>} */ (Object.create(null))
|
|
514
|
-
headers[
|
|
516
|
+
headers[name] = decodeHex(value)
|
|
515
517
|
}
|
|
516
518
|
if (headers !== null) mailtoComponent.headers = headers
|
|
517
519
|
}
|
|
518
520
|
|
|
519
521
|
mailtoComponent.query = undefined
|
|
520
522
|
|
|
523
|
+
// RFC 6068 defines no authority for the `mailto` scheme. A `mailto://` input
|
|
524
|
+
// that retains an authority and also carries recipients (from the path or a
|
|
525
|
+
// `to=` hfield) folds a leading "/" into the recipient local part on the
|
|
526
|
+
// next parse; the generic serializer then reinserts a "/" before that path
|
|
527
|
+
// on every pass, so serialize(parse(x)) is not a fixpoint and a
|
|
528
|
+
// renormalize-to-fixpoint loop accretes one "%2F" per pass. Drop the
|
|
529
|
+
// retained authority so the parse -> serialize round trip is stable. A bare
|
|
530
|
+
// `mailto://host` with no recipients is left as-is: it is idempotent and host
|
|
531
|
+
// normalization for skip-normalize schemes stays intact.
|
|
532
|
+
if (
|
|
533
|
+
to.length > 0 &&
|
|
534
|
+
(mailtoComponent.userinfo !== undefined ||
|
|
535
|
+
mailtoComponent.host !== undefined ||
|
|
536
|
+
mailtoComponent.port !== undefined)
|
|
537
|
+
) {
|
|
538
|
+
mailtoComponent.userinfo = undefined
|
|
539
|
+
mailtoComponent.host = undefined
|
|
540
|
+
mailtoComponent.port = undefined
|
|
541
|
+
mailtoComponent.error = mailtoComponent.error || MAILTO_AUTHORITY_ERROR
|
|
542
|
+
}
|
|
543
|
+
|
|
521
544
|
for (let i = 0; i < to.length; i++) {
|
|
522
545
|
const rawAddr = to[i]
|
|
523
546
|
const atIdx = rawAddr.lastIndexOf('@')
|
|
@@ -536,6 +559,18 @@ function mailtoParse (component, options) {
|
|
|
536
559
|
return mailtoComponent
|
|
537
560
|
}
|
|
538
561
|
|
|
562
|
+
/**
|
|
563
|
+
* Encodes a decoded mailto header name. Unlike `encodeWithAllow`, a percent
|
|
564
|
+
* sign is always data here, even when followed by two hex digits. Preserving
|
|
565
|
+
* such a sequence as an existing escape would decode the name again on the
|
|
566
|
+
* next parse and could turn a generic header into `to`, `subject`, or `body`.
|
|
567
|
+
* @param {string} name
|
|
568
|
+
* @returns {string}
|
|
569
|
+
*/
|
|
570
|
+
function mailtoEncodeHeaderName (name) {
|
|
571
|
+
return encodeWithAllow(name.replace(/%/gu, '%25'), HFNAME)
|
|
572
|
+
}
|
|
573
|
+
|
|
539
574
|
/**
|
|
540
575
|
* @param {import('../types/index').URIComponent} component
|
|
541
576
|
* @param {import('../types/index').Options} options
|
|
@@ -544,7 +579,40 @@ function mailtoParse (component, options) {
|
|
|
544
579
|
function mailtoSerialize (component, options) {
|
|
545
580
|
const mailtoComponent = component
|
|
546
581
|
const to = Array.isArray(mailtoComponent.to) ? mailtoComponent.to.slice() : []
|
|
582
|
+
const sourceHeaders = mailtoComponent.headers && typeof mailtoComponent.headers === 'object'
|
|
583
|
+
? Object.assign(Object.create(null), mailtoComponent.headers)
|
|
584
|
+
: Object.create(null)
|
|
585
|
+
const headers = Object.create(null)
|
|
586
|
+
|
|
587
|
+
// Header field names are case-insensitive. Canonicalize the fields with
|
|
588
|
+
// dedicated component properties so callers cannot smuggle a second spelling
|
|
589
|
+
// through `headers`. As during parsing, all `to` fields are accumulated;
|
|
590
|
+
// repeated subject/body fields use the last value.
|
|
591
|
+
for (const name in sourceHeaders) {
|
|
592
|
+
const normalizedName = name.toLowerCase()
|
|
593
|
+
if (normalizedName === 'to') {
|
|
594
|
+
const addrs = String(sourceHeaders[name]).split(',')
|
|
595
|
+
for (let i = 0; i < addrs.length; i++) to.push(addrs[i])
|
|
596
|
+
} else if (normalizedName === 'subject' || normalizedName === 'body') {
|
|
597
|
+
headers[normalizedName] = sourceHeaders[name]
|
|
598
|
+
} else {
|
|
599
|
+
headers[name] = sourceHeaders[name]
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (mailtoComponent.subject !== undefined) headers.subject = mailtoComponent.subject
|
|
604
|
+
if (mailtoComponent.body !== undefined) headers.body = mailtoComponent.body
|
|
605
|
+
mailtoComponent.headers = headers
|
|
606
|
+
|
|
547
607
|
if (to.length) {
|
|
608
|
+
// RFC 6068 defines no authority for the `mailto` scheme. If an
|
|
609
|
+
// application constructs a component with both an authority and recipients,
|
|
610
|
+
// drop the authority before the generic serializer can insert a "/" path
|
|
611
|
+
// separator that becomes part of the first recipient on the next parse.
|
|
612
|
+
mailtoComponent.userinfo = undefined
|
|
613
|
+
mailtoComponent.host = undefined
|
|
614
|
+
mailtoComponent.port = undefined
|
|
615
|
+
|
|
548
616
|
for (let i = 0; i < to.length; i++) {
|
|
549
617
|
const addr = String(to[i])
|
|
550
618
|
const atIdx = addr.lastIndexOf('@')
|
|
@@ -570,21 +638,13 @@ function mailtoSerialize (component, options) {
|
|
|
570
638
|
mailtoComponent.path = undefined
|
|
571
639
|
}
|
|
572
640
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
: Object.create(null)
|
|
576
|
-
|
|
577
|
-
if (mailtoComponent.subject) headers.subject = mailtoComponent.subject
|
|
578
|
-
if (mailtoComponent.body) headers.body = mailtoComponent.body
|
|
579
|
-
mailtoComponent.headers = headers
|
|
580
|
-
|
|
581
|
-
// `headers` has a null prototype and was filled via Object.assign, so every
|
|
582
|
-
// enumerable key is an own property and no hasOwnProperty guard is needed.
|
|
641
|
+
// `headers` has a null prototype, so every enumerable key is an own property
|
|
642
|
+
// and no hasOwnProperty guard is needed.
|
|
583
643
|
let query = ''
|
|
584
644
|
let count = 0
|
|
585
645
|
for (const name in headers) {
|
|
586
646
|
if (count++ !== 0) query += '&'
|
|
587
|
-
query +=
|
|
647
|
+
query += mailtoEncodeHeaderName(name) + '=' + encodeWithAllow(String(headers[name]), HFNAME)
|
|
588
648
|
}
|
|
589
649
|
|
|
590
650
|
if (count !== 0) {
|
|
@@ -740,6 +740,37 @@ function escapePreservingEscapes (input) {
|
|
|
740
740
|
return output
|
|
741
741
|
}
|
|
742
742
|
|
|
743
|
+
/**
|
|
744
|
+
* Rebuilds the bracketed form of a zoned IPv6 authority from the current
|
|
745
|
+
* `component.host` and its zone identifier (`component.ipv6Zone`), but only
|
|
746
|
+
* when they are consistent and describe a real zoned IPv6 literal that
|
|
747
|
+
* normalizes back to `component.host`. This validates the zone on use, so a
|
|
748
|
+
* caller-supplied `ipv6Zone` that does not match the host (or a host
|
|
749
|
+
* reassigned after `parse`) is never trusted and recomposition falls back to
|
|
750
|
+
* the normal path.
|
|
751
|
+
*
|
|
752
|
+
* @param {import('../types/index').URIComponent} component
|
|
753
|
+
* @returns {string|undefined}
|
|
754
|
+
*/
|
|
755
|
+
function recomposeZonedIPv6Host (component) {
|
|
756
|
+
const zone = component.ipv6Zone
|
|
757
|
+
// Without an ipv6Zone the single-"%" host form is genuinely ambiguous when
|
|
758
|
+
// the zone begins with "25", so we fall through to the best-effort heuristic
|
|
759
|
+
// (which matches the pre-existing behavior for hand-built components that do
|
|
760
|
+
// not carry a zone).
|
|
761
|
+
if (zone === undefined) return undefined
|
|
762
|
+
const host = component.host
|
|
763
|
+
const separator = host.indexOf('%')
|
|
764
|
+
if (separator === -1) return undefined
|
|
765
|
+
const address = host.slice(0, separator)
|
|
766
|
+
const escaped = address + '%25' + zone
|
|
767
|
+
const derived = normalizeIPv6('[' + escaped + ']')
|
|
768
|
+
if (derived.isIPV6 === true && derived.host === host) {
|
|
769
|
+
return '[' + escaped + ']'
|
|
770
|
+
}
|
|
771
|
+
return undefined
|
|
772
|
+
}
|
|
773
|
+
|
|
743
774
|
/**
|
|
744
775
|
* @param {import('../types/index').URIComponent} component
|
|
745
776
|
* @returns {string|undefined}
|
|
@@ -755,17 +786,30 @@ function recomposeAuthority (component) {
|
|
|
755
786
|
if (component.host !== undefined) {
|
|
756
787
|
let host = component.host
|
|
757
788
|
if (!isIPv4(host)) {
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
789
|
+
// A parse-time zone identifier is authoritative only while it still
|
|
790
|
+
// describes the current host. Re-deriving an IPv6 zone from the
|
|
791
|
+
// single-"%" component form is ambiguous when the zone begins with
|
|
792
|
+
// "25": it is misread as a %25 separator, rewriting e.g. zone
|
|
793
|
+
// "25eth0" into "eth0" (or destroying a zone of just "25"). A
|
|
794
|
+
// validated zone keeps parse, normalize, serialize and equal in
|
|
795
|
+
// agreement.
|
|
796
|
+
const zonedHost = recomposeZonedIPv6Host(component)
|
|
797
|
+
if (zonedHost !== undefined) {
|
|
798
|
+
host = zonedHost
|
|
767
799
|
} else {
|
|
768
|
-
|
|
800
|
+
let ipV6res = normalizeIPv6(host)
|
|
801
|
+
if (ipV6res.isIPV6 !== true && ipV6res.isIPVFuture !== true) {
|
|
802
|
+
// Decode only unreserved bytes, once. In particular, keep %25
|
|
803
|
+
// encoded so it cannot introduce a second escape during
|
|
804
|
+
// recomposition.
|
|
805
|
+
host = normalizePercentEncoding(host, true)
|
|
806
|
+
ipV6res = normalizeIPv6(host)
|
|
807
|
+
}
|
|
808
|
+
if (ipV6res.isIPV6 === true || ipV6res.isIPVFuture === true) {
|
|
809
|
+
host = `[${ipV6res.escapedHost}]`
|
|
810
|
+
} else {
|
|
811
|
+
host = reescapeHostDelimiters(host, false)
|
|
812
|
+
}
|
|
769
813
|
}
|
|
770
814
|
}
|
|
771
815
|
uriTokens.push(host)
|
|
@@ -119,6 +119,136 @@ test('IPv6 zone identifiers are validated correctly', (t) => {
|
|
|
119
119
|
t.end()
|
|
120
120
|
})
|
|
121
121
|
|
|
122
|
+
test('zoned IPv6 hosts beginning with "25" round-trip through parse/normalize/serialize/equal', (t) => {
|
|
123
|
+
const cases = [
|
|
124
|
+
// zones beginning with "25" (once ambiguous with a %25 separator)
|
|
125
|
+
['http://[fe80::1%2525]/', 'fe80::1%25', 'http://[fe80::1%2525]/'],
|
|
126
|
+
['http://[fe80::1%2525eth0]/', 'fe80::1%25eth0', 'http://[fe80::1%2525eth0]/'],
|
|
127
|
+
['http://[fe80::1%2525en1]/', 'fe80::1%25en1', 'http://[fe80::1%2525en1]/'],
|
|
128
|
+
// zones that do not begin with "25" must stay idempotent
|
|
129
|
+
['http://[fe80::1%25eth0]/', 'fe80::1%eth0', 'http://[fe80::1%25eth0]/'],
|
|
130
|
+
['http://[fe80::a%en1]/', 'fe80::a%en1', 'http://[fe80::a%25en1]/'],
|
|
131
|
+
['http://[fe80::a%25eth%2D0]/', 'fe80::a%eth%2D0', 'http://[fe80::a%25eth%2D0]/']
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
for (const [input, host, normalized] of cases) {
|
|
135
|
+
const parsed = fastURI.parse(input)
|
|
136
|
+
t.equal(parsed.error, undefined, `${input} parses without error`)
|
|
137
|
+
t.equal(parsed.host, host, `${input} exposes the expected host`)
|
|
138
|
+
t.equal(fastURI.normalize(input), normalized, `${input} normalizes to the canonical form`)
|
|
139
|
+
t.equal(fastURI.serialize(parsed), normalized, `${input} serializes to the canonical form`)
|
|
140
|
+
const reparsed = fastURI.parse(normalized)
|
|
141
|
+
t.equal(reparsed.error, undefined, `${input} canonical output reparses without error`)
|
|
142
|
+
t.equal(reparsed.host, host, `${input} parse-after-normalize is stable`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// distinct zones must not be conflated by equal
|
|
146
|
+
t.equal(
|
|
147
|
+
fastURI.equal('http://[fe80::1%2525eth0]/', 'http://[fe80::1%25eth0]/'),
|
|
148
|
+
false,
|
|
149
|
+
'equal distinguishes zones "25eth0" and "eth0"'
|
|
150
|
+
)
|
|
151
|
+
t.equal(
|
|
152
|
+
fastURI.equal('http://[fe80::1%2525]/', 'http://[fe80::1%25eth0]/'),
|
|
153
|
+
false,
|
|
154
|
+
'equal distinguishes zones "25" and "eth0"'
|
|
155
|
+
)
|
|
156
|
+
// the same zone still compares equal
|
|
157
|
+
t.equal(
|
|
158
|
+
fastURI.equal('http://[fe80::1%2525eth0]/', 'http://[fe80::1%2525eth0]/'),
|
|
159
|
+
true,
|
|
160
|
+
'an identical zoned authority is equal'
|
|
161
|
+
)
|
|
162
|
+
t.equal(
|
|
163
|
+
fastURI.equal('http://[fe80::1%2525]/', 'http://[fe80::1%2525]/'),
|
|
164
|
+
true,
|
|
165
|
+
'zone "25" equals itself'
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
// resolve must preserve the zone too
|
|
169
|
+
t.equal(
|
|
170
|
+
fastURI.resolve('http://example.com/', 'http://[fe80::1%2525eth0]/path'),
|
|
171
|
+
'http://[fe80::1%2525eth0]/path',
|
|
172
|
+
'resolve preserves a zone beginning with "25"'
|
|
173
|
+
)
|
|
174
|
+
t.equal(
|
|
175
|
+
fastURI.resolve('http://[fe80::1%2525eth0]/', 'child'),
|
|
176
|
+
'http://[fe80::1%2525eth0]/child',
|
|
177
|
+
'resolve preserves a zoned base'
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
t.end()
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
test('serialize only trusts the parse-derived IPv6 zone while the host is unchanged', (t) => {
|
|
184
|
+
// Reassigning `host` after parse must take effect for serialize.
|
|
185
|
+
const parsed = fastURI.parse('http://[::1]/admin')
|
|
186
|
+
parsed.host = 'example.com'
|
|
187
|
+
t.equal(
|
|
188
|
+
fastURI.serialize(parsed),
|
|
189
|
+
'http://example.com/admin',
|
|
190
|
+
'a host reassigned after parse wins over the stale zone'
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
// A caller-supplied `ipv6Zone` that does not match `host` must be ignored.
|
|
194
|
+
const forged = fastURI.serialize({
|
|
195
|
+
scheme: 'http',
|
|
196
|
+
host: 'fe80::1%25en1',
|
|
197
|
+
ipv6Zone: 'eth0',
|
|
198
|
+
path: '/'
|
|
199
|
+
})
|
|
200
|
+
t.equal(forged, 'http://[fe80::1%25en1]/', 'a mismatched ipv6Zone is ignored')
|
|
201
|
+
t.equal(fastURI.parse(forged).host, 'fe80::1%en1', 'mismatched ipv6Zone cannot redirect the parsed host')
|
|
202
|
+
|
|
203
|
+
// A caller-supplied `ipv6Zone` on a non-zoned host must be ignored too.
|
|
204
|
+
const noZone = fastURI.serialize({
|
|
205
|
+
scheme: 'http',
|
|
206
|
+
host: 'good.example',
|
|
207
|
+
ipv6Zone: 'eth0',
|
|
208
|
+
path: '/'
|
|
209
|
+
})
|
|
210
|
+
t.equal(noZone, 'http://good.example/', 'ipv6Zone on a non-zoned host is ignored')
|
|
211
|
+
|
|
212
|
+
// Reassigning a zoned host to a different zone must recompute, not reuse.
|
|
213
|
+
const zoned = fastURI.parse('http://[fe80::1%2525eth0]/')
|
|
214
|
+
zoned.host = 'fe80::1%25en1'
|
|
215
|
+
t.equal(
|
|
216
|
+
fastURI.serialize(zoned),
|
|
217
|
+
'http://[fe80::1%25en1]/',
|
|
218
|
+
'rewriting the zone recomposes from the new host'
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
// A consistent caller-supplied zone is honored (explicit disambiguation).
|
|
222
|
+
const disambiguated = fastURI.serialize({
|
|
223
|
+
scheme: 'http',
|
|
224
|
+
host: 'fe80::1%25eth0',
|
|
225
|
+
ipv6Zone: '25eth0',
|
|
226
|
+
path: '/'
|
|
227
|
+
})
|
|
228
|
+
t.equal(
|
|
229
|
+
disambiguated,
|
|
230
|
+
'http://[fe80::1%2525eth0]/',
|
|
231
|
+
'a consistent ipv6Zone disambiguates the component host'
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
// Without ipv6Zone, an ambiguous single-"%" host falls back to the
|
|
235
|
+
// best-effort heuristic (documented behavior for hand-built components).
|
|
236
|
+
t.equal(
|
|
237
|
+
fastURI.serialize({ host: 'fe80::1%25eth0' }),
|
|
238
|
+
'//[fe80::1%25eth0]',
|
|
239
|
+
'an ambiguous host without ipv6Zone uses the best-effort heuristic'
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
// Mutating the host must not poison resolve either.
|
|
243
|
+
t.equal(
|
|
244
|
+
fastURI.resolve('http://example.com/', 'http://[fe80::1%2525eth0]/path'),
|
|
245
|
+
'http://[fe80::1%2525eth0]/path',
|
|
246
|
+
'resolve preserves the parse-derived zone'
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
t.end()
|
|
250
|
+
})
|
|
251
|
+
|
|
122
252
|
test('hosts with unbalanced or misplaced IP-literal brackets are rejected', (t) => {
|
|
123
253
|
const malformed = [
|
|
124
254
|
'http://[fe80',
|