hanzio 1.1.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +832 -219
- package/dist/api-wrapper/builders.d.ts +15 -3
- package/dist/api-wrapper/create-client.d.ts +1 -1
- package/dist/api-wrapper/describe.d.ts +16 -0
- package/dist/api-wrapper/endpoint.d.ts +18 -0
- package/dist/api-wrapper/errors.d.ts +40 -2
- package/dist/api-wrapper/headers.d.ts +2 -1
- package/dist/api-wrapper/index.d.ts +5 -0
- package/dist/api-wrapper/otel.d.ts +44 -0
- package/dist/api-wrapper/paginate.d.ts +12 -0
- package/dist/api-wrapper/redaction.d.ts +2 -0
- package/dist/api-wrapper/shared.d.ts +16 -2
- package/dist/api-wrapper/testing.d.ts +23 -0
- package/dist/api-wrapper/transport.d.ts +12 -10
- package/dist/api-wrapper/types.d.ts +104 -18
- package/dist/array/index.d.ts +50 -4
- package/dist/cache/createCache.d.ts +31 -0
- package/dist/cache/index.d.ts +45 -2
- package/dist/color/contrast.d.ts +40 -0
- package/dist/color/index.d.ts +2 -0
- package/dist/{math → color}/metricsSeriesColors.d.ts +14 -7
- package/dist/cool-console-log/index.d.ts +34 -5
- package/dist/crypto/encoding.d.ts +18 -0
- package/dist/crypto/hash.d.ts +33 -0
- package/dist/crypto/index.d.ts +4 -0
- package/dist/crypto/sealer.d.ts +25 -0
- package/dist/crypto/webhook.d.ts +20 -0
- package/dist/date/index.d.ts +46 -2
- package/dist/emitter/index.d.ts +21 -0
- package/dist/error/index.d.ts +12 -0
- package/dist/format/index.d.ts +102 -0
- package/dist/function/index.d.ts +60 -0
- package/dist/guard/index.d.ts +20 -0
- package/dist/i18n/index.d.ts +37 -0
- package/dist/index-0zvtcbzy.js +224 -0
- package/dist/index-181e6vfw.js +29 -0
- package/dist/index-8r2vdzjg.js +87 -0
- package/dist/index-hy7kbb99.js +157 -0
- package/dist/{src/secrets/loaders.js → index-j1ybktbr.js} +608 -405
- package/dist/index-k9yx6bne.js +89 -0
- package/dist/index-kj1phgrx.js +169 -0
- package/dist/index-mnkcfyx8.js +78 -0
- package/dist/{p-queue/index.js → index-pwt0dz63.js} +7 -11
- package/dist/index-rp5pqghz.js +157 -0
- package/dist/index-srt06xd2.js +15 -0
- package/dist/index-wjyb9bsd.js +263 -0
- package/dist/index.d.ts +7 -7
- package/dist/jwt/index.d.ts +50 -2
- package/dist/math/index.d.ts +2 -2
- package/dist/math/linearRegressionTrend.d.ts +14 -1
- package/dist/math/scale.d.ts +8 -0
- package/dist/object/index.d.ts +19 -0
- package/dist/p-queue/index.d.ts +1 -1
- package/dist/promise/index.d.ts +97 -2
- package/dist/secrets/core.d.ts +47 -0
- package/dist/secrets/deadline.d.ts +6 -0
- package/dist/secrets/environment.d.ts +9 -0
- package/dist/secrets/errors.d.ts +11 -0
- package/dist/secrets/index.d.ts +13 -25
- package/dist/secrets/infisical-cli.d.ts +6 -0
- package/dist/secrets/infisical-config.d.ts +17 -0
- package/dist/secrets/infisical-session.d.ts +4 -0
- package/dist/secrets/infisical.d.ts +8 -13
- package/dist/secrets/loaders.d.ts +5 -12
- package/dist/secrets/vite.d.ts +7 -3
- package/dist/secrets/worker-loader.d.ts +9 -0
- package/dist/secrets/worker.d.ts +3 -0
- package/dist/sitemap/index.d.ts +48 -6
- package/dist/src/api-wrapper/index.js +60 -1100
- package/dist/src/api-wrapper/testing.js +20 -0
- package/dist/{cool-console-log → src/cool-console-log}/index.js +80 -33
- package/dist/src/crypto/index.js +195 -0
- package/dist/src/format/index.js +211 -0
- package/dist/src/i18n/index.js +36 -0
- package/dist/src/index.js +863 -2809
- package/dist/src/jwt/index.js +138 -92
- package/dist/src/p-queue/index.js +9 -437
- package/dist/src/secrets/index.js +261 -1377
- package/dist/src/secrets/vite.js +6 -17
- package/dist/src/secrets/worker.js +12 -0
- package/dist/src/sitemap/index.js +183 -511
- package/dist/src/storage/index.js +119 -0
- package/dist/src/zod/index.js +347 -174
- package/dist/state/index.d.ts +21 -0
- package/dist/storage/index.d.ts +58 -0
- package/dist/string/index.d.ts +75 -2
- package/dist/typedswitch/index.d.ts +37 -26
- package/dist/types/index.d.ts +11 -0
- package/dist/url/index.d.ts +50 -1
- package/dist/zod/index.d.ts +32 -3
- package/dist/zod/introspect.d.ts +19 -0
- package/dist/zod/jsonToZodSchemaMapper.d.ts +3 -1
- package/dist/zod/zodMinMaxFilter.d.ts +22 -3
- package/dist/zod/zodToTypeString.d.ts +8 -3
- package/package.json +87 -7
- package/src/api-wrapper/actions-impl.ts +11 -72
- package/src/api-wrapper/builders.ts +84 -52
- package/src/api-wrapper/create-client.ts +246 -96
- package/src/api-wrapper/describe.ts +157 -0
- package/src/api-wrapper/endpoint.ts +26 -0
- package/src/api-wrapper/error-reporting.ts +7 -1
- package/src/api-wrapper/errors.ts +117 -2
- package/src/api-wrapper/headers.ts +19 -1
- package/src/api-wrapper/index.ts +5 -0
- package/src/api-wrapper/otel.ts +139 -0
- package/src/api-wrapper/paginate.ts +28 -0
- package/src/api-wrapper/redaction.ts +43 -0
- package/src/api-wrapper/shared.ts +21 -2
- package/src/api-wrapper/testing.ts +48 -0
- package/src/api-wrapper/transport.ts +68 -74
- package/src/api-wrapper/types.ts +186 -18
- package/src/array/index.ts +195 -42
- package/src/cache/createCache.ts +157 -0
- package/src/cache/index.ts +235 -83
- package/src/color/contrast.ts +163 -0
- package/src/color/index.ts +22 -0
- package/src/{math → color}/metricsSeriesColors.ts +47 -49
- package/src/cool-console-log/index.ts +128 -41
- package/src/crypto/encoding.ts +83 -0
- package/src/crypto/hash.ts +170 -0
- package/src/crypto/index.ts +26 -0
- package/src/crypto/sealer.ts +168 -0
- package/src/crypto/webhook.ts +86 -0
- package/src/date/index.ts +109 -34
- package/src/emitter/index.ts +82 -0
- package/src/error/index.ts +63 -0
- package/src/format/index.ts +389 -0
- package/src/function/index.ts +201 -0
- package/src/guard/index.ts +80 -0
- package/src/i18n/index.ts +125 -0
- package/src/index.ts +11 -7
- package/src/jwt/index.ts +199 -125
- package/src/math/index.ts +5 -10
- package/src/math/linearRegressionTrend.ts +33 -8
- package/src/math/scale.ts +17 -0
- package/src/object/index.ts +99 -0
- package/src/p-queue/index.ts +2 -0
- package/src/promise/index.ts +304 -9
- package/src/secrets/core.ts +348 -0
- package/src/secrets/deadline.ts +69 -0
- package/src/secrets/environment.ts +69 -0
- package/src/secrets/errors.ts +29 -0
- package/src/secrets/index.ts +79 -210
- package/src/secrets/infisical-cli.ts +99 -0
- package/src/secrets/infisical-config.ts +70 -0
- package/src/secrets/infisical-session.ts +42 -0
- package/src/secrets/infisical.ts +214 -66
- package/src/secrets/loaders.ts +24 -48
- package/src/secrets/vite.ts +44 -15
- package/src/secrets/worker-loader.ts +56 -0
- package/src/secrets/worker.ts +4 -0
- package/src/sitemap/index.ts +293 -130
- package/src/state/index.ts +133 -81
- package/src/storage/index.ts +187 -0
- package/src/string/index.ts +261 -23
- package/src/typedswitch/index.ts +134 -103
- package/src/types/index.ts +14 -0
- package/src/url/index.ts +146 -1
- package/src/zod/index.ts +105 -9
- package/src/zod/introspect.ts +103 -0
- package/src/zod/jsonToZodSchemaMapper.ts +44 -21
- package/src/zod/zodMinMaxFilter.ts +37 -13
- package/src/zod/zodToTypeString.ts +184 -181
- package/dist/api-wrapper/index.js +0 -250
- package/dist/gzip/index.d.ts +0 -2
- package/dist/gzip/index.js +0 -16
- package/dist/index.js +0 -1823
- package/dist/jwt/index.js +0 -19
- package/dist/sitemap/index.js +0 -552
- package/dist/src/gzip/index.js +0 -16
- package/dist/src/secrets/infisical.js +0 -1194
- package/dist/zod/index.js +0 -210
|
@@ -1,21 +1,33 @@
|
|
|
1
|
+
import { lerp, normalize } from '../math/scale'
|
|
2
|
+
import { colorFunctionArgs, parseColor } from './contrast'
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
5
|
+
* Stable series color assignment: the same hash + collision strategy maps a
|
|
6
|
+
* label to the same palette slot index on every platform.
|
|
4
7
|
*/
|
|
5
8
|
|
|
6
|
-
/**
|
|
9
|
+
/** Default palette slot count. */
|
|
7
10
|
export const METRICS_SERIES_PALETTE_SIZE = 10
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
* FNV-1a 32-bit hash — stable palette index for a string key.
|
|
11
14
|
*/
|
|
12
|
-
export function hashStringToColorIndex(
|
|
15
|
+
export function hashStringToColorIndex(
|
|
16
|
+
str: string,
|
|
17
|
+
paletteSize = METRICS_SERIES_PALETTE_SIZE
|
|
18
|
+
): number {
|
|
13
19
|
let hash = 2166136261
|
|
14
20
|
for (let i = 0; i < str.length; i++) {
|
|
15
21
|
hash ^= str.charCodeAt(i)
|
|
16
22
|
hash = Math.imul(hash, 16777619)
|
|
17
23
|
}
|
|
18
|
-
return Math.abs(hash) %
|
|
24
|
+
return Math.abs(hash) % paletteSize
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Stable pick from `palette` for `seed` (same seed → same item). Throws on an empty palette. */
|
|
28
|
+
export function pickFromPalette<T>(seed: string, palette: readonly T[]): T {
|
|
29
|
+
if (palette.length === 0) throw new RangeError('palette is empty')
|
|
30
|
+
return palette[hashStringToColorIndex(seed, palette.length)]!
|
|
19
31
|
}
|
|
20
32
|
|
|
21
33
|
function circularHueDistance(
|
|
@@ -59,23 +71,24 @@ export function seriesColorKey(label: string | undefined, key: string): string {
|
|
|
59
71
|
/**
|
|
60
72
|
* Map each distinct key to a palette index (hash preference, then max-min circular spacing).
|
|
61
73
|
*/
|
|
62
|
-
export function resolveSeriesColorMap(
|
|
74
|
+
export function resolveSeriesColorMap(
|
|
75
|
+
keys: string[],
|
|
76
|
+
paletteSize = METRICS_SERIES_PALETTE_SIZE
|
|
77
|
+
): Map<string, number> {
|
|
63
78
|
const result = new Map<string, number>()
|
|
64
79
|
const uniqueKeys = [...new Set(keys)]
|
|
65
80
|
if (uniqueKeys.length === 0) return result
|
|
66
81
|
|
|
67
|
-
const paletteSize = METRICS_SERIES_PALETTE_SIZE
|
|
68
|
-
|
|
69
82
|
if (uniqueKeys.length === 1) {
|
|
70
83
|
const only = uniqueKeys[0]!
|
|
71
|
-
result.set(only, hashStringToColorIndex(only))
|
|
84
|
+
result.set(only, hashStringToColorIndex(only, paletteSize))
|
|
72
85
|
return result
|
|
73
86
|
}
|
|
74
87
|
|
|
75
88
|
const usedIndices = new Set<number>()
|
|
76
89
|
const entries = uniqueKeys.map((key) => ({
|
|
77
90
|
key,
|
|
78
|
-
preferred: hashStringToColorIndex(key)
|
|
91
|
+
preferred: hashStringToColorIndex(key, paletteSize)
|
|
79
92
|
}))
|
|
80
93
|
|
|
81
94
|
for (const entry of entries) {
|
|
@@ -114,53 +127,38 @@ export type ResolveColorsByKeysResult = {
|
|
|
114
127
|
export function createResolveColorsByKeys(
|
|
115
128
|
keys: string[],
|
|
116
129
|
getSeriesColor: (index: number) => string,
|
|
117
|
-
getSeriesHoverColor: (index: number) => string
|
|
130
|
+
getSeriesHoverColor: (index: number) => string,
|
|
131
|
+
paletteSize = METRICS_SERIES_PALETTE_SIZE
|
|
118
132
|
): ResolveColorsByKeysResult {
|
|
119
|
-
const colorMap = resolveSeriesColorMap(keys)
|
|
133
|
+
const colorMap = resolveSeriesColorMap(keys, paletteSize)
|
|
134
|
+
const indexOf = (key: string) =>
|
|
135
|
+
colorMap.get(key) ?? hashStringToColorIndex(key, paletteSize)
|
|
120
136
|
return {
|
|
121
|
-
getColor: (key: string) =>
|
|
122
|
-
|
|
123
|
-
getHoverColor: (key: string) =>
|
|
124
|
-
getSeriesHoverColor(colorMap.get(key) ?? hashStringToColorIndex(key))
|
|
137
|
+
getColor: (key: string) => getSeriesColor(indexOf(key)),
|
|
138
|
+
getHoverColor: (key: string) => getSeriesHoverColor(indexOf(key))
|
|
125
139
|
}
|
|
126
140
|
}
|
|
127
141
|
|
|
128
|
-
function clamp(value: number, min: number, max: number): number {
|
|
129
|
-
return Math.min(max, Math.max(min, value))
|
|
130
|
-
}
|
|
131
|
-
|
|
132
142
|
/**
|
|
133
|
-
*
|
|
143
|
+
* Apply `alpha` (0..1) to a color, replacing any existing alpha.
|
|
144
|
+
*
|
|
145
|
+
* - `#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa` → `rgba(r, g, b, a)` (works in React Native)
|
|
146
|
+
* - `rgb()`/`rgba()`/`hsl()`/`hsla()`, comma or space syntax → `rgba(…)` / `hsla(…)`
|
|
147
|
+
* - anything else (CSS variables, named colors, `oklch()`, …) →
|
|
148
|
+
* `color-mix(in srgb, <color> <alpha*100>%, transparent)` (web only)
|
|
134
149
|
*/
|
|
135
150
|
export function withAlpha(color: string, alpha: number): string {
|
|
136
151
|
const trimmed = color.trim()
|
|
152
|
+
const rgba = parseColor(trimmed)
|
|
153
|
+
const fn = rgba && colorFunctionArgs(trimmed)
|
|
137
154
|
|
|
138
|
-
if (
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
hex.length === 3
|
|
142
|
-
? hex
|
|
143
|
-
.split('')
|
|
144
|
-
.map((c) => c + c)
|
|
145
|
-
.join('')
|
|
146
|
-
: hex
|
|
147
|
-
if (normalized.length === 6) {
|
|
148
|
-
const r = Number.parseInt(normalized.slice(0, 2), 16)
|
|
149
|
-
const g = Number.parseInt(normalized.slice(2, 4), 16)
|
|
150
|
-
const b = Number.parseInt(normalized.slice(4, 6), 16)
|
|
151
|
-
return `rgba(${r}, ${g}, ${b}, ${alpha})`
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (trimmed.startsWith('rgb(')) {
|
|
156
|
-
return trimmed.replace('rgb(', 'rgba(').replace(')', `, ${alpha})`)
|
|
155
|
+
if (fn?.name === 'hsl') {
|
|
156
|
+
const [h, s, l] = fn.args
|
|
157
|
+
return `hsla(${h}, ${s}, ${l}, ${alpha})`
|
|
157
158
|
}
|
|
159
|
+
if (rgba) return `rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${alpha})`
|
|
158
160
|
|
|
159
|
-
|
|
160
|
-
return trimmed.replace(/,\s*[\d.]+\)$/, `, ${alpha})`)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return trimmed
|
|
161
|
+
return `color-mix(in srgb, ${trimmed} ${alpha * 100}%, transparent)`
|
|
164
162
|
}
|
|
165
163
|
|
|
166
164
|
/**
|
|
@@ -199,10 +197,10 @@ export function getHeatmapCellStyle(params: {
|
|
|
199
197
|
}
|
|
200
198
|
}
|
|
201
199
|
|
|
202
|
-
const normalized = clamp((value - min) / (max - min), 0, 1)
|
|
203
|
-
const alpha = minAlpha + (maxAlpha - minAlpha) * normalized
|
|
204
|
-
|
|
205
200
|
return {
|
|
206
|
-
backgroundColor: withAlpha(
|
|
201
|
+
backgroundColor: withAlpha(
|
|
202
|
+
baseColor,
|
|
203
|
+
lerp(minAlpha, maxAlpha, normalize(value, min, max))
|
|
204
|
+
)
|
|
207
205
|
}
|
|
208
206
|
}
|
|
@@ -40,11 +40,44 @@ export const terminalColors = {
|
|
|
40
40
|
|
|
41
41
|
export type TerminalColor = keyof typeof terminalColors
|
|
42
42
|
|
|
43
|
+
const noColors = Object.fromEntries(
|
|
44
|
+
Object.keys(terminalColors).map((key) => [key, ''])
|
|
45
|
+
) as unknown as typeof terminalColors
|
|
46
|
+
|
|
47
|
+
function getProcessEnvironment():
|
|
48
|
+
| Record<string, string | undefined>
|
|
49
|
+
| undefined {
|
|
50
|
+
return typeof process === 'undefined' ? undefined : process.env
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isDev(): boolean {
|
|
54
|
+
return getProcessEnvironment()?.NODE_ENV !== 'production'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Whether stdout should get ANSI colors: `NO_COLOR` disables, `FORCE_COLOR`
|
|
59
|
+
* (other than `'0'`) enables, otherwise only when stdout is a TTY.
|
|
60
|
+
* Always false where `process` doesn't exist (browsers, Workers).
|
|
61
|
+
*/
|
|
62
|
+
export function supportsColor(): boolean {
|
|
63
|
+
const env = getProcessEnvironment()
|
|
64
|
+
if (!env || env.NO_COLOR) return false
|
|
65
|
+
if (env.FORCE_COLOR !== undefined && env.FORCE_COLOR !== '0') return true
|
|
66
|
+
return process.stdout?.isTTY === true
|
|
67
|
+
}
|
|
68
|
+
|
|
43
69
|
/** Log levels supported by the logger */
|
|
44
70
|
export type LogLevel = 'debug' | 'info' | 'warn' | 'error'
|
|
45
71
|
|
|
46
|
-
|
|
47
|
-
|
|
72
|
+
const levelRank: Record<LogLevel, number> = {
|
|
73
|
+
debug: 0,
|
|
74
|
+
info: 1,
|
|
75
|
+
warn: 2,
|
|
76
|
+
error: 3
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Attributes that can be passed to log methods. Objects are rendered as compact JSON. */
|
|
80
|
+
export type LogAttributes = Record<string, unknown>
|
|
48
81
|
|
|
49
82
|
/** Log method signature */
|
|
50
83
|
export type LogMethod = (message: string, attributes?: LogAttributes) => void
|
|
@@ -55,6 +88,8 @@ export type CoolLogger = {
|
|
|
55
88
|
info: LogMethod
|
|
56
89
|
warn: LogMethod
|
|
57
90
|
error: LogMethod
|
|
91
|
+
/** Returns a logger that adds `attributes` to every entry (call-site attributes win). */
|
|
92
|
+
child: (attributes: LogAttributes) => CoolLogger
|
|
58
93
|
}
|
|
59
94
|
|
|
60
95
|
/** Color config per log level */
|
|
@@ -65,11 +100,24 @@ const levelColors: Record<LogLevel, { badge: string; text: string }> = {
|
|
|
65
100
|
error: { badge: terminalColors.red, text: terminalColors.brightRed }
|
|
66
101
|
}
|
|
67
102
|
|
|
103
|
+
function formatValue(value: unknown): string {
|
|
104
|
+
if (value instanceof Error) return String(value)
|
|
105
|
+
if (typeof value !== 'object' || value === null) return String(value)
|
|
106
|
+
try {
|
|
107
|
+
return JSON.stringify(value)
|
|
108
|
+
} catch (_error) {
|
|
109
|
+
return '[Unserializable]'
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
68
113
|
/** Format attributes as a compact colored string */
|
|
69
114
|
function formatAttributes(attrs?: LogAttributes): string {
|
|
70
115
|
if (!attrs || Object.keys(attrs).length === 0) return ''
|
|
71
116
|
const formatted = Object.entries(attrs)
|
|
72
|
-
.map(
|
|
117
|
+
.map(
|
|
118
|
+
([k, v]) =>
|
|
119
|
+
`${terminalColors.dim}${k}=${terminalColors.reset}${formatValue(v)}`
|
|
120
|
+
)
|
|
73
121
|
.join(' ')
|
|
74
122
|
return ` ${formatted}`
|
|
75
123
|
}
|
|
@@ -78,9 +126,11 @@ function formatAttributes(attrs?: LogAttributes): string {
|
|
|
78
126
|
* Options for creating a cool logger
|
|
79
127
|
*/
|
|
80
128
|
export type CoolLoggerOptions = {
|
|
81
|
-
/** Only use colors in development (default: true) */
|
|
129
|
+
/** Only use colors in development (default: true). Colors also require `supportsColor()`. */
|
|
82
130
|
colorsInDevOnly?: boolean
|
|
83
|
-
/**
|
|
131
|
+
/** Lowest level that is printed (default: 'debug' in dev, 'info' in production) */
|
|
132
|
+
minLevel?: LogLevel
|
|
133
|
+
/** Callback for each log entry, before level filtering (e.g., to add to OpenTelemetry span) */
|
|
84
134
|
onLog?: (level: LogLevel, message: string, attributes?: LogAttributes) => void
|
|
85
135
|
}
|
|
86
136
|
|
|
@@ -93,40 +143,53 @@ export type CoolLoggerOptions = {
|
|
|
93
143
|
* logger.info('User logged in', { userId: '123' })
|
|
94
144
|
* logger.warn('Rate limit approaching', { remaining: 10 })
|
|
95
145
|
* logger.error('Payment failed', { reason: 'insufficient_funds' })
|
|
146
|
+
* const requestLog = logger.child({ requestId: 'abc' })
|
|
96
147
|
* ```
|
|
97
148
|
*/
|
|
98
149
|
export function createCoolLogger(options: CoolLoggerOptions = {}): CoolLogger {
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
|
|
150
|
+
const dev = isDev()
|
|
151
|
+
const {
|
|
152
|
+
colorsInDevOnly = true,
|
|
153
|
+
minLevel = dev ? 'debug' : 'info',
|
|
154
|
+
onLog
|
|
155
|
+
} = options
|
|
156
|
+
const useColors = supportsColor() && (!colorsInDevOnly || dev)
|
|
157
|
+
|
|
158
|
+
const createLogger = (base?: LogAttributes): CoolLogger => {
|
|
159
|
+
const createLogMethod =
|
|
160
|
+
(level: LogLevel): LogMethod =>
|
|
161
|
+
(message, callAttributes) => {
|
|
162
|
+
const attributes =
|
|
163
|
+
base && callAttributes
|
|
164
|
+
? { ...base, ...callAttributes }
|
|
165
|
+
: (callAttributes ?? base)
|
|
166
|
+
|
|
167
|
+
onLog?.(level, message, attributes)
|
|
168
|
+
|
|
169
|
+
if (levelRank[level] < levelRank[minLevel]) return
|
|
170
|
+
|
|
171
|
+
if (useColors) {
|
|
172
|
+
const { badge, text } = levelColors[level]
|
|
173
|
+
const label = level.toUpperCase().padEnd(5)
|
|
174
|
+
const attrStr = formatAttributes(attributes)
|
|
175
|
+
console.log(
|
|
176
|
+
`${badge}[${label}]${terminalColors.reset} ${text}${message}${terminalColors.reset}${attrStr}`
|
|
177
|
+
)
|
|
178
|
+
} else {
|
|
179
|
+
console[level](`[${level.toUpperCase()}]`, message, attributes ?? '')
|
|
180
|
+
}
|
|
121
181
|
}
|
|
122
|
-
}
|
|
123
182
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
183
|
+
return {
|
|
184
|
+
debug: createLogMethod('debug'),
|
|
185
|
+
info: createLogMethod('info'),
|
|
186
|
+
warn: createLogMethod('warn'),
|
|
187
|
+
error: createLogMethod('error'),
|
|
188
|
+
child: (attributes) => createLogger({ ...base, ...attributes })
|
|
189
|
+
}
|
|
129
190
|
}
|
|
191
|
+
|
|
192
|
+
return createLogger()
|
|
130
193
|
}
|
|
131
194
|
|
|
132
195
|
/**
|
|
@@ -151,11 +214,10 @@ export function logOperationSummary(
|
|
|
151
214
|
options: { devOnly?: boolean; error?: Error | unknown } = {}
|
|
152
215
|
): void {
|
|
153
216
|
const { devOnly = true, error } = options
|
|
154
|
-
const isDev = process.env.NODE_ENV !== 'production'
|
|
155
217
|
|
|
156
|
-
if (devOnly && !isDev) return
|
|
218
|
+
if (devOnly && !isDev()) return
|
|
157
219
|
|
|
158
|
-
const c = terminalColors
|
|
220
|
+
const c = supportsColor() ? terminalColors : noColors
|
|
159
221
|
const statusIcon = success
|
|
160
222
|
? `${c.brightGreen}✓${c.reset}`
|
|
161
223
|
: `${c.brightRed}✗${c.reset}`
|
|
@@ -172,7 +234,34 @@ export function logOperationSummary(
|
|
|
172
234
|
}
|
|
173
235
|
|
|
174
236
|
/**
|
|
175
|
-
*
|
|
237
|
+
* Starts timing an operation. Call the returned `done` to log it via `logOperationSummary`.
|
|
238
|
+
*
|
|
239
|
+
* @example
|
|
240
|
+
* ```ts
|
|
241
|
+
* const done = startOperation('user.create')
|
|
242
|
+
* try {
|
|
243
|
+
* await doSomething()
|
|
244
|
+
* done()
|
|
245
|
+
* } catch (error) {
|
|
246
|
+
* done(false, error)
|
|
247
|
+
* }
|
|
248
|
+
* ```
|
|
249
|
+
*/
|
|
250
|
+
export function startOperation(
|
|
251
|
+
operationName: string,
|
|
252
|
+
options: { devOnly?: boolean } = {}
|
|
253
|
+
): (success?: boolean, error?: unknown) => void {
|
|
254
|
+
const start = performance.now()
|
|
255
|
+
return (success = true, error) =>
|
|
256
|
+
logOperationSummary(operationName, performance.now() - start, success, {
|
|
257
|
+
...options,
|
|
258
|
+
error
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Helper to colorize text for console output. Returns `text` unchanged in
|
|
264
|
+
* production or when colors aren't supported (see `supportsColor`).
|
|
176
265
|
*
|
|
177
266
|
* @example
|
|
178
267
|
* ```ts
|
|
@@ -185,8 +274,7 @@ export function colorize(
|
|
|
185
274
|
color: TerminalColor,
|
|
186
275
|
...modifiers: TerminalColor[]
|
|
187
276
|
): string {
|
|
188
|
-
|
|
189
|
-
if (!isDev) return text
|
|
277
|
+
if (!isDev() || !supportsColor()) return text
|
|
190
278
|
|
|
191
279
|
const colorCodes = [
|
|
192
280
|
terminalColors[color],
|
|
@@ -210,9 +298,8 @@ export function logBanner(
|
|
|
210
298
|
options: { devOnly?: boolean } = {}
|
|
211
299
|
): void {
|
|
212
300
|
const { devOnly = true } = options
|
|
213
|
-
const isDev = process.env.NODE_ENV !== 'production'
|
|
214
301
|
|
|
215
|
-
if (devOnly && !isDev) {
|
|
302
|
+
if ((devOnly && !isDev()) || !supportsColor()) {
|
|
216
303
|
console.log(`=== ${text} ===`)
|
|
217
304
|
return
|
|
218
305
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const BYTE_CHUNK_SIZE = 0x8000
|
|
2
|
+
const BASE64 = /^[A-Za-z0-9+/]*={0,2}$/
|
|
3
|
+
const BASE64URL = /^[A-Za-z0-9_-]*={0,2}$/
|
|
4
|
+
const HEX = /^(?:[0-9a-fA-F]{2})*$/
|
|
5
|
+
const textEncoder = new TextEncoder()
|
|
6
|
+
const textDecoder = new TextDecoder()
|
|
7
|
+
|
|
8
|
+
export type BytesLike = Uint8Array | ArrayBuffer
|
|
9
|
+
|
|
10
|
+
export function utf8Encode(value: string): Uint8Array<ArrayBuffer> {
|
|
11
|
+
return textEncoder.encode(value)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Invalid UTF-8 sequences become U+FFFD (same as `TextDecoder` / `Buffer#toString`). */
|
|
15
|
+
export function utf8Decode(bytes: BytesLike): string {
|
|
16
|
+
return textDecoder.decode(bytes)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Standard base64 with `=` padding. */
|
|
20
|
+
export function encodeBase64(bytes: BytesLike): string {
|
|
21
|
+
const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)
|
|
22
|
+
let binary = ''
|
|
23
|
+
for (let offset = 0; offset < view.length; offset += BYTE_CHUNK_SIZE) {
|
|
24
|
+
binary += String.fromCharCode(
|
|
25
|
+
...view.subarray(offset, offset + BYTE_CHUNK_SIZE)
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
return btoa(binary)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Decodes standard base64 (padding optional). Throws `TypeError` on invalid input. */
|
|
32
|
+
export function decodeBase64(value: string): Uint8Array<ArrayBuffer> {
|
|
33
|
+
if (!BASE64.test(value)) throw new TypeError('Invalid base64 string')
|
|
34
|
+
const unpadded = value.replace(/=+$/, '')
|
|
35
|
+
if (unpadded.length % 4 === 1) throw new TypeError('Invalid base64 string')
|
|
36
|
+
|
|
37
|
+
const binary = atob(unpadded.padEnd(Math.ceil(unpadded.length / 4) * 4, '='))
|
|
38
|
+
const bytes = new Uint8Array(binary.length)
|
|
39
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
40
|
+
bytes[index] = binary.charCodeAt(index)
|
|
41
|
+
}
|
|
42
|
+
return bytes
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** URL-safe base64 without padding (RFC 4648 §5). */
|
|
46
|
+
export function encodeBase64Url(bytes: BytesLike): string {
|
|
47
|
+
return encodeBase64(bytes)
|
|
48
|
+
.replaceAll('+', '-')
|
|
49
|
+
.replaceAll('/', '_')
|
|
50
|
+
.replace(/=+$/, '')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Decodes URL-safe base64 (padding optional). Throws `TypeError` on invalid input. */
|
|
54
|
+
export function decodeBase64Url(value: string): Uint8Array<ArrayBuffer> {
|
|
55
|
+
if (!BASE64URL.test(value)) throw new TypeError('Invalid base64url string')
|
|
56
|
+
return decodeBase64(value.replaceAll('-', '+').replaceAll('_', '/'))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Lowercase hex. */
|
|
60
|
+
export function bytesToHex(bytes: BytesLike): string {
|
|
61
|
+
const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)
|
|
62
|
+
let hex = ''
|
|
63
|
+
for (const byte of view) hex += byte.toString(16).padStart(2, '0')
|
|
64
|
+
return hex
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Decodes hex (either case). Throws `TypeError` on odd length or non-hex characters. */
|
|
68
|
+
export function hexToBytes(value: string): Uint8Array<ArrayBuffer> {
|
|
69
|
+
if (!HEX.test(value)) throw new TypeError('Invalid hex string')
|
|
70
|
+
const bytes = new Uint8Array(value.length / 2)
|
|
71
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
72
|
+
bytes[index] = Number.parseInt(value.slice(index * 2, index * 2 + 2), 16)
|
|
73
|
+
}
|
|
74
|
+
return bytes
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** UTF-8 for strings; a copy for bytes, so Web Crypto always gets an `ArrayBuffer`-backed view. */
|
|
78
|
+
export function toBytes(data: string | BytesLike): Uint8Array<ArrayBuffer> {
|
|
79
|
+
if (typeof data === 'string') return utf8Encode(data)
|
|
80
|
+
return data instanceof Uint8Array
|
|
81
|
+
? new Uint8Array(data)
|
|
82
|
+
: new Uint8Array(data.slice(0))
|
|
83
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type BytesLike,
|
|
3
|
+
bytesToHex,
|
|
4
|
+
decodeBase64,
|
|
5
|
+
decodeBase64Url,
|
|
6
|
+
encodeBase64,
|
|
7
|
+
encodeBase64Url,
|
|
8
|
+
hexToBytes,
|
|
9
|
+
toBytes,
|
|
10
|
+
utf8Encode
|
|
11
|
+
} from './encoding'
|
|
12
|
+
|
|
13
|
+
export type HmacAlgorithm = 'SHA-256' | 'SHA-384' | 'SHA-512'
|
|
14
|
+
export type SignatureEncoding = 'hex' | 'base64' | 'base64url'
|
|
15
|
+
|
|
16
|
+
export type HmacSignOptions = {
|
|
17
|
+
/** Default: `'SHA-256'` */
|
|
18
|
+
algorithm?: HmacAlgorithm
|
|
19
|
+
/** Default: `'hex'` */
|
|
20
|
+
encoding?: SignatureEncoding
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type HmacVerifyOptions = HmacSignOptions & {
|
|
24
|
+
/** Required prefix stripped before decoding, e.g. `'sha256='`. Missing prefix → `false`. */
|
|
25
|
+
prefix?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const HMAC_ALGORITHMS: readonly string[] = ['SHA-256', 'SHA-384', 'SHA-512']
|
|
29
|
+
const RANDOM_CHUNK_SIZE = 0x10000
|
|
30
|
+
const ENCODERS: Record<SignatureEncoding, (bytes: Uint8Array) => string> = {
|
|
31
|
+
hex: bytesToHex,
|
|
32
|
+
base64: encodeBase64,
|
|
33
|
+
base64url: encodeBase64Url
|
|
34
|
+
}
|
|
35
|
+
const DECODERS: Record<
|
|
36
|
+
SignatureEncoding,
|
|
37
|
+
(value: string) => Uint8Array<ArrayBuffer>
|
|
38
|
+
> = { hex: hexToBytes, base64: decodeBase64, base64url: decodeBase64Url }
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Constant-time equality for strings (compared as UTF-8) or bytes. Runs over the
|
|
42
|
+
* longer input, so timing reveals at most the lengths, never where they differ.
|
|
43
|
+
*/
|
|
44
|
+
export function safeEqual(
|
|
45
|
+
a: string | Uint8Array,
|
|
46
|
+
b: string | Uint8Array
|
|
47
|
+
): boolean {
|
|
48
|
+
const left = typeof a === 'string' ? utf8Encode(a) : a
|
|
49
|
+
const right = typeof b === 'string' ? utf8Encode(b) : b
|
|
50
|
+
const length = Math.max(left.length, right.length)
|
|
51
|
+
let diff = left.length ^ right.length
|
|
52
|
+
for (let index = 0; index < length; index += 1) {
|
|
53
|
+
diff |= (left[index] ?? 0) ^ (right[index] ?? 0)
|
|
54
|
+
}
|
|
55
|
+
return diff === 0
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Cryptographically secure random bytes. */
|
|
59
|
+
export function randomBytes(byteLength: number): Uint8Array<ArrayBuffer> {
|
|
60
|
+
if (!Number.isSafeInteger(byteLength) || byteLength < 0) {
|
|
61
|
+
throw new RangeError('byteLength must be a non-negative integer')
|
|
62
|
+
}
|
|
63
|
+
const cryptoApi = globalThis.crypto
|
|
64
|
+
if (!cryptoApi?.getRandomValues) {
|
|
65
|
+
throw new Error('Web Crypto is not available in this runtime')
|
|
66
|
+
}
|
|
67
|
+
const bytes = new Uint8Array(byteLength)
|
|
68
|
+
// getRandomValues rejects more than 65536 bytes per call.
|
|
69
|
+
for (let offset = 0; offset < byteLength; offset += RANDOM_CHUNK_SIZE) {
|
|
70
|
+
cryptoApi.getRandomValues(
|
|
71
|
+
bytes.subarray(offset, offset + RANDOM_CHUNK_SIZE)
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
return bytes
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Random base64url token. 32 bytes → 43 characters, 256 bits of entropy. */
|
|
78
|
+
export function randomToken(byteLength = 32): string {
|
|
79
|
+
if (!Number.isSafeInteger(byteLength) || byteLength < 1) {
|
|
80
|
+
throw new RangeError('byteLength must be a positive integer')
|
|
81
|
+
}
|
|
82
|
+
return encodeBase64Url(randomBytes(byteLength))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function sha256(data: string | BytesLike): Promise<Uint8Array> {
|
|
86
|
+
return new Uint8Array(
|
|
87
|
+
await getSubtleCrypto().digest('SHA-256', toBytes(data))
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function sha256Hex(data: string | BytesLike): Promise<string> {
|
|
92
|
+
return bytesToHex(await sha256(data))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function hmacSign(
|
|
96
|
+
secret: string | BytesLike,
|
|
97
|
+
data: string | BytesLike,
|
|
98
|
+
options: HmacSignOptions = {}
|
|
99
|
+
): Promise<string> {
|
|
100
|
+
const { algorithm = 'SHA-256', encoding = 'hex' } = options
|
|
101
|
+
assertEncoding(encoding)
|
|
102
|
+
const key = await importHmacKey(secret, algorithm, 'sign')
|
|
103
|
+
const signature = await getSubtleCrypto().sign('HMAC', key, toBytes(data))
|
|
104
|
+
return ENCODERS[encoding](new Uint8Array(signature))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Checks `signature` against the HMAC of `data` using Web Crypto's `verify`
|
|
109
|
+
* (constant-time). Malformed or wrongly-prefixed signatures return `false`;
|
|
110
|
+
* configuration problems (empty secret, unknown algorithm) throw.
|
|
111
|
+
*/
|
|
112
|
+
export async function verifyHmacSignature(
|
|
113
|
+
secret: string | BytesLike,
|
|
114
|
+
data: string | BytesLike,
|
|
115
|
+
signature: string,
|
|
116
|
+
options: HmacVerifyOptions = {}
|
|
117
|
+
): Promise<boolean> {
|
|
118
|
+
const { algorithm = 'SHA-256', encoding = 'hex', prefix } = options
|
|
119
|
+
assertEncoding(encoding)
|
|
120
|
+
const key = await importHmacKey(secret, algorithm, 'verify')
|
|
121
|
+
|
|
122
|
+
let encoded = signature
|
|
123
|
+
if (prefix !== undefined) {
|
|
124
|
+
if (!signature.startsWith(prefix)) return false
|
|
125
|
+
encoded = signature.slice(prefix.length)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let received: Uint8Array<ArrayBuffer>
|
|
129
|
+
try {
|
|
130
|
+
received = DECODERS[encoding](encoded)
|
|
131
|
+
} catch (_error) {
|
|
132
|
+
return false
|
|
133
|
+
}
|
|
134
|
+
return getSubtleCrypto().verify('HMAC', key, received, toBytes(data))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function importHmacKey(
|
|
138
|
+
secret: string | BytesLike,
|
|
139
|
+
algorithm: HmacAlgorithm,
|
|
140
|
+
usage: 'sign' | 'verify'
|
|
141
|
+
): Promise<CryptoKey> {
|
|
142
|
+
if (!HMAC_ALGORITHMS.includes(algorithm)) {
|
|
143
|
+
throw new TypeError(`Unsupported HMAC algorithm: ${algorithm}`)
|
|
144
|
+
}
|
|
145
|
+
const keyBytes = toBytes(secret)
|
|
146
|
+
if (keyBytes.length === 0)
|
|
147
|
+
throw new TypeError('HMAC secret must not be empty')
|
|
148
|
+
return getSubtleCrypto().importKey(
|
|
149
|
+
'raw',
|
|
150
|
+
keyBytes,
|
|
151
|
+
{ name: 'HMAC', hash: algorithm },
|
|
152
|
+
false,
|
|
153
|
+
[usage]
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function assertEncoding(encoding: SignatureEncoding): void {
|
|
158
|
+
if (!Object.hasOwn(ENCODERS, encoding)) {
|
|
159
|
+
throw new TypeError(`Unsupported signature encoding: ${encoding}`)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** @internal */
|
|
164
|
+
export function getSubtleCrypto(): SubtleCrypto {
|
|
165
|
+
const subtle = globalThis.crypto?.subtle
|
|
166
|
+
if (!subtle) {
|
|
167
|
+
throw new Error('Web Crypto is not available in this runtime')
|
|
168
|
+
}
|
|
169
|
+
return subtle
|
|
170
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export {
|
|
2
|
+
type BytesLike,
|
|
3
|
+
bytesToHex,
|
|
4
|
+
decodeBase64,
|
|
5
|
+
decodeBase64Url,
|
|
6
|
+
encodeBase64,
|
|
7
|
+
encodeBase64Url,
|
|
8
|
+
hexToBytes,
|
|
9
|
+
utf8Decode,
|
|
10
|
+
utf8Encode
|
|
11
|
+
} from './encoding'
|
|
12
|
+
export {
|
|
13
|
+
type HmacAlgorithm,
|
|
14
|
+
type HmacSignOptions,
|
|
15
|
+
type HmacVerifyOptions,
|
|
16
|
+
hmacSign,
|
|
17
|
+
randomBytes,
|
|
18
|
+
randomToken,
|
|
19
|
+
type SignatureEncoding,
|
|
20
|
+
safeEqual,
|
|
21
|
+
sha256,
|
|
22
|
+
sha256Hex,
|
|
23
|
+
verifyHmacSignature
|
|
24
|
+
} from './hash'
|
|
25
|
+
export * from './sealer'
|
|
26
|
+
export * from './webhook'
|