Skip to content

Commit

Permalink
feat: safe-fs and path validator
Browse files Browse the repository at this point in the history
  • Loading branch information
zeyu2001 authored Sep 4, 2024
1 parent da9eb71 commit d99e479
Show file tree
Hide file tree
Showing 29 changed files with 1,146 additions and 35 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
# Starter Kitty

Common app components that are safe-by-default.
Starter Kitty is a collection of common utilities and packages for JavaScript projects. It is designed to provide sensible defaults for common tasks, such as file system operations and input validation.

## 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.

## Documentation

Please refer to the [documentation website](https://kit.open.gov.sg/) for detailed API documentation and usage examples.

## Packages

- [`@opengovsg/starter-kitty-fs`](./packages/safe-fs/): Safe file system operations.
- [`@opengovsg/starter-kitty-validators`](./packages/validators/): Common input validators.
8 changes: 4 additions & 4 deletions api-extractor.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<projectFolder>/temp/"
*/
"reportFolder": "./etc/"
"reportFolder": "./etc/",

/**
* Specifies the folder where the temporary report file is written. The file name portion is determined by
Expand All @@ -183,7 +183,7 @@
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<projectFolder>/temp/"
*/
// "reportTempFolder": "<projectFolder>/temp/",
"reportTempFolder": "./temp/"

/**
* Whether "forgotten exports" should be included in the API report file. Forgotten exports are declarations
Expand All @@ -202,7 +202,7 @@
/**
* (REQUIRED) Whether to generate a doc model file.
*/
"enabled": true
"enabled": true,

/**
* The output path for the doc model file. The file extension should be ".api.json".
Expand All @@ -213,7 +213,7 @@
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<projectFolder>/temp/<unscopedPackageName>.api.json"
*/
// "apiJsonFilePath": "<projectFolder>/temp/<unscopedPackageName>.api.json",
"apiJsonFilePath": "./temp/<unscopedPackageName>.api.json"

/**
* Whether "forgotten exports" should be included in the doc model file. Forgotten exports are declarations
Expand Down
6 changes: 3 additions & 3 deletions apps/docs/.vitepress/utils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import fs from "fs";
import path from "path";
import fs from "node:fs";
import path from "node:path";

export const scanDir = (dir: string) => {
let res = fs
.readdirSync(path.resolve(__dirname, `../${dir}`))
.filter((item) => !item.startsWith("."));
.filter((item) => !item.startsWith(".")) as string[];
if (res) {
const arr = [];
for (let item of res) {
Expand Down
1 change: 1 addition & 0 deletions apps/docs/examples/index.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Examples

- [`@opengovsg/starter-kitty-validators`](./validators.md): Common input validators.
- [`@opengovsg/starter-kitty-fs`](./safe-fs.md): Safe-by-default `fs` wrapper.
32 changes: 32 additions & 0 deletions apps/docs/examples/safe-fs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# @opengovsg/starter-kitty-fs

## Installation

```bash
npm i --save @opengovsg/starter-kitty-fs
```

## Usage

```javascript
import safeFs from '@opengovsg/starter-kitty-fs'

const fs = safeFs('/app/content')

// Writes to /app/content/hello.txt
fs.writeFileSync('hello.txt', 'Hello, world!')

// Tries to read from /app/content/etc/passwd
fs.readFileSync('../../etc/passwd')
```

The interfaces for all `fs` methods are the exact same as the built-in `fs` module, but if a `PathLike` parameter is given,
it will be normalized, stripped of leading traversal characters, then resolved relative to the base directory passed to `safeFs`.

This guarantees that the resolved path will always be within the base directory or its subdirectories.

For example, if the base directory is `/app/content`:

- `hello.txt` resolves to `/app/content/hello.txt`
- `../../etc/passwd` resolves to `/app/content/etc/passwd`
- `/etc/passwd` resolves to `/app/content/etc/passwd`
56 changes: 39 additions & 17 deletions apps/docs/examples/validators.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,45 @@
npm i --save @opengovsg/starter-kitty-validators
```

## Path Validation

```javascript
import { createPathSchema } from '@opengovsg/starter-kitty-validators'

const pathSchema = createPathSchema({
basePath: '/app/content',
})

const contentSubmissionSchema = z.object({
fullPermalink: pathSchema,
title: z.string(),
content: z.string(),
})

type ContentSubmission = z.infer<typeof contentSchema>
```

`fullPermalink`, when resolved relative to the working directory of the Node process, must lie within `/app/content`.

## 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<typeof formSchema>
```

`email` must be a valid email address and have a domain that is `gov.sg` or a subdomain of `gov.sg`.

## URL Validation

```javascript
Expand Down Expand Up @@ -59,20 +98,3 @@ 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<typeof formSchema>
```
15 changes: 15 additions & 0 deletions etc/starter-kitty-fs.api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
## API Report File for "@opengovsg/starter-kitty-fs"

> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts

/// <reference types="node" />

import * as fs from 'node:fs';

// @public
const safeFs: (basePath?: string) => typeof fs;
export default safeFs;

```
8 changes: 8 additions & 0 deletions etc/starter-kitty-validators.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import { ZodSchema } from 'zod';
// @public
export const createEmailSchema: (options?: EmailValidatorOptions) => ZodSchema<string>;

// @public
export const createPathSchema: (options: PathValidatorOptions) => ZodSchema<string>;

// @public
export interface EmailValidatorOptions {
domains?: {
Expand All @@ -25,6 +28,11 @@ export class OptionsError extends Error {
constructor(message: string);
}

// @public
export interface PathValidatorOptions {
basePath: string;
}

// @public
export class UrlValidationError extends Error {
constructor(message: string);
Expand Down
24 changes: 24 additions & 0 deletions packages/safe-fs/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"extends": ["opengovsg"],
"ignorePatterns": ["dist/**/*", "vitest.config.ts", "vitest.setup.ts"],
"plugins": ["import", "eslint-plugin-tsdoc"],
"rules": {
"import/no-unresolved": "error",
"tsdoc/syntax": "error"
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "**/tsconfig.json"
},
"settings": {
"import/parsers": {
"@typescript-eslint/parser": [".ts", ".tsx"]
},
"import/resolver": {
"typescript": {
"alwaysTryTypes": true,
"project": "**/tsconfig.json"
}
}
}
}
1 change: 1 addition & 0 deletions packages/safe-fs/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
tsconfig.json
6 changes: 6 additions & 0 deletions packages/safe-fs/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"trailingComma": "all",
"tabWidth": 2,
"semi": false,
"singleQuote": true
}
21 changes: 21 additions & 0 deletions packages/safe-fs/api-extractor.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Config file for API Extractor. For more info, please visit: https://api-extractor.com
*/
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",

"extends": "../../api-extractor.json",

/**
* (REQUIRED) Specifies the .d.ts file to be used as the starting point for analysis. API Extractor
* analyzes the symbols exported by this module.
*
* The file extension must be ".d.ts" and not ".ts".
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
*/
"mainEntryPointFilePath": "<projectFolder>/dist/index.d.ts"
}
36 changes: 36 additions & 0 deletions packages/safe-fs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "@opengovsg/starter-kitty-fs",
"version": "1.2.3",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "tsc && tsc-alias",
"build:report": "api-extractor run --local --verbose",
"build:docs": "api-documenter markdown --input-folder ../../temp/ --output-folder ../../apps/docs/api/",
"lint": "eslint \"**/*.{js,jsx,ts,tsx}\" --cache",
"test": "vitest",
"ci:report": "api-extractor run --verbose"
},
"devDependencies": {
"@swc/core": "^1.6.13",
"@types/node": "^18.19.47",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.56.0",
"eslint-config-opengovsg": "^3.0.0",
"eslint-config-prettier": "^8.6.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-simple-import-sort": "^10.0.0",
"eslint-plugin-tsdoc": "^0.3.0",
"memfs": "^4.11.1",
"prettier": "^2.8.4",
"tsc-alias": "^1.8.10",
"tsup": "^8.1.0",
"typescript": "^5.4.5",
"vitest": "^2.0.2"
}
}
Loading

0 comments on commit d99e479

Please sign in to comment.