@nymbalhq/http 0.0.0-stage → 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/dist/index.d.ts +106 -0
- package/dist/index.js +567 -0
- package/dist/index.js.map +1 -0
- package/package.json +44 -3
- package/README.md +0 -3
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Logger, HttpAdapter, HttpMethod, RouteHandler, RouteOptions, StreamHandler, MiddlewareHandler, RequestContext, ResponseEnvelope, RateLimitAction, DocumentStoreAdapter } from '@nymbalhq/types';
|
|
2
|
+
import { NymbalConfig } from '@nymbalhq/config';
|
|
3
|
+
|
|
4
|
+
interface CorsOptions {
|
|
5
|
+
allowedOrigins: string[];
|
|
6
|
+
credentials: boolean;
|
|
7
|
+
}
|
|
8
|
+
interface FastifyAdapterOptions {
|
|
9
|
+
logger: Logger;
|
|
10
|
+
/** The guard every route passes before its middlewares and handler: rate limit, then CSRF on unsafe methods. */
|
|
11
|
+
security: NymbalConfig['security'];
|
|
12
|
+
/** Whether `X-Forwarded-For` sets the request IP. Off unless a trusted proxy sits in front. */
|
|
13
|
+
trustProxy?: boolean;
|
|
14
|
+
cors?: CorsOptions;
|
|
15
|
+
}
|
|
16
|
+
declare class FastifyHttpAdapter implements HttpAdapter {
|
|
17
|
+
#private;
|
|
18
|
+
constructor(options: FastifyAdapterOptions);
|
|
19
|
+
get boundPort(): number;
|
|
20
|
+
registerRoute(method: HttpMethod, path: string, handler: RouteHandler, options?: RouteOptions): void;
|
|
21
|
+
registerStreamRoute(method: 'GET', path: string, handler: StreamHandler, options?: RouteOptions): void;
|
|
22
|
+
registerMiddleware(middleware: MiddlewareHandler): void;
|
|
23
|
+
setDefaultHeaders(headers: Record<string, string>): void;
|
|
24
|
+
start(port: number, host?: string): Promise<void>;
|
|
25
|
+
stop(): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
declare module 'fastify' {
|
|
28
|
+
interface FastifyRequest {
|
|
29
|
+
rawBody?: Buffer;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
declare const CSRF_COOKIE = "nymbal.csrf";
|
|
34
|
+
declare const CSRF_HEADER = "x-csrf-token";
|
|
35
|
+
declare function buildSecurityHeaders(security: NymbalConfig['security']): Record<string, string>;
|
|
36
|
+
interface SecurityGuard {
|
|
37
|
+
/**
|
|
38
|
+
* Runs before any middleware or handler: the rate limit, then CSRF on unsafe methods unless the route opted out.
|
|
39
|
+
* Returns the refusal, or null when the request may proceed.
|
|
40
|
+
*/
|
|
41
|
+
admit(ctx: RequestContext, options?: RouteOptions): ResponseEnvelope | null;
|
|
42
|
+
/** Runs on the handler's answer: security headers, the CSRF cookie, and the outcome of a failure-counted login. */
|
|
43
|
+
finish(ctx: RequestContext, envelope: ResponseEnvelope): ResponseEnvelope;
|
|
44
|
+
/** `GET /api/csrf`: the request's CSRF cookie, or a new one set on the response. */
|
|
45
|
+
issueToken(ctx: RequestContext): ResponseEnvelope;
|
|
46
|
+
reset(): void;
|
|
47
|
+
}
|
|
48
|
+
declare function createSecurityGuard(security: NymbalConfig['security']): SecurityGuard;
|
|
49
|
+
|
|
50
|
+
interface RateLimitDecision {
|
|
51
|
+
allowed: boolean;
|
|
52
|
+
action: RateLimitAction;
|
|
53
|
+
retryAfter?: number;
|
|
54
|
+
}
|
|
55
|
+
type RateLimitRules = Record<string, {
|
|
56
|
+
requests: number;
|
|
57
|
+
window: string;
|
|
58
|
+
action: RateLimitAction;
|
|
59
|
+
}>;
|
|
60
|
+
declare class RateLimiter {
|
|
61
|
+
#private;
|
|
62
|
+
constructor(rules: RateLimitRules);
|
|
63
|
+
check(ip: string, path: string): RateLimitDecision | null;
|
|
64
|
+
/**
|
|
65
|
+
* For a route that counts failures only: refuses when any of `keys` (an IP, an account) already holds the rule's
|
|
66
|
+
* full count, without counting this request. `hit` and `clear` record the outcome once the handler has answered.
|
|
67
|
+
*/
|
|
68
|
+
full(path: string, keys: string[]): RateLimitDecision | null;
|
|
69
|
+
hit(path: string, keys: string[]): void;
|
|
70
|
+
clear(path: string, keys: string[]): void;
|
|
71
|
+
reset(): void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface HttpServerDeps {
|
|
75
|
+
config: NymbalConfig;
|
|
76
|
+
logger: Logger;
|
|
77
|
+
documentStore: DocumentStoreAdapter;
|
|
78
|
+
version: string;
|
|
79
|
+
}
|
|
80
|
+
interface HttpServer {
|
|
81
|
+
adapter: HttpAdapter;
|
|
82
|
+
registerRoute(method: HttpMethod, path: string, handler: RouteHandler, options?: RouteOptions): void;
|
|
83
|
+
registerMiddleware(middleware: MiddlewareHandler): void;
|
|
84
|
+
start(): Promise<{
|
|
85
|
+
port: number;
|
|
86
|
+
host: string;
|
|
87
|
+
}>;
|
|
88
|
+
stop(): Promise<void>;
|
|
89
|
+
}
|
|
90
|
+
declare function createHttpServer(deps: HttpServerDeps): HttpServer;
|
|
91
|
+
|
|
92
|
+
declare function createHealthRoute(version: string): RouteHandler;
|
|
93
|
+
|
|
94
|
+
interface ProductListItem {
|
|
95
|
+
id: string;
|
|
96
|
+
slug: string;
|
|
97
|
+
title: string;
|
|
98
|
+
description: string;
|
|
99
|
+
priceMinor: number;
|
|
100
|
+
currency: string;
|
|
101
|
+
categoryId: string;
|
|
102
|
+
imageUrl: string;
|
|
103
|
+
}
|
|
104
|
+
declare function createProductsRoute(documentStore: DocumentStoreAdapter, storeId: string): RouteHandler;
|
|
105
|
+
|
|
106
|
+
export { CSRF_COOKIE, CSRF_HEADER, type CorsOptions, type FastifyAdapterOptions, FastifyHttpAdapter, type HttpServer, type HttpServerDeps, type ProductListItem, RateLimiter, type SecurityGuard, buildSecurityHeaders, createHealthRoute, createHttpServer, createProductsRoute, createSecurityGuard };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
// src/fastify-adapter.ts
|
|
2
|
+
import Fastify from "fastify";
|
|
3
|
+
import fastifyCookie from "@fastify/cookie";
|
|
4
|
+
import fastifyCors from "@fastify/cors";
|
|
5
|
+
import { v7 as uuidv7 } from "uuid";
|
|
6
|
+
import { HttpError, NymbalError } from "@nymbalhq/types";
|
|
7
|
+
|
|
8
|
+
// src/security-middleware.ts
|
|
9
|
+
import { randomBytes, timingSafeEqual } from "crypto";
|
|
10
|
+
|
|
11
|
+
// src/rate-limit.ts
|
|
12
|
+
var BUILT_IN_RATE_LIMITS = {
|
|
13
|
+
"/api/auth/login": { requests: 10, window: "15m", action: "block" },
|
|
14
|
+
"/api/auth/register": { requests: 10, window: "1h", action: "block" }
|
|
15
|
+
};
|
|
16
|
+
var PRUNE_THRESHOLD = 1024;
|
|
17
|
+
var RateLimiter = class {
|
|
18
|
+
#rules;
|
|
19
|
+
#buckets = /* @__PURE__ */ new Map();
|
|
20
|
+
#pruneAt = PRUNE_THRESHOLD;
|
|
21
|
+
constructor(rules) {
|
|
22
|
+
this.#rules = Object.entries(rules).map(([pattern, rule]) => ({
|
|
23
|
+
pattern,
|
|
24
|
+
regex: compilePathPattern(pattern),
|
|
25
|
+
requests: rule.requests,
|
|
26
|
+
windowMs: parseWindow(rule.window),
|
|
27
|
+
action: rule.action
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
check(ip, path) {
|
|
31
|
+
const rule = this.#ruleFor(path);
|
|
32
|
+
if (!rule) return null;
|
|
33
|
+
const key = `${rule.pattern}|${ip}`;
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
const bucket = this.#buckets.get(key);
|
|
36
|
+
if (!bucket || bucket.resetAt <= now) {
|
|
37
|
+
this.#open(key, now, rule.windowMs);
|
|
38
|
+
return { allowed: true, action: rule.action };
|
|
39
|
+
}
|
|
40
|
+
bucket.count += 1;
|
|
41
|
+
if (bucket.count > rule.requests) {
|
|
42
|
+
return { allowed: false, action: rule.action, retryAfter: retryAfter(bucket, now) };
|
|
43
|
+
}
|
|
44
|
+
return { allowed: true, action: rule.action };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* For a route that counts failures only: refuses when any of `keys` (an IP, an account) already holds the rule's
|
|
48
|
+
* full count, without counting this request. `hit` and `clear` record the outcome once the handler has answered.
|
|
49
|
+
*/
|
|
50
|
+
full(path, keys) {
|
|
51
|
+
const rule = this.#ruleFor(path);
|
|
52
|
+
if (!rule) return null;
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
for (const key of keys) {
|
|
55
|
+
const bucket = this.#buckets.get(`${rule.pattern}|${key}`);
|
|
56
|
+
if (bucket && bucket.resetAt > now && bucket.count >= rule.requests) {
|
|
57
|
+
return { allowed: false, action: rule.action, retryAfter: retryAfter(bucket, now) };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { allowed: true, action: rule.action };
|
|
61
|
+
}
|
|
62
|
+
hit(path, keys) {
|
|
63
|
+
const rule = this.#ruleFor(path);
|
|
64
|
+
if (!rule) return;
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
for (const key of keys) {
|
|
67
|
+
const bucketKey = `${rule.pattern}|${key}`;
|
|
68
|
+
const bucket = this.#buckets.get(bucketKey);
|
|
69
|
+
if (!bucket || bucket.resetAt <= now) {
|
|
70
|
+
this.#open(bucketKey, now, rule.windowMs);
|
|
71
|
+
} else {
|
|
72
|
+
bucket.count += 1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
clear(path, keys) {
|
|
77
|
+
const rule = this.#ruleFor(path);
|
|
78
|
+
if (!rule) return;
|
|
79
|
+
for (const key of keys) this.#buckets.delete(`${rule.pattern}|${key}`);
|
|
80
|
+
}
|
|
81
|
+
reset() {
|
|
82
|
+
this.#buckets.clear();
|
|
83
|
+
this.#pruneAt = PRUNE_THRESHOLD;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Starts a bucket. Keys include caller-chosen values (an account email), so expired buckets are swept once the map
|
|
87
|
+
* reaches `#pruneAt`; the next sweep waits until the map doubles past what survived, which keeps the cost amortised
|
|
88
|
+
* constant per insert even when every bucket is live.
|
|
89
|
+
*/
|
|
90
|
+
#open(key, now, windowMs) {
|
|
91
|
+
if (this.#buckets.size >= this.#pruneAt) {
|
|
92
|
+
for (const [k, b] of this.#buckets) if (b.resetAt <= now) this.#buckets.delete(k);
|
|
93
|
+
this.#pruneAt = Math.max(PRUNE_THRESHOLD, this.#buckets.size * 2);
|
|
94
|
+
}
|
|
95
|
+
this.#buckets.set(key, { count: 1, resetAt: now + windowMs });
|
|
96
|
+
}
|
|
97
|
+
#ruleFor(path) {
|
|
98
|
+
return this.#rules.find((r) => r.regex.test(path));
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
function retryAfter(bucket, now) {
|
|
102
|
+
return Math.max(1, Math.ceil((bucket.resetAt - now) / 1e3));
|
|
103
|
+
}
|
|
104
|
+
function parseWindow(window) {
|
|
105
|
+
const match = /^(\d+)(ms|s|m|h)$/.exec(window);
|
|
106
|
+
if (!match) throw new Error(`Invalid rate-limit window: ${window}`);
|
|
107
|
+
const n = Number(match[1]);
|
|
108
|
+
switch (match[2]) {
|
|
109
|
+
case "ms":
|
|
110
|
+
return n;
|
|
111
|
+
case "s":
|
|
112
|
+
return n * 1e3;
|
|
113
|
+
case "m":
|
|
114
|
+
return n * 6e4;
|
|
115
|
+
case "h":
|
|
116
|
+
return n * 36e5;
|
|
117
|
+
default:
|
|
118
|
+
throw new Error(`Unreachable unit: ${match[2]}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function compilePathPattern(pattern) {
|
|
122
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "__DOUBLESTAR__").replace(/\*/g, "[^/]+").replace(/__DOUBLESTAR__/g, ".*");
|
|
123
|
+
return new RegExp(`^${escaped}$`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/security-middleware.ts
|
|
127
|
+
var CSRF_COOKIE = "nymbal.csrf";
|
|
128
|
+
var CSRF_HEADER = "x-csrf-token";
|
|
129
|
+
var UNSAFE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
130
|
+
function buildSecurityHeaders(security) {
|
|
131
|
+
const headers = {};
|
|
132
|
+
if (security.headers.hsts) {
|
|
133
|
+
headers["strict-transport-security"] = "max-age=31536000; includeSubDomains";
|
|
134
|
+
}
|
|
135
|
+
const csp = security.headers.contentSecurityPolicy;
|
|
136
|
+
const cspValue = csp === "strict" ? "default-src 'self'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'" : csp === "relaxed" ? "default-src 'self' https:; img-src * data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'" : csp === "off" ? null : csp;
|
|
137
|
+
if (cspValue) headers["content-security-policy"] = cspValue;
|
|
138
|
+
headers["referrer-policy"] = security.headers.referrerPolicy;
|
|
139
|
+
headers["x-frame-options"] = security.headers.xFrameOptions === "deny" ? "DENY" : "SAMEORIGIN";
|
|
140
|
+
headers["x-content-type-options"] = "nosniff";
|
|
141
|
+
return headers;
|
|
142
|
+
}
|
|
143
|
+
var LOGIN_PATH = "/api/auth/login";
|
|
144
|
+
function csrfCookie(token) {
|
|
145
|
+
return {
|
|
146
|
+
name: CSRF_COOKIE,
|
|
147
|
+
value: token,
|
|
148
|
+
path: "/",
|
|
149
|
+
httpOnly: false,
|
|
150
|
+
sameSite: "lax",
|
|
151
|
+
secure: process.env.NODE_ENV === "production"
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function loginKeys(ctx) {
|
|
155
|
+
const email = ctx.body?.email;
|
|
156
|
+
return typeof email === "string" && email !== "" ? [ctx.ip, `account:${email.toLowerCase()}`] : [ctx.ip];
|
|
157
|
+
}
|
|
158
|
+
function isLogin(ctx) {
|
|
159
|
+
return ctx.method === "POST" && ctx.path === LOGIN_PATH;
|
|
160
|
+
}
|
|
161
|
+
function createSecurityGuard(security) {
|
|
162
|
+
const rateLimiter = new RateLimiter({ ...BUILT_IN_RATE_LIMITS, ...security.rateLimit });
|
|
163
|
+
const baseHeaders = buildSecurityHeaders(security);
|
|
164
|
+
function applyHeaders(envelope) {
|
|
165
|
+
return {
|
|
166
|
+
...envelope,
|
|
167
|
+
headers: { ...baseHeaders, ...envelope.headers ?? {} }
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
admit(ctx, options = {}) {
|
|
172
|
+
const decision = isLogin(ctx) ? rateLimiter.full(ctx.path, loginKeys(ctx)) : rateLimiter.check(ctx.ip, ctx.path);
|
|
173
|
+
if (decision && !decision.allowed) {
|
|
174
|
+
return applyHeaders(rateLimitResponse(decision, ctx));
|
|
175
|
+
}
|
|
176
|
+
if (security.csrf && options.csrf !== false && UNSAFE_METHODS.has(ctx.method)) {
|
|
177
|
+
if (!verifyCsrf(ctx)) {
|
|
178
|
+
ctx.logger.warn({ method: ctx.method, path: ctx.path }, "CSRF validation failed");
|
|
179
|
+
return applyHeaders({
|
|
180
|
+
status: 403,
|
|
181
|
+
body: { error: { code: "csrf.invalid", message: "Invalid or missing CSRF token" } }
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
},
|
|
187
|
+
finish(ctx, envelope) {
|
|
188
|
+
if (isLogin(ctx)) {
|
|
189
|
+
const keys = loginKeys(ctx);
|
|
190
|
+
if (envelope.status >= 200 && envelope.status < 300) {
|
|
191
|
+
rateLimiter.clear(
|
|
192
|
+
ctx.path,
|
|
193
|
+
keys.filter((key) => key.startsWith("account:"))
|
|
194
|
+
);
|
|
195
|
+
} else rateLimiter.hit(ctx.path, keys);
|
|
196
|
+
}
|
|
197
|
+
const finished = applyHeaders(envelope);
|
|
198
|
+
const setsCsrf = finished.cookies?.some((cookie) => cookie.name === CSRF_COOKIE) ?? false;
|
|
199
|
+
if (security.csrf && !ctx.cookies[CSRF_COOKIE] && !setsCsrf) {
|
|
200
|
+
finished.cookies = [...finished.cookies ?? [], csrfCookie(newToken())];
|
|
201
|
+
}
|
|
202
|
+
return finished;
|
|
203
|
+
},
|
|
204
|
+
issueToken(ctx) {
|
|
205
|
+
const existing = ctx.cookies[CSRF_COOKIE];
|
|
206
|
+
if (existing) return { status: 200, body: { data: { token: existing } } };
|
|
207
|
+
const token = newToken();
|
|
208
|
+
return { status: 200, body: { data: { token } }, cookies: [csrfCookie(token)] };
|
|
209
|
+
},
|
|
210
|
+
reset() {
|
|
211
|
+
rateLimiter.reset();
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function newToken() {
|
|
216
|
+
return randomBytes(32).toString("hex");
|
|
217
|
+
}
|
|
218
|
+
function verifyCsrf(ctx) {
|
|
219
|
+
const cookieToken = ctx.cookies[CSRF_COOKIE];
|
|
220
|
+
const rawHeader = ctx.headers[CSRF_HEADER];
|
|
221
|
+
const headerToken = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader;
|
|
222
|
+
if (!cookieToken || !headerToken) return false;
|
|
223
|
+
if (cookieToken.length !== headerToken.length) return false;
|
|
224
|
+
try {
|
|
225
|
+
return timingSafeEqual(Buffer.from(cookieToken), Buffer.from(headerToken));
|
|
226
|
+
} catch {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function rateLimitResponse(decision, ctx) {
|
|
231
|
+
ctx.logger.info({ ip: ctx.ip, path: ctx.path, action: decision.action }, "Rate limit triggered");
|
|
232
|
+
const status = decision.action === "challenge" ? 403 : 429;
|
|
233
|
+
const headers = {};
|
|
234
|
+
if (decision.retryAfter) headers["retry-after"] = String(decision.retryAfter);
|
|
235
|
+
return {
|
|
236
|
+
status,
|
|
237
|
+
headers,
|
|
238
|
+
body: {
|
|
239
|
+
error: {
|
|
240
|
+
code: `rate_limit.${decision.action}`,
|
|
241
|
+
message: "Too many requests",
|
|
242
|
+
...decision.retryAfter !== void 0 && { retryAfter: decision.retryAfter }
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/fastify-adapter.ts
|
|
249
|
+
var FastifyHttpAdapter = class {
|
|
250
|
+
#app;
|
|
251
|
+
#logger;
|
|
252
|
+
#guard;
|
|
253
|
+
#middlewares = [];
|
|
254
|
+
#defaultHeaders = {};
|
|
255
|
+
#started = false;
|
|
256
|
+
#boundPort = 0;
|
|
257
|
+
constructor(options) {
|
|
258
|
+
this.#logger = options.logger;
|
|
259
|
+
this.#guard = createSecurityGuard(options.security);
|
|
260
|
+
this.#app = Fastify({
|
|
261
|
+
logger: false,
|
|
262
|
+
trustProxy: options.trustProxy ?? false,
|
|
263
|
+
disableRequestLogging: true,
|
|
264
|
+
genReqId: () => uuidv7()
|
|
265
|
+
});
|
|
266
|
+
const parseJson = this.#app.getDefaultJsonParser(
|
|
267
|
+
this.#app.initialConfig.onProtoPoisoning ?? "error",
|
|
268
|
+
this.#app.initialConfig.onConstructorPoisoning ?? "error"
|
|
269
|
+
);
|
|
270
|
+
this.#app.removeContentTypeParser("application/json");
|
|
271
|
+
this.#app.addContentTypeParser("application/json", { parseAs: "buffer" }, (req, body, done) => {
|
|
272
|
+
const buffer = body;
|
|
273
|
+
const config = req.routeOptions.config;
|
|
274
|
+
if (config?.routeOptions?.rawBody === true) req.rawBody = buffer;
|
|
275
|
+
parseJson(req, buffer.toString("utf8"), done);
|
|
276
|
+
});
|
|
277
|
+
void this.#app.register(fastifyCookie);
|
|
278
|
+
if (options.cors && options.cors.allowedOrigins.length > 0) {
|
|
279
|
+
void this.#app.register(fastifyCors, {
|
|
280
|
+
origin: options.cors.allowedOrigins,
|
|
281
|
+
credentials: options.cors.credentials,
|
|
282
|
+
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
this.registerRoute("GET", "/api/csrf", (ctx) => this.#guard.issueToken(ctx));
|
|
286
|
+
}
|
|
287
|
+
get boundPort() {
|
|
288
|
+
return this.#boundPort;
|
|
289
|
+
}
|
|
290
|
+
registerRoute(method, path, handler, options) {
|
|
291
|
+
if (this.#started) {
|
|
292
|
+
throw new Error("Cannot register routes after the server has started");
|
|
293
|
+
}
|
|
294
|
+
const routeConfig = {
|
|
295
|
+
method,
|
|
296
|
+
url: path,
|
|
297
|
+
handler: async (req, reply) => {
|
|
298
|
+
const ctx = this.#buildContext(req, options?.rawBody ?? false);
|
|
299
|
+
const refused = this.#guard.admit(ctx, options);
|
|
300
|
+
if (refused) return this.#applyEnvelope(reply, refused);
|
|
301
|
+
let envelope;
|
|
302
|
+
try {
|
|
303
|
+
envelope = await this.#runMiddlewaresThen(ctx, handler);
|
|
304
|
+
} catch (err) {
|
|
305
|
+
envelope = this.#renderError(err, ctx);
|
|
306
|
+
}
|
|
307
|
+
return this.#applyEnvelope(reply, this.#guard.finish(ctx, envelope));
|
|
308
|
+
},
|
|
309
|
+
config: { routeOptions: options ?? {} }
|
|
310
|
+
};
|
|
311
|
+
this.#app.route(routeConfig);
|
|
312
|
+
}
|
|
313
|
+
registerStreamRoute(method, path, handler, options) {
|
|
314
|
+
if (this.#started) {
|
|
315
|
+
throw new Error("Cannot register routes after the server has started");
|
|
316
|
+
}
|
|
317
|
+
this.#app.route({
|
|
318
|
+
method,
|
|
319
|
+
url: path,
|
|
320
|
+
handler: async (req, reply) => {
|
|
321
|
+
const ctx = this.#buildContext(req, false);
|
|
322
|
+
const refused = this.#guard.admit(ctx, options);
|
|
323
|
+
if (refused) return this.#applyEnvelope(reply, refused);
|
|
324
|
+
try {
|
|
325
|
+
for (const mw of this.#middlewares) {
|
|
326
|
+
const out = await mw(ctx);
|
|
327
|
+
if (out) {
|
|
328
|
+
return this.#applyEnvelope(reply, out);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
} catch (err) {
|
|
332
|
+
return this.#applyEnvelope(reply, this.#renderError(err, ctx));
|
|
333
|
+
}
|
|
334
|
+
reply.raw.writeHead(200, {
|
|
335
|
+
...this.#defaultHeaders,
|
|
336
|
+
"content-type": "text/event-stream",
|
|
337
|
+
"cache-control": "no-cache, no-transform",
|
|
338
|
+
connection: "keep-alive",
|
|
339
|
+
"x-accel-buffering": "no"
|
|
340
|
+
});
|
|
341
|
+
reply.hijack();
|
|
342
|
+
const closeCallbacks = [];
|
|
343
|
+
let closed = false;
|
|
344
|
+
const close = () => {
|
|
345
|
+
if (closed) return;
|
|
346
|
+
closed = true;
|
|
347
|
+
try {
|
|
348
|
+
reply.raw.end();
|
|
349
|
+
} catch {
|
|
350
|
+
}
|
|
351
|
+
for (const cb of closeCallbacks) {
|
|
352
|
+
try {
|
|
353
|
+
cb();
|
|
354
|
+
} catch (err) {
|
|
355
|
+
ctx.logger.error({ err }, "stream close callback failed");
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
req.raw.on("close", close);
|
|
360
|
+
req.raw.on("error", close);
|
|
361
|
+
const stream = {
|
|
362
|
+
send(event) {
|
|
363
|
+
if (closed) return;
|
|
364
|
+
let line = "";
|
|
365
|
+
if (event.id !== void 0) line += `id: ${event.id}
|
|
366
|
+
`;
|
|
367
|
+
if (event.event !== void 0) line += `event: ${event.event}
|
|
368
|
+
`;
|
|
369
|
+
const data = typeof event.data === "string" ? event.data : JSON.stringify(event.data);
|
|
370
|
+
line += `data: ${data}
|
|
371
|
+
|
|
372
|
+
`;
|
|
373
|
+
reply.raw.write(line);
|
|
374
|
+
},
|
|
375
|
+
close,
|
|
376
|
+
onClose(cb) {
|
|
377
|
+
closeCallbacks.push(cb);
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
try {
|
|
381
|
+
await handler(ctx, stream);
|
|
382
|
+
} catch (err) {
|
|
383
|
+
ctx.logger.error({ err }, "stream handler threw");
|
|
384
|
+
close();
|
|
385
|
+
}
|
|
386
|
+
return reply;
|
|
387
|
+
},
|
|
388
|
+
config: { routeOptions: options ?? {} }
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
registerMiddleware(middleware) {
|
|
392
|
+
if (this.#started) {
|
|
393
|
+
throw new Error("Cannot register middleware after the server has started");
|
|
394
|
+
}
|
|
395
|
+
this.#middlewares.push(middleware);
|
|
396
|
+
}
|
|
397
|
+
setDefaultHeaders(headers) {
|
|
398
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
399
|
+
this.#defaultHeaders[k.toLowerCase()] = v;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
async start(port, host = "0.0.0.0") {
|
|
403
|
+
await this.#app.listen({ port, host });
|
|
404
|
+
this.#boundPort = this.#app.server.address().port;
|
|
405
|
+
this.#started = true;
|
|
406
|
+
this.#logger.info({ port: this.#boundPort, host }, "HTTP server listening");
|
|
407
|
+
}
|
|
408
|
+
async stop() {
|
|
409
|
+
if (!this.#started) return;
|
|
410
|
+
await this.#app.close();
|
|
411
|
+
this.#started = false;
|
|
412
|
+
}
|
|
413
|
+
async #runMiddlewaresThen(ctx, handler) {
|
|
414
|
+
for (const mw of this.#middlewares) {
|
|
415
|
+
const out = await mw(ctx);
|
|
416
|
+
if (out) return out;
|
|
417
|
+
}
|
|
418
|
+
return handler(ctx);
|
|
419
|
+
}
|
|
420
|
+
#buildContext(req, includeRawBody) {
|
|
421
|
+
const correlationId = req.headers["x-correlation-id"] ?? req.id;
|
|
422
|
+
const logger = this.#logger.child({ requestId: req.id, correlationId });
|
|
423
|
+
const ctx = {
|
|
424
|
+
method: req.method,
|
|
425
|
+
path: req.routeOptions?.url ?? req.url,
|
|
426
|
+
url: req.url,
|
|
427
|
+
params: req.params ?? {},
|
|
428
|
+
query: req.query ?? {},
|
|
429
|
+
body: req.body,
|
|
430
|
+
headers: req.headers,
|
|
431
|
+
cookies: req.cookies ?? {},
|
|
432
|
+
requestId: req.id,
|
|
433
|
+
correlationId,
|
|
434
|
+
logger,
|
|
435
|
+
ip: req.ip
|
|
436
|
+
};
|
|
437
|
+
if (includeRawBody && Buffer.isBuffer(req.rawBody)) {
|
|
438
|
+
ctx.rawBody = req.rawBody;
|
|
439
|
+
}
|
|
440
|
+
return ctx;
|
|
441
|
+
}
|
|
442
|
+
#applyEnvelope(reply, envelope) {
|
|
443
|
+
reply.status(envelope.status);
|
|
444
|
+
for (const [name, value] of Object.entries(this.#defaultHeaders)) {
|
|
445
|
+
reply.header(name, value);
|
|
446
|
+
}
|
|
447
|
+
if (envelope.headers) {
|
|
448
|
+
for (const [name, value] of Object.entries(envelope.headers)) {
|
|
449
|
+
reply.header(name, value);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
if (envelope.cookies) {
|
|
453
|
+
for (const cookie of envelope.cookies) {
|
|
454
|
+
reply.setCookie(cookie.name, cookie.value, {
|
|
455
|
+
path: cookie.path ?? "/",
|
|
456
|
+
...cookie.domain !== void 0 && { domain: cookie.domain },
|
|
457
|
+
...cookie.maxAge !== void 0 && { maxAge: cookie.maxAge },
|
|
458
|
+
httpOnly: cookie.httpOnly ?? false,
|
|
459
|
+
secure: cookie.secure ?? false,
|
|
460
|
+
...cookie.sameSite !== void 0 && { sameSite: cookie.sameSite }
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (envelope.body === void 0) {
|
|
465
|
+
return reply.send();
|
|
466
|
+
}
|
|
467
|
+
return reply.send(envelope.body);
|
|
468
|
+
}
|
|
469
|
+
#renderError(err, ctx) {
|
|
470
|
+
if (err instanceof HttpError) {
|
|
471
|
+
ctx.logger.warn({ err: err.toJSON() }, err.message);
|
|
472
|
+
return {
|
|
473
|
+
status: err.status,
|
|
474
|
+
body: { error: { code: err.code, message: err.message } }
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
if (err instanceof NymbalError) {
|
|
478
|
+
ctx.logger.error({ err: err.toJSON() }, err.message);
|
|
479
|
+
return {
|
|
480
|
+
status: 500,
|
|
481
|
+
body: { error: { code: err.code, message: err.message } }
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
485
|
+
ctx.logger.error({ err }, message);
|
|
486
|
+
return {
|
|
487
|
+
status: 500,
|
|
488
|
+
body: { error: { code: "internal", message: "Internal server error" } }
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
// src/routes/health.ts
|
|
494
|
+
function createHealthRoute(version) {
|
|
495
|
+
return () => ({
|
|
496
|
+
status: 200,
|
|
497
|
+
body: { status: "ok", version, timestamp: (/* @__PURE__ */ new Date()).toISOString() }
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// src/routes/products.ts
|
|
502
|
+
function createProductsRoute(documentStore, storeId) {
|
|
503
|
+
return async (ctx) => {
|
|
504
|
+
const rawLimit = ctx.query.limit;
|
|
505
|
+
const limitValue = Array.isArray(rawLimit) ? rawLimit[0] : rawLimit;
|
|
506
|
+
const limit = limitValue ? Math.min(100, Math.max(1, Number(limitValue) || 20)) : 50;
|
|
507
|
+
const rawCursor = ctx.query.cursor;
|
|
508
|
+
const cursorValue = Array.isArray(rawCursor) ? rawCursor[0] : rawCursor;
|
|
509
|
+
const result = await documentStore.query("products", {
|
|
510
|
+
partitionKey: { field: "storeId", value: storeId },
|
|
511
|
+
limit,
|
|
512
|
+
...cursorValue !== void 0 && { cursor: cursorValue },
|
|
513
|
+
ascending: true
|
|
514
|
+
});
|
|
515
|
+
return {
|
|
516
|
+
status: 200,
|
|
517
|
+
body: { items: result.items, nextCursor: result.nextCursor }
|
|
518
|
+
};
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// src/server.ts
|
|
523
|
+
function createHttpServer(deps) {
|
|
524
|
+
const { config, logger, documentStore, version } = deps;
|
|
525
|
+
if (config.http.adapter !== "fastify") {
|
|
526
|
+
throw new Error(`Unsupported http.adapter: ${config.http.adapter}`);
|
|
527
|
+
}
|
|
528
|
+
const adapter = new FastifyHttpAdapter({
|
|
529
|
+
logger,
|
|
530
|
+
cors: config.http.cors,
|
|
531
|
+
security: config.security,
|
|
532
|
+
trustProxy: config.http.trustProxy
|
|
533
|
+
});
|
|
534
|
+
const server = {
|
|
535
|
+
adapter,
|
|
536
|
+
registerRoute(method, path, handler, options) {
|
|
537
|
+
adapter.registerRoute(method, path, handler, options);
|
|
538
|
+
},
|
|
539
|
+
registerMiddleware(middleware) {
|
|
540
|
+
adapter.registerMiddleware(middleware);
|
|
541
|
+
},
|
|
542
|
+
async start() {
|
|
543
|
+
await adapter.start(config.http.port, config.http.host);
|
|
544
|
+
return { port: adapter.boundPort, host: config.http.host };
|
|
545
|
+
},
|
|
546
|
+
async stop() {
|
|
547
|
+
await adapter.stop();
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
server.registerRoute("GET", "/health", createHealthRoute(version));
|
|
551
|
+
void createProductsRoute;
|
|
552
|
+
void documentStore;
|
|
553
|
+
void config;
|
|
554
|
+
return server;
|
|
555
|
+
}
|
|
556
|
+
export {
|
|
557
|
+
CSRF_COOKIE,
|
|
558
|
+
CSRF_HEADER,
|
|
559
|
+
FastifyHttpAdapter,
|
|
560
|
+
RateLimiter,
|
|
561
|
+
buildSecurityHeaders,
|
|
562
|
+
createHealthRoute,
|
|
563
|
+
createHttpServer,
|
|
564
|
+
createProductsRoute,
|
|
565
|
+
createSecurityGuard
|
|
566
|
+
};
|
|
567
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/fastify-adapter.ts","../src/security-middleware.ts","../src/rate-limit.ts","../src/routes/health.ts","../src/routes/products.ts","../src/server.ts"],"sourcesContent":["import Fastify, { type FastifyInstance, type FastifyReply, type FastifyRequest } from 'fastify'\nimport fastifyCookie from '@fastify/cookie'\nimport fastifyCors from '@fastify/cors'\nimport { v7 as uuidv7 } from 'uuid'\nimport type {\n HttpAdapter,\n HttpMethod,\n Logger,\n MiddlewareHandler,\n RequestContext,\n ResponseEnvelope,\n RouteHandler,\n RouteOptions,\n SseEvent,\n StreamContext,\n StreamHandler,\n} from '@nymbalhq/types'\nimport { HttpError, NymbalError } from '@nymbalhq/types'\nimport type { NymbalConfig } from '@nymbalhq/config'\nimport { createSecurityGuard, type SecurityGuard } from './security-middleware.js'\n\nexport interface CorsOptions {\n allowedOrigins: string[]\n credentials: boolean\n}\n\nexport interface FastifyAdapterOptions {\n logger: Logger\n /** The guard every route passes before its middlewares and handler: rate limit, then CSRF on unsafe methods. */\n security: NymbalConfig['security']\n /** Whether `X-Forwarded-For` sets the request IP. Off unless a trusted proxy sits in front. */\n trustProxy?: boolean\n cors?: CorsOptions\n}\n\nexport class FastifyHttpAdapter implements HttpAdapter {\n readonly #app: FastifyInstance\n readonly #logger: Logger\n readonly #guard: SecurityGuard\n readonly #middlewares: MiddlewareHandler[] = []\n readonly #defaultHeaders: Record<string, string> = {}\n #started = false\n #boundPort = 0\n\n constructor(options: FastifyAdapterOptions) {\n this.#logger = options.logger\n this.#guard = createSecurityGuard(options.security)\n this.#app = Fastify({\n logger: false,\n trustProxy: options.trustProxy ?? false,\n disableRequestLogging: true,\n genReqId: () => uuidv7(),\n })\n // A signed webhook is verified against the bytes sent, so a route registered with `rawBody` keeps them. The\n // body is still parsed by Fastify's own JSON parser, with its poisoning settings and its 400s.\n const parseJson = this.#app.getDefaultJsonParser(\n this.#app.initialConfig.onProtoPoisoning ?? 'error',\n this.#app.initialConfig.onConstructorPoisoning ?? 'error',\n )\n this.#app.removeContentTypeParser('application/json')\n this.#app.addContentTypeParser('application/json', { parseAs: 'buffer' }, (req, body, done) => {\n const buffer = body as Buffer\n const config = req.routeOptions.config as { routeOptions?: RouteOptions } | undefined\n if (config?.routeOptions?.rawBody === true) req.rawBody = buffer\n parseJson(req, buffer.toString('utf8'), done)\n })\n void this.#app.register(fastifyCookie)\n if (options.cors && options.cors.allowedOrigins.length > 0) {\n void this.#app.register(fastifyCors, {\n origin: options.cors.allowedOrigins,\n credentials: options.cors.credentials,\n methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'],\n })\n }\n // The token a client on another origin of the same site echoes in `x-csrf-token`: it cannot read this cookie.\n this.registerRoute('GET', '/api/csrf', (ctx) => this.#guard.issueToken(ctx))\n }\n\n get boundPort(): number {\n return this.#boundPort\n }\n\n registerRoute(\n method: HttpMethod,\n path: string,\n handler: RouteHandler,\n options?: RouteOptions,\n ): void {\n if (this.#started) {\n throw new Error('Cannot register routes after the server has started')\n }\n const routeConfig: {\n method: HttpMethod\n url: string\n handler: (req: FastifyRequest, reply: FastifyReply) => Promise<FastifyReply>\n config: { routeOptions: RouteOptions }\n bodyLimit?: number\n } = {\n method,\n url: path,\n handler: async (req, reply) => {\n const ctx = this.#buildContext(req, options?.rawBody ?? false)\n const refused = this.#guard.admit(ctx, options)\n if (refused) return this.#applyEnvelope(reply, refused)\n let envelope: ResponseEnvelope\n try {\n envelope = await this.#runMiddlewaresThen(ctx, handler)\n } catch (err) {\n envelope = this.#renderError(err, ctx)\n }\n return this.#applyEnvelope(reply, this.#guard.finish(ctx, envelope))\n },\n config: { routeOptions: options ?? {} },\n }\n this.#app.route(routeConfig)\n }\n\n registerStreamRoute(\n method: 'GET',\n path: string,\n handler: StreamHandler,\n options?: RouteOptions,\n ): void {\n if (this.#started) {\n throw new Error('Cannot register routes after the server has started')\n }\n this.#app.route({\n method,\n url: path,\n handler: async (req, reply) => {\n const ctx = this.#buildContext(req, false)\n const refused = this.#guard.admit(ctx, options)\n if (refused) return this.#applyEnvelope(reply, refused)\n try {\n for (const mw of this.#middlewares) {\n const out = await mw(ctx)\n if (out) {\n return this.#applyEnvelope(reply, out)\n }\n }\n } catch (err) {\n return this.#applyEnvelope(reply, this.#renderError(err, ctx))\n }\n reply.raw.writeHead(200, {\n ...this.#defaultHeaders,\n 'content-type': 'text/event-stream',\n 'cache-control': 'no-cache, no-transform',\n connection: 'keep-alive',\n 'x-accel-buffering': 'no',\n })\n reply.hijack()\n const closeCallbacks: Array<() => void> = []\n let closed = false\n const close = (): void => {\n if (closed) return\n closed = true\n try {\n reply.raw.end()\n } catch {\n // ignore\n }\n for (const cb of closeCallbacks) {\n try {\n cb()\n } catch (err) {\n ctx.logger.error({ err }, 'stream close callback failed')\n }\n }\n }\n req.raw.on('close', close)\n req.raw.on('error', close)\n const stream: StreamContext = {\n send(event: SseEvent): void {\n if (closed) return\n let line = ''\n if (event.id !== undefined) line += `id: ${event.id}\\n`\n if (event.event !== undefined) line += `event: ${event.event}\\n`\n const data = typeof event.data === 'string' ? event.data : JSON.stringify(event.data)\n line += `data: ${data}\\n\\n`\n reply.raw.write(line)\n },\n close,\n onClose(cb: () => void): void {\n closeCallbacks.push(cb)\n },\n }\n try {\n await handler(ctx, stream)\n } catch (err) {\n ctx.logger.error({ err }, 'stream handler threw')\n close()\n }\n return reply\n },\n config: { routeOptions: options ?? {} },\n })\n }\n\n registerMiddleware(middleware: MiddlewareHandler): void {\n if (this.#started) {\n throw new Error('Cannot register middleware after the server has started')\n }\n this.#middlewares.push(middleware)\n }\n\n setDefaultHeaders(headers: Record<string, string>): void {\n for (const [k, v] of Object.entries(headers)) {\n this.#defaultHeaders[k.toLowerCase()] = v\n }\n }\n\n async start(port: number, host = '0.0.0.0'): Promise<void> {\n await this.#app.listen({ port, host })\n this.#boundPort = (this.#app.server.address() as { port: number }).port\n this.#started = true\n this.#logger.info({ port: this.#boundPort, host }, 'HTTP server listening')\n }\n\n async stop(): Promise<void> {\n if (!this.#started) return\n await this.#app.close()\n this.#started = false\n }\n\n async #runMiddlewaresThen(ctx: RequestContext, handler: RouteHandler): Promise<ResponseEnvelope> {\n for (const mw of this.#middlewares) {\n const out = await mw(ctx)\n if (out) return out\n }\n return handler(ctx)\n }\n\n #buildContext(req: FastifyRequest, includeRawBody: boolean): RequestContext {\n const correlationId = (req.headers['x-correlation-id'] as string | undefined) ?? req.id\n const logger = this.#logger.child({ requestId: req.id, correlationId })\n const ctx: RequestContext = {\n method: req.method as HttpMethod,\n path: req.routeOptions?.url ?? req.url,\n url: req.url,\n params: (req.params ?? {}) as Record<string, string>,\n query: (req.query ?? {}) as Record<string, string | string[] | undefined>,\n body: req.body,\n headers: req.headers,\n cookies: (req.cookies ?? {}) as Record<string, string>,\n requestId: req.id,\n correlationId,\n logger,\n ip: req.ip,\n }\n if (includeRawBody && Buffer.isBuffer(req.rawBody)) {\n ctx.rawBody = req.rawBody\n }\n return ctx\n }\n\n #applyEnvelope(reply: FastifyReply, envelope: ResponseEnvelope): FastifyReply {\n reply.status(envelope.status)\n for (const [name, value] of Object.entries(this.#defaultHeaders)) {\n reply.header(name, value)\n }\n if (envelope.headers) {\n for (const [name, value] of Object.entries(envelope.headers)) {\n reply.header(name, value)\n }\n }\n if (envelope.cookies) {\n for (const cookie of envelope.cookies) {\n reply.setCookie(cookie.name, cookie.value, {\n path: cookie.path ?? '/',\n ...(cookie.domain !== undefined && { domain: cookie.domain }),\n ...(cookie.maxAge !== undefined && { maxAge: cookie.maxAge }),\n httpOnly: cookie.httpOnly ?? false,\n secure: cookie.secure ?? false,\n ...(cookie.sameSite !== undefined && { sameSite: cookie.sameSite }),\n })\n }\n }\n if (envelope.body === undefined) {\n return reply.send()\n }\n return reply.send(envelope.body)\n }\n\n #renderError(err: unknown, ctx: RequestContext): ResponseEnvelope {\n if (err instanceof HttpError) {\n ctx.logger.warn({ err: err.toJSON() }, err.message)\n return {\n status: err.status,\n body: { error: { code: err.code, message: err.message } },\n }\n }\n if (err instanceof NymbalError) {\n ctx.logger.error({ err: err.toJSON() }, err.message)\n return {\n status: 500,\n body: { error: { code: err.code, message: err.message } },\n }\n }\n const message = err instanceof Error ? err.message : String(err)\n ctx.logger.error({ err }, message)\n return {\n status: 500,\n body: { error: { code: 'internal', message: 'Internal server error' } },\n }\n }\n}\n\ndeclare module 'fastify' {\n interface FastifyRequest {\n rawBody?: Buffer\n }\n}\n","import { randomBytes, timingSafeEqual } from 'node:crypto'\nimport type { NymbalConfig } from '@nymbalhq/config'\nimport type {\n RequestContext,\n ResponseCookie,\n ResponseEnvelope,\n RouteOptions,\n} from '@nymbalhq/types'\nimport { BUILT_IN_RATE_LIMITS, RateLimiter } from './rate-limit.js'\n\nexport const CSRF_COOKIE = 'nymbal.csrf'\nexport const CSRF_HEADER = 'x-csrf-token'\nconst UNSAFE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE'])\n\nexport function buildSecurityHeaders(security: NymbalConfig['security']): Record<string, string> {\n const headers: Record<string, string> = {}\n if (security.headers.hsts) {\n headers['strict-transport-security'] = 'max-age=31536000; includeSubDomains'\n }\n const csp = security.headers.contentSecurityPolicy\n const cspValue =\n csp === 'strict'\n ? \"default-src 'self'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'\"\n : csp === 'relaxed'\n ? \"default-src 'self' https:; img-src * data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'\"\n : csp === 'off'\n ? null\n : csp\n if (cspValue) headers['content-security-policy'] = cspValue\n headers['referrer-policy'] = security.headers.referrerPolicy\n headers['x-frame-options'] = security.headers.xFrameOptions === 'deny' ? 'DENY' : 'SAMEORIGIN'\n headers['x-content-type-options'] = 'nosniff'\n return headers\n}\n\nexport interface SecurityGuard {\n /**\n * Runs before any middleware or handler: the rate limit, then CSRF on unsafe methods unless the route opted out.\n * Returns the refusal, or null when the request may proceed.\n */\n admit(ctx: RequestContext, options?: RouteOptions): ResponseEnvelope | null\n /** Runs on the handler's answer: security headers, the CSRF cookie, and the outcome of a failure-counted login. */\n finish(ctx: RequestContext, envelope: ResponseEnvelope): ResponseEnvelope\n /** `GET /api/csrf`: the request's CSRF cookie, or a new one set on the response. */\n issueToken(ctx: RequestContext): ResponseEnvelope\n reset(): void\n}\n\n/** Counts failures only, keyed by IP and by account, so an office behind one NAT is not blocked by its own logins. */\nconst LOGIN_PATH = '/api/auth/login'\n\nfunction csrfCookie(token: string): ResponseCookie {\n return {\n name: CSRF_COOKIE,\n value: token,\n path: '/',\n httpOnly: false,\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n }\n}\n\nfunction loginKeys(ctx: RequestContext): string[] {\n const email = (ctx.body as { email?: unknown } | undefined)?.email\n return typeof email === 'string' && email !== ''\n ? [ctx.ip, `account:${email.toLowerCase()}`]\n : [ctx.ip]\n}\n\nfunction isLogin(ctx: RequestContext): boolean {\n return ctx.method === 'POST' && ctx.path === LOGIN_PATH\n}\n\nexport function createSecurityGuard(security: NymbalConfig['security']): SecurityGuard {\n const rateLimiter = new RateLimiter({ ...BUILT_IN_RATE_LIMITS, ...security.rateLimit })\n const baseHeaders = buildSecurityHeaders(security)\n\n function applyHeaders(envelope: ResponseEnvelope): ResponseEnvelope {\n return {\n ...envelope,\n headers: { ...baseHeaders, ...(envelope.headers ?? {}) },\n }\n }\n\n return {\n admit(ctx, options = {}) {\n const decision = isLogin(ctx)\n ? rateLimiter.full(ctx.path, loginKeys(ctx))\n : rateLimiter.check(ctx.ip, ctx.path)\n if (decision && !decision.allowed) {\n return applyHeaders(rateLimitResponse(decision, ctx))\n }\n\n if (security.csrf && options.csrf !== false && UNSAFE_METHODS.has(ctx.method)) {\n if (!verifyCsrf(ctx)) {\n ctx.logger.warn({ method: ctx.method, path: ctx.path }, 'CSRF validation failed')\n return applyHeaders({\n status: 403,\n body: { error: { code: 'csrf.invalid', message: 'Invalid or missing CSRF token' } },\n })\n }\n }\n return null\n },\n\n finish(ctx, envelope) {\n if (isLogin(ctx)) {\n const keys = loginKeys(ctx)\n // Success clears the account only: the IP bucket expires with its window, so logging in to one's own\n // account cannot reset a spray across other accounts.\n if (envelope.status >= 200 && envelope.status < 300) {\n rateLimiter.clear(\n ctx.path,\n keys.filter((key) => key.startsWith('account:')),\n )\n } else rateLimiter.hit(ctx.path, keys)\n }\n const finished = applyHeaders(envelope)\n const setsCsrf = finished.cookies?.some((cookie) => cookie.name === CSRF_COOKIE) ?? false\n if (security.csrf && !ctx.cookies[CSRF_COOKIE] && !setsCsrf) {\n finished.cookies = [...(finished.cookies ?? []), csrfCookie(newToken())]\n }\n return finished\n },\n\n issueToken(ctx) {\n const existing = ctx.cookies[CSRF_COOKIE]\n if (existing) return { status: 200, body: { data: { token: existing } } }\n const token = newToken()\n return { status: 200, body: { data: { token } }, cookies: [csrfCookie(token)] }\n },\n\n reset() {\n rateLimiter.reset()\n },\n }\n}\n\nfunction newToken(): string {\n return randomBytes(32).toString('hex')\n}\n\nfunction verifyCsrf(ctx: RequestContext): boolean {\n const cookieToken = ctx.cookies[CSRF_COOKIE]\n const rawHeader = ctx.headers[CSRF_HEADER]\n const headerToken = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader\n if (!cookieToken || !headerToken) return false\n if (cookieToken.length !== headerToken.length) return false\n try {\n return timingSafeEqual(Buffer.from(cookieToken), Buffer.from(headerToken))\n } catch {\n return false\n }\n}\n\nfunction rateLimitResponse(\n decision: { action: string; retryAfter?: number },\n ctx: RequestContext,\n): ResponseEnvelope {\n ctx.logger.info({ ip: ctx.ip, path: ctx.path, action: decision.action }, 'Rate limit triggered')\n const status = decision.action === 'challenge' ? 403 : 429\n const headers: Record<string, string> = {}\n if (decision.retryAfter) headers['retry-after'] = String(decision.retryAfter)\n return {\n status,\n headers,\n body: {\n error: {\n code: `rate_limit.${decision.action}`,\n message: 'Too many requests',\n ...(decision.retryAfter !== undefined && { retryAfter: decision.retryAfter }),\n },\n },\n }\n}\n","import type { RateLimitAction } from '@nymbalhq/types'\n\nexport interface ParsedRule {\n pattern: string\n regex: RegExp\n requests: number\n windowMs: number\n action: RateLimitAction\n}\n\nexport interface RateLimitDecision {\n allowed: boolean\n action: RateLimitAction\n retryAfter?: number\n}\n\ntype RateLimitRules = Record<string, { requests: number; window: string; action: RateLimitAction }>\n\n/**\n * Applied under the configured rules: a configured key with the same pattern replaces its default, and the defaults\n * stay when other keys are configured.\n */\nexport const BUILT_IN_RATE_LIMITS: RateLimitRules = {\n '/api/auth/login': { requests: 10, window: '15m', action: 'block' },\n '/api/auth/register': { requests: 10, window: '1h', action: 'block' },\n}\n\ninterface Bucket {\n count: number\n resetAt: number\n}\n\n/** The map size that triggers the first sweep of expired buckets. */\nconst PRUNE_THRESHOLD = 1024\n\nexport class RateLimiter {\n readonly #rules: ParsedRule[]\n readonly #buckets = new Map<string, Bucket>()\n #pruneAt = PRUNE_THRESHOLD\n\n constructor(rules: RateLimitRules) {\n this.#rules = Object.entries(rules).map(([pattern, rule]) => ({\n pattern,\n regex: compilePathPattern(pattern),\n requests: rule.requests,\n windowMs: parseWindow(rule.window),\n action: rule.action,\n }))\n }\n\n check(ip: string, path: string): RateLimitDecision | null {\n const rule = this.#ruleFor(path)\n if (!rule) return null\n const key = `${rule.pattern}|${ip}`\n const now = Date.now()\n const bucket = this.#buckets.get(key)\n if (!bucket || bucket.resetAt <= now) {\n this.#open(key, now, rule.windowMs)\n return { allowed: true, action: rule.action }\n }\n bucket.count += 1\n if (bucket.count > rule.requests) {\n return { allowed: false, action: rule.action, retryAfter: retryAfter(bucket, now) }\n }\n return { allowed: true, action: rule.action }\n }\n\n /**\n * For a route that counts failures only: refuses when any of `keys` (an IP, an account) already holds the rule's\n * full count, without counting this request. `hit` and `clear` record the outcome once the handler has answered.\n */\n full(path: string, keys: string[]): RateLimitDecision | null {\n const rule = this.#ruleFor(path)\n if (!rule) return null\n const now = Date.now()\n for (const key of keys) {\n const bucket = this.#buckets.get(`${rule.pattern}|${key}`)\n if (bucket && bucket.resetAt > now && bucket.count >= rule.requests) {\n return { allowed: false, action: rule.action, retryAfter: retryAfter(bucket, now) }\n }\n }\n return { allowed: true, action: rule.action }\n }\n\n hit(path: string, keys: string[]): void {\n const rule = this.#ruleFor(path)\n if (!rule) return\n const now = Date.now()\n for (const key of keys) {\n const bucketKey = `${rule.pattern}|${key}`\n const bucket = this.#buckets.get(bucketKey)\n if (!bucket || bucket.resetAt <= now) {\n this.#open(bucketKey, now, rule.windowMs)\n } else {\n bucket.count += 1\n }\n }\n }\n\n clear(path: string, keys: string[]): void {\n const rule = this.#ruleFor(path)\n if (!rule) return\n for (const key of keys) this.#buckets.delete(`${rule.pattern}|${key}`)\n }\n\n reset(): void {\n this.#buckets.clear()\n this.#pruneAt = PRUNE_THRESHOLD\n }\n\n /**\n * Starts a bucket. Keys include caller-chosen values (an account email), so expired buckets are swept once the map\n * reaches `#pruneAt`; the next sweep waits until the map doubles past what survived, which keeps the cost amortised\n * constant per insert even when every bucket is live.\n */\n #open(key: string, now: number, windowMs: number): void {\n if (this.#buckets.size >= this.#pruneAt) {\n for (const [k, b] of this.#buckets) if (b.resetAt <= now) this.#buckets.delete(k)\n this.#pruneAt = Math.max(PRUNE_THRESHOLD, this.#buckets.size * 2)\n }\n this.#buckets.set(key, { count: 1, resetAt: now + windowMs })\n }\n\n #ruleFor(path: string): ParsedRule | undefined {\n return this.#rules.find((r) => r.regex.test(path))\n }\n}\n\nfunction retryAfter(bucket: Bucket, now: number): number {\n return Math.max(1, Math.ceil((bucket.resetAt - now) / 1000))\n}\n\nfunction parseWindow(window: string): number {\n const match = /^(\\d+)(ms|s|m|h)$/.exec(window)\n if (!match) throw new Error(`Invalid rate-limit window: ${window}`)\n const n = Number(match[1])\n switch (match[2]) {\n case 'ms':\n return n\n case 's':\n return n * 1000\n case 'm':\n return n * 60_000\n case 'h':\n return n * 3_600_000\n default:\n throw new Error(`Unreachable unit: ${match[2]}`)\n }\n}\n\nfunction compilePathPattern(pattern: string): RegExp {\n const escaped = pattern\n .replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&')\n .replace(/\\*\\*/g, '__DOUBLESTAR__')\n .replace(/\\*/g, '[^/]+')\n .replace(/__DOUBLESTAR__/g, '.*')\n return new RegExp(`^${escaped}$`)\n}\n","import type { RouteHandler } from '@nymbalhq/types'\n\nexport function createHealthRoute(version: string): RouteHandler {\n return () => ({\n status: 200,\n body: { status: 'ok', version, timestamp: new Date().toISOString() },\n })\n}\n","import type { DocumentStoreAdapter, RouteHandler } from '@nymbalhq/types'\n\nexport interface ProductListItem {\n id: string\n slug: string\n title: string\n description: string\n priceMinor: number\n currency: string\n categoryId: string\n imageUrl: string\n}\n\nexport function createProductsRoute(\n documentStore: DocumentStoreAdapter,\n storeId: string,\n): RouteHandler {\n return async (ctx) => {\n const rawLimit = ctx.query.limit\n const limitValue = Array.isArray(rawLimit) ? rawLimit[0] : rawLimit\n const limit = limitValue ? Math.min(100, Math.max(1, Number(limitValue) || 20)) : 50\n const rawCursor = ctx.query.cursor\n const cursorValue = Array.isArray(rawCursor) ? rawCursor[0] : rawCursor\n\n const result = await documentStore.query<ProductListItem>('products', {\n partitionKey: { field: 'storeId', value: storeId },\n limit,\n ...(cursorValue !== undefined && { cursor: cursorValue }),\n ascending: true,\n })\n return {\n status: 200,\n body: { items: result.items, nextCursor: result.nextCursor },\n }\n }\n}\n","import type { NymbalConfig } from '@nymbalhq/config'\nimport type {\n DocumentStoreAdapter,\n HttpAdapter,\n HttpMethod,\n Logger,\n MiddlewareHandler,\n RouteHandler,\n RouteOptions,\n} from '@nymbalhq/types'\nimport { FastifyHttpAdapter } from './fastify-adapter.js'\nimport { createHealthRoute } from './routes/health.js'\nimport { createProductsRoute } from './routes/products.js'\n\nexport interface HttpServerDeps {\n config: NymbalConfig\n logger: Logger\n documentStore: DocumentStoreAdapter\n version: string\n}\n\nexport interface HttpServer {\n adapter: HttpAdapter\n registerRoute(\n method: HttpMethod,\n path: string,\n handler: RouteHandler,\n options?: RouteOptions,\n ): void\n registerMiddleware(middleware: MiddlewareHandler): void\n start(): Promise<{ port: number; host: string }>\n stop(): Promise<void>\n}\n\nexport function createHttpServer(deps: HttpServerDeps): HttpServer {\n const { config, logger, documentStore, version } = deps\n if (config.http.adapter !== 'fastify') {\n throw new Error(`Unsupported http.adapter: ${config.http.adapter}`)\n }\n const adapter = new FastifyHttpAdapter({\n logger,\n cors: config.http.cors,\n security: config.security,\n trustProxy: config.http.trustProxy,\n })\n\n const server: HttpServer = {\n adapter,\n registerRoute(method, path, handler, options) {\n adapter.registerRoute(method, path, handler, options)\n },\n registerMiddleware(middleware) {\n adapter.registerMiddleware(middleware)\n },\n async start() {\n await adapter.start(config.http.port, config.http.host)\n return { port: adapter.boundPort, host: config.http.host }\n },\n async stop() {\n await adapter.stop()\n },\n }\n\n // Default routes — available without any user route registration.\n server.registerRoute('GET', '/health', createHealthRoute(version))\n // The commerce routes (including `/api/products`) are registered by\n // `platform.attachHttp(...)` — we intentionally do not pre-register them\n // here to avoid FST_ERR_DUPLICATED_ROUTE conflicts when the kernel is wired.\n void createProductsRoute\n void documentStore\n void config\n\n return server\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,47 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nymbalhq/http",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Framework-agnostic HTTP abstraction with Fastify adapter",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/nymbalhq/nymbal.git",
|
|
9
|
+
"directory": "packages/http"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=22"
|
|
17
|
+
},
|
|
18
|
+
"main": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@fastify/cookie": "^9.4.0",
|
|
31
|
+
"@fastify/cors": "^8.5.0",
|
|
32
|
+
"fastify": "^4.28.1",
|
|
33
|
+
"uuid": "^10.0.0",
|
|
34
|
+
"@nymbalhq/config": "0.2.0",
|
|
35
|
+
"@nymbalhq/types": "0.2.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/uuid": "^10.0.0",
|
|
39
|
+
"tsup": "^8.3.5",
|
|
40
|
+
"typescript": "^5.6.3"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsup",
|
|
44
|
+
"dev": "tsup --watch",
|
|
45
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
46
|
+
}
|
|
6
47
|
}
|
package/README.md
DELETED