fix(varlock): enum, url, ip, md5, and port data type fixes - #1064
Conversation
enum: match numeric/boolean members against string values from process.env and overrideValues. url: treat allowedDomains comma-strings as a host list rather than a substring match, and allow a root `/` under noTrailingSlash. ip: fix an over-escaped `\.` in the IPv6 regex so IPv4-mapped addresses validate. md5: accept uppercase hex, normalize to lowercase. port: reject non-integers, including unquoted 80.5.
|
The changes in this PR will be included in the next version bump.
|
📦 Bundle size
dist/ only; native binaries are versioned separately and not counted here. |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| 🔵 In progress View logs |
varlock-website | f573865 | Sep 03 2026, 06:50 PM |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes across the five changed files, including runtime validation, editor diagnostics, documentation, release metadata, and regression coverage.
- Enum coercion: String overrides now resolve to matching numeric and boolean members while preserving exact string matches.
- URL validation: Comma-separated domain lists use normalized exact host matching, and root paths are exempt from
noTrailingSlash. - IP, port, and MD5 validation: IPv4-tailed IPv6 forms validate, fractional ports fail, and uppercase MD5 values normalize to lowercase.
- User-facing alignment: Reference docs and VS Code completion text reflect the runtime contracts, with focused tests covering each fix.
azure/gpt-5.6-sol | 𝕏
An array is the documented form and had no test coverage. Validate it the way allowedProtocols right above it does, erroring clearly when the list holds non-strings, and keep accepting a comma-separated string. Widen the settings type to match, and lead with the array form in the reference docs and the vscode snippet.
There was a problem hiding this comment.
Important
The array-first allowedDomains change leaves VS Code diagnostics inconsistent with runtime validation.
Reviewed changes since the prior Pullfrog review at 6c77eb03, focusing on the expanded allowedDomains contract and its user-facing integrations.
- Promoted array syntax: Made
allowedDomains=[...]the primary documented and suggested form while retaining comma-separated string support. - Hardened runtime validation: Added string-member validation, normalized exact host matching, and regression coverage for arrays, case differences, substrings, and invalid entries.
- Updated user-facing guidance: Revised the reference example, release entry, and VS Code completion text for the array-first behavior.
azure/gpt-5.6-sol | 𝕏
@env-spec/parser
varlock
@varlock/native-helper-darwin
@varlock/native-helper-linux-arm64
@varlock/native-helper-linux-x64
@varlock/native-helper-win32-x64
@varlock/aws-sigv4-plugin
commit: |
Splitting a string on commas would make allowedDomains behave unlike allowedProtocols right beside it, which requires an array. A string is one host; a comma in it errors and names the array to write instead. The single-host string keeps working, which is what the vscode snippet used to insert.
There was a problem hiding this comment.
Important
The new runtime contract is sound, but the existing VS Code diagnostic parity issue remains unresolved and now also lets comma strings pass static validation when runtime rejects them.
Reviewed changes since the prior Pullfrog review at 47f469b1, focusing on the narrowed allowedDomains string contract and its user-facing guidance.
- Preserved single-host compatibility: Treated a bare
allowedDomainsstring as one exact, case-insensitive host to support existing VS Code-generated schemas. - Rejected ambiguous comma strings: Added a targeted validation error that directs multi-host configurations to array syntax, with effective regression coverage.
- Aligned guidance: Updated the reference docs, completion text, and release entry to describe the single-string and array behavior.
azure/gpt-5.6-sol | 𝕏
noTrailingSlash exists so a value is safe to concatenate onto, and `https://example.com/` breaks `${MY_URL}/path` exactly like any other trailing slash, so the root path is not exempt. That restores main's behavior and its test; the reference docs claimed an exemption the code never had, so the docs and the vscode tooltip are corrected instead. Keeps the part of the original change that was a real fix: the check drops any query or hash first, so `https://example.com/path/?q=1` is caught. It reads the written value rather than url.pathname, which normalizes `https://example.com` to a `/` path and cannot tell the two spellings apart.
There was a problem hiding this comment.
ℹ️ No new issues in the latest commit. The prior unresolved review thread still requires attention before approval.
Reviewed changes since the prior Pullfrog review at 1158dd28, focusing on the revised noTrailingSlash contract and its coverage.
- Restored strict root handling: Rejected an explicitly written root slash again so
https://example.com/remains invalid whilehttps://example.comremains valid. - Extended suffix detection: Detected a trailing path slash even when a query string or hash follows, without confusing a slash inside the query with a path suffix.
- Aligned user-facing guidance: Updated the reference docs, VS Code completion text, release entry, and focused tests to describe and verify the revised behavior.
azure/gpt-5.6-sol | 𝕏
The check compared against url.host, which carries the port, so allowedDomains=[localhost] rejected http://localhost:3000. Compare against the hostname instead. An entry that carries a port could then never match, so it errors and names the bare domain to use rather than silently rejecting every URL. The `:\d+$` test leaves a bracketed IPv6 entry like [::1] alone.
There was a problem hiding this comment.
ℹ️ No new issues in the latest commit. The prior unresolved VS Code diagnostic parity thread still requires attention before approval.
Reviewed changes since the prior Pullfrog review at 22beb300, focusing on URL hostname matching when values or allowlist entries contain ports.
- Ignored value ports: Switched exact allowlist matching from
URL.hosttoURL.hostname, allowing entries such aslocalhostto match URL values such ashttp://localhost:3000. - Rejected allowlist ports: Added a targeted validation error for allowlist entries containing numeric ports while preserving bracketed IPv6 hostnames.
- Aligned guidance and coverage: Updated reference docs, completion text, release metadata, and focused tests for the hostname contract.
azure/gpt-5.6-sol | 𝕏
Comparing against url.host meant allowedDomains=[localhost] rejected http://localhost:3000. An entry without a port now matches the hostname and so allows any port, while an entry that names one pins it - which keeps ["localhost:3000"] working as it did before. An entry naming a port is normalized through the same parser as the value, so a protocol default port is dropped from both sides and ["example.com:443"] matches https://example.com.
There was a problem hiding this comment.
Important
The port-pinning implementation accepts non-host authority syntax, which can make an allowlist entry authorize a different host than its text suggests.
Reviewed changes since the prior Pullfrog review at 1297859f, focusing on explicit port constraints in URL domain allowlists.
- Added port pinning: Allowed an
allowedDomainsentry with a numeric port to require that effective port while entries without ports continue to accept any value port. - Normalized default ports: Parsed entries with the value URL's protocol so explicit defaults such as HTTPS port 443 compare with WHATWG-normalized URLs.
- Updated guidance and coverage: Revised docs, completion text, release metadata, and runtime tests for matching, mismatched, and default ports.
azure/gpt-5.6-sol | 𝕏
…hecks Mutation-tested every fix in this PR by breaking it and checking a test caught it. Three branches had no coverage: an entry naming a port against a value without one, a default-port entry under another protocol, and the catch for an unparseable port. Nor did the bracketed IPv6 entry the port guard exists to avoid, mixed bare and port entries, trimming, a non-array non-string setting, or the hash half of the noTrailingSlash split, which a quoted value does reach. Two mutants survived because the code was redundant, not because a test was missing. md5 validated with an /i regex that coerce's lowercasing made unreachable, and port checked Number.isInteger in both coerce and validate. Kept the coerce-side check, which matches the CoercionError the neighbouring string checks throw.
There was a problem hiding this comment.
ℹ️ No new issues in the latest commit. The two prior unresolved threads concerning malformed authority syntax and VS Code diagnostic parity still require attention before approval.
Reviewed changes since the prior Pullfrog review at d12454ce, focusing on expanded URL edge coverage and removal of redundant checks.
- Expanded URL coverage: Added regression cases for port mismatches, protocol-sensitive defaults, mixed allowlist entries, case normalization, bracketed IPv6, malformed ports, normalized entries, invalid option types, and hash suffixes.
- Removed duplicate validation: Kept integer enforcement in port coercion and lowercase normalization in MD5 coercion while removing checks made unreachable by those coercion steps.
azure/gpt-5.6-sol | 𝕏
An entry is compared against a host, so one holding a path could never match and silently rejected every URL. It now errors and names the host to use. A scheme brings `//` and is caught the same way, and a bracketed IPv6 entry holds no slash so it is unaffected. Reported even when another entry would have matched, since the entry is a schema mistake rather than a value that failed.
There was a problem hiding this comment.
ℹ️ No new issues in the latest commit. The new path and scheme validation is sound, but it only partially addresses the prior malformed-authority finding; that thread and the VS Code diagnostic parity thread remain unresolved.
Reviewed changes since the prior Pullfrog review at b57d9b08, focusing on stricter allowedDomains schema validation.
- Rejected path-bearing entries: Added a targeted validation error for allowlist entries containing paths or schemes, including host suggestions where available.
- Validated invalid-list handling: Added coverage showing malformed entries fail even alongside a matching host and that entries without a usable host omit the suggestion.
- Aligned user guidance: Updated the reference docs, VS Code completion text, and release entry to describe host-only allowlist entries.
azure/gpt-5.6-sol | 𝕏
Handing a whole entry to URL let its authority parser read credentials and swap the host: `trusted.example:443@evil.example:8443` parsed with a host of `evil.example:8443`, so the entry authorized that and rejected trusted.example - the opposite of what it reads as. Entries are now checked against a hostname-with-optional-port shape first, which covers the earlier path and scheme cases too. No host is suggested for a credential-bearing entry, since the one it would have authorized is the attacker's. Both branches now compare through the same parser, so an internationalized entry normalizes to punycode and matches. Also brings the vscode diagnostics back in line with the runtime. They lowercased only the value, so [Example.COM] raised a false error; they compared against the port-bearing host; they still exempted a root slash under noTrailingSlash; and they accepted a comma string the runtime now rejects.
There was a problem hiding this comment.
Important
The new host-only and diagnostic parity work still accepts a backslash path in an allowlist entry and reports an empty allowlist differently from runtime.
Reviewed changes since the prior Pullfrog review at 345fa5bc, focusing on the stricter allowedDomains shape contract and VS Code parity.
- Restricted allowlist entries: Added a shared host-plus-optional-port shape check that rejects credentials and other URL components before WHATWG parsing.
- Aligned editor diagnostics: Mirrored runtime hostname and port matching, comma-string rejection, malformed-entry handling, and strict
noTrailingSlashbehavior in VS Code. - Expanded regression coverage: Added runtime and editor tests for credentials, malformed ports, bracketed IPv6 port pinning, IDN normalization, case normalization, and trailing-slash handling.
azure/gpt-5.6-sol | 𝕏
WHATWG treats a backslash as a path separator for special schemes, so `trusted.example\path` passed the hostname-only shape check and then matched on the host alone. Excluded alongside `/`. An empty list, or one whose entries are all empty, could never match and rejected every URL while naming nothing in the message. It now says the list is empty. The vscode diagnostics skipped matching entirely in that case and accepted every URL, so both sides now share the contract, with an empty quoted string still meaning the option is not set.
There was a problem hiding this comment.
ℹ️ No new issues in the latest commit. The prior unresolved VS Code non-string array-member parity thread still requires attention before approval.
Reviewed changes since the prior Pullfrog review at ecf418c2, focusing on the final allowedDomains shape and empty-list handling.
- Rejected backslash paths: Excluded backslashes from host entries in runtime and VS Code validation, with regression coverage proving
trusted.example\\pathis diagnosed. - Defined empty-list behavior: Added an explicit error for empty arrays and arrays containing only empty entries while preserving an empty bare string as an unset option.
- Kept user-facing behavior aligned: Updated reference documentation, release metadata, and editor diagnostics to describe and enforce the same contract.
azure/gpt-5.6-sol | 𝕏
The editor accepted `allowedDomains=[example.com, true]` while a load rejects it, which is the worst shape for a diagnostic to have. Quoting is what decides whether a member is a string, and parseListOption unquotes everything, so this reads the raw array literal instead. Applies to allowedProtocols too, which has the same runtime rule and was silent in the same way.
There was a problem hiding this comment.
Important
The editor parity fix still disagrees with the parser for several unquoted scalar forms and should be corrected before merge.
Reviewed changes since the prior Pullfrog review at f5738659, focusing on the new raw-array member validation in VS Code diagnostics.
- Detected unquoted scalar members: Added raw-literal inspection so boolean and numeric members rejected by runtime are also diagnosed by the editor.
- Extended option parity: Applied the check to both
allowedDomainsandallowedProtocolswhile preserving quoted values as strings. - Added focused regression coverage: Verified bare
trueand42are rejected while quoted"true"remains valid.
azure/gpt-5.6-sol | 𝕏
…oerce The regex I wrote to spot non-string array members disagreed with the parser in six ways: it missed `undefined`, and it flagged `TRUE`, `True`, `00`, `1.0` and unsafe integers, all of which autoCoerce deliberately leaves as strings. So it swapped one wrong diagnostic for another. Export autoCoerce from @env-spec/parser and use it, rather than reimplementing the coercion rules a third time. It bundles into the extension, so no runtime dependency is added. The separate quoting check went with it: members reach autoCoerce with their quotes attached, so a quoted "true" is never the bare literal and the check could not change an outcome.
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes since the prior Pullfrog review at 69e607ff, covering the parser export, editor integration, dependency metadata, and focused regression coverage.
- Shared scalar classification: Exported
autoCoercefrom@env-spec/parserand replaced the editor's approximate regular expression with the parser's exact coercion behavior. - Expanded parity coverage: Verified converted booleans, numbers, and
undefinedare rejected as non-string array members while case variants, formatted numbers, scientific notation, and unsafe integers remain strings. - Updated package metadata: Added the parser workspace dependency to the VS Code extension and included the parser patch in release metadata.
azure/gpt-5.6-sol | 𝕏
…of the repo Importing @env-spec/parser broke `tsc --noEmit` in CI: the plugin's tsconfig stood on its own with moduleResolution node16, so it resolved the dependency through the exports map to `dist/index.d.mts`, which does not exist until that package is built. It passed locally only because a previous build had left the dist behind. Extend the shared base config, which sets customConditions ts-src and moduleResolution bundler - the same way every other package here resolves a workspace dep to its TypeScript source, with no build needed first. Verified by deleting the parser's dist and typechecking.
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes since the prior Pullfrog review at 1eef09b7, covering the shared TypeScript configuration and workspace dependency metadata.
- Aligned compiler resolution: Extended the VS Code package from the shared TypeScript base so
tsc --noEmituses bundler resolution and thets-srccondition to resolve workspace source without prebuilt parser output. - Declared configuration dependency: Added
@varlock/tsconfigto the package and lockfile so clean workspace installs provide the inherited config.
azure/gpt-5.6-sol | 𝕏



Split 1 of 3 out of #1061 by @WalksWithASwagger, who wrote the original fixes. Commit authorship is preserved; the three slices touch disjoint files so they review and land independently.
No existing test expectation changes here: every assertion on
mainstill holds, and the new cases are additive.Fixes
@type=enum: numeric and boolean members now match string values fromprocess.env/overrideValues, so a CI override ofLEVEL=2satisfiesenum(1, 2, 3)@type=url:allowedDomainsis matched in full against the URL host, closing a substring hole (below)@type=url:noTrailingSlashnow catches a trailing slash followed by a query string or hash (below)@type=ip: an over-escaped\.in the IPv6 regex meant the IPv4-mapped branches could never match;::ffff:192.168.1.1now validates@type=md5: accepts uppercase hex, normalized to lowercase@type=port: rejects non-integers. Unquoted80.5is already a number after parse auto-coercion and skipped the existing string checkallowedDomainsThe value was being run through
String.prototype.includes, which is a substring match. Probingmain:mainallowedDomains="example.com"→ hostexample.comallowedDomains="a.com,b.com"→ hosta.comallowedDomains="a.com,b.com"→ hostevil.comUnexpected error during validation(.jointhrowing on a string)allowedDomains="myexample.com"→ hostexample.comallowedDomains=[a.com, b.com]So a host passed an allowlist it was merely a substring of, and a legitimate rejection crashed instead of reporting.
An array is the documented form and is now what we lead with and test, validated the way
allowedProtocolsdirectly above it does. A bare string is treated as a single host rather than being split on commas:main's vscode snippet insertedallowedDomains=${1:"example.com"}, so the single-string form is plausibly in the wild and keeps working. Splitting on commas instead would leave two options on the same type behaving differently, sinceallowedProtocols=postgresalready errors with "must be an array of strings".A comma inside a string now errors and names the fix:
That is the one intentional break. A comma-string appeared to work on
mainonly through the substring bug, and it now fails loudly with a one-line fix rather than silently changing meaning.Ports are handled per entry.
maincompared againsturl.host, which carries the port, soallowedDomains=[localhost]rejectedhttp://localhost:3000withDomain (localhost:3000) is not in allowed list: localhost- a likely thing to hit in local dev. Now an entry without a port matches the hostname and allows any port, and an entry that names a port pins it:allowedDomains[localhost]http://localhost:3000/[localhost]http://localhost/["localhost:3000"]http://localhost:3000/["localhost:3000"]http://localhost:9999/["example.com:443"]https://example.com/["example.com:443"]http://example.com/[example.com]https://evil.com:8443/An entry naming a port is normalized through the same parser as the value, so a protocol default port is dropped from both sides - otherwise
["example.com:443"]would never matchhttps://example.com, whosehostis justexample.com. A bracketed IPv6 entry like["[::1]"]takes the hostname branch and still works.Wildcards are still unsupported and out of scope here:
allowedDomains=["*.example.com"]is taken as a literal hostname and matches nothing. Worth adding later, following theallowWildcardsemantics thedomaintype already has, along with an error for a*entry in the meantime.noTrailingSlashThe original PR exempted a root
/, inverting an existing test, on the grounds that the reference docs already promised an exemption ("except root/"). Reverted: the docs sentence was the thing that was wrong.This option exists so a value is safe to concatenate onto, and
https://example.com/breaks${MY_URL}/pathinto//pathexactly like any other trailing slash.main's behavior stands, its test is untouched, and the docs plus the vscode tooltip are corrected to describe it.Kept the part of the original change that was a real fix.
maintested whether the whole written value ended in/, so a trailing slash followed by a query slipped through:mainhttps://example.comhttps://example.com/https://example.com/path/https://example.com/path/?q=1The check drops any query or hash and then looks at the written value. It cannot use
url.pathname, since WHATWG normalization gives bothhttps://example.comandhttps://example.com/a/path and so cannot tell the two spellings apart.md5 normalization: checked, not a breaking change
Flagged this for review earlier, then verified it against
main. Uppercase and mixed-case md5 values fail validation today:No schema with an uppercase md5 loads successfully on
main, so the only values whose delivered form changes are ones that previously hard-failed. Purely additive, so the coercion stays.Coverage
Mutation-tested rather than eyeballed: each fix was broken in turn to confirm a test caught it. Every mutant is killed - reverting the IPv6 escaping, the hostname comparison, the port guard, the empty-entry filter, the trim, the comma-string error, the port integer check, the md5 lowercasing, and either half of the enum coercion each fails at least one test.
That found three uncovered branches (an entry naming a port against a value without one, a default-port entry under another protocol, and the
catchfor an unparseable port such as["localhost:99999"]), plus the bracketed IPv6 entry the port guard exists to avoid, mixed bare-and-port entries, trimming and empty entries, a non-array non-string setting, and the hash half of thenoTrailingSlashsplit - which is reachable, since a quoted value preserves#.Two mutants survived because the code was redundant, not because a test was missing, so both are removed:
md5validated with a case-insensitive regex thatcoerce's lowercasing made unreachableportcheckedNumber.isIntegerin bothcoerceandvalidate. Kept thecoerce-side one, which matches theCoercionErrorthe neighbouring string checks throwEntries are hosts only. One carrying a path is compared against a host, so it could never match and silently rejected every URL; it now errors and names the host to use:
A scheme brings
//and is caught the same way. This is reported even when another entry in the list would have matched, since a bad entry is a schema mistake rather than a value that failed. A bracketed IPv6 entry holds no slash and is unaffected.Known gap, not fixed here
Wildcards are unsupported and
["*.example.com"]is still accepted silently as a literal host that matches nothing. Left for the wildcard change, which should follow theallowWildcardsemantics thedomaintype already has.Test plan
bunx vitest run --root packages/varlock(123 files, 1986 passed)bunx turbo run build --filter=varlock --filter=env-spec-languagebun run --filter @varlock/website buildbun run lintcleangit diff origin/mainon the test file shows no deleted assertions