@znight/http 0.0.0-stage → 0.1.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.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2333 -2
  3. package/dist/adapter-CGSceVEN.d.mts +876 -0
  4. package/dist/compiler.d.mts +156 -0
  5. package/dist/compiler.mjs +2180 -0
  6. package/dist/compiler.mjs.map +1 -0
  7. package/dist/express.d.mts +39 -0
  8. package/dist/express.mjs +135 -0
  9. package/dist/express.mjs.map +1 -0
  10. package/dist/fastify.d.mts +33 -0
  11. package/dist/fastify.mjs +191 -0
  12. package/dist/fastify.mjs.map +1 -0
  13. package/dist/hono.d.mts +49 -0
  14. package/dist/hono.mjs +319 -0
  15. package/dist/hono.mjs.map +1 -0
  16. package/dist/index.d.mts +1254 -0
  17. package/dist/index.mjs +5618 -0
  18. package/dist/index.mjs.map +1 -0
  19. package/dist/node-BUnkdSsa.mjs +183 -0
  20. package/dist/node-BUnkdSsa.mjs.map +1 -0
  21. package/dist/node-io-BY1ZDLRQ.mjs +1566 -0
  22. package/dist/node-io-BY1ZDLRQ.mjs.map +1 -0
  23. package/dist/node.d.mts +30 -0
  24. package/dist/node.mjs +1 -0
  25. package/dist/path-JI87C4k8.d.mts +109 -0
  26. package/dist/path.d.mts +2 -0
  27. package/dist/path.mjs +245 -0
  28. package/dist/path.mjs.map +1 -0
  29. package/dist/size-D8rsQcV0.mjs +184 -0
  30. package/dist/size-D8rsQcV0.mjs.map +1 -0
  31. package/dist/utils-DCbu3a4w.mjs +27 -0
  32. package/dist/utils-DCbu3a4w.mjs.map +1 -0
  33. package/package.json +142 -4
  34. package/src/adapter.ts +240 -0
  35. package/src/adapters/express.ts +201 -0
  36. package/src/adapters/fastify.ts +280 -0
  37. package/src/adapters/hono.ts +488 -0
  38. package/src/adapters/node-io.ts +368 -0
  39. package/src/adapters/node.ts +135 -0
  40. package/src/adapters/shared.ts +508 -0
  41. package/src/body.ts +135 -0
  42. package/src/coerce.ts +195 -0
  43. package/src/compiler/analyze.ts +1513 -0
  44. package/src/compiler/ast.ts +90 -0
  45. package/src/compiler/coercion.ts +267 -0
  46. package/src/compiler/generate.ts +374 -0
  47. package/src/compiler/index.ts +76 -0
  48. package/src/compiler/model.ts +260 -0
  49. package/src/compiler/route-decorators.ts +405 -0
  50. package/src/compiler/validate.ts +288 -0
  51. package/src/context.ts +533 -0
  52. package/src/cookie-settings.ts +68 -0
  53. package/src/cookie-signer.ts +106 -0
  54. package/src/cookies.ts +726 -0
  55. package/src/cors.ts +349 -0
  56. package/src/decisions.ts +331 -0
  57. package/src/decorators.ts +486 -0
  58. package/src/event-channel.ts +197 -0
  59. package/src/exceptions.ts +257 -0
  60. package/src/files.ts +429 -0
  61. package/src/headers.ts +39 -0
  62. package/src/index.ts +185 -0
  63. package/src/inject-sse.ts +309 -0
  64. package/src/interfaces.ts +101 -0
  65. package/src/metadata.ts +162 -0
  66. package/src/mime.ts +157 -0
  67. package/src/multipart.ts +597 -0
  68. package/src/option-error.ts +31 -0
  69. package/src/path.ts +288 -0
  70. package/src/pipeline.ts +918 -0
  71. package/src/plugin-routes.ts +188 -0
  72. package/src/plugin.ts +1432 -0
  73. package/src/proxy.ts +236 -0
  74. package/src/query.ts +30 -0
  75. package/src/response.ts +240 -0
  76. package/src/router.ts +119 -0
  77. package/src/security-headers.ts +161 -0
  78. package/src/serialize.ts +264 -0
  79. package/src/size.ts +66 -0
  80. package/src/sse-parse.ts +194 -0
  81. package/src/sse.ts +650 -0
  82. package/src/standard-schema.ts +81 -0
  83. package/src/status.ts +78 -0
  84. package/src/stream.ts +200 -0
  85. package/src/tokens.ts +21 -0
  86. package/src/uploads.ts +176 -0
  87. package/src/utils.ts +31 -0
  88. 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
+ }