diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 6f364e3..b7a844b 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -32,7 +32,7 @@ jobs:
- name: Install Dependencies
run: |
pnpm i --frozen-lockfile
- npm install -g @microsoft/api-documenter
+ npm install -g @microsoft/api-documenter@7.23.38
npm install -g @microsoft/api-extractor
- name: Check API report
diff --git a/apps/docs/.gitignore b/apps/docs/.gitignore
new file mode 100644
index 0000000..85145f2
--- /dev/null
+++ b/apps/docs/.gitignore
@@ -0,0 +1,4 @@
+.vitepress/dist
+.vitepress/cache
+.vitepress/.temp
+api/*.md
\ No newline at end of file
diff --git a/apps/docs/.vitepress/config.mts b/apps/docs/.vitepress/config.mts
index ef15121..631fd64 100644
--- a/apps/docs/.vitepress/config.mts
+++ b/apps/docs/.vitepress/config.mts
@@ -14,7 +14,7 @@ export default defineConfig({
// https://vitepress.dev/reference/default-theme-config
nav: [
{ text: "Home", link: "/" },
- { text: "Packages", link: "/packages" },
+ { text: "Examples", link: "/examples" },
{ text: "API", link: "/api" },
],
@@ -28,8 +28,12 @@ export default defineConfig({
},
sidebar: {
- "/packages/": scanDir("packages"),
+ "/examples/": scanDir("examples"),
"/api/": scanDir("api"),
},
},
+ ignoreDeadLinks: [
+ // ignore all localhost links
+ /^https?:\/\/localhost/,
+ ],
});
diff --git a/apps/docs/api/index.md b/apps/docs/api/index.md
deleted file mode 100644
index e6216ea..0000000
--- a/apps/docs/api/index.md
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-[Home](./index.md)
-
-## API Reference
-
-## Packages
-
-
-
-Package
-
-
-
-
-Description
-
-
-
-
-
-[@opengovsg/starter-kitty-validators](./starter-kitty-validators.md)
-
-
-
-
-A set of validators that provide sensible defaults to prevent common security vulnerabilities.
-
-
-
-
diff --git a/apps/docs/api/starter-kitty-validators.md b/apps/docs/api/starter-kitty-validators.md
deleted file mode 100644
index f8100d3..0000000
--- a/apps/docs/api/starter-kitty-validators.md
+++ /dev/null
@@ -1,81 +0,0 @@
-
-
-[Home](./index.md) > [@opengovsg/starter-kitty-validators](./starter-kitty-validators.md)
-
-## starter-kitty-validators package
-
-A set of validators that provide sensible defaults to prevent common security vulnerabilities.
-
-## Classes
-
-
-
-Class
-
-
-
-
-Description
-
-
-
-
-
-[OptionsError](./starter-kitty-validators.optionserror.md)
-
-
-
-
-Invalid options error.
-
-
-
-
-
-[UrlValidationError](./starter-kitty-validators.urlvalidationerror.md)
-
-
-
-
-Invalid URL error.
-
-
-
-
-
-[UrlValidator](./starter-kitty-validators.urlvalidator.md)
-
-
-
-
-Validates URLs against a whitelist of allowed protocols and hostnames, preventing open redirects, XSS, SSRF, and other security vulnerabilities.
-
-
-
-
-
-## Interfaces
-
-
-
-Interface
-
-
-
-
-Description
-
-
-
-
-
-[Options](./starter-kitty-validators.options.md)
-
-
-
-
-The options to use for URL validation.
-
-
-
-
diff --git a/apps/docs/api/starter-kitty-validators.options.baseorigin.md b/apps/docs/api/starter-kitty-validators.options.baseorigin.md
deleted file mode 100644
index 5a4fa93..0000000
--- a/apps/docs/api/starter-kitty-validators.options.baseorigin.md
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-[Home](./index.md) > [@opengovsg/starter-kitty-validators](./starter-kitty-validators.md) > [Options](./starter-kitty-validators.options.md) > [baseOrigin](./starter-kitty-validators.options.baseorigin.md)
-
-## Options.baseOrigin property
-
-The base origin to use for relative URLs. If no base origin is provided, relative URLs will be considered invalid. An origin does not include the path or query parameters. For example, a valid base origin is `https://example.com` or `http://localhost:3000`.
-
-**Signature:**
-
-```typescript
-baseOrigin?: string;
-```
diff --git a/apps/docs/api/starter-kitty-validators.options.md b/apps/docs/api/starter-kitty-validators.options.md
deleted file mode 100644
index 6b63b0f..0000000
--- a/apps/docs/api/starter-kitty-validators.options.md
+++ /dev/null
@@ -1,76 +0,0 @@
-
-
-[Home](./index.md) > [@opengovsg/starter-kitty-validators](./starter-kitty-validators.md) > [Options](./starter-kitty-validators.options.md)
-
-## Options interface
-
-The options to use for URL validation.
-
-**Signature:**
-
-```typescript
-export interface Options
-```
-
-## Properties
-
-
-
-Property
-
-
-
-
-Modifiers
-
-
-
-
-Type
-
-
-
-
-Description
-
-
-
-
-
-[baseOrigin?](./starter-kitty-validators.options.baseorigin.md)
-
-
-
-
-
-
-
-string
-
-
-
-
-_(Optional)_ The base origin to use for relative URLs. If no base origin is provided, relative URLs will be considered invalid. An origin does not include the path or query parameters. For example, a valid base origin is `https://example.com` or `http://localhost:3000`.
-
-
-
-
-
-[whitelist?](./starter-kitty-validators.options.whitelist.md)
-
-
-
-
-
-
-
-Whitelist
-
-
-
-
-_(Optional)_ The list of allowed protocols and hostnames for URL validation. The default whitelist allows only `http` and `https` protocols, and does not restrict hostnames.
-
-
-
-
diff --git a/apps/docs/api/starter-kitty-validators.options.whitelist.md b/apps/docs/api/starter-kitty-validators.options.whitelist.md
deleted file mode 100644
index a4bc980..0000000
--- a/apps/docs/api/starter-kitty-validators.options.whitelist.md
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-[Home](./index.md) > [@opengovsg/starter-kitty-validators](./starter-kitty-validators.md) > [Options](./starter-kitty-validators.options.md) > [whitelist](./starter-kitty-validators.options.whitelist.md)
-
-## Options.whitelist property
-
-The list of allowed protocols and hostnames for URL validation. The default whitelist allows only `http` and `https` protocols, and does not restrict hostnames.
-
-**Signature:**
-
-```typescript
-whitelist?: Whitelist;
-```
-
-## Example
-
-
-```
-{
- protocols: ['http', 'https'],
- hosts: ['example.com']
-}
-```
-
diff --git a/apps/docs/api/starter-kitty-validators.optionserror._constructor_.md b/apps/docs/api/starter-kitty-validators.optionserror._constructor_.md
deleted file mode 100644
index 8f66c96..0000000
--- a/apps/docs/api/starter-kitty-validators.optionserror._constructor_.md
+++ /dev/null
@@ -1,47 +0,0 @@
-
-
-[Home](./index.md) > [@opengovsg/starter-kitty-validators](./starter-kitty-validators.md) > [OptionsError](./starter-kitty-validators.optionserror.md) > [(constructor)](./starter-kitty-validators.optionserror._constructor_.md)
-
-## OptionsError.(constructor)
-
-Constructs a new instance of the `OptionsError` class
-
-**Signature:**
-
-```typescript
-constructor(message: string);
-```
-
-## Parameters
-
-
-
-Parameter
-
-
-
-
-Type
-
-
-
-
-Description
-
-
-
-
-
-message
-
-
-
-
-string
-
-
-
-
-
-
-
diff --git a/apps/docs/api/starter-kitty-validators.optionserror.md b/apps/docs/api/starter-kitty-validators.optionserror.md
deleted file mode 100644
index d39f31d..0000000
--- a/apps/docs/api/starter-kitty-validators.optionserror.md
+++ /dev/null
@@ -1,48 +0,0 @@
-
-
-[Home](./index.md) > [@opengovsg/starter-kitty-validators](./starter-kitty-validators.md) > [OptionsError](./starter-kitty-validators.optionserror.md)
-
-## OptionsError class
-
-Invalid options error.
-
-**Signature:**
-
-```typescript
-export declare class OptionsError extends Error
-```
-**Extends:** Error
-
-## Constructors
-
-
-
-Constructor
-
-
-
-
-Modifiers
-
-
-
-
-Description
-
-
-
-
-
-[(constructor)(message)](./starter-kitty-validators.optionserror._constructor_.md)
-
-
-
-
-
-
-
-Constructs a new instance of the `OptionsError` class
-
-
-
-
diff --git a/apps/docs/api/starter-kitty-validators.urlvalidationerror._constructor_.md b/apps/docs/api/starter-kitty-validators.urlvalidationerror._constructor_.md
deleted file mode 100644
index ab28cc0..0000000
--- a/apps/docs/api/starter-kitty-validators.urlvalidationerror._constructor_.md
+++ /dev/null
@@ -1,47 +0,0 @@
-
-
-[Home](./index.md) > [@opengovsg/starter-kitty-validators](./starter-kitty-validators.md) > [UrlValidationError](./starter-kitty-validators.urlvalidationerror.md) > [(constructor)](./starter-kitty-validators.urlvalidationerror._constructor_.md)
-
-## UrlValidationError.(constructor)
-
-Constructs a new instance of the `UrlValidationError` class
-
-**Signature:**
-
-```typescript
-constructor(message: string);
-```
-
-## Parameters
-
-
-
-Parameter
-
-
-
-
-Type
-
-
-
-
-Description
-
-
-
-
-
-message
-
-
-
-
-string
-
-
-
-
-
-
-
diff --git a/apps/docs/api/starter-kitty-validators.urlvalidationerror.md b/apps/docs/api/starter-kitty-validators.urlvalidationerror.md
deleted file mode 100644
index cc776c3..0000000
--- a/apps/docs/api/starter-kitty-validators.urlvalidationerror.md
+++ /dev/null
@@ -1,48 +0,0 @@
-
-
-[Home](./index.md) > [@opengovsg/starter-kitty-validators](./starter-kitty-validators.md) > [UrlValidationError](./starter-kitty-validators.urlvalidationerror.md)
-
-## UrlValidationError class
-
-Invalid URL error.
-
-**Signature:**
-
-```typescript
-export declare class UrlValidationError extends Error
-```
-**Extends:** Error
-
-## Constructors
-
-
-
-Constructor
-
-
-
-
-Modifiers
-
-
-
-
-Description
-
-
-
-
-
-[(constructor)(message)](./starter-kitty-validators.urlvalidationerror._constructor_.md)
-
-
-
-
-
-
-
-Constructs a new instance of the `UrlValidationError` class
-
-
-
-
diff --git a/apps/docs/api/starter-kitty-validators.urlvalidator._constructor_.md b/apps/docs/api/starter-kitty-validators.urlvalidator._constructor_.md
deleted file mode 100644
index 7c83b4d..0000000
--- a/apps/docs/api/starter-kitty-validators.urlvalidator._constructor_.md
+++ /dev/null
@@ -1,63 +0,0 @@
-
-
-[Home](./index.md) > [@opengovsg/starter-kitty-validators](./starter-kitty-validators.md) > [UrlValidator](./starter-kitty-validators.urlvalidator.md) > [(constructor)](./starter-kitty-validators.urlvalidator._constructor_.md)
-
-## UrlValidator.(constructor)
-
-Creates a new UrlValidator instance. If no options are provided, the validator will use the default options:
-
-```ts
-{
- whitelist: {
- protocols: ['http', 'https'],
- },
-}
-```
-In most cases, you should provide your own whitelist with a list of allowed hostnames to prevent open redirects.
-
-**Signature:**
-
-```typescript
-constructor(options?: Options);
-```
-
-## Parameters
-
-
-
-Parameter
-
-
-
-
-Type
-
-
-
-
-Description
-
-
-
-
-
-options
-
-
-
-
-[Options](./starter-kitty-validators.options.md)
-
-
-
-
-_(Optional)_ The options to use for validation
-
-
-
-
-
-## Exceptions
-
-[OptionsError](./starter-kitty-validators.optionserror.md) If the options are invalid
-
diff --git a/apps/docs/api/starter-kitty-validators.urlvalidator.md b/apps/docs/api/starter-kitty-validators.urlvalidator.md
deleted file mode 100644
index 176e257..0000000
--- a/apps/docs/api/starter-kitty-validators.urlvalidator.md
+++ /dev/null
@@ -1,90 +0,0 @@
-
-
-[Home](./index.md) > [@opengovsg/starter-kitty-validators](./starter-kitty-validators.md) > [UrlValidator](./starter-kitty-validators.urlvalidator.md)
-
-## UrlValidator class
-
-Validates URLs against a whitelist of allowed protocols and hostnames, preventing open redirects, XSS, SSRF, and other security vulnerabilities.
-
-**Signature:**
-
-```typescript
-export declare class UrlValidator
-```
-
-## Constructors
-
-
-
-Constructor
-
-
-
-
-Modifiers
-
-
-
-
-Description
-
-
-
-
-
-[(constructor)(options)](./starter-kitty-validators.urlvalidator._constructor_.md)
-
-
-
-
-
-
-
-Creates a new UrlValidator instance. If no options are provided, the validator will use the default options:
-
-```ts
-{
- whitelist: {
- protocols: ['http', 'https'],
- },
-}
-```
-In most cases, you should provide your own whitelist with a list of allowed hostnames to prevent open redirects.
-
-
-
-
-
-## Methods
-
-
-
-Method
-
-
-
-
-Modifiers
-
-
-
-
-Description
-
-
-
-
-
-[parse(url)](./starter-kitty-validators.urlvalidator.parse.md)
-
-
-
-
-
-
-
-Parses a URL string.
-
-
-
-
diff --git a/apps/docs/api/starter-kitty-validators.urlvalidator.parse.md b/apps/docs/api/starter-kitty-validators.urlvalidator.parse.md
deleted file mode 100644
index e277a22..0000000
--- a/apps/docs/api/starter-kitty-validators.urlvalidator.parse.md
+++ /dev/null
@@ -1,59 +0,0 @@
-
-
-[Home](./index.md) > [@opengovsg/starter-kitty-validators](./starter-kitty-validators.md) > [UrlValidator](./starter-kitty-validators.urlvalidator.md) > [parse](./starter-kitty-validators.urlvalidator.parse.md)
-
-## UrlValidator.parse() method
-
-Parses a URL string.
-
-**Signature:**
-
-```typescript
-parse(url: string): URL;
-```
-
-## Parameters
-
-
-
-Parameter
-
-
-
-
-Type
-
-
-
-
-Description
-
-
-
-
-
-url
-
-
-
-
-string
-
-
-
-
-The URL to validate
-
-
-
-
-**Returns:**
-
-URL
-
-The URL object if the URL is valid
-
-## Exceptions
-
-[UrlValidationError](./starter-kitty-validators.urlvalidationerror.md) If the URL is invalid
-
diff --git a/apps/docs/packages/index.md b/apps/docs/examples/index.md
similarity index 88%
rename from apps/docs/packages/index.md
rename to apps/docs/examples/index.md
index 93e0cc7..714bc9f 100644
--- a/apps/docs/packages/index.md
+++ b/apps/docs/examples/index.md
@@ -1,3 +1,3 @@
-# Packages
+# Examples
- [`@opengovsg/starter-kitty-validators`](./validators.md): Common input validators.
diff --git a/apps/docs/packages/validators.md b/apps/docs/examples/validators.md
similarity index 77%
rename from apps/docs/packages/validators.md
rename to apps/docs/examples/validators.md
index 7ee1775..941c7f1 100644
--- a/apps/docs/packages/validators.md
+++ b/apps/docs/examples/validators.md
@@ -6,7 +6,7 @@
npm i --save @opengovsg/starter-kitty-validators
```
-### Example
+## URL Validation
```javascript
import { UrlValidator } from '@opengovsg/starter-kitty-validators'
@@ -59,3 +59,20 @@ export const callbackUrlSchema = z
})
.catch(new URL(HOME, baseUrl))
```
+
+## Email Validation
+
+```javascript
+import { createEmailSchema } from '@opengovsg/starter-kitty-validators'
+
+const emailSchema = createEmailSchema({
+ domains: [{ domain: 'gov.sg', includeSubdomains: true }],
+})
+
+const formSchema = z.object({
+ name: z.string(),
+ email: emailSchema,
+})
+
+type FormValues = z.infer
+```
diff --git a/apps/docs/img/kitty.jpg b/apps/docs/img/kitty.jpg
new file mode 100644
index 0000000..fbedd60
Binary files /dev/null and b/apps/docs/img/kitty.jpg differ
diff --git a/apps/docs/index.md b/apps/docs/index.md
index 4622c3c..864147e 100644
--- a/apps/docs/index.md
+++ b/apps/docs/index.md
@@ -1,3 +1,14 @@
# @opengovsg/starter-kitty
Common app components that are safe-by-default.
+
+## Why `starter-kitty`?
+
+Application security is hard. There are often many ways to get it wrong, and it's easy to make
+mistakes when you're trying to ship features quickly. This package provides a set of components that
+are safe-by-default, so you can focus on building your app without worrying about common security
+footguns.
+
+With `starter-kitty`, you can sleep easier at night. Just like him:
+
+![Cute Kitty](./img/kitty.jpg)
diff --git a/etc/starter-kitty-validators.api.md b/etc/starter-kitty-validators.api.md
index cd35afd..cd2a5a5 100644
--- a/etc/starter-kitty-validators.api.md
+++ b/etc/starter-kitty-validators.api.md
@@ -7,12 +7,17 @@
///
import { z } from 'zod';
+import { ZodSchema } from 'zod';
// @public
-export interface Options {
- baseOrigin?: string;
- // Warning: (ae-forgotten-export) The symbol "Whitelist" needs to be exported by the entry point index.d.ts
- whitelist?: Whitelist;
+export const createEmailSchema: (options?: EmailValidatorOptions) => ZodSchema;
+
+// @public
+export interface EmailValidatorOptions {
+ domains?: {
+ domain: string;
+ includeSubdomains?: boolean;
+ }[];
}
// @public
@@ -27,8 +32,15 @@ export class UrlValidationError extends Error {
// @public
export class UrlValidator {
- constructor(options?: Options);
+ constructor(options?: UrlValidatorOptions);
parse(url: string): URL;
}
+// @public
+export interface UrlValidatorOptions {
+ baseOrigin?: string;
+ // Warning: (ae-forgotten-export) The symbol "UrlValidatorWhitelist" needs to be exported by the entry point index.d.ts
+ whitelist?: UrlValidatorWhitelist;
+}
+
```
diff --git a/packages/validators/README.md b/packages/validators/README.md
deleted file mode 100644
index fbd9307..0000000
--- a/packages/validators/README.md
+++ /dev/null
@@ -1,99 +0,0 @@
-# Validators
-
-This package provides a set of validators that provide sensible defaults to prevent common security vulnerabilities.
-
-## Installation
-
-```bash
-npm i --save @opengovsg/starter-kitty-validators
-```
-
-### Example
-
-```javascript
-import { UrlValidator } from '@opengovsg/starter-kitty-validators'
-
-const validator = new UrlValidator({
- whitelist: {
- protocols: ['http', 'https', 'mailto'],
- hosts: ['open.gov.sg'],
- },
-})
-```
-
-Validating a post-login redirect URL provided in a query parameter:
-
-```javascript
-try {
- router.push(validator.parse(redirectUrl))
-} catch (error) {
- router.push('/home')
-}
-```
-
-Consider using the validator as part of a Zod schema to validate the URL and fall back to a default URL if the URL is invalid.
-
-```javascript
-const baseUrl = getBaseUrl()
-
-const validator = new UrlValidator({
- baseOrigin: new URL(baseUrl).origin,
- whitelist: {
- protocols: ['http', 'https'],
- hosts: [new URL(baseUrl).host],
- },
-})
-
-export const callbackUrlSchema = z
- .string()
- .optional()
- .default(HOME)
- .transform((url, ctx) => {
- try {
- return validator.parse(url)
- } catch (error) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: (error as Error).message,
- })
- return z.NEVER
- }
- })
- .catch(new URL(HOME, baseUrl))
-```
-
-## Class: `UrlValidator`
-
-Validates URLs against a whitelist of allowed protocols and hostnames, preventing open redirects, XSS, SSRF, and other security vulnerabilities.
-
-### new UrlValidator(options)
-
-`options?`: ``
-
-- `baseOrigin?`: `` - The base origin to use for relative URLs. If no base origin is provided, relative URLs will be considered invalid.
-
- An origin does not include the path or query parameters. For example, a valid base origin is `https://example.com` or `http://localhost:3000`.
-
-- `whitelist?`: ``
- - `protocols`: `` - A list of allowed protocols.
-
- **Caution: allowing `javascript` or `data` protocols can lead to XSS vulnerabilities.**
- - `hostnames`: `` - A list of allowed hostnames. If no hostnames are provided, the validator will allow any hostname.
-
- **It is recommended to provide a list of allowed hostnames to prevent open redirects.**
-
-If no options are provided, the validator will use the default options:
-
-```javascript
-{
- whitelist: {
- protocols: ['http', 'https'],
- },
-}
-```
-
-### UrlValidator.parse(url)
-
-- `url`: `` - The URL to parse.
-- Returns: `` - The parsed URL object.
-- Throws: `` - If the URL is invalid or unsafe.
diff --git a/packages/validators/package.json b/packages/validators/package.json
index 8b4da97..94c1df3 100644
--- a/packages/validators/package.json
+++ b/packages/validators/package.json
@@ -1,6 +1,6 @@
{
"name": "@opengovsg/starter-kitty-validators",
- "version": "1.0.2",
+ "version": "1.1.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
@@ -15,6 +15,7 @@
"ci:report": "api-extractor run --verbose"
},
"dependencies": {
+ "email-addresses": "^5.0.0",
"zod": "^3.23.8",
"zod-validation-error": "^3.3.0"
},
diff --git a/packages/validators/src/__tests__/email.test.ts b/packages/validators/src/__tests__/email.test.ts
new file mode 100644
index 0000000..80c8232
--- /dev/null
+++ b/packages/validators/src/__tests__/email.test.ts
@@ -0,0 +1,144 @@
+import { describe, expect, it } from 'vitest'
+import { ZodError } from 'zod'
+
+import { OptionsError } from '@/common/errors'
+import { createEmailSchema } from '@/index'
+
+describe('EmailValidator with default options', () => {
+ const schema = createEmailSchema()
+
+ it('should parse a valid email', () => {
+ expect(schema.parse('zeyu@open.gov.sg')).toBe('zeyu@open.gov.sg')
+
+ // https://en.wikipedia.org/wiki/Email_address
+ expect(schema.parse('simple@example.com')).toBe('simple@example.com')
+ expect(schema.parse('FirstName.LastName@EasierReading.org')).toBe(
+ 'FirstName.LastName@EasierReading.org',
+ )
+ expect(schema.parse('x@example.com')).toBe('x@example.com')
+ expect(
+ schema.parse(
+ 'long.email-address-with-hyphens@and.subdomains.example.com',
+ ),
+ ).toBe('long.email-address-with-hyphens@and.subdomains.example.com')
+ expect(schema.parse('user.name+tag+sorting@example.com')).toBe(
+ 'user.name+tag+sorting@example.com',
+ )
+ expect(schema.parse('user-@example.org')).toBe('user-@example.org')
+ })
+
+ it('should throw an error for an invalid email', () => {
+ expect(() => schema.parse('zeyu@open')).toThrowError(ZodError)
+
+ // https://en.wikipedia.org/wiki/Email_address
+ expect(() => schema.parse('a@b@c@example.com')).toThrowError(ZodError)
+ expect(() =>
+ schema.parse('a"b(c)d,e:f;gi[jk]l@example.com'),
+ ).toThrowError(ZodError)
+ expect(() => schema.parse('just"not"right@example.com')).toThrowError(
+ ZodError,
+ )
+ expect(() => schema.parse('this is"notallowed@example.com')).toThrowError(
+ ZodError,
+ )
+ expect(() =>
+ schema.parse('this\\ still\\"not\\\\allowed@example.com'),
+ ).toThrowError(ZodError)
+ expect(() =>
+ schema.parse(
+ '1234567890123456789012345678901234567890123456789012345678901234+x@example.com',
+ ),
+ ).toThrowError(ZodError)
+ expect(() =>
+ schema.parse(
+ 'i.like.underscores@but_they_are_not_allowed_in_this_part.example.com',
+ ),
+ ).toThrowError(ZodError)
+
+ // We are stricter than RFC 5322, and disallow some edge cases:
+ // Quoted strings
+ expect(() => schema.parse('" "@example.org')).toThrowError(ZodError)
+ expect(() => schema.parse('"john..doe"@example.org')).toThrowError(ZodError)
+ // Bangified host route
+ expect(() => schema.parse('mailhost!username@example.org')).toThrowError(
+ ZodError,
+ )
+ expect(() =>
+ schema.parse(
+ '"very.(),:;<>[]\\".VERY.\\"very@\\\\ \\"very\\".unusual"@strange.example.com',
+ ),
+ ).toThrowError(ZodError)
+ // %-escaped mail route
+ expect(() => schema.parse('user%example.com@example.org')).toThrowError(
+ ZodError,
+ )
+ // IP address instead of domain
+ expect(() => schema.parse('postmaster@[123.123.123.123]')).toThrowError(
+ ZodError,
+ )
+ })
+
+ it('should clean up unnecessary whitespace', () => {
+ const email = schema.parse(' zeyu@open.gov.sg ')
+ expect(email).toBe('zeyu@open.gov.sg')
+ })
+})
+
+describe('EmailValidator that allows subdomains', () => {
+ const schema = createEmailSchema({
+ domains: [{ domain: 'gov.sg', includeSubdomains: true }],
+ })
+
+ it('should allow a valid subdomain', () => {
+ expect(() => schema.parse('zeyu@open.gov.sg')).not.toThrow()
+ })
+
+ it('should allow a valid subdomain with multiple levels', () => {
+ expect(() => schema.parse('zeyu@a.b.c.d.gov.sg')).not.toThrow()
+ })
+
+ it('should throw an error for an invalid subdomain', () => {
+ expect(() => schema.parse('zeyu@edu.sg')).toThrowError(ZodError)
+ })
+})
+
+describe('EmailValidator that disallows subdomains', () => {
+ const schema = createEmailSchema({
+ domains: [{ domain: 'gov.sg', includeSubdomains: false }],
+ })
+
+ it('should allow a valid domain', () => {
+ expect(() => schema.parse('zeyu@gov.sg')).not.toThrow()
+ })
+
+ it('should throw an error for a subdomain', () => {
+ expect(() => schema.parse('zeyu@open.gov.sg')).toThrowError(ZodError)
+ })
+})
+
+describe('EmailValidator that disallows subdomains (by default)', () => {
+ const schema = createEmailSchema({
+ domains: [{ domain: 'gov.sg' }],
+ })
+
+ it('should allow a valid domain', () => {
+ expect(() => schema.parse('zeyu@gov.sg')).not.toThrow()
+ })
+
+ it('should throw an error for a subdomain', () => {
+ expect(() => schema.parse('zeyu@open.gov.sg')).toThrowError(ZodError)
+ })
+})
+
+describe('EmailValidator with invalid options', () => {
+ it('should throw an error for invalid options', () => {
+ // @ts-expect-error Testing invalid options
+ expect(() => createEmailSchema({ domains: ['gov.sg'] })).toThrowError(
+ OptionsError,
+ )
+ })
+
+ it('should not throw an error when missing options', () => {
+ expect(() => createEmailSchema()).not.toThrow()
+ })
+})
diff --git a/packages/validators/src/__tests__/url.test.ts b/packages/validators/src/__tests__/url.test.ts
index 8551ac3..76308a9 100644
--- a/packages/validators/src/__tests__/url.test.ts
+++ b/packages/validators/src/__tests__/url.test.ts
@@ -1,7 +1,8 @@
import { describe, expect, it } from 'vitest'
-import { UrlValidator } from '@/url'
-import { OptionsError, UrlValidationError } from '@/url/errors'
+import { OptionsError } from '@/common/errors'
+import { UrlValidator } from '@/index'
+import { UrlValidationError } from '@/url/errors'
describe('UrlValidator with default options', () => {
const validator = new UrlValidator()
diff --git a/packages/validators/src/common/errors.ts b/packages/validators/src/common/errors.ts
new file mode 100644
index 0000000..c5573f2
--- /dev/null
+++ b/packages/validators/src/common/errors.ts
@@ -0,0 +1,11 @@
+/**
+ * Invalid options error.
+ *
+ * @public
+ */
+export class OptionsError extends Error {
+ constructor(message: string) {
+ super(message)
+ this.name = 'OptionsError'
+ }
+}
diff --git a/packages/validators/src/email/consts.ts b/packages/validators/src/email/consts.ts
new file mode 100644
index 0000000..be1b1ca
--- /dev/null
+++ b/packages/validators/src/email/consts.ts
@@ -0,0 +1,2 @@
+export const MAX_LOCAL_LENGTH = 64
+export const MAX_DOMAIN_LENGTH = 255
diff --git a/packages/validators/src/email/index.ts b/packages/validators/src/email/index.ts
new file mode 100644
index 0000000..4c4cbd1
--- /dev/null
+++ b/packages/validators/src/email/index.ts
@@ -0,0 +1,25 @@
+import { ZodSchema } from 'zod'
+import { fromError } from 'zod-validation-error'
+
+import { OptionsError } from '@/common/errors'
+import { EmailValidatorOptions, optionsSchema } from '@/email/options'
+import { toSchema } from '@/email/schema'
+
+/**
+ * Create a schema that validates emails against RFC 5322 and a whitelist of domains.
+ *
+ * @param options - The options to use for validation
+ * @throws {@link OptionsError} If the options are invalid
+ * @returns A Zod schema that validates emails according to the provided options
+ *
+ * @public
+ */
+export const createEmailSchema = (
+ options: EmailValidatorOptions = {},
+): ZodSchema => {
+ const result = optionsSchema.safeParse(options)
+ if (result.success) {
+ return toSchema(result.data)
+ }
+ throw new OptionsError(fromError(result.error).toString())
+}
diff --git a/packages/validators/src/email/options.ts b/packages/validators/src/email/options.ts
new file mode 100644
index 0000000..3b42e3d
--- /dev/null
+++ b/packages/validators/src/email/options.ts
@@ -0,0 +1,39 @@
+import { z } from 'zod'
+
+/**
+ * The options to use for email validation.
+ *
+ * @public
+ */
+export interface EmailValidatorOptions {
+ /**
+ * The list of allowed domains for the domain part of the email address.
+ * If not provided, all domains are allowed.
+ *
+ * Each whitelisted domain must be specified as an object with the `domain`
+ * and `includeSubdomains` properties. By default, `includeSubdomains` is `false`,
+ * meaning only the exact domain is allowed. If `includeSubdomains` is `true`, all
+ * subdomains of the domain are also allowed.
+ *
+ * @example
+ * ```javascript
+ * [ { domain: 'gov.sg', includeSubdomains: true } ]
+ * ```
+ *
+ * This will allow `gov.sg` and all subdomains of `gov.sg`, such as `open.gov.sg`.
+ */
+ domains?: { domain: string; includeSubdomains?: boolean }[]
+}
+
+export const optionsSchema = z.object({
+ domains: z
+ .array(
+ z.object({
+ domain: z.string(),
+ includeSubdomains: z.boolean().optional().default(false),
+ }),
+ )
+ .default([]),
+})
+
+export type ParsedEmailValidatorOptions = z.infer
diff --git a/packages/validators/src/email/schema.ts b/packages/validators/src/email/schema.ts
new file mode 100644
index 0000000..3f6d42e
--- /dev/null
+++ b/packages/validators/src/email/schema.ts
@@ -0,0 +1,32 @@
+import { ParsedMailbox, parseOneAddress } from 'email-addresses'
+import { z } from 'zod'
+
+import { ParsedEmailValidatorOptions } from '@/email/options'
+import { isValidEmail } from '@/email/utils'
+
+const createValidationSchema = (options: ParsedEmailValidatorOptions) =>
+ z
+ .string()
+ .transform((email, ctx) => {
+ const parsed = parseOneAddress(email) as ParsedMailbox
+ if (!parsed) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: 'Invalid email address',
+ })
+ return z.NEVER
+ }
+ return parsed
+ })
+ .refine((parsed) => isValidEmail(parsed, options.domains), {
+ message: 'Domain not allowed',
+ })
+ .transform((parsed) => parsed.address)
+
+export const toSchema = (options: ParsedEmailValidatorOptions) => {
+ return z
+ .string()
+ .trim()
+ .email({ message: 'Invalid email address' })
+ .pipe(createValidationSchema(options))
+}
diff --git a/packages/validators/src/email/utils.ts b/packages/validators/src/email/utils.ts
new file mode 100644
index 0000000..9cae6a9
--- /dev/null
+++ b/packages/validators/src/email/utils.ts
@@ -0,0 +1,36 @@
+import { ParsedMailbox } from 'email-addresses'
+
+import { MAX_DOMAIN_LENGTH, MAX_LOCAL_LENGTH } from './consts'
+import { ParsedEmailValidatorOptions } from './options'
+
+export const isValidEmail = (
+ email: ParsedMailbox,
+ whitelisted: ParsedEmailValidatorOptions['domains'],
+) => {
+ const domain = email.domain
+ const local = email.local
+
+ if (local.length > MAX_LOCAL_LENGTH || domain.length > MAX_DOMAIN_LENGTH) {
+ return false
+ }
+
+ if (whitelisted.length === 0) {
+ return true
+ }
+ return isWhitelistedDomain(domain, whitelisted)
+}
+
+export const isWhitelistedDomain = (
+ domain: string,
+ whitelistedDomains: { domain: string; includeSubdomains: boolean }[],
+) => {
+ // Accept in the following cases:
+ // Case 1: The domain is an exact match of a whitelisted domain
+ // Case 2: The domain is a subdomain of a whitelisted domain, AND includeSubdomains is true
+ return whitelistedDomains.some(
+ (whitelisted) =>
+ domain === whitelisted.domain ||
+ (whitelisted.includeSubdomains &&
+ domain.endsWith(`.${whitelisted.domain}`)),
+ )
+}
diff --git a/packages/validators/src/index.ts b/packages/validators/src/index.ts
index 1356977..e04e6cb 100644
--- a/packages/validators/src/index.ts
+++ b/packages/validators/src/index.ts
@@ -4,6 +4,9 @@
* @packageDocumentation
*/
+export type * from '@/common/errors'
+export * from '@/email'
+export type { EmailValidatorOptions } from '@/email/options'
export * from '@/url'
export type * from '@/url/errors'
-export type { Options } from '@/url/options'
+export type { UrlValidatorOptions } from '@/url/options'
diff --git a/packages/validators/src/url/errors.ts b/packages/validators/src/url/errors.ts
index 4c3b59f..7dc47c9 100644
--- a/packages/validators/src/url/errors.ts
+++ b/packages/validators/src/url/errors.ts
@@ -1,15 +1,3 @@
-/**
- * Invalid options error.
- *
- * @public
- */
-export class OptionsError extends Error {
- constructor(message: string) {
- super(message)
- this.name = 'OptionsError'
- }
-}
-
/**
* Invalid URL error.
*
diff --git a/packages/validators/src/url/index.ts b/packages/validators/src/url/index.ts
index 16c2806..fbc4a22 100644
--- a/packages/validators/src/url/index.ts
+++ b/packages/validators/src/url/index.ts
@@ -1,12 +1,18 @@
import { ZodError } from 'zod'
import { fromError } from 'zod-validation-error'
-import { OptionsError, UrlValidationError } from '@/url/errors'
-import { defaultOptions, Options, optionsSchema } from '@/url/options'
+import { OptionsError } from '@/common/errors'
+import { UrlValidationError } from '@/url/errors'
+import {
+ defaultOptions,
+ optionsSchema,
+ UrlValidatorOptions,
+} from '@/url/options'
import { createUrlSchema } from '@/url/schema'
/**
- * Validates URLs against a whitelist of allowed protocols and hostnames, preventing open redirects, XSS, SSRF, and other security vulnerabilities.
+ * Parses URLs according to WHATWG standards and validates against a whitelist of allowed protocols and hostnames,
+ * preventing open redirects, XSS, SSRF, and other security vulnerabilities.
*
* @public
*/
@@ -31,7 +37,7 @@ export class UrlValidator {
*
* @public
*/
- constructor(options: Options = defaultOptions) {
+ constructor(options: UrlValidatorOptions = defaultOptions) {
const result = optionsSchema.safeParse({ ...defaultOptions, ...options })
if (result.success) {
this.schema = createUrlSchema(result.data)
diff --git a/packages/validators/src/url/options.ts b/packages/validators/src/url/options.ts
index 4907d91..cebdcd0 100644
--- a/packages/validators/src/url/options.ts
+++ b/packages/validators/src/url/options.ts
@@ -26,7 +26,7 @@ export const whitelistSchema = z.object({
*
* @public
*/
-export interface Options {
+export interface UrlValidatorOptions {
/**
* The base origin to use for relative URLs. If no base origin is provided, relative URLs will be considered invalid.
* An origin does not include the path or query parameters. For example, a valid base origin is `https://example.com` or `http://localhost:3000`.
@@ -44,7 +44,7 @@ export interface Options {
* }
* ```
* */
- whitelist?: Whitelist
+ whitelist?: UrlValidatorWhitelist
}
/**
@@ -52,7 +52,7 @@ export interface Options {
*
* @public
*/
-export type Whitelist = z.infer
+export type UrlValidatorWhitelist = z.infer
export const optionsSchema = z.object({
baseOrigin: z
@@ -77,4 +77,4 @@ export const optionsSchema = z.object({
whitelist: whitelistSchema,
})
-export type ParsedOptions = z.infer
+export type ParsedUrlValidatorOptions = z.infer
diff --git a/packages/validators/src/url/schema.ts b/packages/validators/src/url/schema.ts
index bf1f436..579fd31 100644
--- a/packages/validators/src/url/schema.ts
+++ b/packages/validators/src/url/schema.ts
@@ -1,9 +1,9 @@
import { z } from 'zod'
-import { ParsedOptions } from '@/url/options'
+import { ParsedUrlValidatorOptions } from '@/url/options'
import { isSafeUrl, resolveRelativeUrl } from '@/url/utils'
-export const createUrlSchema = (options: ParsedOptions) => {
+export const createUrlSchema = (options: ParsedUrlValidatorOptions) => {
return z
.string()
.transform((url) => resolveRelativeUrl(url, options.baseOrigin))
diff --git a/packages/validators/src/url/utils.ts b/packages/validators/src/url/utils.ts
index 3e48694..e3ee81a 100644
--- a/packages/validators/src/url/utils.ts
+++ b/packages/validators/src/url/utils.ts
@@ -1,5 +1,5 @@
import { UrlValidationError } from '@/url/errors'
-import { Whitelist } from '@/url/options'
+import { UrlValidatorWhitelist } from '@/url/options'
const DYNAMIC_ROUTE_SEGMENT_REGEX = /\[\[?([^\]]+)\]?\]/g
@@ -42,7 +42,7 @@ const resolveNextDynamicRoute = (url: URL): URL => {
return result
}
-export const isSafeUrl = (url: URL, whitelist: Whitelist) => {
+export const isSafeUrl = (url: URL, whitelist: UrlValidatorWhitelist) => {
// only allow whitelisted protocols
if (
!whitelist.protocols.some((protocol) => url.protocol === `${protocol}:`)
diff --git a/packages/validators/temp/starter-kitty-validators.api.json b/packages/validators/temp/starter-kitty-validators.api.json
index b175e8a..dfc3d73 100644
--- a/packages/validators/temp/starter-kitty-validators.api.json
+++ b/packages/validators/temp/starter-kitty-validators.api.json
@@ -172,33 +172,80 @@
"name": "",
"preserveMemberOrder": false,
"members": [
+ {
+ "kind": "Function",
+ "canonicalReference": "@opengovsg/starter-kitty-validators!createEmailSchema:function(1)",
+ "docComment": "/**\n * Create a schema that validates emails against RFC 5322 and a whitelist of domains.\n *\n * @param options - The options to use for validation\n *\n * @returns A Zod schema that validates emails according to the provided options\n *\n * @throws\n *\n * {@link OptionsError} If the options are invalid\n *\n * @public\n */\n",
+ "excerptTokens": [
+ {
+ "kind": "Content",
+ "text": "createEmailSchema: (options?: "
+ },
+ {
+ "kind": "Reference",
+ "text": "EmailValidatorOptions",
+ "canonicalReference": "@opengovsg/starter-kitty-validators!EmailValidatorOptions:interface"
+ },
+ {
+ "kind": "Content",
+ "text": ") => "
+ },
+ {
+ "kind": "Reference",
+ "text": "ZodSchema",
+ "canonicalReference": "zod!ZodType:class"
+ },
+ {
+ "kind": "Content",
+ "text": ""
+ }
+ ],
+ "fileUrlPath": "dist/email/index.d.ts",
+ "returnTypeTokenRange": {
+ "startIndex": 3,
+ "endIndex": 5
+ },
+ "releaseTag": "Public",
+ "overloadIndex": 1,
+ "parameters": [
+ {
+ "parameterName": "options",
+ "parameterTypeTokenRange": {
+ "startIndex": 1,
+ "endIndex": 2
+ },
+ "isOptional": true
+ }
+ ],
+ "name": "createEmailSchema"
+ },
{
"kind": "Interface",
- "canonicalReference": "@opengovsg/starter-kitty-validators!Options:interface",
- "docComment": "/**\n * The options to use for URL validation.\n *\n * @public\n */\n",
+ "canonicalReference": "@opengovsg/starter-kitty-validators!EmailValidatorOptions:interface",
+ "docComment": "/**\n * The options to use for email validation.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
- "text": "export interface Options "
+ "text": "export interface EmailValidatorOptions "
}
],
- "fileUrlPath": "dist/url/options.d.ts",
+ "fileUrlPath": "dist/email/options.d.ts",
"releaseTag": "Public",
- "name": "Options",
+ "name": "EmailValidatorOptions",
"preserveMemberOrder": false,
"members": [
{
"kind": "PropertySignature",
- "canonicalReference": "@opengovsg/starter-kitty-validators!Options#baseOrigin:member",
- "docComment": "/**\n * The base origin to use for relative URLs. If no base origin is provided, relative URLs will be considered invalid. An origin does not include the path or query parameters. For example, a valid base origin is `https://example.com` or `http://localhost:3000`.\n */\n",
+ "canonicalReference": "@opengovsg/starter-kitty-validators!EmailValidatorOptions#domains:member",
+ "docComment": "/**\n * The list of allowed domains for the domain part of the email address. If not provided, all domains are allowed.\n *\n * Each whitelisted domain must be specified as an object with the `domain` and `includeSubdomains` properties. By default, `includeSubdomains` is `false`, meaning only the exact domain is allowed. If `includeSubdomains` is `true`, all subdomains of the domain are also allowed.\n *\n * @example\n * ```javascript\n * [ { domain: 'gov.sg', includeSubdomains: true } ]\n * ```\n *\n * This will allow `gov.sg` and all subdomains of `gov.sg`, such as `open.gov.sg`.\n */\n",
"excerptTokens": [
{
"kind": "Content",
- "text": "baseOrigin?: "
+ "text": "domains?: "
},
{
"kind": "Content",
- "text": "string"
+ "text": "{\n domain: string;\n includeSubdomains?: boolean;\n }[]"
},
{
"kind": "Content",
@@ -208,35 +255,7 @@
"isReadonly": false,
"isOptional": true,
"releaseTag": "Public",
- "name": "baseOrigin",
- "propertyTypeTokenRange": {
- "startIndex": 1,
- "endIndex": 2
- }
- },
- {
- "kind": "PropertySignature",
- "canonicalReference": "@opengovsg/starter-kitty-validators!Options#whitelist:member",
- "docComment": "/**\n * The list of allowed protocols and hostnames for URL validation. The default whitelist allows only `http` and `https` protocols, and does not restrict hostnames.\n *\n * @example\n * ```\n * {\n * protocols: ['http', 'https'],\n * hosts: ['example.com']\n * }\n * ```\n *\n */\n",
- "excerptTokens": [
- {
- "kind": "Content",
- "text": "whitelist?: "
- },
- {
- "kind": "Reference",
- "text": "Whitelist",
- "canonicalReference": "@opengovsg/starter-kitty-validators!~Whitelist:type"
- },
- {
- "kind": "Content",
- "text": ";"
- }
- ],
- "isReadonly": false,
- "isOptional": true,
- "releaseTag": "Public",
- "name": "whitelist",
+ "name": "domains",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
@@ -264,7 +283,7 @@
"text": " "
}
],
- "fileUrlPath": "dist/url/errors.d.ts",
+ "fileUrlPath": "dist/common/errors.d.ts",
"releaseTag": "Public",
"isAbstract": false,
"name": "OptionsError",
@@ -376,7 +395,7 @@
{
"kind": "Class",
"canonicalReference": "@opengovsg/starter-kitty-validators!UrlValidator:class",
- "docComment": "/**\n * Validates URLs against a whitelist of allowed protocols and hostnames, preventing open redirects, XSS, SSRF, and other security vulnerabilities.\n *\n * @public\n */\n",
+ "docComment": "/**\n * Parses URLs according to WHATWG standards and validates against a whitelist of allowed protocols and hostnames, preventing open redirects, XSS, SSRF, and other security vulnerabilities.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
@@ -400,8 +419,8 @@
},
{
"kind": "Reference",
- "text": "Options",
- "canonicalReference": "@opengovsg/starter-kitty-validators!Options:interface"
+ "text": "UrlValidatorOptions",
+ "canonicalReference": "@opengovsg/starter-kitty-validators!UrlValidatorOptions:interface"
},
{
"kind": "Content",
@@ -473,6 +492,79 @@
}
],
"implementsTokenRanges": []
+ },
+ {
+ "kind": "Interface",
+ "canonicalReference": "@opengovsg/starter-kitty-validators!UrlValidatorOptions:interface",
+ "docComment": "/**\n * The options to use for URL validation.\n *\n * @public\n */\n",
+ "excerptTokens": [
+ {
+ "kind": "Content",
+ "text": "export interface UrlValidatorOptions "
+ }
+ ],
+ "fileUrlPath": "dist/url/options.d.ts",
+ "releaseTag": "Public",
+ "name": "UrlValidatorOptions",
+ "preserveMemberOrder": false,
+ "members": [
+ {
+ "kind": "PropertySignature",
+ "canonicalReference": "@opengovsg/starter-kitty-validators!UrlValidatorOptions#baseOrigin:member",
+ "docComment": "/**\n * The base origin to use for relative URLs. If no base origin is provided, relative URLs will be considered invalid. An origin does not include the path or query parameters. For example, a valid base origin is `https://example.com` or `http://localhost:3000`.\n */\n",
+ "excerptTokens": [
+ {
+ "kind": "Content",
+ "text": "baseOrigin?: "
+ },
+ {
+ "kind": "Content",
+ "text": "string"
+ },
+ {
+ "kind": "Content",
+ "text": ";"
+ }
+ ],
+ "isReadonly": false,
+ "isOptional": true,
+ "releaseTag": "Public",
+ "name": "baseOrigin",
+ "propertyTypeTokenRange": {
+ "startIndex": 1,
+ "endIndex": 2
+ }
+ },
+ {
+ "kind": "PropertySignature",
+ "canonicalReference": "@opengovsg/starter-kitty-validators!UrlValidatorOptions#whitelist:member",
+ "docComment": "/**\n * The list of allowed protocols and hostnames for URL validation. The default whitelist allows only `http` and `https` protocols, and does not restrict hostnames.\n *\n * @example\n * ```\n * {\n * protocols: ['http', 'https'],\n * hosts: ['example.com']\n * }\n * ```\n *\n */\n",
+ "excerptTokens": [
+ {
+ "kind": "Content",
+ "text": "whitelist?: "
+ },
+ {
+ "kind": "Reference",
+ "text": "UrlValidatorWhitelist",
+ "canonicalReference": "@opengovsg/starter-kitty-validators!~UrlValidatorWhitelist:type"
+ },
+ {
+ "kind": "Content",
+ "text": ";"
+ }
+ ],
+ "isReadonly": false,
+ "isOptional": true,
+ "releaseTag": "Public",
+ "name": "whitelist",
+ "propertyTypeTokenRange": {
+ "startIndex": 1,
+ "endIndex": 2
+ }
+ }
+ ],
+ "extendsTokenRanges": []
}
]
}
diff --git a/packages/validators/temp/starter-kitty-validators.api.md b/packages/validators/temp/starter-kitty-validators.api.md
index cd35afd..cd2a5a5 100644
--- a/packages/validators/temp/starter-kitty-validators.api.md
+++ b/packages/validators/temp/starter-kitty-validators.api.md
@@ -7,12 +7,17 @@
///
import { z } from 'zod';
+import { ZodSchema } from 'zod';
// @public
-export interface Options {
- baseOrigin?: string;
- // Warning: (ae-forgotten-export) The symbol "Whitelist" needs to be exported by the entry point index.d.ts
- whitelist?: Whitelist;
+export const createEmailSchema: (options?: EmailValidatorOptions) => ZodSchema;
+
+// @public
+export interface EmailValidatorOptions {
+ domains?: {
+ domain: string;
+ includeSubdomains?: boolean;
+ }[];
}
// @public
@@ -27,8 +32,15 @@ export class UrlValidationError extends Error {
// @public
export class UrlValidator {
- constructor(options?: Options);
+ constructor(options?: UrlValidatorOptions);
parse(url: string): URL;
}
+// @public
+export interface UrlValidatorOptions {
+ baseOrigin?: string;
+ // Warning: (ae-forgotten-export) The symbol "UrlValidatorWhitelist" needs to be exported by the entry point index.d.ts
+ whitelist?: UrlValidatorWhitelist;
+}
+
```
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index eda49ee..481080e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -32,6 +32,9 @@ importers:
packages/validators:
dependencies:
+ email-addresses:
+ specifier: ^5.0.0
+ version: 5.0.0
zod:
specifier: ^3.23.8
version: 3.23.8
@@ -1092,6 +1095,9 @@ packages:
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
+ email-addresses@5.0.0:
+ resolution: {integrity: sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==}
+
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -3462,6 +3468,8 @@ snapshots:
eastasianwidth@0.2.0: {}
+ email-addresses@5.0.0: {}
+
emoji-regex@8.0.0: {}
emoji-regex@9.2.2: {}