@hypequery/serve 0.1.0 → 0.2.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 (41) hide show
  1. package/README.md +220 -185
  2. package/dist/adapters/node.d.ts +1 -1
  3. package/dist/adapters/node.d.ts.map +1 -1
  4. package/dist/adapters/node.js +114 -21
  5. package/dist/auth.d.ts +47 -18
  6. package/dist/auth.d.ts.map +1 -1
  7. package/dist/auth.js +87 -20
  8. package/dist/cors.d.ts +17 -0
  9. package/dist/cors.d.ts.map +1 -0
  10. package/dist/cors.js +82 -0
  11. package/dist/dev.js +1 -1
  12. package/dist/errors.d.ts +24 -0
  13. package/dist/errors.d.ts.map +1 -0
  14. package/dist/errors.js +22 -0
  15. package/dist/index.d.ts +4 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +4 -0
  18. package/dist/pipeline.d.ts +8 -1
  19. package/dist/pipeline.d.ts.map +1 -1
  20. package/dist/pipeline.js +71 -16
  21. package/dist/rate-limit.d.ts +86 -0
  22. package/dist/rate-limit.d.ts.map +1 -0
  23. package/dist/rate-limit.js +137 -0
  24. package/dist/serve.d.ts +16 -0
  25. package/dist/serve.d.ts.map +1 -0
  26. package/dist/serve.js +88 -0
  27. package/dist/server/builder.d.ts +1 -1
  28. package/dist/server/builder.d.ts.map +1 -1
  29. package/dist/server/builder.js +1 -0
  30. package/dist/server/define-serve.d.ts.map +1 -1
  31. package/dist/server/define-serve.js +3 -0
  32. package/dist/server/execute-query.d.ts.map +1 -1
  33. package/dist/server/execute-query.js +6 -1
  34. package/dist/server/init-serve.d.ts.map +1 -1
  35. package/dist/server/init-serve.js +23 -8
  36. package/dist/type-tests/builder.test-d.d.ts +8 -2
  37. package/dist/type-tests/builder.test-d.d.ts.map +1 -1
  38. package/dist/type-tests/builder.test-d.js +17 -1
  39. package/dist/types.d.ts +108 -5
  40. package/dist/types.d.ts.map +1 -1
  41. package/package.json +9 -1
@@ -1,17 +1,30 @@
1
1
  import { createServer } from "http";
2
2
  import { once } from "node:events";
3
3
  import { normalizeHeaders, parseQueryParams, parseRequestBody, serializeResponseBody, } from "./utils.js";
4
- const readRequestBody = async (req) => {
4
+ const DEFAULT_REQUEST_TIMEOUT = 30000; // 30 seconds
5
+ const DEFAULT_BODY_LIMIT = 1048576; // 1 MB
6
+ const DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT = 10000; // 10 seconds
7
+ const readRequestBody = async (req, bodyLimit) => {
5
8
  const chunks = [];
9
+ let totalLength = 0;
6
10
  for await (const chunk of req) {
7
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
11
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
12
+ totalLength += buf.length;
13
+ if (bodyLimit > 0 && totalLength > bodyLimit) {
14
+ // Destroy the stream to stop reading
15
+ req.destroy();
16
+ const error = new Error("Request body too large");
17
+ error.code = "PAYLOAD_TOO_LARGE";
18
+ throw error;
19
+ }
20
+ chunks.push(buf);
8
21
  }
9
22
  return Buffer.concat(chunks);
10
23
  };
11
- const buildServeRequest = async (req) => {
24
+ const buildServeRequest = async (req, bodyLimit) => {
12
25
  const method = (req.method ?? "GET").toUpperCase();
13
26
  const url = new URL(req.url ?? "/", "http://localhost");
14
- const bodyBuffer = await readRequestBody(req);
27
+ const bodyBuffer = await readRequestBody(req, bodyLimit);
15
28
  const headers = normalizeHeaders(req.headers);
16
29
  const contentType = headers["content-type"] ?? headers["Content-Type"];
17
30
  const body = await parseRequestBody(bodyBuffer, contentType);
@@ -25,6 +38,8 @@ const buildServeRequest = async (req) => {
25
38
  };
26
39
  };
27
40
  const sendResponse = (res, response) => {
41
+ if (res.writableEnded)
42
+ return;
28
43
  res.statusCode = response.status;
29
44
  const headers = response.headers ?? {};
30
45
  for (const [key, value] of Object.entries(headers)) {
@@ -39,6 +54,24 @@ const sendResponse = (res, response) => {
39
54
  res.end(serialized);
40
55
  };
41
56
  const sendError = (res, error) => {
57
+ if (res.writableEnded)
58
+ return;
59
+ // Handle body-too-large errors
60
+ if (error &&
61
+ typeof error === "object" &&
62
+ "code" in error &&
63
+ error.code === "PAYLOAD_TOO_LARGE") {
64
+ sendResponse(res, {
65
+ status: 413,
66
+ body: {
67
+ error: {
68
+ type: "PAYLOAD_TOO_LARGE",
69
+ message: "Request body exceeds the configured size limit",
70
+ },
71
+ },
72
+ });
73
+ return;
74
+ }
42
75
  const payload = error && typeof error === "object" && "status" in error
43
76
  ? error
44
77
  : {
@@ -52,12 +85,39 @@ const sendError = (res, error) => {
52
85
  };
53
86
  sendResponse(res, payload);
54
87
  };
55
- export const createNodeHandler = (handler) => {
88
+ export const createNodeHandler = (handler, options = {}) => {
89
+ const bodyLimit = options.bodyLimit ?? DEFAULT_BODY_LIMIT;
90
+ const requestTimeout = options.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
56
91
  return async (req, res) => {
57
92
  try {
58
- const request = await buildServeRequest(req);
59
- const response = await handler(request);
60
- sendResponse(res, response);
93
+ const request = await buildServeRequest(req, bodyLimit);
94
+ if (requestTimeout > 0) {
95
+ // Race the handler against the timeout
96
+ const timeoutPromise = new Promise((resolve) => {
97
+ const timer = setTimeout(() => {
98
+ resolve({
99
+ status: 504,
100
+ body: {
101
+ error: {
102
+ type: "GATEWAY_TIMEOUT",
103
+ message: `Request timed out after ${requestTimeout}ms`,
104
+ },
105
+ },
106
+ });
107
+ }, requestTimeout);
108
+ // Unref so the timer doesn't keep the process alive during shutdown
109
+ timer.unref();
110
+ });
111
+ const response = await Promise.race([
112
+ handler(request),
113
+ timeoutPromise,
114
+ ]);
115
+ sendResponse(res, response);
116
+ }
117
+ else {
118
+ const response = await handler(request);
119
+ sendResponse(res, response);
120
+ }
61
121
  }
62
122
  catch (error) {
63
123
  sendError(res, error);
@@ -65,12 +125,55 @@ export const createNodeHandler = (handler) => {
65
125
  };
66
126
  };
67
127
  export const startNodeServer = async (handler, options = {}) => {
68
- const listener = createNodeHandler(handler);
128
+ const listener = createNodeHandler(handler, options);
69
129
  const server = createServer(listener);
70
130
  const port = options.port ?? 3000;
71
131
  const hostname = options.hostname ?? "0.0.0.0";
132
+ const gracefulShutdownTimeout = options.gracefulShutdownTimeout ?? DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT;
133
+ // Track in-flight requests for graceful shutdown
134
+ let inFlightRequests = 0;
135
+ let isDraining = false;
136
+ server.on("request", (_req, res) => {
137
+ inFlightRequests++;
138
+ if (isDraining) {
139
+ // Signal to the client that the connection will close
140
+ res.setHeader("connection", "close");
141
+ }
142
+ res.on("close", () => {
143
+ inFlightRequests--;
144
+ });
145
+ });
146
+ const gracefulStop = () => new Promise((resolve) => {
147
+ isDraining = true;
148
+ // Stop accepting new connections
149
+ server.close(() => {
150
+ resolve();
151
+ });
152
+ // If there are no in-flight requests, we're done once server.close completes
153
+ if (inFlightRequests === 0) {
154
+ return;
155
+ }
156
+ // Wait for in-flight requests, with a hard deadline
157
+ const deadline = setTimeout(() => {
158
+ if (!options.quiet) {
159
+ console.log(`[hypequery/serve] Forcing shutdown with ${inFlightRequests} in-flight request(s)`);
160
+ }
161
+ // Force-close all remaining connections
162
+ server.closeAllConnections();
163
+ }, gracefulShutdownTimeout);
164
+ deadline.unref();
165
+ // Also resolve early if all requests finish before the deadline
166
+ const checkInterval = setInterval(() => {
167
+ if (inFlightRequests <= 0) {
168
+ clearTimeout(deadline);
169
+ clearInterval(checkInterval);
170
+ // server.close callback will resolve the promise
171
+ }
172
+ }, 50);
173
+ checkInterval.unref();
174
+ });
72
175
  const onAbort = () => {
73
- server.close();
176
+ gracefulStop();
74
177
  };
75
178
  if (options.signal) {
76
179
  if (options.signal.aborted) {
@@ -88,18 +191,8 @@ export const startNodeServer = async (handler, options = {}) => {
88
191
  : `${hostname}:${port}`;
89
192
  console.log(`hypequery serve listening on ${display}`);
90
193
  }
91
- const stop = () => new Promise((resolve, reject) => {
92
- server.close((err) => {
93
- if (err) {
94
- reject(err);
95
- }
96
- else {
97
- resolve();
98
- }
99
- });
100
- });
101
194
  return {
102
195
  server,
103
- stop,
196
+ stop: gracefulStop,
104
197
  };
105
198
  };
package/dist/auth.d.ts CHANGED
@@ -1,4 +1,23 @@
1
- import type { AuthContext, AuthContextWithRoles, AuthContextWithScopes, AuthStrategy, ServeMiddleware, ServeRequest } from "./types.js";
1
+ import type { AuthContext, AuthContextWithRoles, AuthContextWithScopes, AuthStrategy, AuthErrorInfo, ServeMiddleware, ServeRequest } from "./types.js";
2
+ /**
3
+ * Safely read a header from a ServeRequest with case-insensitive
4
+ * and array-safe normalization.
5
+ */
6
+ export declare const getHeader: (request: ServeRequest, name: string) => string | undefined;
7
+ export declare class AuthError extends Error implements AuthErrorInfo {
8
+ reason: AuthErrorInfo["reason"];
9
+ details?: Record<string, unknown>;
10
+ constructor(reason: AuthErrorInfo["reason"], message: string, details?: Record<string, unknown>);
11
+ }
12
+ export interface ApiKeyAuthOptions<TAuth extends AuthContext = AuthContext> {
13
+ header?: string;
14
+ allowMissing?: boolean;
15
+ validate: (key: string, request: ServeRequest) => Promise<TAuth | null> | TAuth | null;
16
+ }
17
+ /**
18
+ * Simple API key auth adapter with clear missing/invalid errors.
19
+ */
20
+ export declare const apiKeyAuth: <TAuth extends AuthContext = AuthContext>(options: ApiKeyAuthOptions<TAuth>) => AuthStrategy<TAuth>;
2
21
  export interface ApiKeyStrategyOptions<TAuth extends AuthContext = AuthContext> {
3
22
  header?: string;
4
23
  queryParam?: string;
@@ -60,7 +79,7 @@ export declare const checkScopeAuthorization: (auth: AuthContext | null, require
60
79
  *
61
80
  * @deprecated Use `query.requireAuth()` instead for per-endpoint authentication.
62
81
  * This middleware is kept for complex use cases where guards aren't suitable.
63
- * See: https://hypequery.com/docs/serve/authentication#middleware-helpers
82
+ * See: https://hypequery.com/docs/authentication#middleware-helpers
64
83
  *
65
84
  * Use this as a global middleware via `api.use(requireAuthMiddleware())`.
66
85
  * For per-query guards, prefer `query.requireAuth()`.
@@ -72,7 +91,7 @@ export declare const requireAuthMiddleware: <TContext extends Record<string, unk
72
91
  *
73
92
  * @deprecated Use `query.requireRole(...)` instead for per-endpoint authorization.
74
93
  * This middleware is kept for complex use cases where guards aren't suitable.
75
- * See: https://hypequery.com/docs/serve/authentication#middleware-helpers
94
+ * See: https://hypequery.com/docs/authentication#middleware-helpers
76
95
  *
77
96
  * Use this as a global or per-query middleware via `api.use(requireRoleMiddleware('admin'))`.
78
97
  * For per-query guards, prefer `query.requireRole('admin')`.
@@ -84,7 +103,7 @@ export declare const requireRoleMiddleware: <TContext extends Record<string, unk
84
103
  *
85
104
  * @deprecated Use `query.requireScope(...)` instead for per-endpoint authorization.
86
105
  * This middleware is kept for complex use cases where guards aren't suitable.
87
- * See: https://hypequery.com/docs/serve/authentication#middleware-helpers
106
+ * See: https://hypequery.com/docs/authentication#middleware-helpers
88
107
  *
89
108
  * Use this as a global or per-query middleware via `api.use(requireScopeMiddleware('read:metrics'))`.
90
109
  * For per-query guards, prefer `query.requireScope('read:metrics')`.
@@ -127,7 +146,7 @@ export type TypedAuthContext<TRoles extends string, TScopes extends string> = Au
127
146
  *
128
147
  * @example
129
148
  * ```ts
130
- * import { createAuthSystem, defineServe, query } from '@hypequery/serve';
149
+ * import { createAuthSystem, initServe } from '@hypequery/serve';
131
150
  *
132
151
  * // Define your roles and scopes up front
133
152
  * const { useAuth, TypedAuth } = createAuthSystem({
@@ -135,26 +154,36 @@ export type TypedAuthContext<TRoles extends string, TScopes extends string> = Au
135
154
  * scopes: ['read:metrics', 'write:metrics', 'delete:metrics'] as const,
136
155
  * });
137
156
  *
138
- * // Extract the typed auth type for use with defineServe
157
+ * // Extract the typed auth type for use with initServe
139
158
  * type AppAuth = TypedAuth;
140
159
  *
141
- * const api = defineServe<AppAuth>({
160
+ * const { query, serve } = initServe<Record<string, never>, AppAuth>({
142
161
  * auth: useAuth(jwtStrategy),
143
- * queries: {
144
- * adminOnly: query.requireRole('admin').query(async ({ ctx }) => {
145
- * // TypeScript autocomplete for 'admin'
146
- * // ❌ Compile error on typo like 'admn'
147
- * return { secret: true };
148
- * }),
149
- * writeData: query.requireScope('write:metrics').query(async ({ ctx }) => {
150
- * // TypeScript autocomplete for 'write:metrics'
151
- * return { success: true };
152
- * }),
162
+ * });
163
+ *
164
+ * const adminOnly = query({
165
+ * requiredRoles: ['admin'],
166
+ * query: async () => {
167
+ * // ✅ TypeScript autocomplete for 'admin'
168
+ * // Compile error on typo like 'admn'
169
+ * return { secret: true };
153
170
  * },
154
171
  * });
172
+ *
173
+ * const writeData = query({
174
+ * requiredScopes: ['write:metrics'],
175
+ * query: async () => {
176
+ * // ✅ TypeScript autocomplete for 'write:metrics'
177
+ * return { success: true };
178
+ * },
179
+ * });
180
+ *
181
+ * const api = serve({
182
+ * queries: { adminOnly, writeData },
183
+ * });
155
184
  * ```
156
185
  */
157
- export declare const createAuthSystem: <TRoles extends string = string, TScopes extends string = string>(options?: CreateAuthSystemOptions<TRoles, TScopes>) => {
186
+ export declare const createAuthSystem: <TRoles extends string = string, TScopes extends string = string>() => {
158
187
  /**
159
188
  * Type-safe wrapper for auth strategies.
160
189
  * Ensures the strategy returns auth context with the correct role/scope types.
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
package/dist/auth.js CHANGED
@@ -1,3 +1,60 @@
1
+ const resolveHeaderValue = (value) => {
2
+ if (Array.isArray(value)) {
3
+ const first = value.find((item) => typeof item === "string");
4
+ return typeof first === "string" ? first : undefined;
5
+ }
6
+ if (typeof value === "string")
7
+ return value;
8
+ return undefined;
9
+ };
10
+ /**
11
+ * Safely read a header from a ServeRequest with case-insensitive
12
+ * and array-safe normalization.
13
+ */
14
+ export const getHeader = (request, name) => {
15
+ const target = name.toLowerCase();
16
+ const headers = request.headers;
17
+ const direct = headers[target] ?? headers[name] ?? headers[name.toLowerCase()];
18
+ const resolvedDirect = resolveHeaderValue(direct);
19
+ if (resolvedDirect !== undefined) {
20
+ const trimmed = resolvedDirect.trim();
21
+ return trimmed.length > 0 ? trimmed : undefined;
22
+ }
23
+ const match = Object.entries(headers).find(([key]) => key.toLowerCase() === target);
24
+ const resolvedMatch = resolveHeaderValue(match?.[1]);
25
+ if (resolvedMatch === undefined)
26
+ return undefined;
27
+ const trimmed = resolvedMatch.trim();
28
+ return trimmed.length > 0 ? trimmed : undefined;
29
+ };
30
+ export class AuthError extends Error {
31
+ constructor(reason, message, details) {
32
+ super(message);
33
+ this.name = "AuthError";
34
+ this.reason = reason;
35
+ this.details = details;
36
+ }
37
+ }
38
+ /**
39
+ * Simple API key auth adapter with clear missing/invalid errors.
40
+ */
41
+ export const apiKeyAuth = (options) => {
42
+ const headerName = options.header ?? "x-api-key";
43
+ const allowMissing = options.allowMissing ?? false;
44
+ return async ({ request }) => {
45
+ const key = getHeader(request, headerName);
46
+ if (!key) {
47
+ if (allowMissing)
48
+ return null;
49
+ throw new AuthError("MISSING", `Missing API key in "${headerName}" header`, { header: headerName });
50
+ }
51
+ const auth = await options.validate(key, request);
52
+ if (!auth) {
53
+ throw new AuthError("INVALID", `Invalid API key in "${headerName}" header`, { header: headerName });
54
+ }
55
+ return auth;
56
+ };
57
+ };
1
58
  export const createApiKeyStrategy = (options) => {
2
59
  const headerName = options.header ?? "authorization";
3
60
  const queryParam = options.queryParam;
@@ -7,8 +64,8 @@ export const createApiKeyStrategy = (options) => {
7
64
  key = request.query[queryParam];
8
65
  }
9
66
  if (!key) {
10
- const headerValue = request.headers[headerName] ?? request.headers[headerName.toLowerCase()];
11
- if (typeof headerValue === "string") {
67
+ const headerValue = getHeader(request, headerName);
68
+ if (headerValue) {
12
69
  key = headerValue.startsWith("Bearer ")
13
70
  ? headerValue.slice("Bearer ".length)
14
71
  : headerValue;
@@ -24,7 +81,7 @@ export const createBearerTokenStrategy = (options) => {
24
81
  const headerName = options.header ?? "authorization";
25
82
  const prefix = options.prefix ?? "Bearer ";
26
83
  return async ({ request }) => {
27
- const raw = request.headers[headerName] ?? request.headers[headerName.toLowerCase()];
84
+ const raw = getHeader(request, headerName);
28
85
  if (typeof raw !== "string" || !raw.startsWith(prefix)) {
29
86
  return null;
30
87
  }
@@ -95,7 +152,7 @@ export const checkScopeAuthorization = (auth, requiredScopes) => {
95
152
  *
96
153
  * @deprecated Use `query.requireAuth()` instead for per-endpoint authentication.
97
154
  * This middleware is kept for complex use cases where guards aren't suitable.
98
- * See: https://hypequery.com/docs/serve/authentication#middleware-helpers
155
+ * See: https://hypequery.com/docs/authentication#middleware-helpers
99
156
  *
100
157
  * Use this as a global middleware via `api.use(requireAuthMiddleware())`.
101
158
  * For per-query guards, prefer `query.requireAuth()`.
@@ -115,7 +172,7 @@ export const requireAuthMiddleware = () => async (ctx, next) => {
115
172
  *
116
173
  * @deprecated Use `query.requireRole(...)` instead for per-endpoint authorization.
117
174
  * This middleware is kept for complex use cases where guards aren't suitable.
118
- * See: https://hypequery.com/docs/serve/authentication#middleware-helpers
175
+ * See: https://hypequery.com/docs/authentication#middleware-helpers
119
176
  *
120
177
  * Use this as a global or per-query middleware via `api.use(requireRoleMiddleware('admin'))`.
121
178
  * For per-query guards, prefer `query.requireRole('admin')`.
@@ -133,7 +190,7 @@ export const requireRoleMiddleware = (...roles) => async (ctx, next) => {
133
190
  *
134
191
  * @deprecated Use `query.requireScope(...)` instead for per-endpoint authorization.
135
192
  * This middleware is kept for complex use cases where guards aren't suitable.
136
- * See: https://hypequery.com/docs/serve/authentication#middleware-helpers
193
+ * See: https://hypequery.com/docs/authentication#middleware-helpers
137
194
  *
138
195
  * Use this as a global or per-query middleware via `api.use(requireScopeMiddleware('read:metrics'))`.
139
196
  * For per-query guards, prefer `query.requireScope('read:metrics')`.
@@ -155,7 +212,7 @@ export const requireScopeMiddleware = (...scopes) => async (ctx, next) => {
155
212
  *
156
213
  * @example
157
214
  * ```ts
158
- * import { createAuthSystem, defineServe, query } from '@hypequery/serve';
215
+ * import { createAuthSystem, initServe } from '@hypequery/serve';
159
216
  *
160
217
  * // Define your roles and scopes up front
161
218
  * const { useAuth, TypedAuth } = createAuthSystem({
@@ -163,26 +220,36 @@ export const requireScopeMiddleware = (...scopes) => async (ctx, next) => {
163
220
  * scopes: ['read:metrics', 'write:metrics', 'delete:metrics'] as const,
164
221
  * });
165
222
  *
166
- * // Extract the typed auth type for use with defineServe
223
+ * // Extract the typed auth type for use with initServe
167
224
  * type AppAuth = TypedAuth;
168
225
  *
169
- * const api = defineServe<AppAuth>({
226
+ * const { query, serve } = initServe<Record<string, never>, AppAuth>({
170
227
  * auth: useAuth(jwtStrategy),
171
- * queries: {
172
- * adminOnly: query.requireRole('admin').query(async ({ ctx }) => {
173
- * // TypeScript autocomplete for 'admin'
174
- * // ❌ Compile error on typo like 'admn'
175
- * return { secret: true };
176
- * }),
177
- * writeData: query.requireScope('write:metrics').query(async ({ ctx }) => {
178
- * // TypeScript autocomplete for 'write:metrics'
179
- * return { success: true };
180
- * }),
228
+ * });
229
+ *
230
+ * const adminOnly = query({
231
+ * requiredRoles: ['admin'],
232
+ * query: async () => {
233
+ * // ✅ TypeScript autocomplete for 'admin'
234
+ * // Compile error on typo like 'admn'
235
+ * return { secret: true };
181
236
  * },
182
237
  * });
238
+ *
239
+ * const writeData = query({
240
+ * requiredScopes: ['write:metrics'],
241
+ * query: async () => {
242
+ * // ✅ TypeScript autocomplete for 'write:metrics'
243
+ * return { success: true };
244
+ * },
245
+ * });
246
+ *
247
+ * const api = serve({
248
+ * queries: { adminOnly, writeData },
249
+ * });
183
250
  * ```
184
251
  */
185
- export const createAuthSystem = (options = {}) => {
252
+ export const createAuthSystem = () => {
186
253
  return {
187
254
  /**
188
255
  * Type-safe wrapper for auth strategies.
package/dist/cors.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ import type { CorsConfig, ServeRequest, ServeResponse } from './types.js';
2
+ export interface ResolvedCorsConfig {
3
+ origin: string | string[] | ((origin: string) => boolean);
4
+ methods: string[];
5
+ allowedHeaders: string[];
6
+ exposedHeaders: string[];
7
+ credentials: boolean;
8
+ maxAge: number;
9
+ }
10
+ export declare const resolveCorsConfig: (config: boolean | CorsConfig | undefined) => ResolvedCorsConfig | null;
11
+ export declare const buildCorsHeaders: (config: ResolvedCorsConfig, requestOrigin: string | undefined) => Record<string, string>;
12
+ export declare const buildPreflightHeaders: (config: ResolvedCorsConfig, requestOrigin: string | undefined) => Record<string, string>;
13
+ export declare const handleCorsRequest: (config: ResolvedCorsConfig | null, request: ServeRequest) => {
14
+ preflightResponse: ServeResponse | null;
15
+ corsHeaders: Record<string, string>;
16
+ };
17
+ //# sourceMappingURL=cors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cors.d.ts","sourceRoot":"","sources":["../src/cors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAM1E,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,CAAC,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC;IAC1D,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,eAAO,MAAM,iBAAiB,GAC5B,QAAQ,OAAO,GAAG,UAAU,GAAG,SAAS,KACvC,kBAAkB,GAAG,IAavB,CAAC;AA8BF,eAAO,MAAM,gBAAgB,GAC3B,QAAQ,kBAAkB,EAC1B,eAAe,MAAM,GAAG,SAAS,KAChC,MAAM,CAAC,MAAM,EAAE,MAAM,CAqBvB,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAChC,QAAQ,kBAAkB,EAC1B,eAAe,MAAM,GAAG,SAAS,KAChC,MAAM,CAAC,MAAM,EAAE,MAAM,CAWvB,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC5B,QAAQ,kBAAkB,GAAG,IAAI,EACjC,SAAS,YAAY,KACpB;IAAE,iBAAiB,EAAE,aAAa,GAAG,IAAI,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAsBhF,CAAC"}
package/dist/cors.js ADDED
@@ -0,0 +1,82 @@
1
+ const DEFAULT_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'];
2
+ const DEFAULT_ALLOWED_HEADERS = ['Content-Type', 'Authorization', 'X-Request-ID'];
3
+ const DEFAULT_MAX_AGE = 86400; // 24 hours
4
+ export const resolveCorsConfig = (config) => {
5
+ if (!config)
6
+ return null;
7
+ const opts = config === true ? {} : config;
8
+ return {
9
+ origin: opts.origin ?? '*',
10
+ methods: opts.methods ?? DEFAULT_METHODS,
11
+ allowedHeaders: opts.allowedHeaders ?? DEFAULT_ALLOWED_HEADERS,
12
+ exposedHeaders: opts.exposedHeaders ?? [],
13
+ credentials: opts.credentials ?? false,
14
+ maxAge: opts.maxAge ?? DEFAULT_MAX_AGE,
15
+ };
16
+ };
17
+ const matchOrigin = (config, requestOrigin) => {
18
+ if (!requestOrigin)
19
+ return null;
20
+ const { origin } = config;
21
+ if (origin === '*') {
22
+ // When credentials are enabled, we must echo the origin instead of "*"
23
+ return config.credentials ? requestOrigin : '*';
24
+ }
25
+ if (typeof origin === 'string') {
26
+ return origin === requestOrigin ? origin : null;
27
+ }
28
+ if (Array.isArray(origin)) {
29
+ return origin.includes(requestOrigin) ? requestOrigin : null;
30
+ }
31
+ if (typeof origin === 'function') {
32
+ return origin(requestOrigin) ? requestOrigin : null;
33
+ }
34
+ return null;
35
+ };
36
+ export const buildCorsHeaders = (config, requestOrigin) => {
37
+ const headers = {};
38
+ const allowedOrigin = matchOrigin(config, requestOrigin);
39
+ if (!allowedOrigin)
40
+ return headers;
41
+ headers['access-control-allow-origin'] = allowedOrigin;
42
+ if (allowedOrigin !== '*') {
43
+ headers['vary'] = 'Origin';
44
+ }
45
+ if (config.credentials) {
46
+ headers['access-control-allow-credentials'] = 'true';
47
+ }
48
+ if (config.exposedHeaders.length > 0) {
49
+ headers['access-control-expose-headers'] = config.exposedHeaders.join(', ');
50
+ }
51
+ return headers;
52
+ };
53
+ export const buildPreflightHeaders = (config, requestOrigin) => {
54
+ const headers = buildCorsHeaders(config, requestOrigin);
55
+ // No matching origin → don't add preflight headers
56
+ if (!headers['access-control-allow-origin'])
57
+ return headers;
58
+ headers['access-control-allow-methods'] = config.methods.join(', ');
59
+ headers['access-control-allow-headers'] = config.allowedHeaders.join(', ');
60
+ headers['access-control-max-age'] = String(config.maxAge);
61
+ return headers;
62
+ };
63
+ export const handleCorsRequest = (config, request) => {
64
+ if (!config) {
65
+ return { preflightResponse: null, corsHeaders: {} };
66
+ }
67
+ const requestOrigin = request.headers['origin'];
68
+ if (request.method === 'OPTIONS') {
69
+ return {
70
+ preflightResponse: {
71
+ status: 204,
72
+ headers: buildPreflightHeaders(config, requestOrigin),
73
+ body: '',
74
+ },
75
+ corsHeaders: {},
76
+ };
77
+ }
78
+ return {
79
+ preflightResponse: null,
80
+ corsHeaders: buildCorsHeaders(config, requestOrigin),
81
+ };
82
+ };
package/dist/dev.js CHANGED
@@ -7,7 +7,7 @@ export const serveDev = async (api, options = {}) => {
7
7
  const port = options.port ?? Number(process.env.PORT ?? 4000);
8
8
  const hostname = options.hostname ?? "localhost";
9
9
  const logger = options.logger ?? defaultLogger;
10
- const unsubscribe = api.queryLogger.on((event) => {
10
+ api.queryLogger.on((event) => {
11
11
  const line = formatQueryEvent(event);
12
12
  if (line)
13
13
  logger(line);
@@ -0,0 +1,24 @@
1
+ import type { ServeErrorType } from './types.js';
2
+ /**
3
+ * Structured error class for hypequery serve handlers and middleware.
4
+ *
5
+ * Throw this from a handler or middleware to return a specific HTTP status
6
+ * and error type to the client. The pipeline catch block recognises the
7
+ * `status` + `payload` shape and forwards it as-is.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * throw new ServeHttpError(403, 'UNAUTHORIZED', 'Insufficient permissions');
12
+ * throw new ServeHttpError(429, 'RATE_LIMITED', 'Too fast', { 'retry-after': '60' });
13
+ * ```
14
+ */
15
+ export declare class ServeHttpError extends Error {
16
+ readonly status: number;
17
+ readonly payload: {
18
+ type: ServeErrorType;
19
+ message: string;
20
+ };
21
+ readonly headers?: Record<string, string>;
22
+ constructor(status: number, type: ServeErrorType, message: string, headers?: Record<string, string>);
23
+ }
24
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD;;;;;;;;;;;;GAYG;AACH,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE;QAAE,IAAI,EAAE,cAAc,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5D,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;gBAGxC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,cAAc,EACpB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;CAQnC"}
package/dist/errors.js ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Structured error class for hypequery serve handlers and middleware.
3
+ *
4
+ * Throw this from a handler or middleware to return a specific HTTP status
5
+ * and error type to the client. The pipeline catch block recognises the
6
+ * `status` + `payload` shape and forwards it as-is.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * throw new ServeHttpError(403, 'UNAUTHORIZED', 'Insufficient permissions');
11
+ * throw new ServeHttpError(429, 'RATE_LIMITED', 'Too fast', { 'retry-after': '60' });
12
+ * ```
13
+ */
14
+ export class ServeHttpError extends Error {
15
+ constructor(status, type, message, headers) {
16
+ super(message);
17
+ this.name = 'ServeHttpError';
18
+ this.status = status;
19
+ this.payload = { type, message };
20
+ this.headers = headers;
21
+ }
22
+ }
package/dist/index.d.ts CHANGED
@@ -6,10 +6,14 @@ export * from "./endpoint.js";
6
6
  export * from "./openapi.js";
7
7
  export * from "./docs-ui.js";
8
8
  export * from "./auth.js";
9
+ export * from "./cors.js";
10
+ export * from "./errors.js";
11
+ export * from "./rate-limit.js";
9
12
  export * from "./client-config.js";
10
13
  export * from "./utils.js";
11
14
  export * from "./adapters/node.js";
12
15
  export * from "./adapters/fetch.js";
13
16
  export * from "./adapters/vercel.js";
14
17
  export * from "./dev.js";
18
+ export * from "./serve.js";
15
19
  //# sourceMappingURL=index.d.ts.map