@nifrajs/core 2.9.1 → 2.11.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 +17 -0
- package/dist/channel.d.ts +52 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +231 -0
- package/dist/channel.js.map +1 -0
- package/dist/data.d.ts +117 -0
- package/dist/data.d.ts.map +1 -0
- package/dist/data.js +163 -0
- package/dist/data.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/range.d.ts +34 -0
- package/dist/range.d.ts.map +1 -0
- package/dist/range.js +91 -0
- package/dist/range.js.map +1 -0
- package/dist/server/platform-adapters.d.ts +80 -0
- package/dist/server/platform-adapters.d.ts.map +1 -0
- package/dist/server/platform-adapters.js +172 -0
- package/dist/server/platform-adapters.js.map +1 -0
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +5 -1
- package/dist/server/server.js.map +1 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -0
- package/dist/server.js.map +1 -1
- package/package.json +16 -1
- package/src/channel.ts +299 -0
- package/src/data.ts +292 -0
- package/src/index.ts +1 -1
- package/src/range.ts +94 -0
- package/src/server/platform-adapters.ts +255 -0
- package/src/server/server.ts +4 -1
- package/src/server.ts +15 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin adapters for platforms that do not expose a Web `fetch` handler directly.
|
|
3
|
+
*
|
|
4
|
+
* The app remains a Web-standard handler. These adapters only translate the platform event envelope;
|
|
5
|
+
* they do not own identity, persistence, retries, or platform credentials.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Platform } from "./context.ts"
|
|
9
|
+
import type { MaybePromise } from "./server.ts"
|
|
10
|
+
|
|
11
|
+
export interface FetchApp<Env = unknown> {
|
|
12
|
+
fetch(request: Request, platform?: Platform<Env>): MaybePromise<Response>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type VercelHandler = (request: Request) => MaybePromise<Response>
|
|
16
|
+
|
|
17
|
+
/** Adapt a Web-standard app to a Vercel Edge Function default export. */
|
|
18
|
+
export function toVercelHandler<Env = unknown>(app: FetchApp<Env>): VercelHandler {
|
|
19
|
+
return (request) => app.fetch(request)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type HeaderValue = string | readonly string[] | undefined
|
|
23
|
+
type HeaderRecord = Readonly<Record<string, HeaderValue>>
|
|
24
|
+
|
|
25
|
+
function decodeBase64(value: string): Uint8Array<ArrayBuffer> {
|
|
26
|
+
const binary = atob(value)
|
|
27
|
+
const bytes = new Uint8Array(new ArrayBuffer(binary.length))
|
|
28
|
+
for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index)
|
|
29
|
+
return bytes
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function encodeBase64(bytes: Uint8Array): string {
|
|
33
|
+
let binary = ""
|
|
34
|
+
for (let offset = 0; offset < bytes.length; offset += 0x8000) {
|
|
35
|
+
binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000))
|
|
36
|
+
}
|
|
37
|
+
return btoa(binary)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function headersFromRecords(
|
|
41
|
+
single: HeaderRecord | undefined,
|
|
42
|
+
multi: HeaderRecord | undefined,
|
|
43
|
+
): Headers {
|
|
44
|
+
const headers = new Headers()
|
|
45
|
+
for (const [name, value] of Object.entries(single ?? {})) {
|
|
46
|
+
if (value === undefined) continue
|
|
47
|
+
for (const item of Array.isArray(value) ? value : [value]) headers.append(name, item)
|
|
48
|
+
}
|
|
49
|
+
for (const [name, value] of Object.entries(multi ?? {})) {
|
|
50
|
+
headers.delete(name)
|
|
51
|
+
if (value === undefined) continue
|
|
52
|
+
for (const item of Array.isArray(value) ? value : [value]) headers.append(name, item)
|
|
53
|
+
}
|
|
54
|
+
return headers
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function absoluteUrl(
|
|
58
|
+
raw: string | undefined,
|
|
59
|
+
path: string,
|
|
60
|
+
query: string | undefined,
|
|
61
|
+
headers: Headers,
|
|
62
|
+
): string {
|
|
63
|
+
if (raw !== undefined) {
|
|
64
|
+
try {
|
|
65
|
+
return new URL(raw).toString()
|
|
66
|
+
} catch {
|
|
67
|
+
// Fall through to the structured event fields.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const host = headers.get("host") ?? "nifra.invalid"
|
|
71
|
+
const url = new URL(path || "/", `https://${host}`)
|
|
72
|
+
if (query !== undefined && query !== "") url.search = query.startsWith("?") ? query : `?${query}`
|
|
73
|
+
return url.toString()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function requestBody(
|
|
77
|
+
body: string | null | undefined,
|
|
78
|
+
encoded: boolean | undefined,
|
|
79
|
+
): string | Uint8Array<ArrayBuffer> | undefined {
|
|
80
|
+
if (body === undefined || body === null) return undefined
|
|
81
|
+
return encoded === true ? decodeBase64(body) : body
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Every `set-cookie` on a response, as separate values.
|
|
86
|
+
*
|
|
87
|
+
* `Headers.forEach` and `Headers.get` collapse repeated `set-cookie` into one comma-joined string,
|
|
88
|
+
* and a cookie's `Expires` attribute contains a comma - so the joined form cannot be split back
|
|
89
|
+
* apart, and a plain record keyed by header name keeps only one cookie regardless. Both platforms
|
|
90
|
+
* have a dedicated channel for this; `getSetCookie` is how we fill it.
|
|
91
|
+
*/
|
|
92
|
+
function setCookiesOf(headers: Headers): readonly string[] {
|
|
93
|
+
const getSetCookie = (headers as { getSetCookie?: () => string[] }).getSetCookie
|
|
94
|
+
if (typeof getSetCookie === "function") return getSetCookie.call(headers)
|
|
95
|
+
const single = headers.get("set-cookie")
|
|
96
|
+
return single === null ? [] : [single]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function platformResponse(
|
|
100
|
+
response: Response,
|
|
101
|
+
/** `cookies` is API Gateway v2's dedicated array; v1 and Netlify carry them in multi-value headers. */
|
|
102
|
+
cookieChannel: "cookies" | "multiValueHeaders",
|
|
103
|
+
): Promise<PlatformResponse> {
|
|
104
|
+
const bytes = new Uint8Array(await response.arrayBuffer())
|
|
105
|
+
const headers: Record<string, string> = {}
|
|
106
|
+
response.headers.forEach((value, name) => {
|
|
107
|
+
if (name.toLowerCase() === "set-cookie") return
|
|
108
|
+
headers[name] = value
|
|
109
|
+
})
|
|
110
|
+
const cookies = setCookiesOf(response.headers)
|
|
111
|
+
return {
|
|
112
|
+
statusCode: response.status,
|
|
113
|
+
headers,
|
|
114
|
+
...(cookies.length === 0
|
|
115
|
+
? {}
|
|
116
|
+
: cookieChannel === "cookies"
|
|
117
|
+
? { cookies }
|
|
118
|
+
: { multiValueHeaders: { "set-cookie": cookies } }),
|
|
119
|
+
body: encodeBase64(bytes),
|
|
120
|
+
isBase64Encoded: true,
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface NetlifyEvent {
|
|
125
|
+
readonly httpMethod: string
|
|
126
|
+
readonly path: string
|
|
127
|
+
readonly rawUrl?: string
|
|
128
|
+
readonly rawQuery?: string
|
|
129
|
+
readonly headers?: HeaderRecord
|
|
130
|
+
readonly multiValueHeaders?: HeaderRecord
|
|
131
|
+
readonly body?: string | null
|
|
132
|
+
readonly isBase64Encoded?: boolean
|
|
133
|
+
readonly cookies?: readonly string[]
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface PlatformResponse {
|
|
137
|
+
readonly statusCode: number
|
|
138
|
+
/** Single-valued headers. `set-cookie` never appears here - see the two fields below. */
|
|
139
|
+
readonly headers: Readonly<Record<string, string>>
|
|
140
|
+
/**
|
|
141
|
+
* Repeated headers, currently only `set-cookie`. Netlify Functions and API Gateway REST (v1) read
|
|
142
|
+
* this; omitted when the response sets no cookies.
|
|
143
|
+
*/
|
|
144
|
+
readonly multiValueHeaders?: Readonly<Record<string, readonly string[]>>
|
|
145
|
+
/** API Gateway HTTP API (v2) takes cookies here rather than in a header. Omitted when there are none. */
|
|
146
|
+
readonly cookies?: readonly string[]
|
|
147
|
+
readonly body: string
|
|
148
|
+
readonly isBase64Encoded: true
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export type NetlifyHandler = (event: NetlifyEvent) => Promise<PlatformResponse>
|
|
152
|
+
|
|
153
|
+
/** Adapt a Web-standard app to a Netlify Functions event handler. */
|
|
154
|
+
export function toNetlifyHandler<Env = unknown>(app: FetchApp<Env>): NetlifyHandler {
|
|
155
|
+
return async (event) => {
|
|
156
|
+
const headers = headersFromRecords(event.headers, event.multiValueHeaders)
|
|
157
|
+
if (event.cookies !== undefined && !headers.has("cookie")) {
|
|
158
|
+
headers.set("cookie", event.cookies.join("; "))
|
|
159
|
+
}
|
|
160
|
+
const init: RequestInit = {
|
|
161
|
+
method: event.httpMethod,
|
|
162
|
+
headers,
|
|
163
|
+
}
|
|
164
|
+
const body = requestBody(event.body, event.isBase64Encoded)
|
|
165
|
+
if (body !== undefined) init.body = body
|
|
166
|
+
const request = new Request(
|
|
167
|
+
absoluteUrl(event.rawUrl, event.path, event.rawQuery, headers),
|
|
168
|
+
init,
|
|
169
|
+
)
|
|
170
|
+
return platformResponse(await app.fetch(request), "multiValueHeaders")
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface LambdaV2Event {
|
|
175
|
+
readonly version?: "2.0"
|
|
176
|
+
readonly rawPath?: string
|
|
177
|
+
readonly rawQueryString?: string
|
|
178
|
+
readonly headers?: HeaderRecord
|
|
179
|
+
readonly cookies?: readonly string[]
|
|
180
|
+
readonly body?: string | null
|
|
181
|
+
readonly isBase64Encoded?: boolean
|
|
182
|
+
readonly requestContext?: {
|
|
183
|
+
readonly domainName?: string
|
|
184
|
+
readonly http?: { readonly method: string; readonly path?: string }
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface LambdaV1Event {
|
|
189
|
+
readonly httpMethod: string
|
|
190
|
+
readonly path: string
|
|
191
|
+
readonly headers?: HeaderRecord
|
|
192
|
+
readonly multiValueHeaders?: HeaderRecord
|
|
193
|
+
readonly queryStringParameters?: Readonly<Record<string, string | undefined>>
|
|
194
|
+
readonly multiValueQueryStringParameters?: Readonly<Record<string, readonly string[] | undefined>>
|
|
195
|
+
readonly body?: string | null
|
|
196
|
+
readonly isBase64Encoded?: boolean
|
|
197
|
+
readonly requestContext?: { readonly domainName?: string }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export type LambdaEvent = LambdaV2Event | LambdaV1Event
|
|
201
|
+
export type LambdaResponse = PlatformResponse
|
|
202
|
+
export type LambdaHandler = (event: LambdaEvent, context?: unknown) => Promise<LambdaResponse>
|
|
203
|
+
|
|
204
|
+
function isLambdaV2(event: LambdaEvent): event is LambdaV2Event {
|
|
205
|
+
return "version" in event || "rawPath" in event || "rawQueryString" in event
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function queryFromLambdaV1(event: LambdaV1Event): string {
|
|
209
|
+
const params = new URLSearchParams()
|
|
210
|
+
for (const [name, value] of Object.entries(event.multiValueQueryStringParameters ?? {})) {
|
|
211
|
+
for (const item of value ?? []) params.append(name, item)
|
|
212
|
+
}
|
|
213
|
+
for (const [name, value] of Object.entries(event.queryStringParameters ?? {})) {
|
|
214
|
+
if (value !== undefined && !params.has(name)) params.set(name, value)
|
|
215
|
+
}
|
|
216
|
+
return params.toString()
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Adapt API Gateway HTTP API (v2) and REST API (v1) events to a Web-standard app. */
|
|
220
|
+
export function toLambdaHandler<Env = unknown>(app: FetchApp<Env>): LambdaHandler {
|
|
221
|
+
return async (event) => {
|
|
222
|
+
const v2 = isLambdaV2(event)
|
|
223
|
+
const headers = headersFromRecords(event.headers, v2 ? undefined : event.multiValueHeaders)
|
|
224
|
+
let path: string
|
|
225
|
+
let query: string
|
|
226
|
+
let method: string
|
|
227
|
+
let rawUrl: string | undefined
|
|
228
|
+
if (v2) {
|
|
229
|
+
method = event.requestContext?.http?.method ?? "GET"
|
|
230
|
+
path = event.rawPath ?? event.requestContext?.http?.path ?? "/"
|
|
231
|
+
query = event.rawQueryString ?? ""
|
|
232
|
+
const domain = event.requestContext?.domainName
|
|
233
|
+
rawUrl =
|
|
234
|
+
domain === undefined ? undefined : `https://${domain}${path}${query ? `?${query}` : ""}`
|
|
235
|
+
if (event.cookies !== undefined && !headers.has("cookie"))
|
|
236
|
+
headers.set("cookie", event.cookies.join("; "))
|
|
237
|
+
} else {
|
|
238
|
+
method = event.httpMethod
|
|
239
|
+
path = event.path
|
|
240
|
+
query = queryFromLambdaV1(event)
|
|
241
|
+
const domain = event.requestContext?.domainName
|
|
242
|
+
rawUrl =
|
|
243
|
+
domain === undefined ? undefined : `https://${domain}${path}${query ? `?${query}` : ""}`
|
|
244
|
+
}
|
|
245
|
+
const body = event.body
|
|
246
|
+
const init: RequestInit = {
|
|
247
|
+
method,
|
|
248
|
+
headers,
|
|
249
|
+
}
|
|
250
|
+
const requestBodyValue = requestBody(body, event.isBase64Encoded)
|
|
251
|
+
if (requestBodyValue !== undefined) init.body = requestBodyValue
|
|
252
|
+
const request = new Request(absoluteUrl(rawUrl, path, query, headers), init)
|
|
253
|
+
return platformResponse(await app.fetch(request), v2 ? "cookies" : "multiValueHeaders")
|
|
254
|
+
}
|
|
255
|
+
}
|
package/src/server/server.ts
CHANGED
|
@@ -2501,7 +2501,10 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2501
2501
|
): MaybePromise<T> {
|
|
2502
2502
|
const hooks = this.onRequestHooks
|
|
2503
2503
|
const originalRequest = requestOf(source)
|
|
2504
|
-
|
|
2504
|
+
// A Web Request is already the exact object visible to the hooks and the response walk. Only
|
|
2505
|
+
// adapter sources need the source→request mapping; avoiding the WeakMap write keeps the common
|
|
2506
|
+
// Bun/Deno/edge lifecycle allocation-light while preserving Node's lazy Request identity.
|
|
2507
|
+
if (source !== originalRequest) this.responseSources.set(source as object, originalRequest)
|
|
2505
2508
|
let current: RequestSource = source
|
|
2506
2509
|
for (let i = 0; i < hooks.length; i++) {
|
|
2507
2510
|
const outcome = (hooks[i] as RawOnRequest)(requestOf(current), platform)
|
package/src/server.ts
CHANGED
|
@@ -53,6 +53,21 @@ export {
|
|
|
53
53
|
redactLogFields,
|
|
54
54
|
silentLogger,
|
|
55
55
|
} from "./server/logger.ts"
|
|
56
|
+
export {
|
|
57
|
+
type FetchApp,
|
|
58
|
+
type LambdaEvent,
|
|
59
|
+
type LambdaHandler,
|
|
60
|
+
type LambdaResponse,
|
|
61
|
+
type LambdaV1Event,
|
|
62
|
+
type LambdaV2Event,
|
|
63
|
+
type NetlifyEvent,
|
|
64
|
+
type NetlifyHandler,
|
|
65
|
+
type PlatformResponse,
|
|
66
|
+
toLambdaHandler,
|
|
67
|
+
toNetlifyHandler,
|
|
68
|
+
toVercelHandler,
|
|
69
|
+
type VercelHandler,
|
|
70
|
+
} from "./server/platform-adapters.ts"
|
|
56
71
|
export type { Registry, RouteInfo } from "./server/registry.ts"
|
|
57
72
|
export {
|
|
58
73
|
type AdmissionController,
|