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
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
export type PlaceholderValue = string | number
|
|
2
|
+
|
|
3
|
+
// `\w` in PLACEHOLDER_PATTERN, spelled out so the types match the runtime exactly
|
|
4
|
+
type Chars<S extends string> = S extends `${infer C}${infer Rest}`
|
|
5
|
+
? C | Chars<Rest>
|
|
6
|
+
: never
|
|
7
|
+
type Letter = Chars<'abcdefghijklmnopqrstuvwxyz'>
|
|
8
|
+
type WordChar = Letter | Uppercase<Letter> | Chars<'0123456789_'>
|
|
9
|
+
|
|
10
|
+
type IsWord<S extends string> = S extends `${infer C}${infer Rest}`
|
|
11
|
+
? C extends WordChar
|
|
12
|
+
? Rest extends ''
|
|
13
|
+
? true
|
|
14
|
+
: IsWord<Rest>
|
|
15
|
+
: false
|
|
16
|
+
: false
|
|
17
|
+
|
|
18
|
+
// `{a {b}` holds the placeholder `b`: keep the text after the last `{`
|
|
19
|
+
type AfterLastBrace<S extends string> = S extends `${string}{${infer Rest}`
|
|
20
|
+
? AfterLastBrace<Rest>
|
|
21
|
+
: S
|
|
22
|
+
|
|
23
|
+
type ExtractNames<
|
|
24
|
+
S extends string,
|
|
25
|
+
Found extends string = never
|
|
26
|
+
> = S extends `${string}{${infer Body}}${infer Rest}`
|
|
27
|
+
? ExtractNames<
|
|
28
|
+
Rest,
|
|
29
|
+
| Found
|
|
30
|
+
| (IsWord<AfterLastBrace<Body>> extends true
|
|
31
|
+
? AfterLastBrace<Body>
|
|
32
|
+
: never)
|
|
33
|
+
>
|
|
34
|
+
: Found
|
|
35
|
+
|
|
36
|
+
/** `'Hi {name}, you have {count}'` → `'name' | 'count'`; `string` for non-literal templates. */
|
|
37
|
+
export type PlaceholderNames<S extends string> = string extends S
|
|
38
|
+
? string
|
|
39
|
+
: ExtractNames<S>
|
|
40
|
+
|
|
41
|
+
export type PlaceholderParams<S extends string> = Record<
|
|
42
|
+
PlaceholderNames<S>,
|
|
43
|
+
PlaceholderValue
|
|
44
|
+
>
|
|
45
|
+
|
|
46
|
+
/** Params argument: required with placeholders, absent without, optional for non-literal templates. */
|
|
47
|
+
export type PlaceholderArgs<S extends string> = string extends S
|
|
48
|
+
? [params?: PlaceholderParams<S>]
|
|
49
|
+
: [PlaceholderNames<S>] extends [never]
|
|
50
|
+
? []
|
|
51
|
+
: [params: PlaceholderParams<S>]
|
|
52
|
+
|
|
53
|
+
/** A `{name}` placeholder: word characters only, so `{ x }` and `{n, plural}` are plain text. */
|
|
54
|
+
const PLACEHOLDER_PATTERN = /\{(\w+)\}/g
|
|
55
|
+
|
|
56
|
+
/** Unique placeholder names in order of first appearance. */
|
|
57
|
+
export function extractPlaceholders<S extends string>(
|
|
58
|
+
template: S
|
|
59
|
+
): PlaceholderNames<S>[] {
|
|
60
|
+
const names = template.matchAll(PLACEHOLDER_PATTERN).map((match) => match[1])
|
|
61
|
+
return [...new Set(names)] as PlaceholderNames<S>[]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Replaces `{name}` placeholders with `params`. Placeholders without a
|
|
66
|
+
* (non-null) param are left as-is. There is no escape syntax.
|
|
67
|
+
*/
|
|
68
|
+
export function interpolate<S extends string>(
|
|
69
|
+
template: S,
|
|
70
|
+
...args: PlaceholderArgs<S>
|
|
71
|
+
): string {
|
|
72
|
+
const params: Partial<Record<string, PlaceholderValue>> = args[0] ?? {}
|
|
73
|
+
return template.replace(PLACEHOLDER_PATTERN, (match, name: string) => {
|
|
74
|
+
const value = Object.hasOwn(params, name) ? params[name] : undefined
|
|
75
|
+
return value == null ? match : String(value)
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type Messages = { readonly [key: string]: string | Messages }
|
|
80
|
+
|
|
81
|
+
/** Dot-path keys of the string leaves: `{ a: { b: 'x' } }` → `'a.b'`. */
|
|
82
|
+
export type MessageKeys<T> = {
|
|
83
|
+
[K in keyof T & string]: T[K] extends string ? K : `${K}.${MessageKeys<T[K]>}`
|
|
84
|
+
}[keyof T & string]
|
|
85
|
+
|
|
86
|
+
/** Message at a dot-path key. */
|
|
87
|
+
export type MessageAt<T, K extends string> = K extends keyof T
|
|
88
|
+
? T[K]
|
|
89
|
+
: K extends `${infer Head}.${infer Rest}`
|
|
90
|
+
? Head extends keyof T
|
|
91
|
+
? MessageAt<T[Head], Rest>
|
|
92
|
+
: never
|
|
93
|
+
: never
|
|
94
|
+
|
|
95
|
+
/** `{ a: { b: 'x' } }` → `{ 'a.b': 'x' }` */
|
|
96
|
+
export function flattenMessages<const T extends Messages>(
|
|
97
|
+
messages: T
|
|
98
|
+
): Record<MessageKeys<T>, string> {
|
|
99
|
+
const out: Record<string, string> = {}
|
|
100
|
+
const visit = (node: Messages, prefix: string) => {
|
|
101
|
+
for (const [key, value] of Object.entries(node)) {
|
|
102
|
+
if (typeof value === 'string') out[prefix + key] = value
|
|
103
|
+
else visit(value, `${prefix + key}.`)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
visit(messages, '')
|
|
107
|
+
return out
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Typed `t(key, params)` over a nested messages object. With literal messages
|
|
112
|
+
* (inline `const` objects), params are required exactly when the message has
|
|
113
|
+
* placeholders. Unknown keys at runtime return the key.
|
|
114
|
+
*/
|
|
115
|
+
export function createTranslator<const T extends Messages>(messages: T) {
|
|
116
|
+
const flat: Partial<Record<string, string>> = flattenMessages(messages)
|
|
117
|
+
return <K extends MessageKeys<T>>(
|
|
118
|
+
key: K,
|
|
119
|
+
...args: PlaceholderArgs<Extract<MessageAt<T, K>, string>>
|
|
120
|
+
): string =>
|
|
121
|
+
interpolate(
|
|
122
|
+
flat[key] ?? key,
|
|
123
|
+
...(args as [Record<string, PlaceholderValue>?])
|
|
124
|
+
)
|
|
125
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// Dependency-free helpers only. Modules that need zod, network access or a
|
|
2
|
+
// specific runtime live on subpath exports (hanzio/zod, hanzio/api-wrapper,
|
|
3
|
+
// hanzio/jwt, hanzio/sitemap, hanzio/p-queue, hanzio/cool-console-log,
|
|
4
|
+
// hanzio/secrets).
|
|
1
5
|
export * from './typedswitch'
|
|
2
6
|
export * from './array'
|
|
3
7
|
export * from './string'
|
|
@@ -5,12 +9,12 @@ export * from './promise'
|
|
|
5
9
|
export * from './cache'
|
|
6
10
|
export * from './state'
|
|
7
11
|
export * from './math'
|
|
12
|
+
export * from './color'
|
|
8
13
|
export * from './date'
|
|
9
14
|
export * from './url'
|
|
10
|
-
export * from './
|
|
11
|
-
export * from './
|
|
12
|
-
export * from './
|
|
13
|
-
export * from './
|
|
14
|
-
export * from './
|
|
15
|
-
export * from './
|
|
16
|
-
export * from './secrets'
|
|
15
|
+
export * from './error'
|
|
16
|
+
export * from './guard'
|
|
17
|
+
export * from './object'
|
|
18
|
+
export * from './types'
|
|
19
|
+
export * from './function'
|
|
20
|
+
export * from './emitter'
|
package/src/jwt/index.ts
CHANGED
|
@@ -1,171 +1,245 @@
|
|
|
1
|
+
import {
|
|
2
|
+
decodeBase64Url,
|
|
3
|
+
encodeBase64Url,
|
|
4
|
+
utf8Decode,
|
|
5
|
+
utf8Encode
|
|
6
|
+
} from '../crypto/encoding'
|
|
7
|
+
import { getSubtleCrypto } from '../crypto/hash'
|
|
8
|
+
import type { Result } from '../promise'
|
|
9
|
+
import { isRecord } from '../guard'
|
|
10
|
+
|
|
1
11
|
export type JWTPayload = Record<string, unknown>
|
|
2
12
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
13
|
+
export type JwtAlgorithm = 'HS256' | 'HS384' | 'HS512'
|
|
14
|
+
|
|
15
|
+
export type JwtHeader = { alg: string; typ?: string } & Record<string, unknown>
|
|
16
|
+
|
|
17
|
+
export type JwtSignOptions = {
|
|
18
|
+
/** Seconds from now until `exp`. */
|
|
19
|
+
expiresIn?: number
|
|
20
|
+
/** Seconds from now until `nbf`. */
|
|
21
|
+
notBefore?: number
|
|
22
|
+
issuer?: string
|
|
23
|
+
audience?: string | string[]
|
|
24
|
+
/** Default: `'HS256'` */
|
|
25
|
+
algorithm?: JwtAlgorithm
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type JwtVerifyOptions = {
|
|
29
|
+
/** Clock skew tolerance for `exp`/`nbf`, in seconds. Default: 0 */
|
|
30
|
+
leewaySec?: number
|
|
31
|
+
/** Required `iss` value. */
|
|
32
|
+
issuer?: string
|
|
33
|
+
/** Required `aud` value (matches a string `aud` or an entry of an array `aud`). */
|
|
34
|
+
audience?: string
|
|
35
|
+
/** Accepted header `alg` values. Default: all of HS256/HS384/HS512. */
|
|
36
|
+
algorithms?: JwtAlgorithm[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type JwtVerifyErrorCode =
|
|
40
|
+
| 'malformed'
|
|
41
|
+
| 'unsupported_alg'
|
|
42
|
+
| 'invalid_signature'
|
|
43
|
+
| 'expired'
|
|
44
|
+
| 'not_yet_valid'
|
|
45
|
+
| 'invalid_issuer'
|
|
46
|
+
| 'invalid_audience'
|
|
47
|
+
|
|
48
|
+
export class JwtVerifyError extends Error {
|
|
49
|
+
readonly code: JwtVerifyErrorCode
|
|
50
|
+
|
|
51
|
+
constructor(code: JwtVerifyErrorCode) {
|
|
52
|
+
super(`JWT verification failed: ${code}`)
|
|
53
|
+
this.name = 'JwtVerifyError'
|
|
54
|
+
this.code = code
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const HASHES: Record<JwtAlgorithm, string> = {
|
|
59
|
+
HS256: 'SHA-256',
|
|
60
|
+
HS384: 'SHA-384',
|
|
61
|
+
HS512: 'SHA-512'
|
|
62
|
+
}
|
|
63
|
+
const ALGORITHMS = Object.keys(HASHES) as JwtAlgorithm[]
|
|
64
|
+
const BASE64URL = /^[A-Za-z0-9_-]*$/
|
|
65
|
+
const KEY_CACHE_LIMIT = 100
|
|
6
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Signs `payload` as a compact JWS. `iat` is always set to now.
|
|
69
|
+
* Claims derived from `options` (`exp`, `nbf`, `iss`, `aud`) override the same
|
|
70
|
+
* claims in `payload`; payload claims are kept when the option is not given.
|
|
71
|
+
*/
|
|
7
72
|
export async function jwtSign<Payload extends JWTPayload>(
|
|
8
73
|
payload: Payload,
|
|
9
|
-
secret: string
|
|
74
|
+
secret: string,
|
|
75
|
+
options: JwtSignOptions = {}
|
|
10
76
|
): Promise<string> {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
77
|
+
const {
|
|
78
|
+
expiresIn,
|
|
79
|
+
notBefore,
|
|
80
|
+
issuer,
|
|
81
|
+
audience,
|
|
82
|
+
algorithm = 'HS256'
|
|
83
|
+
} = options
|
|
84
|
+
if (!Object.hasOwn(HASHES, algorithm)) {
|
|
85
|
+
throw new TypeError(`Unsupported JWT algorithm: ${algorithm}`)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const now = Math.floor(Date.now() / 1000)
|
|
89
|
+
const claims: JWTPayload = { ...payload, iat: now }
|
|
90
|
+
if (expiresIn !== undefined) claims.exp = now + toSeconds(expiresIn)
|
|
91
|
+
if (notBefore !== undefined) claims.nbf = now + toSeconds(notBefore)
|
|
92
|
+
if (issuer !== undefined) claims.iss = issuer
|
|
93
|
+
if (audience !== undefined) claims.aud = audience
|
|
94
|
+
|
|
95
|
+
const data = `${encodeJson({ alg: algorithm, typ: 'JWT' })}.${encodeJson(claims)}`
|
|
96
|
+
const key = await getKey(secret, algorithm, 'sign')
|
|
97
|
+
const signature = await getSubtleCrypto().sign('HMAC', key, utf8Encode(data))
|
|
18
98
|
|
|
19
99
|
return `${data}.${encodeBase64Url(signature)}`
|
|
20
100
|
}
|
|
21
101
|
|
|
102
|
+
/** Verifies a token. Returns `null` for any failure; use `jwtVerifyResult` for the reason. */
|
|
22
103
|
export async function jwtVerify<Payload extends JWTPayload>(
|
|
23
104
|
token: string,
|
|
24
|
-
secret: string
|
|
105
|
+
secret: string,
|
|
106
|
+
options?: JwtVerifyOptions
|
|
25
107
|
): Promise<Payload | null> {
|
|
26
108
|
try {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const [encodedHeader, encodedPayload, encodedSignature] = parts as [
|
|
31
|
-
string,
|
|
32
|
-
string,
|
|
33
|
-
string
|
|
34
|
-
]
|
|
35
|
-
const header = decodeJson(encodedHeader)
|
|
36
|
-
if (!isJwtHeader(header)) return null
|
|
37
|
-
|
|
38
|
-
const data = `${encodedHeader}.${encodedPayload}`
|
|
39
|
-
const signature = decodeBase64Url(encodedSignature)
|
|
40
|
-
const isValid = await verify(data, signature, secret)
|
|
41
|
-
if (!isValid) return null
|
|
42
|
-
|
|
43
|
-
const payload = decodeJson(encodedPayload)
|
|
44
|
-
if (!isJwtPayload(payload) || !isTimeValid(payload)) return null
|
|
45
|
-
|
|
46
|
-
return payload as Payload
|
|
109
|
+
const result = await jwtVerifyResult<Payload>(token, secret, options)
|
|
110
|
+
return result.ok ? result.data : null
|
|
47
111
|
} catch (_error) {
|
|
48
112
|
return null
|
|
49
113
|
}
|
|
50
114
|
}
|
|
51
115
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Verifies signature, `exp`/`nbf` and optional `iss`/`aud`.
|
|
118
|
+
* Token problems are returned as a failed `Result`; configuration problems
|
|
119
|
+
* (no Web Crypto, empty secret) throw.
|
|
120
|
+
*/
|
|
121
|
+
export async function jwtVerifyResult<Payload extends JWTPayload>(
|
|
122
|
+
token: string,
|
|
123
|
+
secret: string,
|
|
124
|
+
options: JwtVerifyOptions = {}
|
|
125
|
+
): Promise<Result<Payload, JwtVerifyError>> {
|
|
126
|
+
const { leewaySec = 0, issuer, audience, algorithms = ALGORITHMS } = options
|
|
127
|
+
|
|
128
|
+
const decoded = jwtDecode<Payload>(token)
|
|
129
|
+
if (!decoded) return fail('malformed')
|
|
130
|
+
|
|
131
|
+
const { alg } = decoded.header
|
|
132
|
+
// Only header algs that are both known HMAC algs and explicitly allowed; never 'none'.
|
|
133
|
+
if (
|
|
134
|
+
!Object.hasOwn(HASHES, alg) ||
|
|
135
|
+
!algorithms.includes(alg as JwtAlgorithm)
|
|
136
|
+
) {
|
|
137
|
+
return fail('unsupported_alg')
|
|
138
|
+
}
|
|
56
139
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
|
|
140
|
+
const lastDot = token.lastIndexOf('.')
|
|
141
|
+
let signature: Uint8Array<ArrayBuffer>
|
|
142
|
+
try {
|
|
143
|
+
signature = decodeBase64Url(token.slice(lastDot + 1))
|
|
144
|
+
} catch (_error) {
|
|
145
|
+
return fail('malformed')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const key = await getKey(secret, alg as JwtAlgorithm, 'verify')
|
|
149
|
+
const isValid = await getSubtleCrypto().verify(
|
|
64
150
|
'HMAC',
|
|
65
151
|
key,
|
|
66
152
|
signature,
|
|
67
|
-
|
|
153
|
+
utf8Encode(token.slice(0, lastDot))
|
|
68
154
|
)
|
|
69
|
-
|
|
155
|
+
if (!isValid) return fail('invalid_signature')
|
|
70
156
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
)
|
|
75
|
-
return await getSubtleCrypto().importKey(
|
|
76
|
-
'raw',
|
|
77
|
-
encodeText(secret),
|
|
78
|
-
{
|
|
79
|
-
name: 'HMAC',
|
|
80
|
-
hash: 'SHA-256'
|
|
81
|
-
},
|
|
82
|
-
false,
|
|
83
|
-
usages
|
|
84
|
-
)
|
|
85
|
-
}
|
|
157
|
+
const { payload } = decoded
|
|
158
|
+
const { exp, nbf } = payload
|
|
159
|
+
if (exp !== undefined && typeof exp !== 'number') return fail('malformed')
|
|
160
|
+
if (nbf !== undefined && typeof nbf !== 'number') return fail('malformed')
|
|
86
161
|
|
|
87
|
-
|
|
88
|
-
return
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
.replace(/=+$/, '')
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function decodeBase64Url(value: string): ArrayBuffer {
|
|
104
|
-
const base64 = value.replaceAll('-', '+').replaceAll('_', '/')
|
|
105
|
-
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, '=')
|
|
106
|
-
return binaryStringToBytes(atob(padded)).buffer
|
|
107
|
-
}
|
|
162
|
+
const now = Math.floor(Date.now() / 1000)
|
|
163
|
+
if (exp !== undefined && now >= exp + leewaySec) return fail('expired')
|
|
164
|
+
if (nbf !== undefined && now + leewaySec < nbf) return fail('not_yet_valid')
|
|
165
|
+
if (issuer !== undefined && payload.iss !== issuer) {
|
|
166
|
+
return fail('invalid_issuer')
|
|
167
|
+
}
|
|
168
|
+
if (audience !== undefined) {
|
|
169
|
+
const { aud } = payload
|
|
170
|
+
const matches = Array.isArray(aud)
|
|
171
|
+
? aud.includes(audience)
|
|
172
|
+
: aud === audience
|
|
173
|
+
if (!matches) return fail('invalid_audience')
|
|
174
|
+
}
|
|
108
175
|
|
|
109
|
-
|
|
110
|
-
return toArrayBuffer(textEncoder.encode(value))
|
|
176
|
+
return { ok: true, data: payload, error: null }
|
|
111
177
|
}
|
|
112
178
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
179
|
+
/** Decodes header and payload WITHOUT verifying the signature. Never trust the result for auth. */
|
|
180
|
+
export function jwtDecode<Payload extends JWTPayload = JWTPayload>(
|
|
181
|
+
token: string
|
|
182
|
+
): { header: JwtHeader; payload: Payload } | null {
|
|
183
|
+
const parts = token.split('.')
|
|
184
|
+
if (parts.length !== 3 || !parts.every((part) => BASE64URL.test(part))) {
|
|
185
|
+
return null
|
|
186
|
+
}
|
|
119
187
|
|
|
120
|
-
|
|
121
|
-
|
|
188
|
+
try {
|
|
189
|
+
const header = decodeJson(parts[0] as string)
|
|
190
|
+
const payload = decodeJson(parts[1] as string)
|
|
191
|
+
if (!isRecord(header) || typeof header.alg !== 'string') return null
|
|
192
|
+
if (!isRecord(payload)) return null
|
|
122
193
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
)
|
|
194
|
+
return { header: header as JwtHeader, payload: payload as Payload }
|
|
195
|
+
} catch (_error) {
|
|
196
|
+
return null
|
|
127
197
|
}
|
|
128
|
-
|
|
129
|
-
return binary
|
|
130
198
|
}
|
|
131
199
|
|
|
132
|
-
function
|
|
133
|
-
|
|
200
|
+
function fail(code: JwtVerifyErrorCode): Result<never, JwtVerifyError> {
|
|
201
|
+
return { ok: false, data: null, error: new JwtVerifyError(code) }
|
|
202
|
+
}
|
|
134
203
|
|
|
135
|
-
|
|
136
|
-
|
|
204
|
+
function toSeconds(value: number): number {
|
|
205
|
+
// NaN/Infinity would serialize to `null` and silently drop the claim.
|
|
206
|
+
if (!Number.isFinite(value)) {
|
|
207
|
+
throw new TypeError(`Expected a finite number of seconds, got ${value}`)
|
|
137
208
|
}
|
|
138
|
-
|
|
139
|
-
return bytes
|
|
209
|
+
return Math.floor(value)
|
|
140
210
|
}
|
|
141
211
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
throw new Error('Web Crypto is not available in this runtime')
|
|
146
|
-
}
|
|
212
|
+
// ponytail: cleared wholesale at KEY_CACHE_LIMIT entries; swap for an LRU if
|
|
213
|
+
// an app rotates through more than ~100 secrets.
|
|
214
|
+
const keyCache = new Map<string, Promise<CryptoKey>>()
|
|
147
215
|
|
|
148
|
-
|
|
149
|
-
|
|
216
|
+
function getKey(
|
|
217
|
+
secret: string,
|
|
218
|
+
algorithm: JwtAlgorithm,
|
|
219
|
+
usage: 'sign' | 'verify'
|
|
220
|
+
): Promise<CryptoKey> {
|
|
221
|
+
const cacheKey = `${algorithm}:${usage}:${secret}`
|
|
222
|
+
const cached = keyCache.get(cacheKey)
|
|
223
|
+
if (cached) return cached
|
|
150
224
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
'
|
|
156
|
-
|
|
225
|
+
if (keyCache.size >= KEY_CACHE_LIMIT) keyCache.clear()
|
|
226
|
+
const key = getSubtleCrypto().importKey(
|
|
227
|
+
'raw',
|
|
228
|
+
utf8Encode(secret),
|
|
229
|
+
{ name: 'HMAC', hash: HASHES[algorithm] },
|
|
230
|
+
false,
|
|
231
|
+
[usage]
|
|
157
232
|
)
|
|
158
|
-
|
|
233
|
+
keyCache.set(cacheKey, key)
|
|
234
|
+
key.catch(() => keyCache.delete(cacheKey))
|
|
159
235
|
|
|
160
|
-
|
|
161
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
236
|
+
return key
|
|
162
237
|
}
|
|
163
238
|
|
|
164
|
-
function
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if (typeof payload.exp === 'number' && now >= payload.exp) return false
|
|
168
|
-
if (typeof payload.nbf === 'number' && now < payload.nbf) return false
|
|
239
|
+
function encodeJson(value: JWTPayload): string {
|
|
240
|
+
return encodeBase64Url(utf8Encode(JSON.stringify(value)))
|
|
241
|
+
}
|
|
169
242
|
|
|
170
|
-
|
|
243
|
+
function decodeJson(value: string): unknown {
|
|
244
|
+
return JSON.parse(utf8Decode(decodeBase64Url(value)))
|
|
171
245
|
}
|
package/src/math/index.ts
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
export { linearRegressionTrend } from './linearRegressionTrend'
|
|
2
1
|
export {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
resolveSeriesColorMap,
|
|
9
|
-
seriesColorKey,
|
|
10
|
-
withAlpha
|
|
11
|
-
} from './metricsSeriesColors'
|
|
2
|
+
type LinearRegression,
|
|
3
|
+
linearRegression,
|
|
4
|
+
linearRegressionTrend
|
|
5
|
+
} from './linearRegressionTrend'
|
|
6
|
+
export { clamp, lerp, normalize } from './scale'
|
|
@@ -1,12 +1,21 @@
|
|
|
1
|
+
export type LinearRegression = {
|
|
2
|
+
slope: number
|
|
3
|
+
intercept: number
|
|
4
|
+
/** Coefficient of determination (0..1). 1 when all values are equal (exact fit). */
|
|
5
|
+
r2: number
|
|
6
|
+
/** Fitted y for each index. */
|
|
7
|
+
fitted: number[]
|
|
8
|
+
}
|
|
9
|
+
|
|
1
10
|
/**
|
|
2
11
|
* Least-squares linear regression for y at x = 0, 1, …, n − 1.
|
|
3
|
-
* Returns
|
|
12
|
+
* Returns `null` if n < 2 or any input is non-finite.
|
|
4
13
|
*/
|
|
5
|
-
export function
|
|
14
|
+
export function linearRegression(values: number[]): LinearRegression | null {
|
|
6
15
|
const n = values.length
|
|
7
|
-
if (n < 2) return
|
|
16
|
+
if (n < 2) return null
|
|
8
17
|
for (const v of values) {
|
|
9
|
-
if (!Number.isFinite(v)) return
|
|
18
|
+
if (!Number.isFinite(v)) return null
|
|
10
19
|
}
|
|
11
20
|
|
|
12
21
|
let sumX = 0
|
|
@@ -23,14 +32,30 @@ export function linearRegressionTrend(values: number[]): number[] {
|
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
const denom = n * sumX2 - sumX * sumX
|
|
26
|
-
if (denom === 0) return
|
|
35
|
+
if (denom === 0) return null
|
|
27
36
|
|
|
28
37
|
const slope = (n * sumXY - sumX * sumY) / denom
|
|
29
38
|
const intercept = (sumY - slope * sumX) / n
|
|
39
|
+
const mean = sumY / n
|
|
30
40
|
|
|
31
|
-
const
|
|
41
|
+
const fitted: number[] = []
|
|
42
|
+
let ssRes = 0
|
|
43
|
+
let ssTot = 0
|
|
32
44
|
for (let i = 0; i < n; i++) {
|
|
33
|
-
|
|
45
|
+
const y = values[i]!
|
|
46
|
+
const f = intercept + slope * i
|
|
47
|
+
fitted.push(f)
|
|
48
|
+
ssRes += (y - f) ** 2
|
|
49
|
+
ssTot += (y - mean) ** 2
|
|
34
50
|
}
|
|
35
|
-
|
|
51
|
+
|
|
52
|
+
return { slope, intercept, r2: ssTot === 0 ? 1 : 1 - ssRes / ssTot, fitted }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Fitted y for each index of `values` (see `linearRegression`), or [] if n < 2
|
|
57
|
+
* or any input is non-finite.
|
|
58
|
+
*/
|
|
59
|
+
export function linearRegressionTrend(values: number[]): number[] {
|
|
60
|
+
return linearRegression(values)?.fitted ?? []
|
|
36
61
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function clamp(value: number, min: number, max: number): number {
|
|
2
|
+
return Math.min(max, Math.max(min, value))
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/** Linear interpolation: `t = 0` → `start`, `t = 1` → `end`. */
|
|
6
|
+
export function lerp(start: number, end: number, t: number): number {
|
|
7
|
+
return start + (end - start) * t
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Position of `value` within `[min, max]` as 0..1 (clamped). Returns 0 when
|
|
12
|
+
* the range is empty.
|
|
13
|
+
*/
|
|
14
|
+
export function normalize(value: number, min: number, max: number): number {
|
|
15
|
+
if (max <= min) return 0
|
|
16
|
+
return clamp((value - min) / (max - min), 0, 1)
|
|
17
|
+
}
|