Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
359166e
Add tools/nest with the npm originals pinned for shim parity
puskuruk Oct 18, 2026
c4d66ee
Generate the mime-db extension table with mime-types' tie-break applied
puskuruk Oct 18, 2026
40358b6
Add the shim parity harness and a strict typecheck config for runtime…
puskuruk Oct 18, 2026
2d511ec
Resolve shim packages under runtime/node/nest in build.mjs and skip t…
puskuruk Oct 18, 2026
7512d7a
Add a typed cookie 0.7.2 shim (parse, serialize) with its parity table
puskuruk Oct 18, 2026
8b9726b
Add a typed cookie-signature 1.2.2 shim on node:crypto with its parit…
puskuruk Oct 18, 2026
f99a66d
Add a typed media-typer 1.1.1 shim with its parity table
puskuruk Oct 18, 2026
649255b
Add a typed mime-types 3.0.2 shim over the generated mime-db table
puskuruk Oct 18, 2026
59fab65
Add a typed content-type 2.1.0 shim with its parity table
puskuruk Oct 18, 2026
4e0a93c
Add a typed type-is 2.1.0 shim over content-type, media-typer and mim…
puskuruk Oct 18, 2026
491dcd0
Add a typed qs 6.15.3 parse shim for the nine options body-parser passes
puskuruk Oct 18, 2026
710a652
Add a typed secure-json-parse 4.1.0 shim (parse, safeParse, scan)
puskuruk Oct 18, 2026
2f1a8d6
Add a typed fast-querystring shim with fast-decode-uri-component
puskuruk Oct 18, 2026
e88ff7a
Add a typed on-finished 2.4.1 shim with ee-first inlined and a WeakMa…
puskuruk Oct 18, 2026
3de12a3
Add a typed ret 0.5.0 tokenizer shim (tokenizer, sets, strToChars, to…
puskuruk Oct 18, 2026
cae1392
Add a typed safe-regex2 5.1.2 shim (star-height and repetition limit …
puskuruk Oct 18, 2026
130dc1f
Add a typed path-to-regexp 8.4.2 shim with its parity table
puskuruk Oct 18, 2026
9dc52d4
Add a typed ms 2.1.3 parse shim
puskuruk Oct 18, 2026
f2f2d7b
Add a typed etag 1.8.1 shim for Stats-based weak entity tags
puskuruk Oct 18, 2026
d58aafa
Add a typed fresh 2.0.0 shim
puskuruk Oct 18, 2026
b5a531a
Add a typed range-parser 1.3.0 shim
puskuruk Oct 18, 2026
7016549
Add a typed statuses 2.0.2 shim backed by http.STATUS_CODES
puskuruk Oct 18, 2026
dd90050
Add a typed http-errors 2.0.1 createError shim
puskuruk Oct 18, 2026
465ee8b
Add a typed escape-html 1.0.3 shim
puskuruk Oct 18, 2026
daeaaf7
Add a typed encodeurl 2.0.0 shim
puskuruk Oct 18, 2026
0749b94
Add a typed port of find-my-way's lib/url-sanitizer as a shim subpath
puskuruk Oct 18, 2026
d74cc89
Add the find-my-way 9.9.0 shim (radix tree, regex and wildcard params…
puskuruk Oct 18, 2026
042166f
Add the typed send 1.2.1 shim (SendStream over fs.stat, createReadStr…
puskuruk Oct 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions runtime/node/nest/shims/content-type/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@

const TEXT_REGEXP = /^[\u0009\u0020-\u007e\u0080-\u00ff]*$/
const TOKEN_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/
const QUOTE_REGEXP = /[\\"]/g
const TYPE_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/

export interface ContentType {
type: string
index: number
parameters: ContentTypeParameters
}

export interface ContentTypeParameters {
[name: string]: string
}

export interface ContentTypeInput {
type?: string
parameters?: ContentTypeParameters
}

export interface ParseOptions {
parameters?: boolean
comma?: boolean
start?: number
}

export function format(obj: ContentTypeInput): string {
const type = obj.type
const parameters = obj.parameters

if (!type || !TYPE_REGEXP.test(type)) {
throw new TypeError(`Invalid type: ${type}`)
}

let result = type

if (parameters) {
for (const param of Object.keys(parameters)) {
if (!TOKEN_REGEXP.test(param)) {
throw new TypeError(`Invalid parameter name: ${param}`)
}
result += `; ${param}=${qstring(parameters[param])}`
}
}

return result
}

const SP = 32
const HTAB = 9
const SEMI = 59
const EQ = 61
const DQUOTE = 34
const BSLASH = 92
const COMMA = 44
const NO_STOP_CHAR = 65_536

export function parse(header: string, options?: ParseOptions): ContentType {
const stopChar = options?.comma === true ? COMMA : NO_STOP_CHAR
const len = header.length
let index = skipOWS(header, options?.start ?? 0, len)

const valueStart = index
index = skipValue(header, index, len, stopChar)
const valueEnd = trailingOWS(header, valueStart, index)
const type = header.slice(valueStart, valueEnd).toLowerCase()

if (options?.parameters === false) {
return { type, index, parameters: {} }
}

return parseParameters(header, type, index, len, stopChar)
}

function parseParameters(header: string, type: string, index: number, len: number, stopChar: number): ContentType {
const parameters: ContentTypeParameters = {}

parameter: while (index < len) {
if (header.charCodeAt(index) === stopChar) break

index = skipOWS(header, index + 1, len)

const keyStart = index

while (index < len) {
const code = header.charCodeAt(index)
if (code === stopChar) break parameter

if (code === SEMI) continue parameter

if (code === EQ) {
const keyEnd = trailingOWS(header, keyStart, index)
const key = header.slice(keyStart, keyEnd).toLowerCase()

index = skipOWS(header, index + 1, len)

if (index < len && header.charCodeAt(index) === DQUOTE) {
index++

let value = ''
while (index < len) {
const code = header.charCodeAt(index++)
if (code === DQUOTE) {
index = skipValue(header, index, len, stopChar)
if (!Object.hasOwn(parameters, key)) parameters[key] = value
break
}

if (code === BSLASH && index < len) {
value += header[index++]
continue
}

value += String.fromCharCode(code)
}

continue parameter
}

const valueStart = index
index = skipValue(header, index, len, stopChar)

if (!Object.hasOwn(parameters, key)) {
const valueEnd = trailingOWS(header, valueStart, index)
parameters[key] = header.slice(valueStart, valueEnd)
}

continue parameter
}

index++
}
}

return { type, index, parameters }
}

function skipValue(str: string, index: number, len: number, stopChar: number): number {
while (index < len) {
const code = str.charCodeAt(index)
if (code === SEMI || code === stopChar) break
index++
}
return index
}

function skipOWS(header: string, index: number, len: number): number {
while (index < len) {
const char = header.charCodeAt(index)
if (char !== SP && char !== HTAB) break
index++
}
return index
}

function trailingOWS(header: string, start: number, end: number): number {
while (end > start) {
const char = header.charCodeAt(end - 1)
if (char !== SP && char !== HTAB) break
end--
}
return end
}

function qstring(str: string): string {
if (TOKEN_REGEXP.test(str)) return str
if (TEXT_REGEXP.test(str)) return `"${str.replace(QUOTE_REGEXP, '\\$&')}"`

throw new TypeError(`Invalid parameter value: ${str}`)
}
17 changes: 17 additions & 0 deletions runtime/node/nest/shims/cookie-signature/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { createHmac, timingSafeEqual } from 'node:crypto'

export function sign(val: string, secret: string | Uint8Array): string {
if (typeof val !== 'string') throw new TypeError('Cookie value must be provided as a string.')
if (secret == null) throw new TypeError('Secret key must be provided.')
return val + '.' + createHmac('sha256', secret).update(val).digest('base64').replace(/=+$/, '')
}

export function unsign(input: string, secret: string | Uint8Array): string | false {
if (typeof input !== 'string') throw new TypeError('Signed cookie string must be provided.')
if (secret == null) throw new TypeError('Secret key must be provided.')
const tentativeValue = input.slice(0, input.lastIndexOf('.'))
const expectedInput = sign(tentativeValue, secret)
const expectedBuffer = Buffer.from(expectedInput)
const inputBuffer = Buffer.from(input)
return expectedBuffer.length === inputBuffer.length && timingSafeEqual(expectedBuffer, inputBuffer) ? tentativeValue : false
}
179 changes: 179 additions & 0 deletions runtime/node/nest/shims/cookie/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@

export interface ParseOptions {
decode?: (value: string) => string
}

export interface SerializeOptions {
encode?: (value: string) => string
maxAge?: number
domain?: string
path?: string
expires?: Date
httpOnly?: boolean
secure?: boolean
partitioned?: boolean
priority?: string
sameSite?: boolean | string
}

const cookieNameRegExp = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/

const cookieValueRegExp = /^("?)[\u0021\u0023-\u002B\u002D-\u003A\u003C-\u005B\u005D-\u007E]*\1$/

const domainValueRegExp = /^([.]?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)([.][a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/i

const pathValueRegExp = /^[\u0020-\u003A\u003D-\u007E]*$/

export function parse(str: string, opt?: ParseOptions): { [name: string]: string } {
if (typeof str !== 'string') throw new TypeError('argument str must be a string')

const obj: { [name: string]: string } = {}
const len = str.length
if (len < 2) return obj

const dec = (opt && opt.decode) || decode
let index = 0
let eqIdx = 0
let endIdx = 0

do {
eqIdx = str.indexOf('=', index)
if (eqIdx === -1) break

endIdx = str.indexOf(';', index)

if (endIdx === -1) {
endIdx = len
} else if (eqIdx > endIdx) {
index = str.lastIndexOf(';', eqIdx - 1) + 1
continue
}

const keyStartIdx = startIndex(str, index, eqIdx)
const keyEndIdx = endIndex(str, eqIdx, keyStartIdx)
const key = str.slice(keyStartIdx, keyEndIdx)

if (!Object.hasOwn(obj, key)) {
let valStartIdx = startIndex(str, eqIdx + 1, endIdx)
let valEndIdx = endIndex(str, endIdx, valStartIdx)

if (str.charCodeAt(valStartIdx) === 0x22 && str.charCodeAt(valEndIdx - 1) === 0x22) {
valStartIdx++
valEndIdx--
}

const val = str.slice(valStartIdx, valEndIdx)
obj[key] = tryDecode(val, dec)
}

index = endIdx + 1
} while (index < len)

return obj
}

function startIndex(str: string, index: number, max: number): number {
do {
const code = str.charCodeAt(index)
if (code !== 0x20 && code !== 0x09) return index
} while (++index < max)
return max
}

function endIndex(str: string, index: number, min: number): number {
while (index > min) {
const code = str.charCodeAt(--index)
if (code !== 0x20 && code !== 0x09) return index + 1
}
return min
}

export function serialize(name: string, val: string, opt?: SerializeOptions): string {
const enc = (opt && opt.encode) || encodeURIComponent

if (typeof enc !== 'function') throw new TypeError('option encode is invalid')
if (!cookieNameRegExp.test(name)) throw new TypeError('argument name is invalid')

const value = enc(val)
if (!cookieValueRegExp.test(value)) throw new TypeError('argument val is invalid')

let str = name + '=' + value
if (!opt) return str

if (opt.maxAge != null) {
const maxAge = Math.floor(opt.maxAge)
if (!isFinite(maxAge)) throw new TypeError('option maxAge is invalid')
str += '; Max-Age=' + maxAge
}

if (opt.domain) {
if (!domainValueRegExp.test(opt.domain)) throw new TypeError('option domain is invalid')
str += '; Domain=' + opt.domain
}

if (opt.path) {
if (!pathValueRegExp.test(opt.path)) throw new TypeError('option path is invalid')
str += '; Path=' + opt.path
}

if (opt.expires) {
const expires = opt.expires
if (!(expires instanceof Date) || isNaN(expires.valueOf())) throw new TypeError('option expires is invalid')
str += '; Expires=' + expires.toUTCString()
}

if (opt.httpOnly) str += '; HttpOnly'
if (opt.secure) str += '; Secure'
if (opt.partitioned) str += '; Partitioned'

if (opt.priority) {
const priority = typeof opt.priority === 'string' ? opt.priority.toLowerCase() : opt.priority
switch (priority) {
case 'low':
str += '; Priority=Low'
break
case 'medium':
str += '; Priority=Medium'
break
case 'high':
str += '; Priority=High'
break
default:
throw new TypeError('option priority is invalid')
}
}

if (opt.sameSite) {
const sameSite = typeof opt.sameSite === 'string' ? opt.sameSite.toLowerCase() : opt.sameSite
switch (sameSite) {
case true:
str += '; SameSite=Strict'
break
case 'lax':
str += '; SameSite=Lax'
break
case 'strict':
str += '; SameSite=Strict'
break
case 'none':
str += '; SameSite=None'
break
default:
throw new TypeError('option sameSite is invalid')
}
}

return str
}

function decode(str: string): string {
return str.indexOf('%') !== -1 ? decodeURIComponent(str) : str
}

function tryDecode(str: string, decode: (value: string) => string): string {
try {
return decode(str)
} catch {
return str
}
}
12 changes: 12 additions & 0 deletions runtime/node/nest/shims/encodeurl/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

const ENCODE_CHARS_REGEXP = /(?:[^\x21\x23-\x3B\x3D\x3F-\x5F\x61-\x7A\x7C\x7E]|%(?:[^0-9A-Fa-f]|[0-9A-Fa-f][^0-9A-Fa-f]|$))+/g

const UNMATCHED_SURROGATE_PAIR_REGEXP = /(^|[^\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF]([^\uDC00-\uDFFF]|$)/g

const UNMATCHED_SURROGATE_PAIR_REPLACE = '$1\uFFFD$2'

export default function encodeUrl(url: string): string {
return url
.replace(UNMATCHED_SURROGATE_PAIR_REGEXP, UNMATCHED_SURROGATE_PAIR_REPLACE)
.replace(ENCODE_CHARS_REGEXP, (match: string) => encodeURI(match))
}
Loading
Loading