@iflow-mcp/apple-rag-mcp 4.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/.github/workflows/release.yml +62 -0
  2. package/.releaserc.json +38 -0
  3. package/CHANGELOG.md +161 -0
  4. package/README.md +114 -0
  5. package/README.zh-CN.md +119 -0
  6. package/apple-rag-mcp_process.log +8 -0
  7. package/biome.json +59 -0
  8. package/dist/src/auth/auth-middleware.d.ts +26 -0
  9. package/dist/src/auth/auth-middleware.d.ts.map +1 -0
  10. package/dist/src/auth/auth-middleware.js +77 -0
  11. package/dist/src/auth/auth-middleware.js.map +1 -0
  12. package/dist/src/auth/token-validator.d.ts +22 -0
  13. package/dist/src/auth/token-validator.d.ts.map +1 -0
  14. package/dist/src/auth/token-validator.js +64 -0
  15. package/dist/src/auth/token-validator.js.map +1 -0
  16. package/dist/src/mcp/formatters/response-formatter.d.ts +26 -0
  17. package/dist/src/mcp/formatters/response-formatter.d.ts.map +1 -0
  18. package/dist/src/mcp/formatters/response-formatter.js +119 -0
  19. package/dist/src/mcp/formatters/response-formatter.js.map +1 -0
  20. package/dist/src/mcp/manifest.d.ts +48 -0
  21. package/dist/src/mcp/manifest.d.ts.map +1 -0
  22. package/dist/src/mcp/manifest.js +46 -0
  23. package/dist/src/mcp/manifest.js.map +1 -0
  24. package/dist/src/mcp/middleware/request-validator.d.ts +48 -0
  25. package/dist/src/mcp/middleware/request-validator.d.ts.map +1 -0
  26. package/dist/src/mcp/middleware/request-validator.js +102 -0
  27. package/dist/src/mcp/middleware/request-validator.js.map +1 -0
  28. package/dist/src/mcp/protocol-handler.d.ts +70 -0
  29. package/dist/src/mcp/protocol-handler.d.ts.map +1 -0
  30. package/dist/src/mcp/protocol-handler.js +285 -0
  31. package/dist/src/mcp/protocol-handler.js.map +1 -0
  32. package/dist/src/mcp/tools/fetch-tool.d.ts +18 -0
  33. package/dist/src/mcp/tools/fetch-tool.d.ts.map +1 -0
  34. package/dist/src/mcp/tools/fetch-tool.js +76 -0
  35. package/dist/src/mcp/tools/fetch-tool.js.map +1 -0
  36. package/dist/src/mcp/tools/search-tool.d.ts +20 -0
  37. package/dist/src/mcp/tools/search-tool.d.ts.map +1 -0
  38. package/dist/src/mcp/tools/search-tool.js +86 -0
  39. package/dist/src/mcp/tools/search-tool.js.map +1 -0
  40. package/dist/src/services/database.d.ts +37 -0
  41. package/dist/src/services/database.d.ts.map +1 -0
  42. package/dist/src/services/database.js +166 -0
  43. package/dist/src/services/database.js.map +1 -0
  44. package/dist/src/services/deepinfra-base.d.ts +22 -0
  45. package/dist/src/services/deepinfra-base.d.ts.map +1 -0
  46. package/dist/src/services/deepinfra-base.js +55 -0
  47. package/dist/src/services/deepinfra-base.js.map +1 -0
  48. package/dist/src/services/embedding.d.ts +44 -0
  49. package/dist/src/services/embedding.d.ts.map +1 -0
  50. package/dist/src/services/embedding.js +61 -0
  51. package/dist/src/services/embedding.js.map +1 -0
  52. package/dist/src/services/index.d.ts +10 -0
  53. package/dist/src/services/index.d.ts.map +1 -0
  54. package/dist/src/services/index.js +52 -0
  55. package/dist/src/services/index.js.map +1 -0
  56. package/dist/src/services/ip-authentication.d.ts +12 -0
  57. package/dist/src/services/ip-authentication.d.ts.map +1 -0
  58. package/dist/src/services/ip-authentication.js +39 -0
  59. package/dist/src/services/ip-authentication.js.map +1 -0
  60. package/dist/src/services/rag.d.ts +35 -0
  61. package/dist/src/services/rag.d.ts.map +1 -0
  62. package/dist/src/services/rag.js +106 -0
  63. package/dist/src/services/rag.js.map +1 -0
  64. package/dist/src/services/rate-limit.d.ts +27 -0
  65. package/dist/src/services/rate-limit.d.ts.map +1 -0
  66. package/dist/src/services/rate-limit.js +91 -0
  67. package/dist/src/services/rate-limit.js.map +1 -0
  68. package/dist/src/services/reranker.d.ts +40 -0
  69. package/dist/src/services/reranker.d.ts.map +1 -0
  70. package/dist/src/services/reranker.js +97 -0
  71. package/dist/src/services/reranker.js.map +1 -0
  72. package/dist/src/services/search-engine.d.ts +89 -0
  73. package/dist/src/services/search-engine.d.ts.map +1 -0
  74. package/dist/src/services/search-engine.js +225 -0
  75. package/dist/src/services/search-engine.js.map +1 -0
  76. package/dist/src/services/tool-call-logger.d.ts +36 -0
  77. package/dist/src/services/tool-call-logger.d.ts.map +1 -0
  78. package/dist/src/services/tool-call-logger.js +34 -0
  79. package/dist/src/services/tool-call-logger.js.map +1 -0
  80. package/dist/src/types/env.d.ts +18 -0
  81. package/dist/src/types/env.d.ts.map +1 -0
  82. package/dist/src/types/env.js +2 -0
  83. package/dist/src/types/env.js.map +1 -0
  84. package/dist/src/types/index.d.ts +145 -0
  85. package/dist/src/types/index.d.ts.map +1 -0
  86. package/dist/src/types/index.js +6 -0
  87. package/dist/src/types/index.js.map +1 -0
  88. package/dist/src/utils/d1-utils.d.ts +6 -0
  89. package/dist/src/utils/d1-utils.d.ts.map +1 -0
  90. package/dist/src/utils/d1-utils.js +29 -0
  91. package/dist/src/utils/d1-utils.js.map +1 -0
  92. package/dist/src/utils/logger.d.ts +11 -0
  93. package/dist/src/utils/logger.d.ts.map +1 -0
  94. package/dist/src/utils/logger.js +26 -0
  95. package/dist/src/utils/logger.js.map +1 -0
  96. package/dist/src/utils/query-cleaner.d.ts +20 -0
  97. package/dist/src/utils/query-cleaner.d.ts.map +1 -0
  98. package/dist/src/utils/query-cleaner.js +117 -0
  99. package/dist/src/utils/query-cleaner.js.map +1 -0
  100. package/dist/src/utils/request-info.d.ts +18 -0
  101. package/dist/src/utils/request-info.d.ts.map +1 -0
  102. package/dist/src/utils/request-info.js +32 -0
  103. package/dist/src/utils/request-info.js.map +1 -0
  104. package/dist/src/utils/telegram-notifier.d.ts +4 -0
  105. package/dist/src/utils/telegram-notifier.d.ts.map +1 -0
  106. package/dist/src/utils/telegram-notifier.js +33 -0
  107. package/dist/src/utils/telegram-notifier.js.map +1 -0
  108. package/dist/src/utils/url-processor.d.ts +15 -0
  109. package/dist/src/utils/url-processor.d.ts.map +1 -0
  110. package/dist/src/utils/url-processor.js +54 -0
  111. package/dist/src/utils/url-processor.js.map +1 -0
  112. package/dist/src/worker.d.ts +15 -0
  113. package/dist/src/worker.d.ts.map +1 -0
  114. package/dist/src/worker.js +136 -0
  115. package/dist/src/worker.js.map +1 -0
  116. package/migrations/schema.sql +155 -0
  117. package/package.json +49 -0
  118. package/scripts/semantic-release-server-json.js +34 -0
  119. package/server.json +25 -0
  120. package/src/auth/auth-middleware.ts +104 -0
  121. package/src/auth/token-validator.ts +96 -0
  122. package/src/mcp/formatters/response-formatter.ts +157 -0
  123. package/src/mcp/manifest.ts +48 -0
  124. package/src/mcp/middleware/request-validator.ts +135 -0
  125. package/src/mcp/protocol-handler.ts +412 -0
  126. package/src/mcp/tools/fetch-tool.ts +146 -0
  127. package/src/mcp/tools/search-tool.ts +165 -0
  128. package/src/services/database.ts +202 -0
  129. package/src/services/deepinfra-base.ts +81 -0
  130. package/src/services/embedding.ts +96 -0
  131. package/src/services/index.ts +59 -0
  132. package/src/services/ip-authentication.ts +62 -0
  133. package/src/services/rag.ts +158 -0
  134. package/src/services/rate-limit.ts +141 -0
  135. package/src/services/reranker.ts +171 -0
  136. package/src/services/search-engine.ts +333 -0
  137. package/src/services/tool-call-logger.ts +98 -0
  138. package/src/types/env.ts +22 -0
  139. package/src/types/index.ts +189 -0
  140. package/src/utils/d1-utils.ts +45 -0
  141. package/src/utils/logger.ts +33 -0
  142. package/src/utils/query-cleaner.ts +151 -0
  143. package/src/utils/request-info.ts +47 -0
  144. package/src/utils/telegram-notifier.ts +47 -0
  145. package/src/utils/url-processor.ts +65 -0
  146. package/src/worker.ts +176 -0
  147. package/tsconfig.json +32 -0
  148. package/wrangler.toml.example +39 -0
@@ -0,0 +1,202 @@
1
+ /**
2
+ * PostgreSQL Database Service with pgvector
3
+ * Optimized for Cloudflare Workers with external database connection
4
+ */
5
+ import postgres from "postgres";
6
+ import type { AppConfig, SearchOptions, SearchResult } from "../types/index.js";
7
+ import { logger } from "../utils/logger.js";
8
+
9
+ export class DatabaseService {
10
+ private sql: ReturnType<typeof postgres>;
11
+ constructor(config: AppConfig) {
12
+ // Direct PostgreSQL connection - no checks, no logs
13
+ this.sql = postgres({
14
+ host: config.RAG_DB_HOST,
15
+ port: config.RAG_DB_PORT,
16
+ database: config.RAG_DB_DATABASE,
17
+ username: config.RAG_DB_USER,
18
+ password: config.RAG_DB_PASSWORD,
19
+ ssl: config.RAG_DB_SSLMODE === "require",
20
+ max: 5,
21
+ idle_timeout: 60000,
22
+ connect_timeout: 10000,
23
+ prepare: true,
24
+ connection: {
25
+ application_name: "apple-rag-mcp",
26
+ },
27
+ transform: {
28
+ undefined: null,
29
+ },
30
+ });
31
+ }
32
+
33
+ /**
34
+ * Initialize database - no checks, trust ready state
35
+ */
36
+ async initialize(): Promise<void> {
37
+ // Database assumed ready - no checks, no logs, instant return
38
+ }
39
+
40
+ /**
41
+ * Semantic search using vector similarity
42
+ */
43
+ async semanticSearch(
44
+ queryEmbedding: number[],
45
+ options: SearchOptions = {}
46
+ ): Promise<SearchResult[]> {
47
+ const { resultCount = 5 } = options;
48
+
49
+ try {
50
+ const results = await this.sql`
51
+ SELECT id, url, title, content, chunk_index, total_chunks
52
+ FROM chunks
53
+ WHERE embedding IS NOT NULL
54
+ ORDER BY embedding <=> ${JSON.stringify(queryEmbedding)}::halfvec
55
+ LIMIT ${resultCount}
56
+ `;
57
+
58
+ return results.map((row) => ({
59
+ id: row.id as string,
60
+ url: row.url as string,
61
+ title: row.title as string | null,
62
+ content: row.content as string,
63
+ contentLength: (row.content as string).length,
64
+ chunk_index: row.chunk_index as number,
65
+ total_chunks: row.total_chunks as number,
66
+ }));
67
+ } catch (error) {
68
+ logger.error(
69
+ `Database semantic search failed (operation: semantic_search, embeddingDimensions: ${queryEmbedding.length}, resultCount: ${resultCount}): ${String(error)}`
70
+ );
71
+ throw new Error(`Vector search failed: ${error}`);
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Keyword search optimized for Apple Developer Documentation
77
+ * Uses PostgreSQL 'simple' configuration for precise matching of technical terms,
78
+ * API names, and special symbols (@State, SecItemAdd, etc.)
79
+ */
80
+ async keywordSearch(
81
+ query: string,
82
+ options: SearchOptions = {}
83
+ ): Promise<SearchResult[]> {
84
+ const { resultCount = 5 } = options;
85
+
86
+ try {
87
+ const results = await this.sql`
88
+ SELECT id, url, title, content, chunk_index, total_chunks
89
+ FROM chunks
90
+ WHERE to_tsvector('simple', COALESCE(title, '') || ' ' || content)
91
+ @@ plainto_tsquery('simple', ${query})
92
+ LIMIT ${resultCount}
93
+ `;
94
+
95
+ return results.map((row) => ({
96
+ id: row.id as string,
97
+ url: row.url as string,
98
+ title: row.title as string | null,
99
+ content: row.content as string,
100
+ contentLength: (row.content as string).length,
101
+ chunk_index: row.chunk_index as number,
102
+ total_chunks: row.total_chunks as number,
103
+ }));
104
+ } catch (error) {
105
+ logger.error(
106
+ `Database keyword search failed (operation: keyword_search, query: ${query.substring(0, 50)}, resultCount: ${resultCount}): ${String(error)}`
107
+ );
108
+ throw new Error(`Keyword search failed: ${error}`);
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Normalize URL for flexible matching
114
+ */
115
+ private normalizeUrl(url: string): string {
116
+ // Remove trailing slash
117
+ let normalized = url.replace(/\/$/, "");
118
+
119
+ // Ensure https:// prefix
120
+ if (
121
+ !normalized.startsWith("http://") &&
122
+ !normalized.startsWith("https://")
123
+ ) {
124
+ normalized = `https://${normalized}`;
125
+ }
126
+
127
+ // Convert http:// to https://
128
+ if (normalized.startsWith("http://")) {
129
+ normalized = normalized.replace("http://", "https://");
130
+ }
131
+
132
+ return normalized;
133
+ }
134
+
135
+ /**
136
+ * Get page content by URL from pages table with flexible matching
137
+ */
138
+ async getPageByUrl(url: string): Promise<{
139
+ id: string;
140
+ url: string;
141
+ title: string | null;
142
+ content: string;
143
+ } | null> {
144
+ const normalizedUrl = this.normalizeUrl(url);
145
+
146
+ try {
147
+ // Try exact match first
148
+ let results = await this.sql`
149
+ SELECT id, url, title, content
150
+ FROM pages
151
+ WHERE url = ${normalizedUrl}
152
+ LIMIT 1
153
+ `;
154
+
155
+ // If no exact match, try flexible matching
156
+ if (results.length === 0) {
157
+ // Try with/without trailing slash
158
+ const alternativeUrl = normalizedUrl.endsWith("/")
159
+ ? normalizedUrl.slice(0, -1)
160
+ : `${normalizedUrl}/`;
161
+
162
+ results = await this.sql`
163
+ SELECT id, url, title, content
164
+ FROM pages
165
+ WHERE url = ${alternativeUrl}
166
+ LIMIT 1
167
+ `;
168
+ }
169
+
170
+ if (results.length === 0) {
171
+ return null;
172
+ }
173
+
174
+ const row = results[0];
175
+ return {
176
+ id: row.id as string,
177
+ url: row.url as string,
178
+ title: row.title as string | null,
179
+ content: row.content as string,
180
+ };
181
+ } catch (error) {
182
+ logger.error(
183
+ `Database page lookup failed (operation: page_lookup, url: ${url.substring(0, 100)}, normalizedUrl: ${this.normalizeUrl(url).substring(0, 100)}): ${String(error)}`
184
+ );
185
+ throw new Error(`Page lookup failed: ${error}`);
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Close database connection
191
+ */
192
+ async close(): Promise<void> {
193
+ try {
194
+ await this.sql.end();
195
+ } catch (error) {
196
+ logger.error(
197
+ `Database close failed (operation: database_close): ${String(error)}`
198
+ );
199
+ // Don't re-throw - closing errors are not critical
200
+ }
201
+ }
202
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * DeepInfra client utilities (config + base service)
3
+ * Minimal single-key client with retry support.
4
+ */
5
+
6
+ import { logger } from "../utils/logger.js";
7
+
8
+ export const DEEPINFRA_CONFIG = {
9
+ BASE_URL: "https://api.deepinfra.com",
10
+ TIMEOUT_MS: 5_000,
11
+ USER_AGENT: "Apple-RAG-MCP/2.0.0",
12
+ EMBEDDING_MODEL: "Qwen/Qwen3-Embedding-4B",
13
+ RERANKER_MODEL_PRIMARY: "Qwen/Qwen3-Reranker-8B",
14
+ RERANKER_MODEL_FALLBACK: "Qwen/Qwen3-Reranker-4B",
15
+ } as const;
16
+
17
+ export abstract class DeepInfraService<TRequest, TResponse, TResult> {
18
+ protected abstract readonly endpoint: string;
19
+ private readonly apiKey: string;
20
+
21
+ constructor(apiKey: string) {
22
+ if (!apiKey) throw new Error("DEEPINFRA_API_KEY is required");
23
+ this.apiKey = apiKey;
24
+ }
25
+
26
+ protected async call(
27
+ input: TRequest,
28
+ operationName: string
29
+ ): Promise<TResult> {
30
+ const startTime = Date.now();
31
+ const payload = this.buildPayload(input);
32
+ let lastError!: Error;
33
+
34
+ for (let i = 0; i < 3; i++) {
35
+ try {
36
+ const json = await this.singleRequest(this.endpoint, payload);
37
+ logger.info(
38
+ `${operationName} completed (${((Date.now() - startTime) / 1000).toFixed(1)}s)`
39
+ );
40
+ return this.processResponse(json, input);
41
+ } catch (e) {
42
+ lastError = e instanceof Error ? e : new Error(String(e));
43
+ }
44
+ }
45
+
46
+ logger.error(
47
+ `${operationName} failed after 3 attempts (${((Date.now() - startTime) / 1000).toFixed(1)}s): ${lastError.message}`
48
+ );
49
+ throw lastError;
50
+ }
51
+
52
+ protected async singleRequest(
53
+ endpoint: string,
54
+ payload: unknown
55
+ ): Promise<TResponse> {
56
+ const res = await fetch(`${DEEPINFRA_CONFIG.BASE_URL}${endpoint}`, {
57
+ method: "POST",
58
+ headers: {
59
+ Authorization: `Bearer ${this.apiKey}`,
60
+ "Content-Type": "application/json",
61
+ "User-Agent": DEEPINFRA_CONFIG.USER_AGENT,
62
+ },
63
+ body: JSON.stringify(payload),
64
+ signal: AbortSignal.timeout(DEEPINFRA_CONFIG.TIMEOUT_MS),
65
+ });
66
+
67
+ if (!res.ok) {
68
+ throw new Error(
69
+ `API error ${res.status}: ${await res.text().catch(() => "")}`
70
+ );
71
+ }
72
+
73
+ return (await res.json()) as TResponse;
74
+ }
75
+
76
+ protected abstract buildPayload(input: TRequest): unknown;
77
+ protected abstract processResponse(
78
+ response: TResponse,
79
+ input: TRequest
80
+ ): TResult;
81
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Modern Embedding Service - MCP Optimized
3
+ * DeepInfra API integration (single provider, no fallback)
4
+ */
5
+
6
+ import type { EmbeddingService as IEmbeddingService } from "../types/index.js";
7
+ import { logger } from "../utils/logger.js";
8
+ import { DEEPINFRA_CONFIG, DeepInfraService } from "./deepinfra-base.js";
9
+
10
+ interface EmbeddingInput {
11
+ text: string;
12
+ }
13
+
14
+ interface EmbeddingPayload {
15
+ model: "Qwen/Qwen3-Embedding-4B";
16
+ input: string;
17
+ encoding_format: "float";
18
+ }
19
+
20
+ interface EmbeddingResponse {
21
+ data: Array<{
22
+ embedding: number[];
23
+ }>;
24
+ }
25
+
26
+ export class EmbeddingService
27
+ extends DeepInfraService<EmbeddingInput, EmbeddingResponse, number[]>
28
+ implements IEmbeddingService
29
+ {
30
+ protected readonly endpoint = "/v1/openai/embeddings";
31
+
32
+ /**
33
+ * Create embedding (single provider)
34
+ */
35
+ async createEmbedding(text: string): Promise<number[]> {
36
+ if (!text?.trim()) {
37
+ throw new Error("Text cannot be empty for embedding generation");
38
+ }
39
+
40
+ const input: EmbeddingInput = { text: text.trim() };
41
+ return this.call(input, "Embedding generation");
42
+ }
43
+
44
+ /**
45
+ * Build API payload from request
46
+ */
47
+ protected buildPayload(input: EmbeddingInput): EmbeddingPayload {
48
+ return {
49
+ model: DEEPINFRA_CONFIG.EMBEDDING_MODEL,
50
+ input: input.text,
51
+ encoding_format: "float",
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Process API response and return normalized embedding
57
+ */
58
+ protected processResponse(
59
+ response: EmbeddingResponse,
60
+ _input: EmbeddingInput
61
+ ): number[] {
62
+ const embedding = this.extractEmbedding(response);
63
+ return this.normalizeL2(embedding);
64
+ }
65
+
66
+ /**
67
+ * Extract embedding from API response
68
+ */
69
+ private extractEmbedding(response: EmbeddingResponse): number[] {
70
+ const embedding = response.data?.[0]?.embedding;
71
+
72
+ if (!embedding || !Array.isArray(embedding)) {
73
+ throw new Error("No embedding data received from DeepInfra API");
74
+ }
75
+
76
+ if (embedding.length === 0) {
77
+ throw new Error("Empty embedding received from DeepInfra API");
78
+ }
79
+
80
+ return embedding;
81
+ }
82
+
83
+ /**
84
+ * L2 normalization for optimal vector search performance
85
+ */
86
+ private normalizeL2(embedding: number[]): number[] {
87
+ const norm = Math.sqrt(embedding.reduce((sum, val) => sum + val * val, 0));
88
+
89
+ if (norm === 0) {
90
+ logger.warn("Zero norm embedding detected, returning original");
91
+ return [...embedding];
92
+ }
93
+
94
+ return embedding.map((val) => val / norm);
95
+ }
96
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Modern Service Factory - Cloudflare Worker Native
3
+ * Creates and configures all services with optimal performance
4
+ */
5
+
6
+ import { AuthMiddleware } from "../auth/auth-middleware.js";
7
+ import type { AppConfig, Services, WorkerEnv } from "../types/index.js";
8
+ import { RAGService } from "./rag.js";
9
+ import { RateLimitService } from "./rate-limit.js";
10
+ import { ToolCallLogger } from "./tool-call-logger.js";
11
+
12
+ /**
13
+ * Create all services from Worker environment with validation
14
+ */
15
+ export async function createServices(env: WorkerEnv): Promise<Services> {
16
+ try {
17
+ // Convert Worker env to app config
18
+ const config = createAppConfig(env);
19
+
20
+ // Initialize services
21
+ const auth = new AuthMiddleware(env.DB);
22
+ const rag = new RAGService(config, env);
23
+ const rateLimit = new RateLimitService(env.DB);
24
+ const logger = new ToolCallLogger(env.DB);
25
+
26
+ // Initialize async services
27
+ await rag.initialize();
28
+
29
+ return {
30
+ rag,
31
+ auth,
32
+ database: rag.database,
33
+ embedding: rag.embedding,
34
+ rateLimit,
35
+ logger,
36
+ };
37
+ } catch (error) {
38
+ // Import logger here to avoid circular dependency
39
+ const { logger } = await import("../utils/logger.js");
40
+ logger.error(
41
+ `Service initialization failed: ${error instanceof Error ? error.message : String(error)}`
42
+ );
43
+ throw error;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Convert Worker environment to app configuration
49
+ */
50
+ function createAppConfig(env: WorkerEnv): AppConfig {
51
+ return {
52
+ RAG_DB_HOST: env.RAG_DB_HOST,
53
+ RAG_DB_PORT: parseInt(env.RAG_DB_PORT, 10),
54
+ RAG_DB_DATABASE: env.RAG_DB_DATABASE,
55
+ RAG_DB_USER: env.RAG_DB_USER,
56
+ RAG_DB_PASSWORD: env.RAG_DB_PASSWORD,
57
+ RAG_DB_SSLMODE: env.RAG_DB_SSLMODE,
58
+ };
59
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * IP Authentication Service with D1 timeout protection
3
+ */
4
+
5
+ import type { UserTokenData } from "../auth/token-validator.js";
6
+ import { backgroundD1Write, withD1Timeout } from "../utils/d1-utils.js";
7
+ import { logger } from "../utils/logger.js";
8
+
9
+ interface UserRecord {
10
+ user_id: string;
11
+ email?: string;
12
+ name?: string;
13
+ }
14
+
15
+ export class IPAuthenticationService {
16
+ constructor(private d1: D1Database) {}
17
+
18
+ async checkIPAuthentication(clientIP: string): Promise<UserTokenData | null> {
19
+ const user = await withD1Timeout(
20
+ () => this.queryIP(clientIP),
21
+ null,
22
+ "ip_auth"
23
+ );
24
+
25
+ if (!user) return null;
26
+
27
+ backgroundD1Write(
28
+ logger.getContext(),
29
+ () => this.updateLastUsed(clientIP, user.user_id),
30
+ "ip_last_used"
31
+ );
32
+
33
+ return {
34
+ userId: user.user_id,
35
+ email: user.email || "ip-authenticated",
36
+ name: user.name || "IP User",
37
+ };
38
+ }
39
+
40
+ private async queryIP(clientIP: string): Promise<UserRecord | null> {
41
+ const result = await this.d1
42
+ .prepare(
43
+ `SELECT uai.user_id, u.email, u.name
44
+ FROM user_authorized_ips uai
45
+ JOIN users u ON uai.user_id = u.id
46
+ WHERE uai.ip_address = ?`
47
+ )
48
+ .bind(clientIP)
49
+ .all();
50
+
51
+ return (result.results?.[0] as unknown as UserRecord | undefined) ?? null;
52
+ }
53
+
54
+ private async updateLastUsed(ipAddress: string, userId: string): Promise<void> {
55
+ await this.d1
56
+ .prepare(
57
+ "UPDATE user_authorized_ips SET last_used_at = ? WHERE ip_address = ? AND user_id = ?"
58
+ )
59
+ .bind(new Date().toISOString(), ipAddress, userId)
60
+ .run();
61
+ }
62
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Modern RAG Service - Cloudflare Worker Native
3
+ * Optimized for edge computing with zero-dependency architecture
4
+ */
5
+
6
+ import type {
7
+ AppConfig,
8
+ RAGQuery,
9
+ RAGResult,
10
+ SearchResult,
11
+ WorkerEnv,
12
+ } from "../types/index.js";
13
+ import { logger } from "../utils/logger.js";
14
+ import { DatabaseService } from "./database.js";
15
+ import { EmbeddingService } from "./embedding.js";
16
+ import { RerankerService } from "./reranker.js";
17
+ import { type RankedSearchResult, SearchEngine } from "./search-engine.js";
18
+
19
+ export class RAGService {
20
+ readonly database: DatabaseService;
21
+ readonly embedding: EmbeddingService;
22
+ private readonly reranker: RerankerService;
23
+ private readonly searchEngine: SearchEngine;
24
+
25
+ constructor(config: AppConfig, env: WorkerEnv) {
26
+ this.database = new DatabaseService(config);
27
+ this.embedding = new EmbeddingService(env.DEEPINFRA_API_KEY);
28
+ this.reranker = new RerankerService(env.DEEPINFRA_API_KEY);
29
+ this.searchEngine = new SearchEngine(
30
+ this.database,
31
+ this.embedding,
32
+ this.reranker
33
+ );
34
+ }
35
+
36
+ /**
37
+ * Initialize - no-op since database initialization is removed
38
+ */
39
+ async initialize(): Promise<void> {
40
+ // No initialization needed - database trusted ready
41
+ }
42
+
43
+ /**
44
+ * Perform RAG query with intelligent processing and detailed timing
45
+ */
46
+ async query(request: RAGQuery): Promise<RAGResult> {
47
+ const startTime = Date.now();
48
+ const { query, result_count = 4 } = request;
49
+
50
+ // No started log - only completion with timing
51
+
52
+ // Input validation
53
+ if (!query?.trim()) {
54
+ return this.createErrorResponse(
55
+ query,
56
+ "Query cannot be empty. Please provide a search query to find relevant Apple Developer Documentation.",
57
+ "Try searching for topics like 'SwiftUI navigation', 'iOS app development', or 'API documentation'.",
58
+ startTime
59
+ );
60
+ }
61
+
62
+ const trimmedQuery = query.trim();
63
+ if (trimmedQuery.length > 10000) {
64
+ return this.createErrorResponse(
65
+ query,
66
+ "Query is too long. Please limit your query to 10000 characters or less.",
67
+ "Try to make your query more concise and specific.",
68
+ startTime
69
+ );
70
+ }
71
+
72
+ try {
73
+ // Initialize services (if not already initialized)
74
+ await this.initialize();
75
+
76
+ // Execute search
77
+ const resultCount = Math.min(Math.max(result_count, 1), 20);
78
+
79
+ const searchResult = await this.searchEngine.search(trimmedQuery, {
80
+ resultCount,
81
+ });
82
+
83
+ // Format results
84
+ const formattedResults = this.formatResults(searchResult.results);
85
+ const totalTime = Date.now() - startTime;
86
+
87
+ // Log completion with timing
88
+ logger.info(
89
+ `RAG query completed (${(totalTime / 1000).toFixed(1)}s) - results: ${formattedResults.length}, query: ${query.substring(0, 50)}`
90
+ );
91
+
92
+ return {
93
+ success: true,
94
+ query: trimmedQuery,
95
+ results: formattedResults,
96
+ additionalUrls: searchResult.additionalUrls,
97
+ count: formattedResults.length,
98
+ processing_time_ms: totalTime,
99
+ };
100
+ } catch (error) {
101
+ logger.error(
102
+ `RAG query failed for query "${trimmedQuery.substring(0, 50)}": ${error instanceof Error ? error.message : "Unknown error"}`
103
+ );
104
+ return this.createErrorResponse(
105
+ trimmedQuery,
106
+ `Search failed: ${error instanceof Error ? error.message : "Unknown error"}`,
107
+ "Please try again with a different query or check your connection.",
108
+ startTime
109
+ );
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Format search results for MCP response
115
+ */
116
+ private formatResults(
117
+ results: readonly RankedSearchResult[]
118
+ ): SearchResult[] {
119
+ return results.map((result) => ({
120
+ id: result.id,
121
+ url: result.url,
122
+ title: result.title,
123
+ content: result.content,
124
+ contentLength: result.content.length,
125
+ chunk_index: result.chunk_index,
126
+ total_chunks: result.total_chunks,
127
+ mergedChunkIndices: result.mergedChunkIndices,
128
+ }));
129
+ }
130
+
131
+ /**
132
+ * Create standardized error response
133
+ */
134
+ private createErrorResponse(
135
+ query: string,
136
+ _error: string,
137
+ _suggestion: string,
138
+ startTime: number
139
+ ): RAGResult {
140
+ return {
141
+ success: false,
142
+ query,
143
+ results: [],
144
+ additionalUrls: [],
145
+ count: 0,
146
+ processing_time_ms: Date.now() - startTime,
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Clean up resources
152
+ */
153
+ async close(): Promise<void> {
154
+ if (this.database) {
155
+ await this.database.close();
156
+ }
157
+ }
158
+ }