@nifrajs/core 3.3.0 → 4.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 +26 -0
- package/dist/assurance.d.ts +7 -0
- package/dist/assurance.d.ts.map +1 -1
- package/dist/assurance.js +39 -3
- package/dist/assurance.js.map +1 -1
- package/dist/capabilities.d.ts +20 -2
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +47 -15
- package/dist/capabilities.js.map +1 -1
- package/dist/diff.d.ts +6 -1
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +36 -2
- package/dist/diff.js.map +1 -1
- package/dist/durable-adapters.d.ts.map +1 -1
- package/dist/durable-adapters.js +16 -3
- package/dist/durable-adapters.js.map +1 -1
- package/dist/durable-effect.d.ts.map +1 -1
- package/dist/durable-effect.js +6 -3
- package/dist/durable-effect.js.map +1 -1
- package/dist/edge-kit.d.ts +2 -0
- package/dist/edge-kit.d.ts.map +1 -1
- package/dist/edge-kit.js +2 -0
- package/dist/edge-kit.js.map +1 -1
- package/dist/effect-lifecycle.d.ts.map +1 -1
- package/dist/effect-lifecycle.js +2 -1
- package/dist/effect-lifecycle.js.map +1 -1
- package/dist/effect-scope.d.ts.map +1 -1
- package/dist/effect-scope.js +1 -1
- package/dist/effect-scope.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/evidence.d.ts +36 -1
- package/dist/evidence.d.ts.map +1 -1
- package/dist/evidence.js +219 -13
- package/dist/evidence.js.map +1 -1
- package/dist/idempotency.d.ts +31 -0
- package/dist/idempotency.d.ts.map +1 -1
- package/dist/idempotency.js +48 -9
- package/dist/idempotency.js.map +1 -1
- package/dist/index.d.ts +9 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/internal/code-unit-order.d.ts +7 -0
- package/dist/internal/code-unit-order.d.ts.map +1 -0
- package/dist/internal/code-unit-order.js +9 -0
- package/dist/internal/code-unit-order.js.map +1 -0
- package/dist/internal/durable-saga-protocol.d.ts.map +1 -1
- package/dist/internal/durable-saga-protocol.js +2 -0
- package/dist/internal/durable-saga-protocol.js.map +1 -1
- package/dist/internal/route-catalog.d.ts +6 -6
- package/dist/internal/route-catalog.d.ts.map +1 -1
- package/dist/internal/route-catalog.js +20 -18
- package/dist/internal/route-catalog.js.map +1 -1
- package/dist/internal/route-compiler.d.ts +7 -6
- package/dist/internal/route-compiler.d.ts.map +1 -1
- package/dist/internal/route-compiler.js +36 -4
- package/dist/internal/route-compiler.js.map +1 -1
- package/dist/internal/route-execution.d.ts +7 -3
- package/dist/internal/route-execution.d.ts.map +1 -1
- package/dist/internal/route-execution.js.map +1 -1
- package/dist/internal/route-lanes.d.ts +1 -0
- package/dist/internal/route-lanes.d.ts.map +1 -1
- package/dist/internal/route-lanes.js +34 -29
- package/dist/internal/route-lanes.js.map +1 -1
- package/dist/internal/route-program.d.ts +8 -2
- package/dist/internal/route-program.d.ts.map +1 -1
- package/dist/internal/route-program.js +222 -20
- package/dist/internal/route-program.js.map +1 -1
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +8 -5
- package/dist/ledger.js.map +1 -1
- package/dist/manifest.d.ts +1 -1
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +52 -46
- package/dist/manifest.js.map +1 -1
- package/dist/mount.d.ts +36 -0
- package/dist/mount.d.ts.map +1 -1
- package/dist/mount.js +32 -0
- package/dist/mount.js.map +1 -1
- package/dist/reflection.d.ts +26 -0
- package/dist/reflection.d.ts.map +1 -1
- package/dist/reflection.js +65 -1
- package/dist/reflection.js.map +1 -1
- package/dist/response-observer.d.ts +1 -1
- package/dist/response-observer.d.ts.map +1 -1
- package/dist/response-observer.js.map +1 -1
- package/dist/router/overlap.d.ts +23 -0
- package/dist/router/overlap.d.ts.map +1 -0
- package/dist/router/overlap.js +224 -0
- package/dist/router/overlap.js.map +1 -0
- package/dist/router/pattern.d.ts +117 -2
- package/dist/router/pattern.d.ts.map +1 -1
- package/dist/router/pattern.js +283 -23
- package/dist/router/pattern.js.map +1 -1
- package/dist/router/router.d.ts +18 -4
- package/dist/router/router.d.ts.map +1 -1
- package/dist/router/router.js +51 -44
- package/dist/router/router.js.map +1 -1
- package/dist/server/auth.d.ts +53 -0
- package/dist/server/auth.d.ts.map +1 -0
- package/dist/server/auth.js +9 -0
- package/dist/server/auth.js.map +1 -0
- package/dist/server/bare-error-lane.d.ts +12 -7
- package/dist/server/bare-error-lane.d.ts.map +1 -1
- package/dist/server/bare-error-lane.js +13 -11
- package/dist/server/bare-error-lane.js.map +1 -1
- package/dist/server/body-lane.d.ts +20 -2
- package/dist/server/body-lane.d.ts.map +1 -1
- package/dist/server/body-lane.js +26 -7
- package/dist/server/body-lane.js.map +1 -1
- package/dist/server/body-parser.d.ts +68 -0
- package/dist/server/body-parser.d.ts.map +1 -0
- package/dist/server/body-parser.js +172 -0
- package/dist/server/body-parser.js.map +1 -0
- package/dist/server/body.d.ts.map +1 -1
- package/dist/server/body.js +62 -7
- package/dist/server/body.js.map +1 -1
- package/dist/server/client-ip.d.ts +8 -0
- package/dist/server/client-ip.d.ts.map +1 -1
- package/dist/server/client-ip.js +8 -0
- package/dist/server/client-ip.js.map +1 -1
- package/dist/server/context.d.ts +71 -6
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/contract.d.ts +15 -5
- package/dist/server/contract.d.ts.map +1 -1
- package/dist/server/contract.js +25 -4
- package/dist/server/contract.js.map +1 -1
- package/dist/server/cookies.d.ts +4 -1
- package/dist/server/cookies.d.ts.map +1 -1
- package/dist/server/cookies.js +40 -2
- package/dist/server/cookies.js.map +1 -1
- package/dist/server/core-copies.d.ts +2 -0
- package/dist/server/core-copies.d.ts.map +1 -0
- package/dist/server/core-copies.js +20 -0
- package/dist/server/core-copies.js.map +1 -0
- package/dist/server/edge.d.ts.map +1 -1
- package/dist/server/edge.js +2 -0
- package/dist/server/edge.js.map +1 -1
- package/dist/server/http.d.ts +11 -0
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +36 -0
- package/dist/server/http.js.map +1 -1
- package/dist/server/idempotency-lane.d.ts +6 -0
- package/dist/server/idempotency-lane.d.ts.map +1 -1
- package/dist/server/idempotency-lane.js +130 -73
- package/dist/server/idempotency-lane.js.map +1 -1
- package/dist/server/install.d.ts +4 -0
- package/dist/server/install.d.ts.map +1 -1
- package/dist/server/install.js +4 -0
- package/dist/server/install.js.map +1 -1
- package/dist/server/methods.d.ts +45 -0
- package/dist/server/methods.d.ts.map +1 -0
- package/dist/server/methods.js +37 -0
- package/dist/server/methods.js.map +1 -0
- package/dist/server/multipart.d.ts +63 -0
- package/dist/server/multipart.d.ts.map +1 -0
- package/dist/server/multipart.js +374 -0
- package/dist/server/multipart.js.map +1 -0
- package/dist/server/node-outcome-hook.d.ts +20 -0
- package/dist/server/node-outcome-hook.d.ts.map +1 -1
- package/dist/server/node-outcome-hook.js +15 -6
- package/dist/server/node-outcome-hook.js.map +1 -1
- package/dist/server/not-found-answer.d.ts +40 -0
- package/dist/server/not-found-answer.d.ts.map +1 -0
- package/dist/server/not-found-answer.js +56 -0
- package/dist/server/not-found-answer.js.map +1 -0
- package/dist/server/not-found.d.ts +23 -0
- package/dist/server/not-found.d.ts.map +1 -0
- package/dist/server/not-found.js +81 -0
- package/dist/server/not-found.js.map +1 -0
- package/dist/server/plugin.d.ts +26 -1
- package/dist/server/plugin.d.ts.map +1 -1
- package/dist/server/plugin.js.map +1 -1
- package/dist/server/proto-guard.d.ts +18 -5
- package/dist/server/proto-guard.d.ts.map +1 -1
- package/dist/server/proto-guard.js +114 -9
- package/dist/server/proto-guard.js.map +1 -1
- package/dist/server/query.d.ts +7 -0
- package/dist/server/query.d.ts.map +1 -1
- package/dist/server/query.js +47 -0
- package/dist/server/query.js.map +1 -1
- package/dist/server/registry.d.ts +13 -2
- package/dist/server/registry.d.ts.map +1 -1
- package/dist/server/request-context.d.ts +11 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +33 -19
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/request-lineage.d.ts +8 -0
- package/dist/server/request-lineage.d.ts.map +1 -0
- package/dist/server/request-lineage.js +14 -0
- package/dist/server/request-lineage.js.map +1 -0
- package/dist/server/request-target.d.ts +31 -0
- package/dist/server/request-target.d.ts.map +1 -0
- package/dist/server/request-target.js +44 -0
- package/dist/server/request-target.js.map +1 -0
- package/dist/server/respond.d.ts +1 -1
- package/dist/server/respond.d.ts.map +1 -1
- package/dist/server/respond.js +16 -4
- package/dist/server/respond.js.map +1 -1
- package/dist/server/response-contract-lane.d.ts +19 -1
- package/dist/server/response-contract-lane.d.ts.map +1 -1
- package/dist/server/response-contract-lane.js +59 -14
- package/dist/server/response-contract-lane.js.map +1 -1
- package/dist/server/response-observer-runtime.d.ts +9 -5
- package/dist/server/response-observer-runtime.d.ts.map +1 -1
- package/dist/server/response-observer-runtime.js +10 -6
- package/dist/server/response-observer-runtime.js.map +1 -1
- package/dist/server/runtime-core.d.ts +2 -0
- package/dist/server/runtime-core.d.ts.map +1 -1
- package/dist/server/runtime-core.js.map +1 -1
- package/dist/server/server-types.d.ts +55 -3
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +194 -19
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +1342 -274
- package/dist/server/server.js.map +1 -1
- package/dist/server/sse.d.ts.map +1 -1
- package/dist/server/sse.js +10 -7
- package/dist/server/sse.js.map +1 -1
- package/dist/server/static-headers.d.ts +3 -0
- package/dist/server/static-headers.d.ts.map +1 -1
- package/dist/server/static-headers.js +19 -1
- package/dist/server/static-headers.js.map +1 -1
- package/dist/server/transport-lane.d.ts +8 -0
- package/dist/server/transport-lane.d.ts.map +1 -1
- package/dist/server/transport-lane.js +9 -8
- package/dist/server/transport-lane.js.map +1 -1
- package/dist/server/validation.d.ts.map +1 -1
- package/dist/server/validation.js +5 -1
- package/dist/server/validation.js.map +1 -1
- package/dist/server/websocket.d.ts +10 -3
- package/dist/server/websocket.d.ts.map +1 -1
- package/dist/server/websocket.js +7 -4
- package/dist/server/websocket.js.map +1 -1
- package/dist/server/ws-bun.d.ts +2 -0
- package/dist/server/ws-bun.d.ts.map +1 -1
- package/dist/server/ws-bun.js +6 -3
- package/dist/server/ws-bun.js.map +1 -1
- package/dist/server/ws-hook.d.ts +2 -1
- package/dist/server/ws-hook.d.ts.map +1 -1
- package/dist/server.d.ts +8 -5
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +5 -2
- package/dist/server.js.map +1 -1
- package/dist/single-copy-register.js +5 -0
- package/dist/single-copy-register.js.map +1 -1
- package/dist/single-copy.d.ts +46 -3
- package/dist/single-copy.d.ts.map +1 -1
- package/dist/single-copy.js +125 -12
- package/dist/single-copy.js.map +1 -1
- package/dist/tool-contract.d.ts +23 -0
- package/dist/tool-contract.d.ts.map +1 -1
- package/dist/tool-contract.js +37 -3
- package/dist/tool-contract.js.map +1 -1
- package/dist/transport-codec.d.ts.map +1 -1
- package/dist/transport-codec.js +2 -1
- package/dist/transport-codec.js.map +1 -1
- package/package.json +27 -2
- package/src/assurance.ts +52 -2
- package/src/capabilities.ts +81 -22
- package/src/diff.ts +54 -7
- package/src/durable-adapters.ts +20 -3
- package/src/durable-effect.ts +10 -3
- package/src/edge-kit.ts +7 -0
- package/src/effect-lifecycle.ts +5 -1
- package/src/effect-scope.ts +2 -1
- package/src/errors.ts +3 -0
- package/src/evidence.ts +302 -12
- package/src/idempotency.ts +80 -9
- package/src/index.ts +41 -3
- package/src/internal/code-unit-order.ts +8 -0
- package/src/internal/durable-saga-protocol.ts +2 -0
- package/src/internal/route-catalog.ts +19 -18
- package/src/internal/route-compiler.ts +62 -13
- package/src/internal/route-execution.ts +13 -3
- package/src/internal/route-lanes.ts +37 -28
- package/src/internal/route-program.ts +434 -20
- package/src/ledger.ts +11 -5
- package/src/manifest.ts +52 -46
- package/src/mount.ts +62 -0
- package/src/reflection.ts +84 -1
- package/src/response-observer.ts +1 -0
- package/src/router/overlap.ts +258 -0
- package/src/router/pattern.ts +349 -25
- package/src/router/router.ts +76 -51
- package/src/server/auth.ts +75 -0
- package/src/server/bare-error-lane.ts +12 -11
- package/src/server/body-lane.ts +46 -6
- package/src/server/body-parser.ts +241 -0
- package/src/server/body.ts +82 -9
- package/src/server/client-ip.ts +9 -0
- package/src/server/context.ts +219 -12
- package/src/server/contract.ts +54 -9
- package/src/server/cookies.ts +34 -2
- package/src/server/core-copies.ts +21 -0
- package/src/server/edge.ts +2 -0
- package/src/server/http.ts +34 -0
- package/src/server/idempotency-lane.ts +147 -72
- package/src/server/install.ts +6 -0
- package/src/server/methods.ts +143 -0
- package/src/server/multipart.ts +424 -0
- package/src/server/node-outcome-hook.ts +40 -7
- package/src/server/not-found-answer.ts +109 -0
- package/src/server/not-found.ts +121 -0
- package/src/server/plugin.ts +45 -1
- package/src/server/proto-guard.ts +128 -9
- package/src/server/query.ts +49 -0
- package/src/server/registry.ts +17 -2
- package/src/server/request-context.ts +44 -16
- package/src/server/request-lineage.ts +15 -0
- package/src/server/request-target.ts +46 -0
- package/src/server/respond.ts +14 -3
- package/src/server/response-contract-lane.ts +97 -16
- package/src/server/response-observer-runtime.ts +37 -8
- package/src/server/runtime-core.ts +2 -0
- package/src/server/server-types.ts +61 -2
- package/src/server/server.ts +1798 -355
- package/src/server/sse.ts +10 -8
- package/src/server/static-headers.ts +22 -1
- package/src/server/transport-lane.ts +17 -9
- package/src/server/validation.ts +7 -1
- package/src/server/websocket.ts +23 -9
- package/src/server/ws-bun.ts +8 -3
- package/src/server/ws-hook.ts +2 -1
- package/src/server.ts +39 -2
- package/src/single-copy-register.ts +5 -0
- package/src/single-copy.ts +160 -12
- package/src/tool-contract.ts +66 -3
- package/src/transport-codec.ts +2 -1
package/src/server/context.ts
CHANGED
|
@@ -15,6 +15,12 @@ export interface IdempotencyConfig {
|
|
|
15
15
|
readonly scope: IdempotencyScope
|
|
16
16
|
/** Retention for the stored response. Default 24h. */
|
|
17
17
|
readonly ttlMs?: number
|
|
18
|
+
/**
|
|
19
|
+
* How long a key whose handler is still running stays reserved without a renewal. The server
|
|
20
|
+
* renews it every third of this while the handler runs, so only a dead process lets it lapse and a
|
|
21
|
+
* retry waits this long rather than `ttlMs`. Needs a store with `renew()`; default 60s with one.
|
|
22
|
+
*/
|
|
23
|
+
readonly pendingTtlMs?: number
|
|
18
24
|
/** Store override. Defaults to the server's shared in-memory store; inject a durable store here. */
|
|
19
25
|
readonly store?: IdempotencyStore
|
|
20
26
|
/** Header carrying the key. Default `idempotency-key`. */
|
|
@@ -42,26 +48,213 @@ export interface IdempotencyConfig {
|
|
|
42
48
|
/** Flattens an intersection into a single object type for readable hovers. */
|
|
43
49
|
export type Prettify<T> = { [K in keyof T]: T[K] } & {}
|
|
44
50
|
|
|
51
|
+
type ParamNameStart =
|
|
52
|
+
| "a"
|
|
53
|
+
| "b"
|
|
54
|
+
| "c"
|
|
55
|
+
| "d"
|
|
56
|
+
| "e"
|
|
57
|
+
| "f"
|
|
58
|
+
| "g"
|
|
59
|
+
| "h"
|
|
60
|
+
| "i"
|
|
61
|
+
| "j"
|
|
62
|
+
| "k"
|
|
63
|
+
| "l"
|
|
64
|
+
| "m"
|
|
65
|
+
| "n"
|
|
66
|
+
| "o"
|
|
67
|
+
| "p"
|
|
68
|
+
| "q"
|
|
69
|
+
| "r"
|
|
70
|
+
| "s"
|
|
71
|
+
| "t"
|
|
72
|
+
| "u"
|
|
73
|
+
| "v"
|
|
74
|
+
| "w"
|
|
75
|
+
| "x"
|
|
76
|
+
| "y"
|
|
77
|
+
| "z"
|
|
78
|
+
| "A"
|
|
79
|
+
| "B"
|
|
80
|
+
| "C"
|
|
81
|
+
| "D"
|
|
82
|
+
| "E"
|
|
83
|
+
| "F"
|
|
84
|
+
| "G"
|
|
85
|
+
| "H"
|
|
86
|
+
| "I"
|
|
87
|
+
| "J"
|
|
88
|
+
| "K"
|
|
89
|
+
| "L"
|
|
90
|
+
| "M"
|
|
91
|
+
| "N"
|
|
92
|
+
| "O"
|
|
93
|
+
| "P"
|
|
94
|
+
| "Q"
|
|
95
|
+
| "R"
|
|
96
|
+
| "S"
|
|
97
|
+
| "T"
|
|
98
|
+
| "U"
|
|
99
|
+
| "V"
|
|
100
|
+
| "W"
|
|
101
|
+
| "X"
|
|
102
|
+
| "Y"
|
|
103
|
+
| "Z"
|
|
104
|
+
| "_"
|
|
105
|
+
type ParamNameChar = ParamNameStart | "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
|
|
106
|
+
|
|
107
|
+
/** The longest parameter name at the start of `Text`, and the text after it. */
|
|
108
|
+
type TakeParamName<
|
|
109
|
+
Text extends string,
|
|
110
|
+
Name extends string = "",
|
|
111
|
+
> = Text extends `${infer Char}${infer Rest}`
|
|
112
|
+
? Char extends (Name extends "" ? ParamNameStart : ParamNameChar)
|
|
113
|
+
? TakeParamName<Rest, `${Name}${Char}`>
|
|
114
|
+
: [Name, Text]
|
|
115
|
+
: [Name, ""]
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Whether a colon is literal text: one with no name after it, or one that follows a name character
|
|
119
|
+
* and whose name runs to the end of the segment (`/things:batchGet`).
|
|
120
|
+
*/
|
|
121
|
+
type IsLiteralColon<
|
|
122
|
+
Before extends string,
|
|
123
|
+
Name extends string,
|
|
124
|
+
Rest extends string,
|
|
125
|
+
> = Name extends ""
|
|
126
|
+
? true
|
|
127
|
+
: Before extends `${string}${ParamNameChar}`
|
|
128
|
+
? Rest extends "" | `/${string}`
|
|
129
|
+
? true
|
|
130
|
+
: false
|
|
131
|
+
: false
|
|
132
|
+
|
|
45
133
|
/**
|
|
46
134
|
* Extracts `:param` and trailing `*wildcard` names from a route-path literal into
|
|
47
135
|
* a string→string record: `/users/:id/posts/:postId` → `{ id: string; postId:
|
|
48
136
|
* string }`, `/files/*path` → `{ path: string }`, `/files/*` → `{ "*": string }`.
|
|
137
|
+
* A name ends where the router ends it, at the first character that cannot be part of one, so
|
|
138
|
+
* `/files/:name.json` → `{ name: string }` and `/v:major.:minor` → `{ major: string; minor: string }`.
|
|
139
|
+
* A constraint does not change the type: `/users/:id{[0-9]+}` → `{ id: string }`.
|
|
49
140
|
* A non-literal `string` path widens to `Record<string, string>`.
|
|
50
141
|
*/
|
|
51
142
|
type RawParams<Path extends string> = string extends Path
|
|
52
143
|
? Record<string, string>
|
|
53
|
-
: Path extends `${infer
|
|
54
|
-
?
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
144
|
+
: Path extends `${infer Before}:${infer After}`
|
|
145
|
+
? TakeParamName<After> extends [infer Name extends string, infer Rest extends string]
|
|
146
|
+
? IsLiteralColon<Before, Name, Rest> extends true
|
|
147
|
+
? RawParams<Rest>
|
|
148
|
+
: Record<Name, string> & RawParams<Rest>
|
|
149
|
+
: never
|
|
150
|
+
: Path extends `${infer _Start}*${infer Wild}`
|
|
151
|
+
? Record<Wild extends "" ? "*" : Wild, string>
|
|
152
|
+
: Record<never, string>
|
|
153
|
+
|
|
154
|
+
/** `[constraint, rest]` for a leading `{...}`, else `["", Text]`. One nested level, for `{[0-9]{2}}`. */
|
|
155
|
+
type TakeConstraint<Text extends string> = Text extends `{${infer Inner}}${infer Rest}`
|
|
156
|
+
? Inner extends `[${string}` | `\\${string}` | `${string}|${string}`
|
|
157
|
+
? Inner extends `${string}{${string}`
|
|
158
|
+
? Rest extends `}${infer After}`
|
|
159
|
+
? [`{${Inner}}}`, After]
|
|
160
|
+
: ["", Text]
|
|
161
|
+
: [`{${Inner}}`, Rest]
|
|
162
|
+
: ["", Text]
|
|
163
|
+
: ["", Text]
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Whether `Run` is nothing but whole-segment optional parameters, each with or without a constraint:
|
|
167
|
+
* `/:a?`, `/:a?/:b?`, `/:id{[0-9]+}?`.
|
|
168
|
+
*/
|
|
169
|
+
type IsOptionalRun<Run extends string> = Run extends ""
|
|
170
|
+
? true
|
|
171
|
+
: Run extends `/:${infer After}`
|
|
172
|
+
? TakeParamName<After> extends [infer Name extends string, infer Tail extends string]
|
|
173
|
+
? TakeConstraint<Tail> extends [string, `?${infer Rest}`]
|
|
174
|
+
? Name extends ""
|
|
175
|
+
? false
|
|
176
|
+
: IsOptionalRun<Rest>
|
|
177
|
+
: false
|
|
178
|
+
: false
|
|
179
|
+
: false
|
|
180
|
+
|
|
181
|
+
/** `[head, run]` for a path ending in `:name?` segments, else `false`; the same cut the router makes. */
|
|
182
|
+
type SplitOptionalRun<
|
|
183
|
+
Path extends string,
|
|
184
|
+
Head extends string = "",
|
|
185
|
+
> = Path extends `${infer Before}/:${infer After}`
|
|
186
|
+
? IsOptionalRun<`/:${After}`> extends true
|
|
187
|
+
? [`${Head}${Before}`, `/:${After}`]
|
|
188
|
+
: SplitOptionalRun<After, `${Head}${Before}/:`>
|
|
189
|
+
: false
|
|
190
|
+
|
|
191
|
+
/** `Head`, then `Head` with each longer prefix of `Run` appended - the `?` marks dropped. */
|
|
192
|
+
type OptionalForms<Head extends string, Run extends string, Done extends string = ""> =
|
|
193
|
+
| (`${Head}${Done}` extends "" ? "/" : `${Head}${Done}`)
|
|
194
|
+
| (Run extends `/:${infer After}`
|
|
195
|
+
? TakeParamName<After> extends [infer Name extends string, infer Tail extends string]
|
|
196
|
+
? TakeConstraint<Tail> extends [infer Constraint extends string, `?${infer Rest}`]
|
|
197
|
+
? OptionalForms<Head, Rest, `${Done}/:${Name}${Constraint}`>
|
|
198
|
+
: never
|
|
199
|
+
: never
|
|
200
|
+
: never)
|
|
60
201
|
|
|
61
|
-
|
|
202
|
+
/** The registry keys of a route path: `/users/:id?` is `"/users" | "/users/:id"`, others are themselves. */
|
|
203
|
+
export type RoutePaths<Path extends string> = Path extends `${string}?`
|
|
204
|
+
? SplitOptionalRun<Path> extends [infer Head extends string, infer Run extends string]
|
|
205
|
+
? OptionalForms<Head, Run>
|
|
206
|
+
: Path
|
|
207
|
+
: Path
|
|
208
|
+
|
|
209
|
+
/** One concrete path's request text: each parameter replaced by `${string}`, literal text kept. */
|
|
210
|
+
type PathText<Path extends string> = Path extends `${infer Before}:${infer After}`
|
|
211
|
+
? TakeParamName<After> extends [infer Name extends string, infer Rest extends string]
|
|
212
|
+
? IsLiteralColon<Before, Name, Rest> extends true
|
|
213
|
+
? `${Before}:${Name}${PathText<Rest>}`
|
|
214
|
+
: TakeConstraint<Rest> extends [string, infer Tail extends string]
|
|
215
|
+
? `${Before}${string}${PathText<Tail>}`
|
|
216
|
+
: never
|
|
217
|
+
: never
|
|
218
|
+
: Path extends `${infer Start}/*${string}`
|
|
219
|
+
? `${Start}/${string}`
|
|
220
|
+
: Path
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The text of a request a route path serves, as a template: `/users/:id` → `` `/users/${string}` ``,
|
|
224
|
+
* `/files/:name.json` → `` `/files/${string}.json` ``, `/files/*path` → `` `/files/${string}` ``.
|
|
225
|
+
* Literal text is kept, so a value has to carry it. A constraint is not checked - `/users/:id{[0-9]+}`
|
|
226
|
+
* is `` `/users/${string}` `` - and a path ending in optional parameters is one template per form:
|
|
227
|
+
* `/users/:id?` → `` "/users" | `/users/${string}` ``. A non-literal `string` path stays `string`.
|
|
228
|
+
*/
|
|
229
|
+
export type RequestPath<Path extends string> = string extends Path
|
|
230
|
+
? string
|
|
231
|
+
: RoutePaths<Path> extends infer Form extends string
|
|
232
|
+
? Form extends unknown
|
|
233
|
+
? PathText<Form>
|
|
234
|
+
: never
|
|
235
|
+
: never
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The params a handler for `Path` reads. A parameter in a trailing optional run is optional, since
|
|
239
|
+
* the one handler serves the path with and without it: `/users/:id?` → `{ id?: string }`.
|
|
240
|
+
*/
|
|
241
|
+
export type Params<Path extends string> = Path extends `${string}?`
|
|
242
|
+
? SplitOptionalRun<Path> extends [infer Head extends string, infer Run extends string]
|
|
243
|
+
? Prettify<RawParams<Head> & Partial<RawParams<Run>>>
|
|
244
|
+
: Prettify<RawParams<Path>>
|
|
245
|
+
: Prettify<RawParams<Path>>
|
|
62
246
|
|
|
63
247
|
/** Per-route input schemas. Each is any Standard Schema (zod/valibot/arktype/…). */
|
|
64
248
|
export interface RouteSchema {
|
|
249
|
+
/** Explicit external-protocol mode. The handler owns raw `c.req` parsing/streaming; no JSON body
|
|
250
|
+
* parser or schema-driven 422 path is installed. A finite transport cap still applies by default. */
|
|
251
|
+
readonly wire?: "raw"
|
|
252
|
+
/**
|
|
253
|
+
* Choose trust-boundary order for sensitive routes. Public routes validate first by default;
|
|
254
|
+
* routes with `authenticate()` default to auth first. `validate-before-auth` is an explicit
|
|
255
|
+
* compatibility escape hatch, while `auth-before-validation` can reject without parsing the body.
|
|
256
|
+
*/
|
|
257
|
+
readonly validationOrder?: "validate-before-auth" | "auth-before-validation"
|
|
65
258
|
/** Transport body cap for this route. Defaults to the server's maxBodyBytes. Use a finite
|
|
66
259
|
* smaller/larger limit for a specific endpoint. The 'unlimited' value is an explicit
|
|
67
260
|
* streaming/upload exemption and requires a non-empty bodyLimitReason for auditability.
|
|
@@ -122,6 +315,12 @@ export interface RouteSchema {
|
|
|
122
315
|
* output is exposed as `c.headers`; use lower-case keys because HTTP header names are
|
|
123
316
|
* case-insensitive. */
|
|
124
317
|
readonly headers?: StandardSchemaV1
|
|
318
|
+
/** Optional request-cookie schema. It validates the cookies parsed from the `Cookie` header -
|
|
319
|
+
* names as sent, values URL-decoded strings, the first of a repeated name - before the handler
|
|
320
|
+
* runs, and the validated output is `c.cookies`. A failure is a `422`, like `headers`. A browser
|
|
321
|
+
* sends every cookie the site has set, so declare the ones the route reads with an open schema
|
|
322
|
+
* (`t.cookies`, `t.looseObject`) rather than rejecting the rest. */
|
|
323
|
+
readonly cookies?: StandardSchemaV1
|
|
125
324
|
readonly body?: StandardSchemaV1
|
|
126
325
|
readonly query?: StandardSchemaV1
|
|
127
326
|
/** Optional **response contract**. When declared: the handler's return is type-checked against it
|
|
@@ -146,7 +345,7 @@ export interface RouteSchema {
|
|
|
146
345
|
*/
|
|
147
346
|
readonly sse?: StandardSchemaV1
|
|
148
347
|
/**
|
|
149
|
-
* Hook fired when the request fails `headers`/`params`/`body`/`query` validation, before the handler
|
|
348
|
+
* Hook fired when the request fails `headers`/`cookies`/`params`/`body`/`query` validation, before the handler
|
|
150
349
|
* runs. `kind` says which input failed. Its return value selects one of three outcomes (may be async):
|
|
151
350
|
* - a **`Response`** → returned as-is, short-circuiting the route (custom error envelope, redirect, …).
|
|
152
351
|
* - **any other value** → treated as a repaired payload and **re-validated once** against the same
|
|
@@ -161,7 +360,7 @@ export interface RouteSchema {
|
|
|
161
360
|
readonly onValidationError?: (
|
|
162
361
|
issues: ReadonlyArray<StandardIssue>,
|
|
163
362
|
ctx: Context,
|
|
164
|
-
kind: "body" | "query" | "params" | "headers",
|
|
363
|
+
kind: "body" | "query" | "params" | "headers" | "cookies",
|
|
165
364
|
) => Response | unknown | Promise<Response | unknown>
|
|
166
365
|
}
|
|
167
366
|
|
|
@@ -182,6 +381,13 @@ type HeadersOf<S extends RouteSchema> = S extends {
|
|
|
182
381
|
? InferOutput<H>
|
|
183
382
|
: Readonly<Record<string, string>>
|
|
184
383
|
|
|
384
|
+
/** The validated cookie type when a cookie schema is declared, else the parsed name → value record. */
|
|
385
|
+
type CookiesOf<S extends RouteSchema> = S extends {
|
|
386
|
+
cookies: infer C extends StandardSchemaV1
|
|
387
|
+
}
|
|
388
|
+
? InferOutput<C>
|
|
389
|
+
: Readonly<Record<string, string>>
|
|
390
|
+
|
|
185
391
|
/** The validated params type when a params schema is declared, else the path-inferred `Params<Path>`. */
|
|
186
392
|
type ParamsOf<S extends RouteSchema, Path extends string> = S extends {
|
|
187
393
|
params: infer P extends StandardSchemaV1
|
|
@@ -244,8 +450,9 @@ export interface Context<Path extends string = string, S extends RouteSchema = R
|
|
|
244
450
|
readonly query: QueryOf<S>
|
|
245
451
|
readonly body: BodyOf<S>
|
|
246
452
|
/** The request's cookies, parsed from the `Cookie` header (values URL-decoded). Parsed lazily on
|
|
247
|
-
* first access + cached
|
|
248
|
-
|
|
453
|
+
* first access + cached; validated/coerced when `schema.cookies` is declared. Signed cookies arrive
|
|
454
|
+
* as `value.signature` - verify with `unsignValue`. */
|
|
455
|
+
readonly cookies: CookiesOf<S>
|
|
249
456
|
readonly set: ResponseControls
|
|
250
457
|
/**
|
|
251
458
|
* Aborts when the server's `requestTimeoutMs` elapses (and never, when no timeout
|
package/src/server/contract.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { DataClassification } from "../classification.ts"
|
|
2
2
|
import { RouteConfigError } from "../errors.ts"
|
|
3
3
|
import { normalizeRouteCapabilities } from "../internal/capability-runtime.ts"
|
|
4
|
+
import { expandOptionalParams } from "../router/pattern.ts"
|
|
4
5
|
import { METHODS, type Method } from "../router/router.ts"
|
|
5
6
|
import type { InferInput, InferOutput, StandardSchemaV1 } from "../schema/standard.ts"
|
|
6
7
|
import { assertByteLimit } from "./body.ts"
|
|
7
|
-
import type { Context, IdempotencyConfig, Params, RouteSchema } from "./context.ts"
|
|
8
|
+
import type { Context, IdempotencyConfig, Params, RoutePaths, RouteSchema } from "./context.ts"
|
|
8
9
|
import type { EmptyRegistry, OutputOf, Registry, RouteInfoFor } from "./registry.ts"
|
|
9
10
|
import { Server } from "./server.ts"
|
|
10
11
|
|
|
@@ -33,7 +34,13 @@ export interface OperationDef {
|
|
|
33
34
|
readonly params?: StandardSchemaV1
|
|
34
35
|
/** Optional request-header schema (header names are normalized to lower-case). */
|
|
35
36
|
readonly headers?: StandardSchemaV1
|
|
37
|
+
/** Optional request-cookie schema (see {@link RouteSchema.cookies}). */
|
|
38
|
+
readonly cookies?: StandardSchemaV1
|
|
36
39
|
readonly body?: StandardSchemaV1
|
|
40
|
+
/** Explicit external-protocol mode; raw bytes/text are read by the handler from `c.req`. */
|
|
41
|
+
readonly wire?: "raw"
|
|
42
|
+
/** Run derive/beforeHandle authentication before body/query validation on sensitive routes. */
|
|
43
|
+
readonly validationOrder?: "validate-before-auth" | "auth-before-validation"
|
|
37
44
|
readonly query?: StandardSchemaV1
|
|
38
45
|
readonly response?: StandardSchemaV1
|
|
39
46
|
/** Declared effect tokens, carried into route reflection and capability assurance. */
|
|
@@ -153,8 +160,10 @@ type RouteInfoForOp<O extends OperationDef> = {
|
|
|
153
160
|
|
|
154
161
|
/** Re-key the name-keyed ops into the `path → method → RouteInfo` registry. */
|
|
155
162
|
export type RegistryFor<C extends ContractShape> = {
|
|
156
|
-
[P in C[keyof C]["path"]]: {
|
|
157
|
-
[K in keyof C as C[K]["path"]
|
|
163
|
+
[P in RoutePaths<C[keyof C]["path"]>]: {
|
|
164
|
+
[K in keyof C as P extends RoutePaths<C[K]["path"]> ? C[K]["method"] : never]: RouteInfoForOp<
|
|
165
|
+
C[K]
|
|
166
|
+
>
|
|
158
167
|
}
|
|
159
168
|
}
|
|
160
169
|
|
|
@@ -166,6 +175,9 @@ type SchemaForOp<O extends OperationDef> = (O extends { body: infer B extends St
|
|
|
166
175
|
(O extends { headers: infer H extends StandardSchemaV1 }
|
|
167
176
|
? { headers: H }
|
|
168
177
|
: Record<never, never>) &
|
|
178
|
+
(O extends { cookies: infer K extends StandardSchemaV1 }
|
|
179
|
+
? { cookies: K }
|
|
180
|
+
: Record<never, never>) &
|
|
169
181
|
(O extends { params: infer P extends StandardSchemaV1 } ? { params: P } : Record<never, never>)
|
|
170
182
|
|
|
171
183
|
type OpResponseSchemas<O extends OperationDef> = O extends {
|
|
@@ -267,12 +279,37 @@ export function defineContract<const C extends ContractShape>(contract: C): C {
|
|
|
267
279
|
)
|
|
268
280
|
}
|
|
269
281
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
282
|
+
if (op.wire !== undefined && op.wire !== "raw") {
|
|
283
|
+
throw new RouteConfigError(
|
|
284
|
+
"INVALID_WIRE",
|
|
285
|
+
`operation "${name}": wire must be "raw" when specified`,
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
if (op.wire === "raw" && op.body !== undefined) {
|
|
289
|
+
throw new RouteConfigError(
|
|
290
|
+
"INVALID_WIRE",
|
|
291
|
+
`operation "${name}": wire: "raw" cannot be combined with a body schema`,
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
if (
|
|
295
|
+
op.validationOrder !== undefined &&
|
|
296
|
+
op.validationOrder !== "validate-before-auth" &&
|
|
297
|
+
op.validationOrder !== "auth-before-validation"
|
|
298
|
+
) {
|
|
299
|
+
throw new RouteConfigError(
|
|
300
|
+
"INVALID_VALIDATION_ORDER",
|
|
301
|
+
`operation "${name}": validationOrder is invalid`,
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
// Compared per concrete path, so `/users` collides with the short form of `/users/:id?`.
|
|
305
|
+
for (const path of expandOptionalParams(op.path)) {
|
|
306
|
+
const key = `${method} ${path}`
|
|
307
|
+
if (seen.has(key)) {
|
|
308
|
+
throw new RouteConfigError("DUPLICATE_ROUTE", `duplicate operation route: ${key}`)
|
|
309
|
+
}
|
|
310
|
+
seen.add(key)
|
|
273
311
|
}
|
|
274
312
|
normalizeRouteCapabilities(op.capabilities)
|
|
275
|
-
seen.add(key)
|
|
276
313
|
}
|
|
277
314
|
return contract
|
|
278
315
|
}
|
|
@@ -291,8 +328,10 @@ export type RegistryFromImpl<
|
|
|
291
328
|
Ctx = NonNullable<unknown>,
|
|
292
329
|
HookOutput = never,
|
|
293
330
|
> = {
|
|
294
|
-
[P in C[keyof C]["path"]]: {
|
|
295
|
-
[K in keyof C as
|
|
331
|
+
[P in RoutePaths<C[keyof C]["path"]>]: {
|
|
332
|
+
[K in keyof C as P extends RoutePaths<C[K]["path"]>
|
|
333
|
+
? C[K]["method"]
|
|
334
|
+
: never]: H[K] extends AnyFn
|
|
296
335
|
? RouteInfoFor<C[K]["path"], RouteSchemaForOp<C[K]>, OutputOf<H[K]>, HookOutput>
|
|
297
336
|
: RouteInfoFor<C[K]["path"], RouteSchemaForOp<C[K]>, unknown, HookOutput>
|
|
298
337
|
}
|
|
@@ -349,7 +388,10 @@ export function implement<
|
|
|
349
388
|
const schema: RouteSchema | undefined =
|
|
350
389
|
op.params !== undefined ||
|
|
351
390
|
op.headers !== undefined ||
|
|
391
|
+
op.cookies !== undefined ||
|
|
352
392
|
op.body !== undefined ||
|
|
393
|
+
op.wire !== undefined ||
|
|
394
|
+
op.validationOrder !== undefined ||
|
|
353
395
|
op.query !== undefined ||
|
|
354
396
|
op.response !== undefined ||
|
|
355
397
|
(errors !== undefined && Object.keys(errors).length > 0) ||
|
|
@@ -362,7 +404,10 @@ export function implement<
|
|
|
362
404
|
? {
|
|
363
405
|
...(op.params !== undefined ? { params: op.params } : {}),
|
|
364
406
|
...(op.headers !== undefined ? { headers: op.headers } : {}),
|
|
407
|
+
...(op.cookies !== undefined ? { cookies: op.cookies } : {}),
|
|
365
408
|
...(op.body !== undefined ? { body: op.body } : {}),
|
|
409
|
+
...(op.wire !== undefined ? { wire: op.wire } : {}),
|
|
410
|
+
...(op.validationOrder !== undefined ? { validationOrder: op.validationOrder } : {}),
|
|
366
411
|
...(op.query !== undefined ? { query: op.query } : {}),
|
|
367
412
|
...(op.response !== undefined ? { response: op.response } : {}),
|
|
368
413
|
...(errors !== undefined && Object.keys(errors).length > 0 ? { errors } : {}),
|
package/src/server/cookies.ts
CHANGED
|
@@ -66,7 +66,8 @@ const safeDecode = (v: string): string => {
|
|
|
66
66
|
*
|
|
67
67
|
* Hand-rolled index walk instead of `split(";")` + `trim()` chains: those allocated an array + up
|
|
68
68
|
* to three substrings per pair and showed up at ~6% of a realistic (auth + cookie) request. Same
|
|
69
|
-
* semantics, two slices per pair, no intermediate array.
|
|
69
|
+
* semantics, two slices per pair, no intermediate array. Duplicate names use the first value
|
|
70
|
+
* deterministically; security-sensitive callers can use {@link hasDuplicateCookie} and fail closed. */
|
|
70
71
|
export function parseCookies(header: string | null | undefined): Record<string, string> {
|
|
71
72
|
// Null-prototype: cookie NAMES come straight from the untrusted header, so a cookie named
|
|
72
73
|
// `constructor`/`__proto__`/`toString` must be an inert own key, not a shadow of a prototype
|
|
@@ -106,7 +107,11 @@ export function parseCookies(header: string | null | undefined): Record<string,
|
|
|
106
107
|
}
|
|
107
108
|
// Interned: cookie names repeat across requests, and V8 pays ~300ns to internalize a
|
|
108
109
|
// fresh sliced key on a null-proto store (see internParseKey).
|
|
109
|
-
|
|
110
|
+
const key = internParseKey(name)
|
|
111
|
+
// Cookie headers can contain same-name cookies from different paths/domains. Do not let a
|
|
112
|
+
// later value silently replace the first one: session/auth callers can separately reject the
|
|
113
|
+
// ambiguity, while ordinary callers get deterministic first-value semantics.
|
|
114
|
+
if (!Object.hasOwn(out, key)) out[key] = safeDecode(value)
|
|
110
115
|
}
|
|
111
116
|
}
|
|
112
117
|
pos = end + 1
|
|
@@ -114,6 +119,33 @@ export function parseCookies(header: string | null | undefined): Record<string,
|
|
|
114
119
|
return out
|
|
115
120
|
}
|
|
116
121
|
|
|
122
|
+
/** True when a raw Cookie header contains the same exact cookie name more than once. */
|
|
123
|
+
export function hasDuplicateCookie(header: string | null | undefined, targetName: string): boolean {
|
|
124
|
+
if (!header || targetName.length === 0) return false
|
|
125
|
+
let found = false
|
|
126
|
+
const len = header.length
|
|
127
|
+
let pos = 0
|
|
128
|
+
while (pos < len) {
|
|
129
|
+
let end = header.indexOf(";", pos)
|
|
130
|
+
if (end === -1) end = len
|
|
131
|
+
let start = pos
|
|
132
|
+
while (start < end && header.charCodeAt(start) === 32) start++
|
|
133
|
+
let stop = end
|
|
134
|
+
while (stop > start && header.charCodeAt(stop - 1) === 32) stop--
|
|
135
|
+
const eq = header.indexOf("=", start)
|
|
136
|
+
if (eq > start && eq < stop) {
|
|
137
|
+
let nameEnd = eq
|
|
138
|
+
while (nameEnd > start && header.charCodeAt(nameEnd - 1) === 32) nameEnd--
|
|
139
|
+
if (header.slice(start, nameEnd) === targetName) {
|
|
140
|
+
if (found) return true
|
|
141
|
+
found = true
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
pos = end + 1
|
|
145
|
+
}
|
|
146
|
+
return false
|
|
147
|
+
}
|
|
148
|
+
|
|
117
149
|
/**
|
|
118
150
|
* Serialize a `Set-Cookie` header value. Pure - applies **no** security defaults (the caller, e.g.
|
|
119
151
|
* `c.set.cookie`, layers `HttpOnly`/`Secure`/`SameSite` on). Throws on an invalid cookie name, a
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two physical copies of `@nifrajs/core` in one process (a linked sibling checkout, a nested install)
|
|
3
|
+
* split every piece of module-scoped identity: a route from one copy's `server()` merged into the
|
|
4
|
+
* other answers 500 from an internal TypeError as soon as it sets a cookie. The install is the
|
|
5
|
+
* defect, so it is named the moment the second copy loads, not at the first failing request.
|
|
6
|
+
* `nifra check` is the gate that fails the build on it; this is the runtime notice.
|
|
7
|
+
*
|
|
8
|
+
* Each copy records its module URL in a process-wide list. The query is dropped, so a dev server that
|
|
9
|
+
* re-evaluates this same file (HMR, `--hot`) is one copy, not two.
|
|
10
|
+
*/
|
|
11
|
+
const registry = globalThis as unknown as Record<symbol, string[] | undefined>
|
|
12
|
+
const COPIES = Symbol.for("nifra.core.copies")
|
|
13
|
+
registry[COPIES] ??= []
|
|
14
|
+
const copies = registry[COPIES]
|
|
15
|
+
const here = String(import.meta.url).split("?")[0] as string
|
|
16
|
+
|
|
17
|
+
if (!copies.includes(here) && copies.push(here) > 1) {
|
|
18
|
+
console.warn(
|
|
19
|
+
`[nifra] @nifrajs/core is loaded ${copies.length} times; \`nifra check\` names each importer:\n ${copies.join("\n ")}`,
|
|
20
|
+
)
|
|
21
|
+
}
|
package/src/server/edge.ts
CHANGED
|
@@ -140,6 +140,8 @@ export function toFetchHandler<Env = unknown>(
|
|
|
140
140
|
outcome.attach(server, outcome.handler, outcome.data, {
|
|
141
141
|
openNow: true,
|
|
142
142
|
pubsub: outcome.pubsub,
|
|
143
|
+
// `wsMaxPayloadBytes`, as the Bun, Node, Deno and hub lanes enforce it.
|
|
144
|
+
maxPayloadBytes: outcome.maxPayloadBytes,
|
|
143
145
|
})
|
|
144
146
|
// `webSocket` is a Workers-only `ResponseInit` field (absent from the standard type), and a
|
|
145
147
|
// 101 status is only valid on the Workers runtime - both gated by the `Pair` feature check.
|
package/src/server/http.ts
CHANGED
|
@@ -33,6 +33,40 @@ export function isSameOriginPath(value: string): boolean {
|
|
|
33
33
|
return true
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Whether `method` is a token a route can be registered under: uppercase ASCII letters, digits and
|
|
38
|
+
* hyphens, a letter first, 1 to 32 characters. That covers every method in the IANA registry.
|
|
39
|
+
*
|
|
40
|
+
* Every registered method has this shape and the router compares tokens exactly, so a request whose
|
|
41
|
+
* token does not have it can never reach a handler. The server uses this to keep such a request away
|
|
42
|
+
* from everything that runs around routing as well - `onRequest` hooks and mounted handlers - because
|
|
43
|
+
* those see the raw token, and code that tests `req.method === "POST"` would otherwise read a
|
|
44
|
+
* differently-cased token as a method it does not guard.
|
|
45
|
+
*/
|
|
46
|
+
export function isRoutableMethod(method: string): boolean {
|
|
47
|
+
// The common verbs first: a string switch settles them without walking the token, which keeps the
|
|
48
|
+
// check close to free on the lanes that run it per request.
|
|
49
|
+
switch (method) {
|
|
50
|
+
case "GET":
|
|
51
|
+
case "POST":
|
|
52
|
+
case "PUT":
|
|
53
|
+
case "PATCH":
|
|
54
|
+
case "DELETE":
|
|
55
|
+
case "HEAD":
|
|
56
|
+
case "OPTIONS":
|
|
57
|
+
return true
|
|
58
|
+
}
|
|
59
|
+
const length = method.length
|
|
60
|
+
if (length === 0 || length > 32) return false
|
|
61
|
+
for (let i = 0; i < length; i++) {
|
|
62
|
+
const code = method.charCodeAt(i)
|
|
63
|
+
if (code >= 65 && code <= 90) continue
|
|
64
|
+
if (i > 0 && (code === 45 || (code >= 48 && code <= 57))) continue
|
|
65
|
+
return false
|
|
66
|
+
}
|
|
67
|
+
return true
|
|
68
|
+
}
|
|
69
|
+
|
|
36
70
|
/** A uniform JSON error envelope: `{ ok: false, error }` at the given status. */
|
|
37
71
|
export function jsonError(
|
|
38
72
|
status: number,
|