@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.
- package/README.md +220 -185
- package/dist/adapters/node.d.ts +1 -1
- package/dist/adapters/node.d.ts.map +1 -1
- package/dist/adapters/node.js +114 -21
- package/dist/auth.d.ts +47 -18
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +87 -20
- package/dist/cors.d.ts +17 -0
- package/dist/cors.d.ts.map +1 -0
- package/dist/cors.js +82 -0
- package/dist/dev.js +1 -1
- package/dist/errors.d.ts +24 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +22 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/pipeline.d.ts +8 -1
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +71 -16
- package/dist/rate-limit.d.ts +86 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +137 -0
- package/dist/serve.d.ts +16 -0
- package/dist/serve.d.ts.map +1 -0
- package/dist/serve.js +88 -0
- package/dist/server/builder.d.ts +1 -1
- package/dist/server/builder.d.ts.map +1 -1
- package/dist/server/builder.js +1 -0
- package/dist/server/define-serve.d.ts.map +1 -1
- package/dist/server/define-serve.js +3 -0
- package/dist/server/execute-query.d.ts.map +1 -1
- package/dist/server/execute-query.js +6 -1
- package/dist/server/init-serve.d.ts.map +1 -1
- package/dist/server/init-serve.js +23 -8
- package/dist/type-tests/builder.test-d.d.ts +8 -2
- package/dist/type-tests/builder.test-d.d.ts.map +1 -1
- package/dist/type-tests/builder.test-d.js +17 -1
- package/dist/types.d.ts +108 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +9 -1
package/dist/adapters/node.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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/
|
|
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/
|
|
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/
|
|
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,
|
|
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
|
|
157
|
+
* // Extract the typed auth type for use with initServe
|
|
139
158
|
* type AppAuth = TypedAuth;
|
|
140
159
|
*
|
|
141
|
-
* const
|
|
160
|
+
* const { query, serve } = initServe<Record<string, never>, AppAuth>({
|
|
142
161
|
* auth: useAuth(jwtStrategy),
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
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>(
|
|
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.
|
package/dist/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,oBAAoB,EACpB,qBAAqB,EACrB,YAAY,EACZ,eAAe,EACf,YAAY,EACb,MAAM,YAAY,CAAC;
|
|
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
|
|
11
|
-
if (
|
|
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
|
|
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/
|
|
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/
|
|
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/
|
|
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,
|
|
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
|
|
223
|
+
* // Extract the typed auth type for use with initServe
|
|
167
224
|
* type AppAuth = TypedAuth;
|
|
168
225
|
*
|
|
169
|
-
* const
|
|
226
|
+
* const { query, serve } = initServe<Record<string, never>, AppAuth>({
|
|
170
227
|
* auth: useAuth(jwtStrategy),
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
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 = (
|
|
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
|
-
|
|
10
|
+
api.queryLogger.on((event) => {
|
|
11
11
|
const line = formatQueryEvent(event);
|
|
12
12
|
if (line)
|
|
13
13
|
logger(line);
|
package/dist/errors.d.ts
ADDED
|
@@ -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
|