@brainfish-ai/devdoc 0.1.48 → 0.1.49

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 (36) hide show
  1. package/dist/cli/commands/deploy.js +16 -11
  2. package/package.json +1 -1
  3. package/renderer/app/[...slug]/client.js +17 -0
  4. package/renderer/app/[...slug]/page.js +125 -0
  5. package/renderer/app/api/assets/[...path]/route.js +23 -4
  6. package/renderer/app/api/chat/route.js +188 -25
  7. package/renderer/app/api/collections/route.js +95 -2
  8. package/renderer/app/api/deploy/route.js +4 -0
  9. package/renderer/app/api/suggestions/route.js +98 -10
  10. package/renderer/app/globals.css +33 -0
  11. package/renderer/app/layout.js +83 -8
  12. package/renderer/components/docs/mdx/cards.js +16 -45
  13. package/renderer/components/docs/mdx/file-tree.js +102 -0
  14. package/renderer/components/docs/mdx/index.js +7 -0
  15. package/renderer/components/docs-viewer/agent/agent-chat.js +75 -11
  16. package/renderer/components/docs-viewer/agent/messages/assistant-message.js +67 -3
  17. package/renderer/components/docs-viewer/agent/messages/tool-call-display.js +49 -4
  18. package/renderer/components/docs-viewer/content/content-router.js +1 -1
  19. package/renderer/components/docs-viewer/content/doc-page.js +36 -28
  20. package/renderer/components/docs-viewer/index.js +223 -58
  21. package/renderer/components/docs-viewer/playground/graphql-playground.js +131 -33
  22. package/renderer/components/docs-viewer/shared/method-badge.js +11 -2
  23. package/renderer/components/docs-viewer/sidebar/collection-tree.js +44 -6
  24. package/renderer/components/docs-viewer/sidebar/index.js +2 -1
  25. package/renderer/components/docs-viewer/sidebar/right-sidebar.js +3 -1
  26. package/renderer/components/docs-viewer/sidebar/sidebar-item.js +5 -7
  27. package/renderer/hooks/use-route-state.js +44 -56
  28. package/renderer/lib/api-docs/agent/indexer.js +73 -12
  29. package/renderer/lib/api-docs/agent/use-suggestions.js +26 -16
  30. package/renderer/lib/api-docs/code-editor/mode-context.js +16 -18
  31. package/renderer/lib/api-docs/parsers/openapi/transformer.js +8 -1
  32. package/renderer/lib/cache/purge.js +98 -0
  33. package/renderer/lib/docs-link-utils.js +146 -0
  34. package/renderer/lib/docs-navigation-context.js +3 -2
  35. package/renderer/lib/docs-navigation.js +50 -41
  36. package/renderer/lib/rate-limit.js +203 -0
@@ -4,20 +4,26 @@
4
4
  * These functions provide simple URL updates without state management.
5
5
  * The URL is the single source of truth - components derive their state from the URL.
6
6
  *
7
- * URL Schema:
8
- * - #tab/page/{slug} → Doc page (docs view)
9
- * - #tab/endpoint/{id} → API endpoint (docs view)
10
- * - #tab/endpoint/{id}/playground → API endpoint (playground view)
11
- * - #tab/endpoint/{id}/notes → API endpoint (notes view)
12
- * - #tab/page/{slug}/notes → Doc page (notes view)
7
+ * URL Schema (path-based for SEO):
8
+ * - /[tab]/page/[slug] → Doc page (docs view)
9
+ * - /[tab]/endpoint/[id] → API endpoint (docs view)
10
+ * - /[tab]/endpoint/[id]/playground → API endpoint (playground view)
11
+ * - /[tab]/endpoint/[id]/notes → API endpoint (notes view)
12
+ * - /[tab]/page/[slug]/notes → Doc page (notes view)
13
13
  */ /**
14
+ * Helper to update URL and trigger popstate for React to pick up the change
15
+ */ function pushPath(path) {
16
+ window.history.pushState(null, '', path);
17
+ window.dispatchEvent(new PopStateEvent('popstate'));
18
+ }
19
+ /**
14
20
  * Navigate to a documentation page
15
21
  * @param tab - Tab ID (e.g., 'guides', 'graphql-api')
16
22
  * @param slug - Page slug (e.g., 'graphql/introduction')
17
23
  * @param view - View mode (docs, playground, notes). Defaults to 'docs'
18
24
  */ export function navigateToPage(tab, slug, view) {
19
25
  const viewSuffix = view && view !== 'docs' ? `/${view}` : '';
20
- window.location.hash = `${tab}/page/${slug}${viewSuffix}`;
26
+ pushPath(`/${tab}/page/${slug}${viewSuffix}`);
21
27
  }
22
28
  /**
23
29
  * Navigate to an API endpoint or GraphQL operation
@@ -26,77 +32,80 @@
26
32
  * @param view - View mode (docs, playground, notes). Defaults to 'docs'
27
33
  */ export function navigateToEndpoint(tab, id, view) {
28
34
  const viewSuffix = view && view !== 'docs' ? `/${view}` : '';
29
- window.location.hash = `${tab}/endpoint/${id}${viewSuffix}`;
35
+ pushPath(`/${tab}/endpoint/${id}${viewSuffix}`);
30
36
  }
31
37
  /**
32
38
  * Navigate to a section within a page (scroll to element)
33
39
  * @param tab - Tab ID
34
40
  * @param sectionId - Section/heading ID to scroll to
35
41
  */ export function navigateToSection(tab, sectionId) {
36
- window.location.hash = `${tab}/section/${sectionId}`;
42
+ pushPath(`/${tab}/section/${sectionId}`);
37
43
  }
38
44
  /**
39
45
  * Navigate to a tab (show default content for that tab)
40
46
  * @param tab - Tab ID
41
47
  */ export function navigateToTab(tab) {
42
- window.location.hash = tab;
48
+ pushPath(`/${tab}`);
43
49
  }
44
50
  /**
45
51
  * Switch view mode while keeping the current content
46
52
  * @param view - View mode to switch to
47
53
  */ export function switchView(view) {
48
- const hash = window.location.hash.slice(1);
49
- if (!hash) return;
54
+ const pathname = window.location.pathname;
55
+ if (!pathname || pathname === '/') return;
50
56
  // Remove existing view suffix if present
51
- const cleanHash = hash.replace(/\/(playground|notes)$/, '');
57
+ const cleanPath = pathname.replace(/\/(playground|notes)$/, '');
52
58
  // Add new view suffix (unless switching to docs, which is the default)
53
59
  const viewSuffix = view !== 'docs' ? `/${view}` : '';
54
- window.location.hash = `${cleanHash}${viewSuffix}`;
60
+ pushPath(`${cleanPath}${viewSuffix}`);
55
61
  }
56
62
  /**
57
63
  * Clear current selection, optionally staying on a tab
58
64
  * @param tab - Optional tab to stay on
59
65
  */ export function clearSelection(tab) {
60
66
  if (tab) {
61
- window.location.hash = tab;
67
+ pushPath(`/${tab}`);
62
68
  } else {
63
- // Remove hash entirely using pushState to avoid scroll jump
64
- window.history.pushState(null, '', window.location.pathname + window.location.search);
69
+ pushPath('/');
65
70
  }
66
71
  }
67
72
  /**
68
73
  * Update URL without triggering navigation (for history management)
69
- * @param hash - Hash value (without #)
70
- */ export function updateUrlSilently(hash) {
71
- window.history.replaceState(null, '', `#${hash}`);
74
+ * @param path - Path value (without leading /)
75
+ */ export function updateUrlSilently(path) {
76
+ const fullPath = path.startsWith('/') ? path : `/${path}`;
77
+ window.history.replaceState(null, '', fullPath);
72
78
  }
73
79
  /**
74
80
  * Push a new URL to history (for back/forward navigation)
75
- * @param hash - Hash value (without #)
76
- */ export function pushUrl(hash) {
77
- window.history.pushState(null, '', `#${hash}`);
81
+ * @param path - Path value (without leading /)
82
+ */ export function pushUrl(path) {
83
+ const fullPath = path.startsWith('/') ? path : `/${path}`;
84
+ pushPath(fullPath);
78
85
  }
79
86
  /**
80
- * Build hash URL strings (for href attributes)
81
- */ export const hashUrls = {
87
+ * Build URL strings (for href attributes)
88
+ */ export const urls = {
82
89
  page: (tab, slug, view)=>{
83
90
  const viewSuffix = view && view !== 'docs' ? `/${view}` : '';
84
- return `#${tab}/page/${slug}${viewSuffix}`;
91
+ return `/${tab}/page/${slug}${viewSuffix}`;
85
92
  },
86
93
  endpoint: (tab, id, view)=>{
87
94
  const viewSuffix = view && view !== 'docs' ? `/${view}` : '';
88
- return `#${tab}/endpoint/${id}${viewSuffix}`;
95
+ return `/${tab}/endpoint/${id}${viewSuffix}`;
89
96
  },
90
- section: (tab, sectionId)=>`#${tab}/section/${sectionId}`,
91
- tab: (tab)=>`#${tab}`
97
+ section: (tab, sectionId)=>`/${tab}/section/${sectionId}`,
98
+ tab: (tab)=>`/${tab}`
92
99
  };
93
100
  /**
94
- * Parse a hash URL into its components
95
- * @param hash - Hash value (with or without #)
101
+ * Parse a URL path into its components
102
+ * @param path - Path value (with or without leading /)
96
103
  * @returns Parsed components
97
- */ export function parseHashUrl(hash) {
98
- const cleanHash = hash.startsWith('#') ? hash.slice(1) : hash;
99
- if (!cleanHash) {
104
+ */ export function parseUrl(path) {
105
+ // Remove leading slash and hash if present
106
+ let cleanPath = path.startsWith('#') ? path.slice(1) : path;
107
+ cleanPath = cleanPath.startsWith('/') ? cleanPath.slice(1) : cleanPath;
108
+ if (!cleanPath) {
100
109
  return {
101
110
  tab: null,
102
111
  type: null,
@@ -104,7 +113,7 @@
104
113
  view: 'docs'
105
114
  };
106
115
  }
107
- const parts = cleanHash.split('/');
116
+ const parts = cleanPath.split('/');
108
117
  const tab = parts[0] || null;
109
118
  const type = parts[1];
110
119
  // Check if last part is a view mode
@@ -129,12 +138,12 @@
129
138
  };
130
139
  }
131
140
  /**
132
- * Check if two hash URLs point to the same content (ignoring view mode)
133
- * @param hash1 - First hash
134
- * @param hash2 - Second hash
141
+ * Check if two URLs point to the same content (ignoring view mode)
142
+ * @param path1 - First path
143
+ * @param path2 - Second path
135
144
  * @returns True if they point to the same content
136
- */ export function isSameContent(hash1, hash2) {
137
- const parsed1 = parseHashUrl(hash1);
138
- const parsed2 = parseHashUrl(hash2);
145
+ */ export function isSameContent(path1, path2) {
146
+ const parsed1 = parseUrl(path1);
147
+ const parsed2 = parseUrl(path2);
139
148
  return parsed1.tab === parsed2.tab && parsed1.type === parsed2.type && parsed1.id === parsed2.id;
140
149
  }
@@ -0,0 +1,203 @@
1
+ function _define_property(obj, key, value) {
2
+ if (key in obj) {
3
+ Object.defineProperty(obj, key, {
4
+ value: value,
5
+ enumerable: true,
6
+ configurable: true,
7
+ writable: true
8
+ });
9
+ } else {
10
+ obj[key] = value;
11
+ }
12
+ return obj;
13
+ }
14
+ /**
15
+ * IP-based Rate Limiting Utility
16
+ *
17
+ * Uses CacheUtils (Vercel KV in production, node-cache locally) for storage.
18
+ * Implements a sliding window rate limiter with configurable limits.
19
+ *
20
+ * Usage:
21
+ * ```typescript
22
+ * import { RateLimiter } from '@/lib/rate-limit';
23
+ *
24
+ * const limiter = new RateLimiter({
25
+ * prefix: 'suggestions',
26
+ * limit: 10, // 10 requests
27
+ * windowSeconds: 60, // per 60 seconds
28
+ * });
29
+ *
30
+ * // In your API route:
31
+ * const { success, remaining, reset } = await limiter.check(request);
32
+ * if (!success) {
33
+ * return new Response('Too Many Requests', {
34
+ * status: 429,
35
+ * headers: {
36
+ * 'X-RateLimit-Limit': limit.toString(),
37
+ * 'X-RateLimit-Remaining': '0',
38
+ * 'X-RateLimit-Reset': reset.toString(),
39
+ * 'Retry-After': Math.ceil((reset - Date.now()) / 1000).toString(),
40
+ * }
41
+ * });
42
+ * }
43
+ * ```
44
+ */ import { CacheUtils } from '@/lib/cache';
45
+ /**
46
+ * Get client IP from request headers
47
+ * Handles various proxy scenarios (Vercel, Cloudflare, nginx, etc.)
48
+ */ export function getClientIp(request) {
49
+ const headers = request.headers;
50
+ // Vercel / general proxy
51
+ const xForwardedFor = headers.get('x-forwarded-for');
52
+ if (xForwardedFor) {
53
+ // Take the first IP (original client) from the chain
54
+ const firstIp = xForwardedFor.split(',')[0].trim();
55
+ if (firstIp) return firstIp;
56
+ }
57
+ // Vercel-specific
58
+ const xRealIp = headers.get('x-real-ip');
59
+ if (xRealIp) return xRealIp;
60
+ // Cloudflare
61
+ const cfConnectingIp = headers.get('cf-connecting-ip');
62
+ if (cfConnectingIp) return cfConnectingIp;
63
+ // True-Client-IP (Akamai, Cloudflare Enterprise)
64
+ const trueClientIp = headers.get('true-client-ip');
65
+ if (trueClientIp) return trueClientIp;
66
+ return null;
67
+ }
68
+ /**
69
+ * Rate limiter class using sliding window algorithm
70
+ */ export class RateLimiter {
71
+ /**
72
+ * Generate cache key for rate limiting
73
+ */ getCacheKey(identifier) {
74
+ return `ratelimit:${this.config.prefix}:${identifier}`;
75
+ }
76
+ /**
77
+ * Check if request is allowed and update counter
78
+ */ async check(request) {
79
+ const { limit, windowSeconds, skipIps, getIdentifier } = this.config;
80
+ // Get identifier (IP or custom)
81
+ const identifier = getIdentifier ? getIdentifier(request) : getClientIp(request);
82
+ // If we can't identify the client, allow the request but log it
83
+ if (!identifier) {
84
+ console.warn('[RateLimiter] Could not identify client, allowing request');
85
+ return {
86
+ success: true,
87
+ remaining: limit,
88
+ reset: Date.now() + windowSeconds * 1000,
89
+ count: 0,
90
+ limit
91
+ };
92
+ }
93
+ // Skip rate limiting for certain IPs
94
+ if (skipIps?.includes(identifier)) {
95
+ return {
96
+ success: true,
97
+ remaining: limit,
98
+ reset: Date.now() + windowSeconds * 1000,
99
+ count: 0,
100
+ limit
101
+ };
102
+ }
103
+ const cacheKey = this.getCacheKey(identifier);
104
+ const now = Date.now();
105
+ // Get current rate limit entry
106
+ let entry = await CacheUtils.get(cacheKey);
107
+ // If no entry or window has expired, create new window
108
+ if (!entry || entry.resetAt <= now) {
109
+ entry = {
110
+ count: 1,
111
+ resetAt: now + windowSeconds * 1000
112
+ };
113
+ await CacheUtils.set(cacheKey, entry, windowSeconds);
114
+ return {
115
+ success: true,
116
+ remaining: limit - 1,
117
+ reset: entry.resetAt,
118
+ count: 1,
119
+ limit
120
+ };
121
+ }
122
+ // Window still active, check if limit exceeded
123
+ if (entry.count >= limit) {
124
+ console.log(`[RateLimiter] Rate limit exceeded for ${identifier} on ${this.config.prefix}`);
125
+ return {
126
+ success: false,
127
+ remaining: 0,
128
+ reset: entry.resetAt,
129
+ count: entry.count,
130
+ limit
131
+ };
132
+ }
133
+ // Increment counter
134
+ entry.count += 1;
135
+ const remainingTtl = Math.ceil((entry.resetAt - now) / 1000);
136
+ await CacheUtils.set(cacheKey, entry, remainingTtl);
137
+ return {
138
+ success: true,
139
+ remaining: limit - entry.count,
140
+ reset: entry.resetAt,
141
+ count: entry.count,
142
+ limit
143
+ };
144
+ }
145
+ /**
146
+ * Create rate limit headers for response
147
+ */ static createHeaders(result) {
148
+ const headers = {
149
+ 'X-RateLimit-Limit': result.limit.toString(),
150
+ 'X-RateLimit-Remaining': result.remaining.toString(),
151
+ 'X-RateLimit-Reset': result.reset.toString()
152
+ };
153
+ if (!result.success) {
154
+ const retryAfter = Math.ceil((result.reset - Date.now()) / 1000);
155
+ headers['Retry-After'] = Math.max(1, retryAfter).toString();
156
+ }
157
+ return headers;
158
+ }
159
+ /**
160
+ * Create a 429 Too Many Requests response
161
+ */ static tooManyRequestsResponse(result, message) {
162
+ return new Response(JSON.stringify({
163
+ error: 'Too Many Requests',
164
+ message: message || 'Rate limit exceeded. Please try again later.',
165
+ retryAfter: Math.ceil((result.reset - Date.now()) / 1000)
166
+ }), {
167
+ status: 429,
168
+ headers: {
169
+ 'Content-Type': 'application/json',
170
+ ...RateLimiter.createHeaders(result)
171
+ }
172
+ });
173
+ }
174
+ constructor(config){
175
+ _define_property(this, "config", void 0);
176
+ this.config = {
177
+ skipIps: [
178
+ '127.0.0.1',
179
+ '::1'
180
+ ],
181
+ ...config
182
+ };
183
+ }
184
+ }
185
+ // Pre-configured rate limiters for common use cases
186
+ export const rateLimiters = {
187
+ /** Suggestions API: 30 requests per minute */ suggestions: new RateLimiter({
188
+ prefix: 'suggestions',
189
+ limit: 30,
190
+ windowSeconds: 60
191
+ }),
192
+ /** Chat API: 20 requests per minute */ chat: new RateLimiter({
193
+ prefix: 'chat',
194
+ limit: 20,
195
+ windowSeconds: 60
196
+ }),
197
+ /** General API: 100 requests per minute */ general: new RateLimiter({
198
+ prefix: 'general',
199
+ limit: 100,
200
+ windowSeconds: 60
201
+ })
202
+ };
203
+ export default RateLimiter;