• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

flyingsquirrel0419 / layercache / 29149114635

11 Jul 2026 10:17AM UTC coverage: 95.423% (-0.5%) from 95.882%
29149114635

Pull #101

github

web-flow
Merge 3a92bb146 into e5107fec2
Pull Request #101: Fix cache security hardening

1951 of 2112 branches covered (92.38%)

Branch coverage included in aggregate %.

287 of 307 new or added lines in 23 files covered. (93.49%)

5 existing lines in 2 files now uncovered.

3428 of 3525 relevant lines covered (97.25%)

302.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.3
/src/internal/CacheKeySerialization.ts
1
import type { CacheGetOptions } from '../types'
2

3
export const DANGEROUS_OBJECT_KEYS = new Set(['__proto__', 'prototype', 'constructor'])
15✔
4

5
export function normalizeForSerialization(value: unknown): unknown {
6
  return normalizeValue(value, new WeakSet<object>())
14✔
7
}
8

9
function normalizeValue(value: unknown, ancestors: WeakSet<object>): unknown {
10
  if (Array.isArray(value)) {
32✔
11
    return withAncestor(value, ancestors, () => value.map((entry) => normalizeValue(entry, ancestors)))
2✔
12
  }
13

14
  if (value && typeof value === 'object') {
30✔
15
    if (value instanceof Date) {
14✔
16
      if (Number.isNaN(value.getTime())) throw new TypeError('Cannot serialize an invalid Date as a cache key.')
2!
17
      return { $type: 'Date', value: value.toISOString() }
2✔
18
    }
19
    if (value instanceof URL) return { $type: 'URL', value: value.href }
12!
20
    if (value instanceof RegExp) return { $type: 'RegExp', source: value.source, flags: value.flags }
12!
21
    if (value instanceof Map) {
12✔
22
      return withAncestor(value, ancestors, () => ({
2✔
23
        $type: 'Map',
24
        entries: [...value.entries()]
25
          .map(([key, entry]) => [normalizeValue(key, ancestors), normalizeValue(entry, ancestors)])
2✔
NEW
26
          .sort(([left], [right]) => JSON.stringify(left).localeCompare(JSON.stringify(right)))
×
27
      }))
28
    }
29
    if (value instanceof Set) {
10!
NEW
30
      return withAncestor(value, ancestors, () => ({
×
31
        $type: 'Set',
NEW
32
        values: [...value].map((entry) => normalizeValue(entry, ancestors)).sort(compareNormalizedValues)
×
33
      }))
34
    }
35

36
    const prototype = Object.getPrototypeOf(value)
10✔
37
    const constructorName = prototype?.constructor?.name
10✔
38
    if (prototype !== null && constructorName !== 'Object') {
14✔
39
      throw new TypeError(`Unsupported cache-key object type: ${prototype?.constructor?.name ?? 'unknown'}.`)
1!
40
    }
41

42
    return withAncestor(value, ancestors, () =>
9✔
43
      Object.keys(value as Record<string, unknown>)
8✔
44
        .sort()
45
        .reduce<Record<string, unknown>>((normalized, key) => {
46
          if (DANGEROUS_OBJECT_KEYS.has(key)) return normalized
14✔
47
          normalized[key] = normalizeValue((value as Record<string, unknown>)[key], ancestors)
12✔
48
          return normalized
12✔
49
        }, {})
50
    )
51
  }
52

53
  if (typeof value === 'function' || typeof value === 'symbol') {
16!
NEW
54
    throw new TypeError(`Unsupported cache-key value type: ${typeof value}.`)
×
55
  }
56

57
  return value
16✔
58
}
59

60
function withAncestor<T>(value: object, ancestors: WeakSet<object>, operation: () => T): T {
61
  if (ancestors.has(value)) throw new TypeError('Cannot serialize a circular value as a cache key.')
13✔
62
  ancestors.add(value)
12✔
63
  try {
12✔
64
    return operation()
12✔
65
  } finally {
66
    ancestors.delete(value)
12✔
67
  }
68
}
69

70
function compareNormalizedValues(left: unknown, right: unknown): number {
NEW
71
  return JSON.stringify(left).localeCompare(JSON.stringify(right))
×
72
}
73

74
export function serializeKeyPart(value: unknown): string {
75
  if (typeof value === 'string') {
21✔
76
    return `s:${value.replace(/%/g, '%25').replace(/:/g, '%3A')}`
2✔
77
  }
78

79
  if (typeof value === 'number') {
19✔
80
    return `n:${value}`
11✔
81
  }
82

83
  if (typeof value === 'boolean') {
8✔
84
    return `b:${value}`
1✔
85
  }
86

87
  return `j:${JSON.stringify(normalizeForSerialization(value))}`
7✔
88
}
89

90
export function serializeOptions(options: CacheGetOptions | undefined): string {
91
  return JSON.stringify(normalizeForSerialization(options) ?? null)
6✔
92
}
93

94
export function createInstanceId(): string {
95
  if (globalThis.crypto?.randomUUID) {
281✔
96
    return globalThis.crypto.randomUUID()
279✔
97
  }
98

99
  if (globalThis.crypto?.getRandomValues) {
2✔
100
    const bytes = new Uint8Array(16)
1✔
101
    globalThis.crypto.getRandomValues(bytes)
1✔
102
    return `layercache-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('')}`
16✔
103
  }
104

105
  throw new Error(
1✔
106
    'layercache requires a cryptographic random source. ' +
107
      'Neither crypto.randomUUID nor crypto.getRandomValues is available in this runtime.'
108
  )
109
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc