cfw-graphql-bootstrap 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1052 @@
1
+ // src/logger/index.ts
2
+ import pino from "pino/browser";
3
+ var isDev = process.env.NODE_ENV !== "production";
4
+ var logLevel = process.env.LOG_LEVEL || (isDev ? "debug" : "info");
5
+ var serializers = {
6
+ err: (err) => {
7
+ if (!err) return err;
8
+ return {
9
+ type: err.constructor?.name || err.name,
10
+ message: err.message,
11
+ stack: err.stack,
12
+ code: err.code,
13
+ statusCode: err.statusCode
14
+ };
15
+ },
16
+ req: (req) => {
17
+ if (!req) return req;
18
+ return {
19
+ method: req.method,
20
+ url: req.url,
21
+ headers: Object.fromEntries(req.headers.entries())
22
+ };
23
+ },
24
+ res: (res) => {
25
+ if (!res) return res;
26
+ return {
27
+ statusCode: res.status,
28
+ headers: Object.fromEntries(res.headers.entries())
29
+ };
30
+ }
31
+ };
32
+ var logger = pino({
33
+ browser: {
34
+ asObject: true,
35
+ write: {
36
+ info: (o) => {
37
+ if (!isDev && globalThis.logshipper) {
38
+ globalThis.logshipper.log(o);
39
+ }
40
+ console.log(JSON.stringify(o));
41
+ },
42
+ error: (o) => {
43
+ if (!isDev && globalThis.logshipper) {
44
+ globalThis.logshipper.log(o);
45
+ }
46
+ console.error(JSON.stringify(o));
47
+ },
48
+ debug: (o) => {
49
+ if (isDev) {
50
+ console.debug(JSON.stringify(o));
51
+ }
52
+ },
53
+ warn: (o) => {
54
+ if (!isDev && globalThis.logshipper) {
55
+ globalThis.logshipper.log(o);
56
+ }
57
+ console.warn(JSON.stringify(o));
58
+ },
59
+ fatal: (o) => {
60
+ if (!isDev && globalThis.logshipper) {
61
+ globalThis.logshipper.log(o);
62
+ }
63
+ console.error(JSON.stringify(o));
64
+ },
65
+ trace: (o) => {
66
+ if (isDev) {
67
+ console.trace(JSON.stringify(o));
68
+ }
69
+ }
70
+ }
71
+ },
72
+ level: logLevel,
73
+ base: {
74
+ env: process.env.NODE_ENV,
75
+ service: "graphql-subgraph",
76
+ runtime: "cloudflare-workers"
77
+ },
78
+ serializers,
79
+ timestamp: () => `,"timestamp":"${(/* @__PURE__ */ new Date()).toISOString()}"`,
80
+ formatters: {
81
+ level: (label) => {
82
+ return { level: label.toUpperCase() };
83
+ },
84
+ log: (object) => {
85
+ if (globalThis.requestContext) {
86
+ object.traceId = globalThis.requestContext.traceId;
87
+ object.spanId = globalThis.requestContext.spanId;
88
+ object.userId = globalThis.requestContext.userId;
89
+ }
90
+ return object;
91
+ }
92
+ }
93
+ });
94
+ function createLogger(context) {
95
+ return logger.child(context);
96
+ }
97
+
98
+ // src/context/constants.ts
99
+ var Headers = {
100
+ GATEWAY_USER: "x-gateway-user",
101
+ GATEWAY_USER_SIGNATURE: "x-gateway-user-signature",
102
+ AUTH: "authorization",
103
+ CLIENT_ID: "x-client-id"
104
+ };
105
+
106
+ // src/context/auth.ts
107
+ async function verifyAndParseUser(encodedUser, signature, secret) {
108
+ if (!encodedUser || !signature) {
109
+ return void 0;
110
+ }
111
+ try {
112
+ const encoder = new TextEncoder();
113
+ const key = await crypto.subtle.importKey(
114
+ "raw",
115
+ encoder.encode(secret),
116
+ { name: "HMAC", hash: "SHA-256" },
117
+ false,
118
+ ["verify"]
119
+ );
120
+ const signatureBuffer = hexToBuffer(signature);
121
+ const dataBuffer = encoder.encode(encodedUser);
122
+ const isValid = await crypto.subtle.verify(
123
+ "HMAC",
124
+ key,
125
+ signatureBuffer,
126
+ dataBuffer
127
+ );
128
+ if (!isValid) {
129
+ console.error("Invalid user signature");
130
+ return void 0;
131
+ }
132
+ const user = JSON.parse(atob(encodedUser));
133
+ return user;
134
+ } catch (error) {
135
+ console.error("Error verifying user:", error);
136
+ return void 0;
137
+ }
138
+ }
139
+ function hexToBuffer(hex) {
140
+ const bytes = new Uint8Array(hex.length / 2);
141
+ for (let i = 0; i < hex.length; i += 2) {
142
+ bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
143
+ }
144
+ return bytes.buffer;
145
+ }
146
+
147
+ // src/cache/kv-cache.ts
148
+ var KVCache = class {
149
+ constructor(kv) {
150
+ this.kv = kv;
151
+ }
152
+ /**
153
+ * Get a value from cache
154
+ */
155
+ async get(key) {
156
+ try {
157
+ const value = await this.kv.get(key);
158
+ if (!value) return null;
159
+ try {
160
+ return JSON.parse(value);
161
+ } catch {
162
+ return value;
163
+ }
164
+ } catch (error) {
165
+ console.error(`Cache get error for key ${key}:`, error);
166
+ return null;
167
+ }
168
+ }
169
+ /**
170
+ * Set a value in cache
171
+ */
172
+ async set(key, value, options = {}) {
173
+ try {
174
+ const serialized = typeof value === "string" ? value : JSON.stringify(value);
175
+ const kvOptions = {};
176
+ if (options.ttl) {
177
+ kvOptions.expirationTtl = options.ttl;
178
+ }
179
+ await this.kv.put(key, serialized, kvOptions);
180
+ } catch (error) {
181
+ console.error(`Cache set error for key ${key}:`, error);
182
+ }
183
+ }
184
+ /**
185
+ * Delete a value from cache
186
+ */
187
+ async delete(key) {
188
+ try {
189
+ await this.kv.delete(key);
190
+ } catch (error) {
191
+ console.error(`Cache delete error for key ${key}:`, error);
192
+ }
193
+ }
194
+ /**
195
+ * Check if a key exists in cache
196
+ */
197
+ async has(key) {
198
+ try {
199
+ const value = await this.kv.get(key);
200
+ return value !== null;
201
+ } catch (error) {
202
+ console.error(`Cache has error for key ${key}:`, error);
203
+ return false;
204
+ }
205
+ }
206
+ /**
207
+ * Clear multiple keys matching a prefix
208
+ */
209
+ async clearPrefix(prefix) {
210
+ try {
211
+ const list = await this.kv.list({ prefix });
212
+ const promises = list.keys.map((key) => this.kv.delete(key.name));
213
+ await Promise.all(promises);
214
+ } catch (error) {
215
+ console.error(`Cache clear prefix error for ${prefix}:`, error);
216
+ }
217
+ }
218
+ /**
219
+ * Get or set a value with a factory function
220
+ * Useful for cache-aside pattern
221
+ */
222
+ async getOrSet(key, factory, options = {}) {
223
+ const cached2 = await this.get(key);
224
+ if (cached2 !== null) {
225
+ return cached2;
226
+ }
227
+ const fresh = await factory();
228
+ await this.set(key, fresh, options);
229
+ return fresh;
230
+ }
231
+ };
232
+ function createCacheKey(...parts) {
233
+ return parts.filter((part) => part !== void 0).map((part) => String(part)).join(":");
234
+ }
235
+
236
+ // src/cache/redis-cache.ts
237
+ var RedisCache = class {
238
+ // Use Bun.redis or @upstash/redis
239
+ constructor(config = {}) {
240
+ if (config.url && config.token) {
241
+ this.initUpstash({ url: config.url, token: config.token });
242
+ } else if (typeof globalThis !== "undefined" && globalThis.Bun) {
243
+ this.redis = globalThis.Bun.redis;
244
+ } else {
245
+ this.redis = this.createMockRedis();
246
+ }
247
+ }
248
+ async initUpstash(config) {
249
+ this.redis = this.createMockRedis();
250
+ }
251
+ createMockRedis() {
252
+ const store = /* @__PURE__ */ new Map();
253
+ return {
254
+ get: async (key) => store.get(key),
255
+ set: async (key, value) => {
256
+ store.set(key, value);
257
+ return "OK";
258
+ },
259
+ setex: async (key, ttl, value) => {
260
+ store.set(key, value);
261
+ return "OK";
262
+ },
263
+ del: async (...keys) => {
264
+ keys.forEach((k) => store.delete(k));
265
+ return keys.length;
266
+ },
267
+ exists: async (key) => store.has(key) ? 1 : 0,
268
+ keys: async (pattern) => {
269
+ const regex = new RegExp(pattern.replace("*", ".*"));
270
+ return Array.from(store.keys()).filter((k) => regex.test(k));
271
+ },
272
+ incrby: async (key, by) => {
273
+ const val = store.get(key) || 0;
274
+ const newVal = val + by;
275
+ store.set(key, newVal);
276
+ return newVal;
277
+ },
278
+ expire: async (key, seconds) => true,
279
+ ttl: async (key) => -1
280
+ };
281
+ }
282
+ async get(key) {
283
+ try {
284
+ const value = await this.redis.get(key);
285
+ if (!value) return null;
286
+ return value;
287
+ } catch (error) {
288
+ console.error(`Redis get error for key ${key}:`, error);
289
+ return null;
290
+ }
291
+ }
292
+ async set(key, value, options = {}) {
293
+ try {
294
+ if (options.ttl) {
295
+ await this.redis.setex(key, options.ttl, value);
296
+ } else {
297
+ await this.redis.set(key, value);
298
+ }
299
+ } catch (error) {
300
+ console.error(`Redis set error for key ${key}:`, error);
301
+ }
302
+ }
303
+ async delete(key) {
304
+ try {
305
+ await this.redis.del(key);
306
+ } catch (error) {
307
+ console.error(`Redis delete error for key ${key}:`, error);
308
+ }
309
+ }
310
+ async has(key) {
311
+ try {
312
+ const exists = await this.redis.exists(key);
313
+ return exists === 1;
314
+ } catch (error) {
315
+ console.error(`Redis has error for key ${key}:`, error);
316
+ return false;
317
+ }
318
+ }
319
+ async clearPrefix(prefix) {
320
+ try {
321
+ const keys = await this.redis.keys(`${prefix}*`);
322
+ if (keys.length > 0) {
323
+ await this.redis.del(...keys);
324
+ }
325
+ } catch (error) {
326
+ console.error(`Redis clear prefix error for ${prefix}:`, error);
327
+ }
328
+ }
329
+ async getOrSet(key, factory, options = {}) {
330
+ const cached2 = await this.get(key);
331
+ if (cached2 !== null) {
332
+ return cached2;
333
+ }
334
+ const fresh = await factory();
335
+ await this.set(key, fresh, options);
336
+ return fresh;
337
+ }
338
+ // Additional Redis-specific methods
339
+ async increment(key, by = 1) {
340
+ return this.redis.incrby(key, by);
341
+ }
342
+ async expire(key, seconds) {
343
+ return this.redis.expire(key, seconds);
344
+ }
345
+ async ttl(key) {
346
+ return this.redis.ttl(key);
347
+ }
348
+ };
349
+
350
+ // src/cache/index.ts
351
+ function createCache(env) {
352
+ if (env.CACHE_KV) {
353
+ return new KVCache(env.CACHE_KV);
354
+ }
355
+ if (env.REDIS_URL && env.REDIS_TOKEN) {
356
+ return new RedisCache({
357
+ url: env.REDIS_URL,
358
+ token: env.REDIS_TOKEN
359
+ });
360
+ }
361
+ return null;
362
+ }
363
+ function cached(options = {}) {
364
+ return function(target, propertyKey, descriptor) {
365
+ const originalMethod = descriptor.value;
366
+ descriptor.value = async function(...args) {
367
+ const context = args[2];
368
+ const cache = context?.cache;
369
+ if (!cache) {
370
+ return originalMethod.apply(this, args);
371
+ }
372
+ const key = options.keyGenerator ? options.keyGenerator(...args) : createCacheKey(propertyKey, JSON.stringify(args[1]));
373
+ return cache.getOrSet(
374
+ key,
375
+ () => originalMethod.apply(this, args),
376
+ options
377
+ );
378
+ };
379
+ return descriptor;
380
+ };
381
+ }
382
+ var CachedDataLoader = class {
383
+ constructor(batchFn, cache, options = {}) {
384
+ this.options = options;
385
+ this.cache = cache;
386
+ }
387
+ async load(key) {
388
+ const cacheKey = createCacheKey("loader", String(key));
389
+ return this.cache.getOrSet(
390
+ cacheKey,
391
+ async () => {
392
+ return this.loader ? this.loader.load(key) : null;
393
+ },
394
+ this.options
395
+ );
396
+ }
397
+ async loadMany(keys) {
398
+ return Promise.all(keys.map((key) => this.load(key)));
399
+ }
400
+ async clear(key) {
401
+ const cacheKey = createCacheKey("loader", String(key));
402
+ await this.cache.delete(cacheKey);
403
+ }
404
+ async clearAll() {
405
+ await this.cache.clearPrefix("loader:");
406
+ }
407
+ };
408
+
409
+ // src/env/config.ts
410
+ var EnvParser = class {
411
+ constructor(env) {
412
+ this.env = env;
413
+ }
414
+ string(key, defaultValue) {
415
+ const value = this.env[key];
416
+ return value !== void 0 && value !== "" ? String(value) : defaultValue;
417
+ }
418
+ int(key, defaultValue) {
419
+ const value = this.env[key];
420
+ if (value === void 0 || value === "") return defaultValue;
421
+ const parsed = parseInt(String(value), 10);
422
+ return isNaN(parsed) ? defaultValue : parsed;
423
+ }
424
+ bool(key, defaultValue) {
425
+ const value = this.env[key];
426
+ if (value === void 0 || value === "") return defaultValue;
427
+ const str = String(value).toLowerCase();
428
+ return str === "true" || str === "1";
429
+ }
430
+ array(key, defaultValue) {
431
+ const value = this.env[key];
432
+ if (value === void 0 || value === "") return defaultValue;
433
+ if (Array.isArray(value)) return value;
434
+ return String(value).split(",").map((s) => s.trim()).filter(Boolean);
435
+ }
436
+ enum(key, validValues, defaultValue) {
437
+ const value = this.string(key);
438
+ if (value && validValues.includes(value)) {
439
+ return value;
440
+ }
441
+ return defaultValue;
442
+ }
443
+ };
444
+ var EnvironmentLoader = class {
445
+ constructor() {
446
+ this.config = null;
447
+ }
448
+ /**
449
+ * Load environment from Cloudflare Workers env object
450
+ */
451
+ load(env) {
452
+ const parser = new EnvParser(env);
453
+ this.config = {
454
+ NODE_ENV: parser.enum(
455
+ "NODE_ENV",
456
+ ["development", "test", "production"],
457
+ "production"
458
+ ),
459
+ PORT: parser.int("PORT", 3344),
460
+ GATEWAY_SECRET: parser.string("GATEWAY_SECRET"),
461
+ ALLOWED_ORIGINS: parser.array("ALLOWED_ORIGINS", [
462
+ "http://localhost:3000"
463
+ ]),
464
+ LOG_LEVEL: parser.enum(
465
+ "LOG_LEVEL",
466
+ ["trace", "debug", "info", "warn", "error", "fatal"],
467
+ "info"
468
+ ),
469
+ RATE_LIMIT_WINDOW_MS: parser.int("RATE_LIMIT_WINDOW_MS", 6e4),
470
+ RATE_LIMIT_MAX: parser.int("RATE_LIMIT_MAX", 100),
471
+ ENABLE_METRICS: parser.bool("ENABLE_METRICS", true),
472
+ ENABLE_TRACING: parser.bool("ENABLE_TRACING", true),
473
+ MAX_QUERY_DEPTH: parser.int("MAX_QUERY_DEPTH", 10),
474
+ MAX_QUERY_COMPLEXITY: parser.int("MAX_QUERY_COMPLEXITY", 1e3),
475
+ INTROSPECTION_ENABLED: parser.bool("INTROSPECTION_ENABLED", false),
476
+ REDIS_URL: parser.string("REDIS_URL"),
477
+ REDIS_TOKEN: parser.string("REDIS_TOKEN"),
478
+ LOG_SERVICE_URL: parser.string("LOG_SERVICE_URL"),
479
+ LOG_SERVICE_TOKEN: parser.string("LOG_SERVICE_TOKEN")
480
+ };
481
+ return this.config;
482
+ }
483
+ /**
484
+ * Get current configuration
485
+ */
486
+ getConfig() {
487
+ if (!this.config) {
488
+ throw new Error("Environment not loaded. Call load() first.");
489
+ }
490
+ return this.config;
491
+ }
492
+ /**
493
+ * Helper getters for common checks
494
+ */
495
+ get isDev() {
496
+ return this.config?.NODE_ENV === "development";
497
+ }
498
+ get isTest() {
499
+ return this.config?.NODE_ENV === "test";
500
+ }
501
+ get isProd() {
502
+ return this.config?.NODE_ENV === "production";
503
+ }
504
+ };
505
+ var envLoader = new EnvironmentLoader();
506
+
507
+ // src/context/index.ts
508
+ async function createBaseContext(honoContext) {
509
+ const request = honoContext.request;
510
+ const rawEnv = honoContext.env;
511
+ const env = envLoader.load(rawEnv);
512
+ let user;
513
+ const encodedUser = request.headers.get(Headers.GATEWAY_USER);
514
+ if (encodedUser) {
515
+ if (env.GATEWAY_SECRET) {
516
+ const signature = request.headers.get(Headers.GATEWAY_USER_SIGNATURE);
517
+ user = await verifyAndParseUser(
518
+ encodedUser,
519
+ signature,
520
+ env.GATEWAY_SECRET
521
+ );
522
+ } else if (env.NODE_ENV !== "production") {
523
+ try {
524
+ user = JSON.parse(atob(encodedUser));
525
+ } catch (e) {
526
+ console.error("Failed to parse user in dev mode:", e);
527
+ }
528
+ }
529
+ }
530
+ const cache = createCache(rawEnv);
531
+ if (cache) {
532
+ logger.info("KV cache initialized for GraphQL resolvers");
533
+ }
534
+ return {
535
+ user,
536
+ logger,
537
+ cache,
538
+ env,
539
+ // Pass validated env config
540
+ executionCtx: honoContext.ctx,
541
+ authorization: request.headers.get(Headers.AUTH) || "",
542
+ clientId: request.headers.get(Headers.CLIENT_ID) || ""
543
+ };
544
+ }
545
+ function createContextFunction(extendContext) {
546
+ if (!extendContext) {
547
+ return (honoContext) => createBaseContext(honoContext);
548
+ }
549
+ return async (honoContext) => {
550
+ const baseContext = await createBaseContext(honoContext);
551
+ return extendContext(baseContext, honoContext);
552
+ };
553
+ }
554
+
555
+ // src/utils/builder.util.ts
556
+ function assertDefined(t, msg) {
557
+ if (t === void 0) {
558
+ throw new Error("Assertion failed when configuring server: " + msg);
559
+ }
560
+ return true;
561
+ }
562
+
563
+ // src/server/server.builder.ts
564
+ import { ApolloServer } from "@apollo/server";
565
+ import { startServerAndCreateCloudflareWorkersHandler } from "@as-integrations/cloudflare-workers";
566
+
567
+ // src/server/hono.configure.ts
568
+ import { Hono } from "hono";
569
+ import { cors } from "hono/cors";
570
+ import { compress } from "hono/compress";
571
+
572
+ // src/middleware/validation.ts
573
+ function createValidationMiddleware(options = {}) {
574
+ return async (c, next) => {
575
+ try {
576
+ const env = c.get("env");
577
+ const maxQueryDepth = options.maxQueryDepth || env?.MAX_QUERY_DEPTH || 10;
578
+ const maxQueryComplexity = options.maxQueryComplexity || env?.MAX_QUERY_COMPLEXITY || 1e3;
579
+ const maxAliasCount = options.maxAliasCount || 15;
580
+ const contentType = c.req.header("content-type");
581
+ if (c.req.method === "POST" && !contentType?.includes("application/json")) {
582
+ return c.json(
583
+ {
584
+ errors: [
585
+ {
586
+ message: "Content-Type must be application/json",
587
+ extensions: { code: "BAD_REQUEST" }
588
+ }
589
+ ]
590
+ },
591
+ 400
592
+ );
593
+ }
594
+ const clientId = c.req.header("x-client-id");
595
+ if (!clientId && env?.NODE_ENV === "production") {
596
+ return c.json(
597
+ {
598
+ errors: [
599
+ {
600
+ message: "Client identification required",
601
+ extensions: { code: "BAD_REQUEST" }
602
+ }
603
+ ]
604
+ },
605
+ 400
606
+ );
607
+ }
608
+ await next();
609
+ } catch (error) {
610
+ console.error("Validation middleware error:", error);
611
+ return c.json(
612
+ {
613
+ errors: [
614
+ {
615
+ message: "Request validation failed",
616
+ extensions: { code: "INTERNAL_SERVER_ERROR" }
617
+ }
618
+ ]
619
+ },
620
+ 500
621
+ );
622
+ }
623
+ };
624
+ }
625
+
626
+ // src/middleware/rate-limit.ts
627
+ function createRateLimitMiddleware(options = {}) {
628
+ const {
629
+ keyGenerator = (c) => c.req.header("x-client-id") || c.req.header("cf-connecting-ip") || "anonymous"
630
+ } = options;
631
+ return async (c, next) => {
632
+ const env = c.get("env");
633
+ const windowMs = options.windowMs || env?.RATE_LIMIT_WINDOW_MS || 60 * 1e3;
634
+ const max = options.max || env?.RATE_LIMIT_MAX || 100;
635
+ const key = keyGenerator(c);
636
+ const rateLimitKey = `rate_limit:${key}`;
637
+ if (c.env?.RATE_LIMIT_KV) {
638
+ const current = await c.env.RATE_LIMIT_KV.get(rateLimitKey);
639
+ const count = current ? parseInt(current, 10) : 0;
640
+ if (count >= max) {
641
+ return c.json(
642
+ {
643
+ errors: [
644
+ {
645
+ message: "Too many requests",
646
+ extensions: {
647
+ code: "RATE_LIMITED",
648
+ retryAfter: windowMs / 1e3
649
+ }
650
+ }
651
+ ]
652
+ },
653
+ 429,
654
+ {
655
+ "Retry-After": String(windowMs / 1e3),
656
+ "X-RateLimit-Limit": String(max),
657
+ "X-RateLimit-Remaining": "0",
658
+ "X-RateLimit-Reset": String(Date.now() + windowMs)
659
+ }
660
+ );
661
+ }
662
+ await c.env.RATE_LIMIT_KV.put(rateLimitKey, String(count + 1), {
663
+ expirationTtl: windowMs / 1e3
664
+ });
665
+ c.header("X-RateLimit-Limit", String(max));
666
+ c.header("X-RateLimit-Remaining", String(max - count - 1));
667
+ c.header("X-RateLimit-Reset", String(Date.now() + windowMs));
668
+ }
669
+ await next();
670
+ };
671
+ }
672
+
673
+ // src/middleware/tracing.ts
674
+ function createTracingMiddleware() {
675
+ return async (c, next) => {
676
+ const env = c.get("env");
677
+ const traceId = c.req.header("x-trace-id") || generateTraceId();
678
+ const spanId = generateSpanId();
679
+ const parentSpanId = c.req.header("x-parent-span-id");
680
+ const userId = c.get("userId");
681
+ globalThis.requestContext = {
682
+ traceId,
683
+ spanId,
684
+ userId
685
+ };
686
+ c.set("traceId", traceId);
687
+ c.set("spanId", spanId);
688
+ c.header("x-trace-id", traceId);
689
+ c.header("x-span-id", spanId);
690
+ const startTime = Date.now();
691
+ try {
692
+ await next();
693
+ } finally {
694
+ const duration = Date.now() - startTime;
695
+ logger.info(
696
+ {
697
+ type: "request",
698
+ traceId,
699
+ spanId,
700
+ parentSpanId,
701
+ method: c.req.method,
702
+ path: c.req.path,
703
+ status: c.res.status,
704
+ duration,
705
+ userAgent: c.req.header("user-agent"),
706
+ ip: c.req.header("cf-connecting-ip") || c.req.header("x-forwarded-for")
707
+ },
708
+ `${c.req.method} ${c.req.path} - ${c.res.status} (${duration}ms)`
709
+ );
710
+ c.header("Server-Timing", `total;dur=${duration}`);
711
+ delete globalThis.requestContext;
712
+ }
713
+ };
714
+ }
715
+ function generateTraceId() {
716
+ return Array.from(crypto.getRandomValues(new Uint8Array(16))).map((b) => b.toString(16).padStart(2, "0")).join("");
717
+ }
718
+ function generateSpanId() {
719
+ return Array.from(crypto.getRandomValues(new Uint8Array(8))).map((b) => b.toString(16).padStart(2, "0")).join("");
720
+ }
721
+
722
+ // src/middleware/health.ts
723
+ function configureHealthChecks(app, options = {}) {
724
+ app.get("/health", (c) => {
725
+ return c.json({
726
+ status: "ok",
727
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
728
+ uptime: process.uptime ? process.uptime() : 0
729
+ });
730
+ });
731
+ app.get("/ready", async (c) => {
732
+ const checks = options.checks || {};
733
+ const results = {};
734
+ let allHealthy = true;
735
+ for (const [name, check] of Object.entries(checks)) {
736
+ try {
737
+ results[name] = await check();
738
+ if (!results[name]) allHealthy = false;
739
+ } catch (error) {
740
+ results[name] = false;
741
+ allHealthy = false;
742
+ }
743
+ }
744
+ const status = allHealthy ? 200 : 503;
745
+ return c.json(
746
+ {
747
+ status: allHealthy ? "ready" : "not_ready",
748
+ checks: results,
749
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
750
+ },
751
+ status
752
+ );
753
+ });
754
+ app.get("/metrics", (c) => {
755
+ return c.text(`# HELP graphql_requests_total Total number of GraphQL requests
756
+ # TYPE graphql_requests_total counter
757
+ graphql_requests_total 0
758
+
759
+ # HELP graphql_errors_total Total number of GraphQL errors
760
+ # TYPE graphql_errors_total counter
761
+ graphql_errors_total 0
762
+
763
+ # HELP graphql_duration_seconds GraphQL request duration
764
+ # TYPE graphql_duration_seconds histogram
765
+ graphql_duration_seconds_bucket{le="0.1"} 0
766
+ graphql_duration_seconds_bucket{le="0.5"} 0
767
+ graphql_duration_seconds_bucket{le="1"} 0
768
+ graphql_duration_seconds_bucket{le="+Inf"} 0
769
+ graphql_duration_seconds_sum 0
770
+ graphql_duration_seconds_count 0`);
771
+ });
772
+ }
773
+
774
+ // src/server/hono.configure.ts
775
+ function configureHono(builder) {
776
+ const app = new Hono();
777
+ app.use("*", async (c, next) => {
778
+ const rawEnv = c.env || {};
779
+ const env = typeof rawEnv.NODE_ENV !== "undefined" ? rawEnv : {};
780
+ c.set("env", env);
781
+ await next();
782
+ });
783
+ app.use("*", createTracingMiddleware());
784
+ app.use("*", compress());
785
+ app.use("*", createValidationMiddleware());
786
+ app.use("*", createRateLimitMiddleware());
787
+ configureHealthChecks(app, {
788
+ checks: {
789
+ graphql: async () => true
790
+ // Check GraphQL schema is loaded
791
+ }
792
+ });
793
+ app.use(
794
+ "/*",
795
+ cors({
796
+ origin: (origin, c) => {
797
+ const env = c.env;
798
+ const isProd = env.NODE_ENV === "production";
799
+ const allowedOrigins = env.ALLOWED_ORIGINS ? typeof env.ALLOWED_ORIGINS === "string" ? env.ALLOWED_ORIGINS.split(",").map((s) => s.trim()) : env.ALLOWED_ORIGINS : ["http://localhost:3000"];
800
+ if (!isProd) {
801
+ return origin || "*";
802
+ }
803
+ return allowedOrigins.includes(origin) ? origin : allowedOrigins[0];
804
+ },
805
+ allowHeaders: [
806
+ "Content-Type",
807
+ "Authorization",
808
+ "x-apollo-operation-name",
809
+ "apollo-require-preflight"
810
+ ],
811
+ allowMethods: ["POST", "GET", "OPTIONS"],
812
+ credentials: true,
813
+ maxAge: 86400
814
+ })
815
+ );
816
+ app.use("*", async (c, next) => {
817
+ await next();
818
+ const env = c.get("env");
819
+ const isProd = env?.NODE_ENV === "production";
820
+ c.header("X-Content-Type-Options", "nosniff");
821
+ c.header("X-Frame-Options", "DENY");
822
+ c.header("X-XSS-Protection", "1; mode=block");
823
+ c.header("Referrer-Policy", "strict-origin-when-cross-origin");
824
+ c.header("Server", "GraphQL");
825
+ if (isProd) {
826
+ c.header(
827
+ "Content-Security-Policy",
828
+ "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';"
829
+ );
830
+ c.header(
831
+ "Strict-Transport-Security",
832
+ "max-age=31536000; includeSubDomains; preload"
833
+ );
834
+ }
835
+ });
836
+ return app;
837
+ }
838
+
839
+ // src/utils/graphql.util.ts
840
+ import { ApolloServerErrorCode } from "@apollo/server/errors";
841
+ var BAD_USER_INPUT_CODE = ApolloServerErrorCode.BAD_USER_INPUT;
842
+ var getNonUserInputErrors = (errors) => errors.filter((e) => e.extensions?.code !== BAD_USER_INPUT_CODE);
843
+
844
+ // src/utils/tracing.util.ts
845
+ var getOpsName = (gqlCtx) => gqlCtx.contextValue.rootSpan?.getBaggageItem(
846
+ "root_op_name" /* ROOT_OP_NAME */
847
+ ) || gqlCtx.request.operationName || gqlCtx.operationName || "UNKNOWN";
848
+
849
+ // src/logger/apollo.logger.ts
850
+ var getUserId = (gqlCtx) => gqlCtx.user?.id ?? "UNKNOWN";
851
+ function createApolloLoggingPlugin() {
852
+ return {
853
+ async requestDidStart(requestContext) {
854
+ const start = Date.now();
855
+ const operationName = requestContext.request.operationName;
856
+ const requestLogger = createLogger({
857
+ source: "apollo",
858
+ operationName
859
+ });
860
+ return {
861
+ async didResolveOperation(gqlCtx) {
862
+ const userId = getUserId(gqlCtx.contextValue);
863
+ const operation = getOpsName(gqlCtx);
864
+ requestLogger.info(
865
+ {
866
+ event: "operation_resolved",
867
+ operation,
868
+ userId,
869
+ operationName: gqlCtx.operationName,
870
+ variables: gqlCtx.request.variables
871
+ },
872
+ `Operation [${operation}] resolved for user [${userId}]`
873
+ );
874
+ },
875
+ async willSendResponse(gqlCtx) {
876
+ const errors = getNonUserInputErrors(gqlCtx.errors || []);
877
+ const duration = Date.now() - start;
878
+ const userId = getUserId(gqlCtx.contextValue);
879
+ const operation = getOpsName(gqlCtx);
880
+ if (errors.length > 0) {
881
+ requestLogger.error(
882
+ {
883
+ event: "operation_error",
884
+ operation,
885
+ userId,
886
+ duration,
887
+ errors: errors.map((e) => ({
888
+ message: e.message,
889
+ path: e.path,
890
+ extensions: e.extensions,
891
+ stack: e.stack
892
+ }))
893
+ },
894
+ `Operation [${operation}] failed with ${errors.length} error(s)`
895
+ );
896
+ }
897
+ requestLogger.info(
898
+ {
899
+ event: "operation_complete",
900
+ operation,
901
+ userId,
902
+ duration,
903
+ success: errors.length === 0,
904
+ errorCount: errors.length
905
+ },
906
+ `Operation [${operation}] completed in ${duration}ms${errors.length > 0 ? " with errors" : " successfully"}`
907
+ );
908
+ },
909
+ async didEncounterErrors(gqlCtx) {
910
+ const errors = gqlCtx.errors || [];
911
+ const userId = getUserId(gqlCtx.contextValue);
912
+ requestLogger.error(
913
+ {
914
+ event: "graphql_errors",
915
+ userId,
916
+ errors: errors.map((e) => ({
917
+ message: e.message,
918
+ path: e.path,
919
+ extensions: e.extensions
920
+ }))
921
+ },
922
+ `GraphQL execution encountered ${errors.length} error(s)`
923
+ );
924
+ }
925
+ };
926
+ }
927
+ };
928
+ }
929
+
930
+ // src/server/plugin.configure.ts
931
+ import { ApolloServerPluginLandingPageDisabled } from "@apollo/server/plugin/disabled";
932
+ function configurePlugins(builder) {
933
+ builder.plugins.push(ApolloServerPluginLandingPageDisabled());
934
+ builder.plugins.push(createApolloLoggingPlugin());
935
+ }
936
+
937
+ // src/server/server.builder.ts
938
+ var ServerBuilder = class {
939
+ constructor(config) {
940
+ this.config = config;
941
+ this.plugins = [];
942
+ this.assert();
943
+ this.app = configureHono(this);
944
+ this.schema = this.config.apollo.schema;
945
+ }
946
+ configure() {
947
+ const honoApp = this.app;
948
+ configurePlugins(this);
949
+ const server = new ApolloServer({
950
+ schema: this.schema,
951
+ plugins: this.plugins
952
+ });
953
+ const cfHandler = startServerAndCreateCloudflareWorkersHandler(
954
+ server,
955
+ {
956
+ context: async (c) => createContextFunction(this.config.extendContext)(c)
957
+ }
958
+ );
959
+ honoApp.all(this.config.path ?? "/", async (c) => {
960
+ let executionCtx;
961
+ try {
962
+ executionCtx = c.executionCtx;
963
+ } catch (e) {
964
+ executionCtx = {
965
+ waitUntil: (promise) => promise,
966
+ passThroughOnException: () => {
967
+ }
968
+ };
969
+ }
970
+ return cfHandler(c.req.raw, c.env, executionCtx);
971
+ });
972
+ return {
973
+ // fetch: app.fetch,
974
+ async fetch(request, env, ctx) {
975
+ await Promise.resolve();
976
+ return honoApp.fetch(request, env, ctx);
977
+ },
978
+ port: 3344
979
+ // Only used in local development
980
+ };
981
+ }
982
+ assert() {
983
+ assertDefined(
984
+ this.config.apollo.schema,
985
+ "Apollo server config must have a schema"
986
+ );
987
+ }
988
+ };
989
+
990
+ // src/server/index.ts
991
+ var SERVICE_NAME_REGEX = /^[a-z0-9\-_]+$/i;
992
+ function createGraphQLServer(options) {
993
+ if (!SERVICE_NAME_REGEX.test(options.name)) {
994
+ throw new Error("Service name must be alphanumeric, hyphen, or underscore");
995
+ }
996
+ logger.info(`Creating GraphQL server ${options.name} v${options.version}`);
997
+ return new ServerBuilder(options.config);
998
+ }
999
+
1000
+ // src/utils/error.util.ts
1001
+ import { GraphQLError } from "graphql";
1002
+ import { ApolloServerErrorCode as ApolloServerErrorCode2 } from "@apollo/server/errors";
1003
+ var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
1004
+ ErrorCode2["UNAUTHENTICATED"] = "UNAUTHENTICATED";
1005
+ ErrorCode2["FORBIDDEN"] = "FORBIDDEN";
1006
+ ErrorCode2["VALIDATION_FAILED"] = "VALIDATION_FAILED";
1007
+ ErrorCode2["RATE_LIMITED"] = "RATE_LIMITED";
1008
+ ErrorCode2["EXTERNAL_SERVICE_ERROR"] = "EXTERNAL_SERVICE_ERROR";
1009
+ ErrorCode2["NOT_FOUND"] = "NOT_FOUND";
1010
+ ErrorCode2["CONFLICT"] = "CONFLICT";
1011
+ return ErrorCode2;
1012
+ })(ErrorCode || {});
1013
+ var AppError = class extends GraphQLError {
1014
+ constructor(message, code, extensions) {
1015
+ super(message, {
1016
+ extensions: {
1017
+ code,
1018
+ ...extensions
1019
+ }
1020
+ });
1021
+ }
1022
+ };
1023
+
1024
+ // src/utils/schema.util.ts
1025
+ import { gql } from "graphql-tag";
1026
+ var createSchemaModule = (typeDefs, resolvers) => ({
1027
+ typeDefs,
1028
+ resolvers
1029
+ });
1030
+ export {
1031
+ AppError,
1032
+ CachedDataLoader,
1033
+ ErrorCode,
1034
+ KVCache,
1035
+ RedisCache,
1036
+ ServerBuilder,
1037
+ cached,
1038
+ configureHealthChecks,
1039
+ createApolloLoggingPlugin,
1040
+ createCache,
1041
+ createContextFunction,
1042
+ createGraphQLServer,
1043
+ createRateLimitMiddleware,
1044
+ createSchemaModule,
1045
+ createTracingMiddleware,
1046
+ createValidationMiddleware,
1047
+ envLoader,
1048
+ getNonUserInputErrors,
1049
+ gql,
1050
+ logger
1051
+ };
1052
+ //# sourceMappingURL=index.js.map