Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add domain name action #907

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

paksa92
Copy link

@paksa92 paksa92 commented Nov 1, 2024

Because I love valibot so much I wanted to do something in return for all the headache she saved me from so here's a PR that adds domain name validation as requested by #894.

@paksa92
Copy link
Author

paksa92 commented Nov 1, 2024

It still needs docs for the website

@fabian-hiller fabian-hiller self-assigned this Nov 1, 2024
@fabian-hiller fabian-hiller added the enhancement New feature or request label Nov 1, 2024
@fabian-hiller
Copy link
Owner

Thank you so much! 🙏 I am focusing on our v1 release first before reviewing this PR. In the meantime, I recommend using the regex with our regex action as a workaround.

@JacKyDev
Copy link

JacKyDev commented Nov 1, 2024

Too bad, I wanted to do it right away :( But isn't the following regex better suited for this purpose?

/^(?=.{1,253}$)(?![-_])(?!.+[-_]\.)([\w-]{1,63}\.)+[a-z]{2,63}$/iu

It better aligns with RFC 1035, doesn't it? This way, it also includes the restrictions that apply to a domain and its subdomains.

@fabian-hiller
Copy link
Owner

fabian-hiller commented Nov 1, 2024

I haven't checked the regex yet. Almost every regex of Valibot is handmade. I will check the regex before merging. But please feel free to discuss and research so that the process of checking and merging will be faster.

@JacKyDev
Copy link

JacKyDev commented Nov 1, 2024

This regex is used to validate domain names. Exactly that was my intention. I looked more closely into RFC 1035 and created this regex with the goal of adhering to the specifications as much as possible so that tools like Netscaler or DNS processes will consistently accept these domains. There is a lot of guidance in RFC 1035, especially concerning octets and the conditions for when and how certain characters are allowed.

Here’s a detailed breakdown of how the regex works:

/^(?=.{1,253}$)(?![-])(?!.+[-].)([\w-]{1,63}.)+[a-z]{2,63}$/iu

Explanation of each part:

  1. ^(?=.{1,253}$): This part checks that the total length of the input is between 1 and 253 characters, which is the maximum allowable length for domains.

  2. (?![-_]): This ensures that the domain name does not begin with a hyphen (-) or an underscore (_).

  3. (?!.+[-_]\.): This negative lookahead ensures there is no combination of a hyphen or underscore immediately before a period (.). This prevents invalid patterns like domain-.com.

  4. ([\w-]{1,63}\.)+: This section validates each label (segment) of the domain name:

    • [\w-]{1,63} allows between 1 and 63 alphanumeric characters or hyphens (but no _, which is not allowed in domain names).
    • \. at the end ensures each label ends with a period.
    • The + at the end allows one or more repetitions of this pattern, supporting subdomains.
  5. [a-z]{2,63}$: This section at the end checks if the TLD (top-level domain) contains only lowercase letters and is between 2 and 63 characters long, as is typical for TLDs (e.g., .com, .online).

  6. iu: The flags i and u represent:

    • i (ignore case): Case-insensitive, so .COM and .com are both accepted as valid.
    • u (unicode): Enables Unicode support for character classes like \w.

Examples of valid domains:

  • example.com
  • sub.domain.co.uk
  • my-site123.org

Examples of invalid domains:

  • -example.com (begins with a hyphen)
  • example-.com (hyphen before a period)
  • toolongtoolongtoolongtoolongtoolongtoolongtoolong.com (label is longer than 63 characters)

This regex is designed to follow DNS standards closely, enabling reliable domain validation across different DNS systems and tools.

This way, I can still help a bit :)

@paksa92
Copy link
Author

paksa92 commented Nov 1, 2024

Sorry @JacKyDev I didn't mean to steal your thunder. I just had some spare time left and since it been a few days since you responded I figured I'd step in instead.

I will look into the regex you proposed when I get the chance today!

@JacKyDev
Copy link

JacKyDev commented Nov 1, 2024

@paksa92 All good, I'll survive :D
But since I already put together the regex yesterday based on what I’d read, I thought it might be a way to help out. In the end, it saves me time too :)

@JacKyDev
Copy link

JacKyDev commented Nov 1, 2024

I'm not sure if this is common practice at Valibot, so I’ll just mention it. I would assume that the documentation could also be updated so that domainName not only exists but can also be displayed in the documentation in the future. I’ve also asked Fabian about this in the issue.

Additionally, I would suggest that something similar to email should also be included in the necessary documentation files.

If I haven't overlooked anything, those would be:

  • index.mdx (website/src/routes/api/(actions)/domainName & website/src/routes/api/(async)/customAsync & website/src/routes/api/(async)/fallbackAsync & website/src/routes/api/(async)/intersectAsync & website/src/routes/api/(async)/lazyAsync & website/src/routes/api/(async)/unionAsync)
  • properties.ts (website/src/routes/api/(actions)/domainName)

Also, the menu.md in routes/api to find domainName in the docs.

But I'm not sure if this is common practice at Valibot or if I might be overthinking it. Perhaps you should wait for Fabian to respond, as I also asked him this question here: #894.

@paksa92
Copy link
Author

paksa92 commented Nov 1, 2024

@JacKyDev yeah, I already mentioned that it is missing in the PR, but I will add it.

CONTRIBUTING.md does not mention a "definition of done" for new features, maybe it should? @fabian-hiller

@fabian-hiller
Copy link
Owner

I suppose devs rarely look at the CONTRIBUTING.md. So it is not too important. But feel free to create a PR.

@paksa92
Copy link
Author

paksa92 commented Nov 5, 2024

/^(?=.{1,253}$)(?![-])(?!.+[-].)([\w-]{1,63}.)+[a-z]{2,63}$/iu

@JacKyDev I'm currently testing this regex and I think it's wrong. First off, TLD's may not exceed a length of 6. Changing the regex to:

/^(?=.{1,253}$)(?![-])(?!.+[-].)([\w-]{1,63}.)+[a-z]{2,6}$/iu

But, this regex still allows TLD's exceeding a length of 6. I suspect one of the negative lookaheads (?![-])(?!.+[-].) causes the problem, but I'm not sure. I can't seem to fix it.

I tweaked the original regex in my PR to account for the max length of a domain name, which now should cover all the cases you mentioned:

/^(?=.{1,253}$)(?!-)([a-z0-9-]{1,63}(?<!-)\.)+[a-z]{2,6}$/iu

Please test it and let me know if you find any issues.

@JacKyDev
Copy link

JacKyDev commented Nov 5, 2024

In the RFC, I don’t see any limitation on the domain suffix
itself but rather on the entire domain and its segments.

Generally, the value is that a domain, without a . separator,
is at least 2 characters long.

It’s true that the usual domains are about 6 characters long,
but when I check online, I see valid domains like:

  • .saarland
  • .cologne
  • .immobilien
  • .computer
  • .international
  • .website

So, I believe the restriction isn’t quite correct.
A domain suffix is an official label that, as far as I know,
can theoretically be up to 63 octets as long as it doesn’t
exceed 255 octets for the entire domain.

Correct me if you have other information, but in that case,
the regex should look like this:
/^(?=.{1,253}$)(?!-)([a-z0-9-]{1,63}(?<!-)\.)+[a-z]{2,63}$/iu;

Anyway, roughly speaking, I’ll take another look later so we can
also cover this better with tests.

@JacKyDev
Copy link

JacKyDev commented Nov 5, 2024

I also did some additional research and couldn’t find anything that limits it to 6 characters.

From my understanding, that would mean the regex should match as specified. Do you have anything that suggests otherwise?

In the meantime, I ran your tests, and apart from the one mentioned below, all tests are passing. The failing one makes sense, though, since a TLD could now be longer than 6 characters after the change.

test("should not match 'example.commmerce' - TLD too long (more than 6 characters)", () => {
    expectActionIssue(action, baseIssue, ['example.commmerce']);
});
	  

Fun fact: there’s also a .shopping TLD, which is offered for ecommerce

If you don’t find anything else, then the regex should be all set, and the next step would be to get feedback from Fabian hopefully, we’ve managed to save him some work :)

@paksa92
Copy link
Author

paksa92 commented Nov 11, 2024

You're right @JacKyDev, there is no limit of 6 characters on the TLD. My assumption was based on some stackoverflow posts I've seen.

I think the regex is all set now! I will commit it today :)

@tats-u
Copy link

tats-u commented Nov 11, 2024

^(?=.{1,253}$) references the number of UTF-32 code points if u flag is added. length property of the input string references UTF-16.
I don't know which is better for domain validation.
If both can be used, I recommend to check length first to avoid cheking the regex for too long strings.

/^(?=.{1,253})/u.test("😀".repeat(253)) yields true while "😀".repeat(253).length <= 253 yields false.

@JacKyDev
Copy link

@tats-u : I understand what you mean, but when I test this regex with emojis or other Unicode characters, it consistently flags them as invalid. This aligns with what I expected, since domains technically only allow ASCII characters. However, I can't replicate what you're describing in a unit test—here, all tests fail as soon as characters outside of a-z, 0-9, or - are included. So, I'm not entirely sure what you're aiming for.

From a technical standpoint, this is exactly what I'd expect, as any characters outside the ASCII range should be represented in Punycode to be valid in a domain. Converting to Punycode means that an emoji, for example, doesn't count as a single character, but as multiple ASCII characters, which would also increase the string length—especially since Punycode includes a prefix.

Do you have a test example we can try? Currently, emojis and special characters like "ü" are rejected, just as intended.

@tats-u
Copy link

tats-u commented Nov 11, 2024

since domains technically only allow ASCII characters. However, I can't replicate what you're describing in a unit test—here, all tests fail as soon as characters outside of a-z, 0-9, or - are included.

If so we don't have to care about surrogate pairs. You designed this validator on the premise that we need to make Unicode domains punycoded in advance.
I was reminded that users care about bundle size. The current regex is small compared to code using length property and sufficient.

@JacKyDev
Copy link

I think we’re in a good place by strictly requiring specific values... This way, the domain itself is validated very precisely. And we wouldn’t be offering any special exceptions. Separately, one could consider adding a toPunyCode action in the future to initially standardize a value, making it usable for everyone. This approach, I think, creates more potential.

I wouldn’t really be against the additional check, because you’re right—it’s not much more code.

I still lack experience with the process here, as a regex action might feel messy if it includes code checks. But a length Check ist really small... :)
@fabian-hiller could probably answer that better, I think.

@tats-u
Copy link

tats-u commented Nov 12, 2024

toPunyCode

I agree with it. There are 2 strategies:

  1. Test the return value of toPunyCode by the regex, then pass the return value of toPunyCode to the return value of parse
  2. Test the return value of toPunyCode by the regex, then pass the input to the return value of parse as is

Users pass the punycoded domains to other libraries in 1. while they utilize unpunycoded Unicode domains as are.

@JacKyDev
Copy link

JacKyDev commented Nov 12, 2024

I think in the end it might need to be more of a combination
of all options to cover every use case. :)

domainName validates a string as a real domain as the status quo.
There’s an additional action, punycodeDomainName, which implements
Strategy 2.
toPunyCode converts a domain, enabling both options 1 and 2.

Background:

Point 1:
Only valid domains, excluding Punycode.

Point 2:
The action converts the string into a Punycode string, validates
it against the domain, but still returns the specified domain as
the result.

Basically, as you described, it’s for cases where someone wants
to pass on the original domain without processing it into Punycode.

Point 3:
For someone validating a domain for Netscaler processes,
they would likely use the following function:

domainSchema = v.pipe(v.string(), v.toPunyCode(), v.domainName())

The aim is to check the actual domain in DNS or another service.

This approach would cover all use cases, and if someone wants
to use toPunyCode for other applications, like email, that would also work.

My suggestion would be to rename the current action to rfcDomain,
so we can later introduce punycodeDomain and toPunyCode to handle
further use cases. I’d like some feedback from Fabian on this.
Then we could finish up the current work, and consider
developing the following actions as potential future features:

  • toPunycode()
  • punycodeDomain()
  • fromPunycode() – maybe to enable the option of converting Punycode back as well.

The advantage here would be that we offer a variety of options,
covering all use cases comprehensively. Additionally,
this approach would still support tree-shaking and other optimizations.

@fabian-hiller
Copy link
Owner

Hey 👋 I am pretty busy at the moment. However, I will try to give you feedback this week to point you in the right direction if necessary. Thanks for your contribution!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants