@znight/http 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +2333 -2
- package/dist/adapter-CGSceVEN.d.mts +876 -0
- package/dist/compiler.d.mts +156 -0
- package/dist/compiler.mjs +2180 -0
- package/dist/compiler.mjs.map +1 -0
- package/dist/express.d.mts +39 -0
- package/dist/express.mjs +135 -0
- package/dist/express.mjs.map +1 -0
- package/dist/fastify.d.mts +33 -0
- package/dist/fastify.mjs +191 -0
- package/dist/fastify.mjs.map +1 -0
- package/dist/hono.d.mts +49 -0
- package/dist/hono.mjs +319 -0
- package/dist/hono.mjs.map +1 -0
- package/dist/index.d.mts +1254 -0
- package/dist/index.mjs +5618 -0
- package/dist/index.mjs.map +1 -0
- package/dist/node-BUnkdSsa.mjs +183 -0
- package/dist/node-BUnkdSsa.mjs.map +1 -0
- package/dist/node-io-BY1ZDLRQ.mjs +1566 -0
- package/dist/node-io-BY1ZDLRQ.mjs.map +1 -0
- package/dist/node.d.mts +30 -0
- package/dist/node.mjs +1 -0
- package/dist/path-JI87C4k8.d.mts +109 -0
- package/dist/path.d.mts +2 -0
- package/dist/path.mjs +245 -0
- package/dist/path.mjs.map +1 -0
- package/dist/size-D8rsQcV0.mjs +184 -0
- package/dist/size-D8rsQcV0.mjs.map +1 -0
- package/dist/utils-DCbu3a4w.mjs +27 -0
- package/dist/utils-DCbu3a4w.mjs.map +1 -0
- package/package.json +142 -4
- package/src/adapter.ts +240 -0
- package/src/adapters/express.ts +201 -0
- package/src/adapters/fastify.ts +280 -0
- package/src/adapters/hono.ts +488 -0
- package/src/adapters/node-io.ts +368 -0
- package/src/adapters/node.ts +135 -0
- package/src/adapters/shared.ts +508 -0
- package/src/body.ts +135 -0
- package/src/coerce.ts +195 -0
- package/src/compiler/analyze.ts +1513 -0
- package/src/compiler/ast.ts +90 -0
- package/src/compiler/coercion.ts +267 -0
- package/src/compiler/generate.ts +374 -0
- package/src/compiler/index.ts +76 -0
- package/src/compiler/model.ts +260 -0
- package/src/compiler/route-decorators.ts +405 -0
- package/src/compiler/validate.ts +288 -0
- package/src/context.ts +533 -0
- package/src/cookie-settings.ts +68 -0
- package/src/cookie-signer.ts +106 -0
- package/src/cookies.ts +726 -0
- package/src/cors.ts +349 -0
- package/src/decisions.ts +331 -0
- package/src/decorators.ts +486 -0
- package/src/event-channel.ts +197 -0
- package/src/exceptions.ts +257 -0
- package/src/files.ts +429 -0
- package/src/headers.ts +39 -0
- package/src/index.ts +185 -0
- package/src/inject-sse.ts +309 -0
- package/src/interfaces.ts +101 -0
- package/src/metadata.ts +162 -0
- package/src/mime.ts +157 -0
- package/src/multipart.ts +597 -0
- package/src/option-error.ts +31 -0
- package/src/path.ts +288 -0
- package/src/pipeline.ts +918 -0
- package/src/plugin-routes.ts +188 -0
- package/src/plugin.ts +1432 -0
- package/src/proxy.ts +236 -0
- package/src/query.ts +30 -0
- package/src/response.ts +240 -0
- package/src/router.ts +119 -0
- package/src/security-headers.ts +161 -0
- package/src/serialize.ts +264 -0
- package/src/size.ts +66 -0
- package/src/sse-parse.ts +194 -0
- package/src/sse.ts +650 -0
- package/src/standard-schema.ts +81 -0
- package/src/status.ts +78 -0
- package/src/stream.ts +200 -0
- package/src/tokens.ts +21 -0
- package/src/uploads.ts +176 -0
- package/src/utils.ts +31 -0
- package/src/validate.ts +65 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signed cookies (RFC 0006 §4.5): HMAC-SHA256 bound to the cookie name, with key rotation. Uses
|
|
3
|
+
* `node:crypto` (synchronous, Node.js and Bun): only imported by the runtime entry, never by the
|
|
4
|
+
* compiler extension.
|
|
5
|
+
*/
|
|
6
|
+
import crypto, { createHmac, createSecretKey, type KeyObject } from 'node:crypto'
|
|
7
|
+
|
|
8
|
+
/** Length of a signature: base64url of 32 bytes, without padding. */
|
|
9
|
+
const SIGNATURE_LENGTH = 43
|
|
10
|
+
/** Minimum size of a secret, in bytes. */
|
|
11
|
+
export const MIN_SECRET_BYTES = 32
|
|
12
|
+
|
|
13
|
+
/** A secret given to the plugin option `cookies.secrets`. */
|
|
14
|
+
export type CookieSecret = string | Uint8Array
|
|
15
|
+
|
|
16
|
+
const ENCODER = new TextEncoder()
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Signs and verifies cookie values. The wire form of a signed cookie is
|
|
20
|
+
* `<encoded value>.<signature>` with `signature = base64url(HMAC-SHA256(secret, "<name>=<encoded value>"))`:
|
|
21
|
+
* the MAC covers the name, so a value of `a` replayed as `b` fails. The first secret signs, every
|
|
22
|
+
* secret verifies (rotation).
|
|
23
|
+
*/
|
|
24
|
+
export class CookieSigner {
|
|
25
|
+
readonly #keys: readonly KeyObject[]
|
|
26
|
+
|
|
27
|
+
/** @param secrets validated by {@link checkSecrets} (at least one, ≥ 32 bytes each, no duplicate). */
|
|
28
|
+
constructor(secrets: readonly CookieSecret[]) {
|
|
29
|
+
this.#keys = secrets.map(secret => createSecretKey(toBytes(secret)))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** `<encoded>.<signature>` signed with the first secret. `encoded` is an encoded cookie value. */
|
|
33
|
+
sign(name: string, encoded: string): string {
|
|
34
|
+
return `${encoded}.${mac(this.#keys[0] as KeyObject, name, encoded)}`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The encoded value of a signed wire value, `undefined` when the signature is malformed or made
|
|
39
|
+
* with none of the secrets (constant-time comparison).
|
|
40
|
+
*/
|
|
41
|
+
verify(name: string, wire: string): string | undefined {
|
|
42
|
+
const dot = wire.lastIndexOf('.')
|
|
43
|
+
if (dot === -1) return undefined
|
|
44
|
+
const signature = wire.slice(dot + 1)
|
|
45
|
+
if (!isSignature(signature)) return undefined
|
|
46
|
+
const encoded = wire.slice(0, dot)
|
|
47
|
+
const given = Buffer.from(signature, 'latin1')
|
|
48
|
+
for (const key of this.#keys) {
|
|
49
|
+
const expected = Buffer.from(mac(key, name, encoded), 'latin1')
|
|
50
|
+
// through the module object: observable in tests (equal lengths only, see isSignature)
|
|
51
|
+
if (crypto.timingSafeEqual(expected, given)) return encoded
|
|
52
|
+
}
|
|
53
|
+
return undefined
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function mac(key: KeyObject, name: string, encoded: string): string {
|
|
58
|
+
return createHmac('sha256', key).update(`${name}=${encoded}`).digest('base64url')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 43 characters of the base64url alphabet. */
|
|
62
|
+
function isSignature(text: string): boolean {
|
|
63
|
+
if (text.length !== SIGNATURE_LENGTH) return false
|
|
64
|
+
for (let index = 0; index < text.length; index++) {
|
|
65
|
+
const code = text.charCodeAt(index)
|
|
66
|
+
const valid =
|
|
67
|
+
(code >= 0x30 && code <= 0x39) ||
|
|
68
|
+
(code >= 0x41 && code <= 0x5a) ||
|
|
69
|
+
(code >= 0x61 && code <= 0x7a) ||
|
|
70
|
+
code === 0x2d ||
|
|
71
|
+
code === 0x5f
|
|
72
|
+
if (!valid) return false
|
|
73
|
+
}
|
|
74
|
+
return true
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function toBytes(secret: CookieSecret): Uint8Array {
|
|
78
|
+
return typeof secret === 'string' ? ENCODER.encode(secret) : secret
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Normalises the option `cookies.secrets` to a list, or explains what is wrong with it (a secret
|
|
83
|
+
* shorter than 32 bytes, a duplicate, an empty list, a value that is neither a string nor bytes).
|
|
84
|
+
*/
|
|
85
|
+
export function checkSecrets(
|
|
86
|
+
value: unknown,
|
|
87
|
+
): { readonly secrets: readonly CookieSecret[] } | { readonly problem: string } {
|
|
88
|
+
const list: unknown[] = Array.isArray(value) ? value : [value]
|
|
89
|
+
if (list.length === 0) return { problem: 'at least one secret' }
|
|
90
|
+
const seen = new Set<string>()
|
|
91
|
+
for (const [index, secret] of list.entries()) {
|
|
92
|
+
if (typeof secret !== 'string' && !(secret instanceof Uint8Array)) {
|
|
93
|
+
return { problem: `strings or Uint8Arrays (secret #${index} is not)` }
|
|
94
|
+
}
|
|
95
|
+
const bytes = toBytes(secret)
|
|
96
|
+
if (bytes.byteLength < MIN_SECRET_BYTES) {
|
|
97
|
+
return {
|
|
98
|
+
problem: `secrets of at least ${MIN_SECRET_BYTES} bytes (secret #${index} has ${bytes.byteLength}): crypto.randomBytes(32).toString('base64url')`,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const fingerprint = Buffer.from(bytes).toString('base64')
|
|
102
|
+
if (seen.has(fingerprint)) return { problem: `distinct secrets (secret #${index} is a duplicate)` }
|
|
103
|
+
seen.add(fingerprint)
|
|
104
|
+
}
|
|
105
|
+
return { secrets: list as CookieSecret[] }
|
|
106
|
+
}
|