@blaaiz/docs-core 0.1.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.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +103 -0
  3. package/dist/chunk-3ZX4WIE3.js +2984 -0
  4. package/dist/chunk-3ZX4WIE3.js.map +1 -0
  5. package/dist/chunk-JCYR6RPE.js +31 -0
  6. package/dist/chunk-JCYR6RPE.js.map +1 -0
  7. package/dist/chunk-ZKOOKLZ3.js +124 -0
  8. package/dist/chunk-ZKOOKLZ3.js.map +1 -0
  9. package/dist/cli.js +534 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/generator.cjs +508 -0
  12. package/dist/generator.cjs.map +1 -0
  13. package/dist/generator.d.cts +122 -0
  14. package/dist/generator.d.ts +122 -0
  15. package/dist/generator.js +177 -0
  16. package/dist/generator.js.map +1 -0
  17. package/dist/index.cjs +3024 -0
  18. package/dist/index.cjs.map +1 -0
  19. package/dist/index.d.cts +1182 -0
  20. package/dist/index.d.ts +1182 -0
  21. package/dist/index.js +3 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/navigation-CGqFIPlP.d.cts +498 -0
  24. package/dist/navigation-CGqFIPlP.d.ts +498 -0
  25. package/dist/openapi-types-CJ6p5Cux.d.cts +78 -0
  26. package/dist/openapi-types-CJ6p5Cux.d.ts +78 -0
  27. package/dist/ui/api-try-it.cjs +654 -0
  28. package/dist/ui/api-try-it.cjs.map +1 -0
  29. package/dist/ui/api-try-it.d.cts +78 -0
  30. package/dist/ui/api-try-it.d.ts +78 -0
  31. package/dist/ui/api-try-it.js +509 -0
  32. package/dist/ui/api-try-it.js.map +1 -0
  33. package/dist/ui/ask-ai.cjs +810 -0
  34. package/dist/ui/ask-ai.cjs.map +1 -0
  35. package/dist/ui/ask-ai.d.cts +57 -0
  36. package/dist/ui/ask-ai.d.ts +57 -0
  37. package/dist/ui/ask-ai.js +808 -0
  38. package/dist/ui/ask-ai.js.map +1 -0
  39. package/dist/ui/copy-page.cjs +312 -0
  40. package/dist/ui/copy-page.cjs.map +1 -0
  41. package/dist/ui/copy-page.d.cts +33 -0
  42. package/dist/ui/copy-page.d.ts +33 -0
  43. package/dist/ui/copy-page.js +183 -0
  44. package/dist/ui/copy-page.js.map +1 -0
  45. package/dist/ui/mermaid.cjs +363 -0
  46. package/dist/ui/mermaid.cjs.map +1 -0
  47. package/dist/ui/mermaid.d.cts +13 -0
  48. package/dist/ui/mermaid.d.ts +13 -0
  49. package/dist/ui/mermaid.js +361 -0
  50. package/dist/ui/mermaid.js.map +1 -0
  51. package/dist/ui.cjs +661 -0
  52. package/dist/ui.cjs.map +1 -0
  53. package/dist/ui.d.cts +428 -0
  54. package/dist/ui.d.ts +428 -0
  55. package/dist/ui.js +537 -0
  56. package/dist/ui.js.map +1 -0
  57. package/package.json +145 -0
  58. package/patches/fumadocs-openapi.patch +173 -0
  59. package/skills/AGENTS-section.md +36 -0
  60. package/skills/SKILL.md +363 -0
  61. package/styles/api-reference.css +1417 -0
  62. package/styles/ask-ai.css +563 -0
  63. package/styles/auth.css +462 -0
  64. package/styles/docs.css +247 -0
  65. package/styles/home.css +376 -0
  66. package/templates/init/content/docs/index.mdx.tmpl +52 -0
  67. package/templates/init/content/docs/meta.json.tmpl +3 -0
  68. package/templates/init/content/docs.json.tmpl +12 -0
  69. package/templates/init/content/nav.json.tmpl +7 -0
  70. package/templates/init/docs.config.ts.tmpl +36 -0
  71. package/templates/init/env.example.tmpl +15 -0
@@ -0,0 +1,2984 @@
1
+ // src/core/ai/models.ts
2
+ var AI_PROVIDER_NAMES = Object.freeze([
3
+ "anthropic",
4
+ "openai"
5
+ ]);
6
+ var AI_PROVIDER_MODELS = Object.freeze(
7
+ {
8
+ anthropic: Object.freeze({
9
+ defaultModel: "claude-sonnet-5",
10
+ models: Object.freeze([
11
+ "claude-sonnet-5",
12
+ "claude-haiku-4-5",
13
+ "claude-opus-5",
14
+ "claude-opus-4-8",
15
+ "claude-sonnet-4-6"
16
+ ]),
17
+ defaultApiKeyEnv: "ANTHROPIC_API_KEY"
18
+ }),
19
+ openai: Object.freeze({
20
+ defaultModel: "gpt-5-mini",
21
+ models: Object.freeze([
22
+ "gpt-5-mini",
23
+ "gpt-5",
24
+ "gpt-5.2",
25
+ "gpt-5-nano",
26
+ "gpt-4.1",
27
+ "gpt-4.1-mini",
28
+ "gpt-4o",
29
+ "gpt-4o-mini"
30
+ ]),
31
+ defaultApiKeyEnv: "OPENAI_API_KEY"
32
+ })
33
+ }
34
+ );
35
+ function isAiProviderName(value) {
36
+ return typeof value === "string" && AI_PROVIDER_NAMES.includes(value);
37
+ }
38
+ function isAllowedAiModel(provider, model) {
39
+ return AI_PROVIDER_MODELS[provider].models.includes(model);
40
+ }
41
+
42
+ // src/core/ai/config.ts
43
+ var DEFAULT_RATE_LIMIT_PER_MINUTE = 10;
44
+ var DEFAULT_MAX_CONTEXT_PAGES = 6;
45
+ var MIN_RATE_LIMIT_PER_MINUTE = 1;
46
+ var MAX_RATE_LIMIT_PER_MINUTE = 600;
47
+ var MIN_MAX_CONTEXT_PAGES = 1;
48
+ var MAX_MAX_CONTEXT_PAGES = 20;
49
+ var MAX_SYSTEM_PROMPT_CHARS = 4e3;
50
+ var AiConfigError = class extends Error {
51
+ constructor(message) {
52
+ super(message);
53
+ this.name = "AiConfigError";
54
+ }
55
+ };
56
+ function describe(value) {
57
+ if (typeof value === "string") {
58
+ return `'${value}'`;
59
+ }
60
+ if (typeof value === "number" || typeof value === "boolean" || value === null) {
61
+ return String(value);
62
+ }
63
+ if (value === void 0) {
64
+ return "undefined";
65
+ }
66
+ return Array.isArray(value) ? "an array" : `a ${typeof value}`;
67
+ }
68
+ function quoteList(values) {
69
+ return values.map((value) => `'${value}'`).join(", ");
70
+ }
71
+ function requireOptionalString(value, field) {
72
+ if (value === void 0) {
73
+ return void 0;
74
+ }
75
+ if (typeof value !== "string" || value.trim() === "") {
76
+ throw new AiConfigError(
77
+ `${field} must be a non-empty string, got ${describe(value)}. Remove the field to use the default.`
78
+ );
79
+ }
80
+ return value.trim();
81
+ }
82
+ function requireOptionalIntegerInRange(value, field, min, max, fallback) {
83
+ if (value === void 0) {
84
+ return fallback;
85
+ }
86
+ if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max) {
87
+ throw new AiConfigError(
88
+ `${field} must be an integer between ${String(min)} and ${String(max)}, got ${describe(value)}.`
89
+ );
90
+ }
91
+ return value;
92
+ }
93
+ function resolveAiConfig(config) {
94
+ const candidate = config;
95
+ if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) {
96
+ throw new AiConfigError(
97
+ `ai must be an object, got ${describe(candidate)}. Example: ai: { provider: 'anthropic' }.`
98
+ );
99
+ }
100
+ const raw = candidate;
101
+ const provider = raw["provider"];
102
+ if (!isAiProviderName(provider)) {
103
+ throw new AiConfigError(
104
+ `ai.provider must be one of ${quoteList(AI_PROVIDER_NAMES)}, got ${describe(provider)}.`
105
+ );
106
+ }
107
+ const registry = AI_PROVIDER_MODELS[provider];
108
+ const model = requireOptionalString(raw["model"], "ai.model") ?? registry.defaultModel;
109
+ if (!isAllowedAiModel(provider, model)) {
110
+ throw new AiConfigError(
111
+ `ai.model must be one of ${quoteList(registry.models)} for provider '${provider}', got ${describe(model)}.`
112
+ );
113
+ }
114
+ const apiKeyEnv = requireOptionalString(raw["apiKeyEnv"], "ai.apiKeyEnv") ?? registry.defaultApiKeyEnv;
115
+ const rateLimitPerMinute = requireOptionalIntegerInRange(
116
+ raw["rateLimitPerMinute"],
117
+ "ai.rateLimitPerMinute",
118
+ MIN_RATE_LIMIT_PER_MINUTE,
119
+ MAX_RATE_LIMIT_PER_MINUTE,
120
+ DEFAULT_RATE_LIMIT_PER_MINUTE
121
+ );
122
+ const maxContextPages = requireOptionalIntegerInRange(
123
+ raw["maxContextPages"],
124
+ "ai.maxContextPages",
125
+ MIN_MAX_CONTEXT_PAGES,
126
+ MAX_MAX_CONTEXT_PAGES,
127
+ DEFAULT_MAX_CONTEXT_PAGES
128
+ );
129
+ const systemPrompt = raw["systemPrompt"];
130
+ if (systemPrompt !== void 0) {
131
+ if (typeof systemPrompt !== "string") {
132
+ throw new AiConfigError(`ai.systemPrompt must be a string, got ${describe(systemPrompt)}.`);
133
+ }
134
+ if (systemPrompt.length > MAX_SYSTEM_PROMPT_CHARS) {
135
+ throw new AiConfigError(
136
+ `ai.systemPrompt must be ${String(MAX_SYSTEM_PROMPT_CHARS)} characters or fewer, got ${String(systemPrompt.length)}.`
137
+ );
138
+ }
139
+ }
140
+ const trimmedPrompt = typeof systemPrompt === "string" ? systemPrompt.trim() : "";
141
+ return Object.freeze({
142
+ provider,
143
+ model,
144
+ apiKeyEnv,
145
+ defaultApiKeyEnv: registry.defaultApiKeyEnv,
146
+ rateLimitPerMinute,
147
+ maxContextPages,
148
+ ...trimmedPrompt === "" ? {} : { systemPrompt: trimmedPrompt }
149
+ });
150
+ }
151
+
152
+ // src/core/search.ts
153
+ var K1 = 1.2;
154
+ var B = 0.75;
155
+ var TITLE_BONUS = 1;
156
+ var SECTION_BONUS = 0.5;
157
+ var PREFIX_WEIGHT = 0.4;
158
+ var STOPWORDS = /* @__PURE__ */ new Set([
159
+ "a",
160
+ "an",
161
+ "and",
162
+ "are",
163
+ "as",
164
+ "at",
165
+ "be",
166
+ "but",
167
+ "by",
168
+ "can",
169
+ "do",
170
+ "does",
171
+ "for",
172
+ "from",
173
+ "get",
174
+ "has",
175
+ "have",
176
+ "how",
177
+ "i",
178
+ "if",
179
+ "in",
180
+ "is",
181
+ "it",
182
+ "its",
183
+ "me",
184
+ "my",
185
+ "need",
186
+ "no",
187
+ "not",
188
+ "of",
189
+ "on",
190
+ "or",
191
+ "our",
192
+ "should",
193
+ "so",
194
+ "that",
195
+ "the",
196
+ "their",
197
+ "then",
198
+ "there",
199
+ "these",
200
+ "they",
201
+ "this",
202
+ "to",
203
+ "up",
204
+ "us",
205
+ "want",
206
+ "was",
207
+ "we",
208
+ "what",
209
+ "when",
210
+ "where",
211
+ "which",
212
+ "who",
213
+ "why",
214
+ "will",
215
+ "with",
216
+ "would",
217
+ "you",
218
+ "your"
219
+ ]);
220
+ var DOUBLED_CONSONANT = /([bcdfghjklmnpqrstvz])\1$/;
221
+ function stem(token) {
222
+ let word = token;
223
+ if (word.length >= 5 && word.endsWith("ies")) {
224
+ word = `${word.slice(0, -3)}y`;
225
+ } else if (word.length >= 5 && /(?:ss|sh|ch|x|z)es$/.test(word)) {
226
+ word = word.slice(0, -2);
227
+ } else if (word.length >= 4 && word.endsWith("s") && !word.endsWith("ss") && !word.endsWith("us") && // Protect -is singulars (analysis, basis), but only long ones: a short
228
+ // word ending in -is is far more likely a plural like `apis` or `uris`.
229
+ !(word.endsWith("is") && word.length >= 6)) {
230
+ word = word.slice(0, -1);
231
+ }
232
+ if (word.length >= 6 && word.endsWith("ing")) {
233
+ word = word.slice(0, -3).replace(DOUBLED_CONSONANT, "$1");
234
+ } else if (word.length >= 5 && word.endsWith("ed")) {
235
+ word = word.slice(0, -2).replace(DOUBLED_CONSONANT, "$1");
236
+ }
237
+ if (word.length >= 7 && word.endsWith("ation")) {
238
+ word = `${word.slice(0, -5)}ate`;
239
+ }
240
+ if (word.length >= 7 && word.endsWith("ate")) {
241
+ word = word.slice(0, -3);
242
+ }
243
+ if (word.length >= 5 && word.endsWith("e")) {
244
+ word = word.slice(0, -1);
245
+ }
246
+ return word;
247
+ }
248
+ function tokenize(text) {
249
+ return text.toLowerCase().match(/[a-z0-9]+/g) ?? [];
250
+ }
251
+ function stemmedTokens(text) {
252
+ return tokenize(text).map(stem);
253
+ }
254
+ function buildSearchIndex(documents) {
255
+ const documentFrequency = {};
256
+ let totalLength = 0;
257
+ const indexed = documents.map((doc) => {
258
+ const tokens = stemmedTokens(`${doc.title} ${doc.section ?? ""} ${doc.body ?? ""}`);
259
+ const terms = {};
260
+ for (const token of tokens) {
261
+ terms[token] = (terms[token] ?? 0) + 1;
262
+ }
263
+ for (const term of Object.keys(terms)) {
264
+ documentFrequency[term] = (documentFrequency[term] ?? 0) + 1;
265
+ }
266
+ totalLength += tokens.length;
267
+ return {
268
+ id: doc.id,
269
+ title: doc.title,
270
+ path: doc.path,
271
+ terms,
272
+ length: tokens.length,
273
+ titleTerms: [...new Set(stemmedTokens(doc.title))],
274
+ ...doc.section !== void 0 ? { section: doc.section, sectionTerms: [...new Set(stemmedTokens(doc.section))] } : {}
275
+ };
276
+ });
277
+ return {
278
+ documents: indexed,
279
+ documentFrequency,
280
+ vocabulary: Object.keys(documentFrequency).sort(),
281
+ averageLength: indexed.length > 0 ? totalLength / indexed.length : 0
282
+ };
283
+ }
284
+ function inverseDocumentFrequency(documentCount, withTerm) {
285
+ return Math.max(0.05, Math.log(1 + (documentCount - withTerm + 0.5) / (withTerm + 0.5)));
286
+ }
287
+ function queryTerms(query) {
288
+ const tokens = tokenize(query);
289
+ const meaningful = tokens.filter((token) => !STOPWORDS.has(token) && token.length > 1);
290
+ return (meaningful.length > 0 ? meaningful : tokens).map(stem);
291
+ }
292
+ function lowerBound(vocabulary, prefix) {
293
+ let low = 0;
294
+ let high = vocabulary.length;
295
+ while (low < high) {
296
+ const mid = low + high >> 1;
297
+ if ((vocabulary[mid] ?? "") < prefix) {
298
+ low = mid + 1;
299
+ } else {
300
+ high = mid;
301
+ }
302
+ }
303
+ return low;
304
+ }
305
+ function termsWithPrefix(vocabulary, prefix) {
306
+ const matches = [];
307
+ for (let at = lowerBound(vocabulary, prefix); at < vocabulary.length; at++) {
308
+ const term = vocabulary[at] ?? "";
309
+ if (!term.startsWith(prefix)) {
310
+ break;
311
+ }
312
+ matches.push(term);
313
+ }
314
+ return matches;
315
+ }
316
+ function collectPrefixStats(index, unmatchedTerms) {
317
+ const stats = /* @__PURE__ */ new Map();
318
+ for (const term of unmatchedTerms) {
319
+ const matching = termsWithPrefix(index.vocabulary, term);
320
+ const frequencyByDocument = /* @__PURE__ */ new Map();
321
+ if (matching.length > 0) {
322
+ index.documents.forEach((doc, position) => {
323
+ let frequency = 0;
324
+ for (const match of matching) {
325
+ frequency += doc.terms[match] ?? 0;
326
+ }
327
+ if (frequency > 0) {
328
+ frequencyByDocument.set(position, frequency);
329
+ }
330
+ });
331
+ }
332
+ stats.set(term, { frequencyByDocument, documentsWithPrefix: frequencyByDocument.size });
333
+ }
334
+ return stats;
335
+ }
336
+ function compareStrings(a, b) {
337
+ return a < b ? -1 : a > b ? 1 : 0;
338
+ }
339
+ function searchIndex(index, query, limit = 8) {
340
+ const terms = queryTerms(query);
341
+ if (terms.length === 0 || index.documents.length === 0) {
342
+ return [];
343
+ }
344
+ const documentCount = index.documents.length;
345
+ const unmatched = terms.filter(
346
+ (term) => term.length >= 2 && (index.documentFrequency[term] ?? 0) === 0
347
+ );
348
+ const prefixStats = unmatched.length > 0 ? collectPrefixStats(index, unmatched) : void 0;
349
+ const results = [];
350
+ index.documents.forEach((doc, position) => {
351
+ const lengthNorm = 1 - B + B * (index.averageLength > 0 ? doc.length / index.averageLength : 1);
352
+ let score = 0;
353
+ for (const term of terms) {
354
+ const exactDocuments = index.documentFrequency[term] ?? 0;
355
+ let frequency;
356
+ let idf;
357
+ let weight;
358
+ let inTitle;
359
+ let inSection;
360
+ if (exactDocuments > 0) {
361
+ frequency = doc.terms[term] ?? 0;
362
+ idf = inverseDocumentFrequency(documentCount, exactDocuments);
363
+ weight = 1;
364
+ inTitle = doc.titleTerms.includes(term);
365
+ inSection = doc.sectionTerms?.includes(term) ?? false;
366
+ } else {
367
+ const stats = prefixStats?.get(term);
368
+ if (stats === void 0 || stats.documentsWithPrefix === 0) {
369
+ continue;
370
+ }
371
+ frequency = stats.frequencyByDocument.get(position) ?? 0;
372
+ idf = inverseDocumentFrequency(documentCount, stats.documentsWithPrefix);
373
+ weight = PREFIX_WEIGHT;
374
+ inTitle = doc.titleTerms.some((titleTerm) => titleTerm.startsWith(term));
375
+ inSection = doc.sectionTerms?.some((sectionTerm) => sectionTerm.startsWith(term)) ?? false;
376
+ }
377
+ if (frequency > 0) {
378
+ score += weight * idf * (frequency * (K1 + 1) / (frequency + K1 * lengthNorm));
379
+ }
380
+ if (inTitle) {
381
+ score += weight * idf * TITLE_BONUS;
382
+ }
383
+ if (inSection) {
384
+ score += weight * idf * SECTION_BONUS;
385
+ }
386
+ }
387
+ if (score > 0) {
388
+ results.push({
389
+ id: doc.id,
390
+ title: doc.title,
391
+ path: doc.path,
392
+ score,
393
+ ...doc.section !== void 0 ? { section: doc.section } : {}
394
+ });
395
+ }
396
+ });
397
+ results.sort(
398
+ (a, b) => b.score - a.score || compareStrings(a.title, b.title) || compareStrings(a.path, b.path)
399
+ );
400
+ return results.slice(0, limit);
401
+ }
402
+
403
+ // src/core/rate-limit.ts
404
+ var SWEEP_THRESHOLD = 5e3;
405
+ var RATE_LIMIT_WINDOW_MS = 6e4;
406
+ function clientRateLimitKey(request) {
407
+ const forwarded = request.headers.get("x-forwarded-for");
408
+ const first = forwarded?.split(",")[0]?.trim() ?? "";
409
+ if (first !== "") {
410
+ return first;
411
+ }
412
+ const realIp = request.headers.get("x-real-ip")?.trim() ?? "";
413
+ return realIp === "" ? "unknown" : realIp;
414
+ }
415
+ function createRateLimiter(options) {
416
+ const now = options.now ?? (() => Date.now());
417
+ const hits = /* @__PURE__ */ new Map();
418
+ function prune(timestamps, cutoff) {
419
+ return timestamps.filter((at) => at > cutoff);
420
+ }
421
+ function sweep(cutoff) {
422
+ for (const [key, timestamps] of hits) {
423
+ const fresh = prune(timestamps, cutoff);
424
+ if (fresh.length === 0) {
425
+ hits.delete(key);
426
+ } else {
427
+ hits.set(key, fresh);
428
+ }
429
+ }
430
+ }
431
+ return {
432
+ allow(key) {
433
+ const at = now();
434
+ const cutoff = at - options.windowMs;
435
+ if (hits.size > SWEEP_THRESHOLD) {
436
+ sweep(cutoff);
437
+ }
438
+ const timestamps = prune(hits.get(key) ?? [], cutoff);
439
+ if (timestamps.length >= options.limit) {
440
+ hits.set(key, timestamps);
441
+ const oldest = timestamps[0] ?? at;
442
+ const waitMs = oldest + options.windowMs - at;
443
+ return { allowed: false, retryAfterSeconds: Math.max(1, Math.ceil(waitMs / 1e3)) };
444
+ }
445
+ timestamps.push(at);
446
+ hits.set(key, timestamps);
447
+ return { allowed: true };
448
+ }
449
+ };
450
+ }
451
+
452
+ // src/core/ai/prompt.ts
453
+ var RULES = [
454
+ "Answer only from the documentation below. Never use outside knowledge, and never guess.",
455
+ "When the documentation does not answer the question, say so plainly and name what is missing. Do not invent an answer.",
456
+ "Never invent a URL, an endpoint, a parameter, a field name, or a value. Copy them exactly from the documentation.",
457
+ "The pages are ordered best-match first. Prefer the most relevant page, but combine pages when the answer spans them \u2014 for example an endpoint page and its related guide.",
458
+ "The pages are re-retrieved for every question, so earlier turns in this conversation may have used pages that are not shown now. Never call an earlier answer wrong just because its page is absent here.",
459
+ "A page ending in [content truncated] has more on the site. If the answer may sit in the missing part, say so and point the reader to that page.",
460
+ "Be concise. Answer the question first, then add only the detail that matters.",
461
+ "Use Markdown: short paragraphs, lists where they help, and fenced code blocks for code.",
462
+ "Refer to a page by its title, in the sentence, where it helps the reader go deeper.",
463
+ "Reply in the language the reader used."
464
+ ];
465
+ function renderPage(page, position) {
466
+ return [
467
+ `=== PAGE ${String(position)}: ${page.title} (${page.url}) ===`,
468
+ page.markdown,
469
+ `=== END PAGE ${String(position)} ===`
470
+ ].join("\n");
471
+ }
472
+ function buildSystemPrompt(options) {
473
+ const sections = [
474
+ `You are the documentation assistant for ${options.siteName}. You answer questions about ${options.siteName} using its documentation.`,
475
+ RULES.map((rule) => `- ${rule}`).join("\n")
476
+ ];
477
+ if (options.pages.length === 0) {
478
+ sections.push(
479
+ "DOCUMENTATION\n\nNo documentation page matched this question. Tell the reader you cannot find it in the documentation, and suggest they rephrase the question or use the search box."
480
+ );
481
+ } else {
482
+ sections.push(
483
+ [
484
+ "DOCUMENTATION",
485
+ `The following ${String(options.pages.length)} ${options.pages.length === 1 ? "page is" : "pages are"} the only source you may use.`,
486
+ options.pages.map((page, position) => renderPage(page, position + 1)).join("\n\n")
487
+ ].join("\n\n")
488
+ );
489
+ }
490
+ const extra = options.extraInstructions?.trim() ?? "";
491
+ if (extra !== "") {
492
+ sections.push(`ADDITIONAL INSTRUCTIONS FROM THIS SITE
493
+
494
+ ${extra}`);
495
+ }
496
+ return sections.join("\n\n");
497
+ }
498
+
499
+ // src/core/ai/providers/error.ts
500
+ var MAX_MESSAGE_CHARS = 300;
501
+ var SECRET_PATTERNS = [
502
+ /sk-[A-Za-z0-9_*-]{6,}/g,
503
+ /\bBearer\s+[A-Za-z0-9._-]{8,}/gi
504
+ ];
505
+ function redactSecrets(text) {
506
+ return SECRET_PATTERNS.reduce((masked, pattern) => masked.replace(pattern, "[redacted]"), text);
507
+ }
508
+ function messageFromJson(payload) {
509
+ if (typeof payload !== "object" || payload === null) {
510
+ return void 0;
511
+ }
512
+ const record = payload;
513
+ const error = record["error"];
514
+ if (typeof error === "string" && error.trim() !== "") {
515
+ return error;
516
+ }
517
+ if (typeof error === "object" && error !== null) {
518
+ const errorRecord = error;
519
+ for (const key of ["message", "type", "code"]) {
520
+ const value = errorRecord[key];
521
+ if (typeof value === "string" && value.trim() !== "") {
522
+ return value;
523
+ }
524
+ }
525
+ }
526
+ const message = record["message"];
527
+ return typeof message === "string" && message.trim() !== "" ? message : void 0;
528
+ }
529
+ function shorten(text) {
530
+ return text.length > MAX_MESSAGE_CHARS ? `${text.slice(0, MAX_MESSAGE_CHARS - 1)}\u2026` : text;
531
+ }
532
+ async function readProviderError(response, providerLabel) {
533
+ const statusLine = `${providerLabel} refused the request: ${String(response.status)} ${response.statusText === "" ? "error" : response.statusText}.`;
534
+ let raw;
535
+ try {
536
+ raw = (await response.text()).trim();
537
+ } catch {
538
+ return statusLine;
539
+ }
540
+ if (raw === "" || raw.startsWith("<") || raw.length > 4e3) {
541
+ return statusLine;
542
+ }
543
+ let parsed;
544
+ try {
545
+ parsed = JSON.parse(raw);
546
+ } catch {
547
+ parsed = void 0;
548
+ }
549
+ const message = messageFromJson(parsed);
550
+ if (message === void 0) {
551
+ return raw.length > MAX_MESSAGE_CHARS ? statusLine : `${providerLabel} refused the request: ${redactSecrets(shorten(raw.replace(/\s+/g, " ")))}`;
552
+ }
553
+ return `${providerLabel} refused the request: ${redactSecrets(shorten(message.replace(/\s+/g, " ")))}`;
554
+ }
555
+
556
+ // src/core/ai/providers/sse.ts
557
+ function takeLine(buffer) {
558
+ const match = /\r\n|\n|\r/.exec(buffer);
559
+ if (match === null) {
560
+ return null;
561
+ }
562
+ if (match[0] === "\r" && match.index + 1 === buffer.length) {
563
+ return null;
564
+ }
565
+ return {
566
+ line: buffer.slice(0, match.index),
567
+ rest: buffer.slice(match.index + match[0].length)
568
+ };
569
+ }
570
+ var FrameBuilder = class {
571
+ event;
572
+ data = [];
573
+ /** Apply one non-blank line. */
574
+ add(line) {
575
+ if (line.startsWith(":")) {
576
+ return;
577
+ }
578
+ const colon = line.indexOf(":");
579
+ const field = colon === -1 ? line : line.slice(0, colon);
580
+ const rawValue = colon === -1 ? "" : line.slice(colon + 1);
581
+ const value = rawValue.startsWith(" ") ? rawValue.slice(1) : rawValue;
582
+ if (field === "event") {
583
+ this.event = value;
584
+ } else if (field === "data") {
585
+ this.data.push(value);
586
+ }
587
+ }
588
+ /** Take the pending frame, or `null` when nothing has accumulated. */
589
+ take() {
590
+ if (this.event === void 0 && this.data.length === 0) {
591
+ return null;
592
+ }
593
+ const frame = { event: this.event, data: this.data.join("\n") };
594
+ this.event = void 0;
595
+ this.data = [];
596
+ return frame;
597
+ }
598
+ };
599
+ async function* readSseFrames(body) {
600
+ if (body === null) {
601
+ return;
602
+ }
603
+ const reader = body.getReader();
604
+ const decoder2 = new TextDecoder();
605
+ const builder = new FrameBuilder();
606
+ let buffer = "";
607
+ try {
608
+ for (; ; ) {
609
+ const { done, value } = await reader.read();
610
+ if (done) {
611
+ break;
612
+ }
613
+ buffer += decoder2.decode(value, { stream: true });
614
+ for (; ; ) {
615
+ const taken = takeLine(buffer);
616
+ if (taken === null) {
617
+ break;
618
+ }
619
+ buffer = taken.rest;
620
+ if (taken.line === "") {
621
+ const frame = builder.take();
622
+ if (frame !== null) {
623
+ yield frame;
624
+ }
625
+ } else {
626
+ builder.add(taken.line);
627
+ }
628
+ }
629
+ }
630
+ buffer += decoder2.decode();
631
+ if (buffer !== "") {
632
+ builder.add(buffer);
633
+ }
634
+ const last = builder.take();
635
+ if (last !== null) {
636
+ yield last;
637
+ }
638
+ } finally {
639
+ reader.releaseLock();
640
+ }
641
+ }
642
+ function parseFrameData(data) {
643
+ if (data === "") {
644
+ return void 0;
645
+ }
646
+ try {
647
+ return JSON.parse(data);
648
+ } catch {
649
+ return void 0;
650
+ }
651
+ }
652
+ function frameEventType(frame, payload) {
653
+ if (frame.event !== void 0 && frame.event !== "") {
654
+ return frame.event;
655
+ }
656
+ if (typeof payload === "object" && payload !== null) {
657
+ const type = payload["type"];
658
+ if (typeof type === "string" && type !== "") {
659
+ return type;
660
+ }
661
+ }
662
+ return void 0;
663
+ }
664
+
665
+ // src/core/ai/providers/anthropic.ts
666
+ var MESSAGES_URL = "https://api.anthropic.com/v1/messages";
667
+ var API_VERSION = "2023-06-01";
668
+ var PROVIDER_LABEL = "Anthropic";
669
+ function requestBody(request, model) {
670
+ return JSON.stringify({
671
+ model,
672
+ max_tokens: request.maxOutputTokens,
673
+ system: request.system,
674
+ messages: request.messages.map((message) => ({
675
+ role: message.role,
676
+ content: message.content
677
+ })),
678
+ stream: true
679
+ });
680
+ }
681
+ function textDelta(payload) {
682
+ if (typeof payload !== "object" || payload === null) {
683
+ return void 0;
684
+ }
685
+ const delta = payload["delta"];
686
+ if (typeof delta !== "object" || delta === null) {
687
+ return void 0;
688
+ }
689
+ const record = delta;
690
+ if (record["type"] !== "text_delta") {
691
+ return void 0;
692
+ }
693
+ const text = record["text"];
694
+ return typeof text === "string" ? text : void 0;
695
+ }
696
+ function errorMessage(payload) {
697
+ if (typeof payload === "object" && payload !== null) {
698
+ const error = payload["error"];
699
+ if (typeof error === "object" && error !== null) {
700
+ const message = error["message"];
701
+ if (typeof message === "string" && message.trim() !== "") {
702
+ return message;
703
+ }
704
+ }
705
+ }
706
+ return "The Anthropic stream failed.";
707
+ }
708
+ function createAnthropicProvider(options) {
709
+ const doFetch = options.fetch ?? ((url, init) => fetch(url, init));
710
+ return {
711
+ async *stream(request) {
712
+ let response;
713
+ try {
714
+ response = await doFetch(MESSAGES_URL, {
715
+ method: "POST",
716
+ headers: {
717
+ "x-api-key": options.apiKey,
718
+ "anthropic-version": API_VERSION,
719
+ "content-type": "application/json"
720
+ },
721
+ body: requestBody(request, options.model),
722
+ ...request.signal ? { signal: request.signal } : {}
723
+ });
724
+ } catch {
725
+ yield { type: "error", message: `Could not reach ${PROVIDER_LABEL}.` };
726
+ return;
727
+ }
728
+ if (!response.ok) {
729
+ yield { type: "error", message: await readProviderError(response, PROVIDER_LABEL) };
730
+ return;
731
+ }
732
+ for await (const frame of readSseFrames(response.body)) {
733
+ const payload = parseFrameData(frame.data);
734
+ const type = frameEventType(frame, payload);
735
+ if (type === "error") {
736
+ yield { type: "error", message: errorMessage(payload) };
737
+ return;
738
+ }
739
+ if (type === "message_stop") {
740
+ yield { type: "done" };
741
+ return;
742
+ }
743
+ if (type === "content_block_delta") {
744
+ const text = textDelta(payload);
745
+ if (text !== void 0 && text !== "") {
746
+ yield { type: "text", text };
747
+ }
748
+ }
749
+ }
750
+ yield { type: "done" };
751
+ }
752
+ };
753
+ }
754
+
755
+ // src/core/ai/providers/openai.ts
756
+ var RESPONSES_URL = "https://api.openai.com/v1/responses";
757
+ var PROVIDER_LABEL2 = "OpenAI";
758
+ function requestBody2(request, model) {
759
+ return JSON.stringify({
760
+ model,
761
+ instructions: request.system,
762
+ input: request.messages.map((message) => ({
763
+ role: message.role,
764
+ content: message.content
765
+ })),
766
+ stream: true,
767
+ max_output_tokens: request.maxOutputTokens
768
+ });
769
+ }
770
+ function textDelta2(payload) {
771
+ if (typeof payload !== "object" || payload === null) {
772
+ return void 0;
773
+ }
774
+ const delta = payload["delta"];
775
+ return typeof delta === "string" ? delta : void 0;
776
+ }
777
+ function errorMessage2(payload) {
778
+ if (typeof payload === "object" && payload !== null) {
779
+ const record = payload;
780
+ const response = record["response"];
781
+ if (typeof response === "object" && response !== null) {
782
+ const error = response["error"];
783
+ if (typeof error === "object" && error !== null) {
784
+ const message = error["message"];
785
+ if (typeof message === "string" && message.trim() !== "") {
786
+ return message;
787
+ }
788
+ }
789
+ }
790
+ for (const key of ["message", "code"]) {
791
+ const value = record[key];
792
+ if (typeof value === "string" && value.trim() !== "") {
793
+ return value;
794
+ }
795
+ }
796
+ }
797
+ return "The OpenAI stream failed.";
798
+ }
799
+ function createOpenAiProvider(options) {
800
+ const doFetch = options.fetch ?? ((url, init) => fetch(url, init));
801
+ return {
802
+ async *stream(request) {
803
+ let response;
804
+ try {
805
+ response = await doFetch(RESPONSES_URL, {
806
+ method: "POST",
807
+ headers: {
808
+ authorization: `Bearer ${options.apiKey}`,
809
+ "content-type": "application/json"
810
+ },
811
+ body: requestBody2(request, options.model),
812
+ ...request.signal ? { signal: request.signal } : {}
813
+ });
814
+ } catch {
815
+ yield { type: "error", message: `Could not reach ${PROVIDER_LABEL2}.` };
816
+ return;
817
+ }
818
+ if (!response.ok) {
819
+ yield { type: "error", message: await readProviderError(response, PROVIDER_LABEL2) };
820
+ return;
821
+ }
822
+ for await (const frame of readSseFrames(response.body)) {
823
+ const payload = parseFrameData(frame.data);
824
+ const type = frameEventType(frame, payload);
825
+ if (type === "error" || type === "response.failed") {
826
+ yield { type: "error", message: errorMessage2(payload) };
827
+ return;
828
+ }
829
+ if (type === "response.completed" || type === "response.incomplete") {
830
+ yield { type: "done" };
831
+ return;
832
+ }
833
+ if (type === "response.output_text.delta") {
834
+ const text = textDelta2(payload);
835
+ if (text !== void 0 && text !== "") {
836
+ yield { type: "text", text };
837
+ }
838
+ }
839
+ }
840
+ yield { type: "done" };
841
+ }
842
+ };
843
+ }
844
+
845
+ // src/core/ai/retrieval.ts
846
+ var PAGE_CHAR_BUDGET = 8e3;
847
+ var MIN_USEFUL_CHARS = 400;
848
+ var TRUNCATION_MARKER = "\n\n[content truncated]";
849
+ var PRIOR_TURN_CHAR_CAP = 300;
850
+ function buildRetrievalQuery(messages) {
851
+ const userTurns = messages.filter((message) => message.role === "user");
852
+ const newest = userTurns.at(-1)?.content ?? "";
853
+ const prior = userTurns.slice(0, -1).slice(-2).map((turn) => turn.content.slice(0, PRIOR_TURN_CHAR_CAP));
854
+ return [...prior, newest].join("\n").trim();
855
+ }
856
+ function truncate(text, budget) {
857
+ if (text.length <= budget) {
858
+ return text;
859
+ }
860
+ const room = budget - TRUNCATION_MARKER.length;
861
+ const slice = text.slice(0, room);
862
+ const lastBreak = slice.lastIndexOf("\n");
863
+ const cut = lastBreak >= Math.floor(room / 2) ? slice.slice(0, lastBreak) : slice;
864
+ return `${cut}${TRUNCATION_MARKER}`;
865
+ }
866
+ async function selectContext(options) {
867
+ const hits = searchIndex(options.index, options.query, options.maxPages);
868
+ if (hits.length === 0) {
869
+ return [];
870
+ }
871
+ const bodyByPath = /* @__PURE__ */ new Map();
872
+ for (const document of options.documents) {
873
+ if (document.body !== void 0 && document.body !== "") {
874
+ bodyByPath.set(document.path, document.body);
875
+ }
876
+ }
877
+ const pages = [];
878
+ const seen = /* @__PURE__ */ new Set();
879
+ let used = 0;
880
+ const totalBudget = options.maxPages * PAGE_CHAR_BUDGET;
881
+ for (const hit of hits) {
882
+ if (seen.has(hit.path)) {
883
+ continue;
884
+ }
885
+ seen.add(hit.path);
886
+ const remaining = totalBudget - used;
887
+ if (remaining < MIN_USEFUL_CHARS) {
888
+ break;
889
+ }
890
+ const loaded = await options.loadMarkdown(hit.path);
891
+ const source = loaded !== null && loaded.trim() !== "" ? loaded : bodyByPath.get(hit.path);
892
+ if (source === void 0 || source.trim() === "") {
893
+ continue;
894
+ }
895
+ const markdown = truncate(source.trim(), Math.min(PAGE_CHAR_BUDGET, remaining));
896
+ used += markdown.length;
897
+ pages.push({ url: hit.path, title: hit.title, markdown });
898
+ }
899
+ return pages;
900
+ }
901
+
902
+ // src/core/ai/route.ts
903
+ var DEFAULT_MAX_OUTPUT_TOKENS = 2048;
904
+ var MAX_MESSAGES = 20;
905
+ var MAX_CONTENT_CHARS = 4e3;
906
+ function readProcessEnv(name) {
907
+ const runtime = globalThis;
908
+ return runtime.process?.env?.[name];
909
+ }
910
+ function jsonResponse(status, payload, headers = {}) {
911
+ return new Response(JSON.stringify(payload), {
912
+ status,
913
+ headers: { "content-type": "application/json", "cache-control": "no-store", ...headers }
914
+ });
915
+ }
916
+ function parseMessages(value) {
917
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
918
+ return { error: "Request body must be a JSON object." };
919
+ }
920
+ const raw = value["messages"];
921
+ if (!Array.isArray(raw)) {
922
+ return { error: 'Request body must include a "messages" array.' };
923
+ }
924
+ if (raw.length === 0) {
925
+ return { error: "messages must contain at least one message." };
926
+ }
927
+ if (raw.length > MAX_MESSAGES) {
928
+ return {
929
+ error: `messages must contain ${String(MAX_MESSAGES)} messages or fewer, got ${String(raw.length)}.`
930
+ };
931
+ }
932
+ const messages = [];
933
+ for (const [position, entry] of raw.entries()) {
934
+ const at = `messages[${String(position)}]`;
935
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
936
+ return { error: `${at} must be an object with "role" and "content".` };
937
+ }
938
+ const record = entry;
939
+ const role = record["role"];
940
+ if (role !== "user" && role !== "assistant") {
941
+ return { error: `${at}.role must be 'user' or 'assistant'.` };
942
+ }
943
+ const content = record["content"];
944
+ if (typeof content !== "string") {
945
+ return { error: `${at}.content must be a string.` };
946
+ }
947
+ if (content.trim() === "") {
948
+ return { error: `${at}.content must not be empty.` };
949
+ }
950
+ if (content.length > MAX_CONTENT_CHARS) {
951
+ return {
952
+ error: `${at}.content must be ${String(MAX_CONTENT_CHARS)} characters or fewer, got ${String(content.length)}.`
953
+ };
954
+ }
955
+ messages.push({ role, content });
956
+ }
957
+ if (messages.at(-1)?.role !== "user") {
958
+ return { error: `The last message must have role 'user'.` };
959
+ }
960
+ return { messages };
961
+ }
962
+ function createProvider(resolved, apiKey, doFetch) {
963
+ const providerOptions = {
964
+ apiKey,
965
+ model: resolved.model,
966
+ ...doFetch ? { fetch: doFetch } : {}
967
+ };
968
+ return resolved.provider === "anthropic" ? createAnthropicProvider(providerOptions) : createOpenAiProvider(providerOptions);
969
+ }
970
+ function createAskAiRoute(options) {
971
+ const resolved = resolveAiConfig(options.config);
972
+ if (typeof options.siteName !== "string" || options.siteName.trim() === "") {
973
+ throw new AiConfigError("siteName must be a non-empty string, e.g. theme.name.");
974
+ }
975
+ if (typeof options.loadPageMarkdown !== "function") {
976
+ throw new AiConfigError(
977
+ "loadPageMarkdown must be a function that returns a page's Markdown, or null."
978
+ );
979
+ }
980
+ const documentsSource = options.documents;
981
+ if (!Array.isArray(documentsSource) && typeof documentsSource !== "function") {
982
+ throw new AiConfigError(
983
+ "documents must be an array of SearchDocument, or a function returning one."
984
+ );
985
+ }
986
+ const readEnv = options.env ?? readProcessEnv;
987
+ const maxOutputTokens = options.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS;
988
+ const limiter = createRateLimiter({
989
+ limit: resolved.rateLimitPerMinute,
990
+ windowMs: RATE_LIMIT_WINDOW_MS,
991
+ ...options.now ? { now: options.now } : {}
992
+ });
993
+ let corpus;
994
+ function loadCorpus() {
995
+ if (corpus === void 0) {
996
+ const pending = (async () => {
997
+ const documents = typeof documentsSource === "function" ? await documentsSource() : documentsSource;
998
+ return { documents, index: buildSearchIndex(documents) };
999
+ })();
1000
+ void pending.catch(() => {
1001
+ if (corpus === pending) {
1002
+ corpus = void 0;
1003
+ }
1004
+ });
1005
+ corpus = pending;
1006
+ }
1007
+ return corpus;
1008
+ }
1009
+ const encoder2 = new TextEncoder();
1010
+ function streamAnswer(provider, system, messages, sources, upstream) {
1011
+ return new ReadableStream({
1012
+ async start(controller) {
1013
+ let open = true;
1014
+ const send = (event, data) => {
1015
+ if (!open) {
1016
+ return;
1017
+ }
1018
+ try {
1019
+ controller.enqueue(
1020
+ encoder2.encode(`event: ${event}
1021
+ data: ${JSON.stringify(data)}
1022
+
1023
+ `)
1024
+ );
1025
+ } catch {
1026
+ open = false;
1027
+ }
1028
+ };
1029
+ send("sources", sources);
1030
+ let closed = false;
1031
+ try {
1032
+ for await (const event of provider.stream({
1033
+ system,
1034
+ messages,
1035
+ maxOutputTokens,
1036
+ signal: upstream.signal
1037
+ })) {
1038
+ if (event.type === "text") {
1039
+ send("text", { text: event.text });
1040
+ continue;
1041
+ }
1042
+ if (event.type === "error") {
1043
+ send("error", { message: event.message });
1044
+ } else {
1045
+ send("done", {});
1046
+ }
1047
+ closed = true;
1048
+ break;
1049
+ }
1050
+ if (!closed) {
1051
+ send("done", {});
1052
+ }
1053
+ } catch {
1054
+ send("error", { message: "The answer stream stopped unexpectedly." });
1055
+ }
1056
+ open = false;
1057
+ try {
1058
+ controller.close();
1059
+ } catch {
1060
+ }
1061
+ },
1062
+ cancel() {
1063
+ upstream.abort();
1064
+ }
1065
+ });
1066
+ }
1067
+ return {
1068
+ async POST(request) {
1069
+ let body;
1070
+ try {
1071
+ body = await request.json();
1072
+ } catch {
1073
+ return jsonResponse(400, { error: "Request body must be JSON." });
1074
+ }
1075
+ const parsed = parseMessages(body);
1076
+ if ("error" in parsed) {
1077
+ return jsonResponse(400, { error: parsed.error });
1078
+ }
1079
+ const decision = limiter.allow(clientRateLimitKey(request));
1080
+ if (!decision.allowed) {
1081
+ const retryAfter = decision.retryAfterSeconds ?? 60;
1082
+ return jsonResponse(
1083
+ 429,
1084
+ {
1085
+ error: `Too many questions. Try again in ${String(retryAfter)} seconds.`,
1086
+ retryAfterSeconds: retryAfter
1087
+ },
1088
+ { "retry-after": String(retryAfter) }
1089
+ );
1090
+ }
1091
+ const apiKey = (options.apiKey ?? readEnv(resolved.apiKeyEnv) ?? "").trim();
1092
+ if (apiKey === "") {
1093
+ return jsonResponse(500, {
1094
+ error: `Ask AI is not configured: set the ${resolved.apiKeyEnv} environment variable on the server.`
1095
+ });
1096
+ }
1097
+ let pages;
1098
+ try {
1099
+ const { documents, index } = await loadCorpus();
1100
+ pages = await selectContext({
1101
+ index,
1102
+ documents,
1103
+ query: buildRetrievalQuery(parsed.messages),
1104
+ maxPages: resolved.maxContextPages,
1105
+ loadMarkdown: options.loadPageMarkdown
1106
+ });
1107
+ } catch {
1108
+ return jsonResponse(500, {
1109
+ error: "Ask AI could not read the documentation for this site."
1110
+ });
1111
+ }
1112
+ const system = buildSystemPrompt({
1113
+ siteName: options.siteName,
1114
+ pages,
1115
+ ...resolved.systemPrompt === void 0 ? {} : { extraInstructions: resolved.systemPrompt }
1116
+ });
1117
+ const sources = pages.map((page) => ({ url: page.url, title: page.title }));
1118
+ const upstream = new AbortController();
1119
+ const clientSignal = request.signal;
1120
+ if (clientSignal !== void 0) {
1121
+ if (clientSignal.aborted) {
1122
+ upstream.abort();
1123
+ } else {
1124
+ clientSignal.addEventListener(
1125
+ "abort",
1126
+ () => {
1127
+ upstream.abort();
1128
+ },
1129
+ { once: true }
1130
+ );
1131
+ }
1132
+ }
1133
+ const provider = createProvider(resolved, apiKey, options.fetch);
1134
+ return new Response(streamAnswer(provider, system, parsed.messages, sources, upstream), {
1135
+ headers: {
1136
+ "content-type": "text/event-stream; charset=utf-8",
1137
+ "cache-control": "no-store",
1138
+ connection: "keep-alive",
1139
+ // Tell a buffering reverse proxy to pass the stream straight through.
1140
+ "x-accel-buffering": "no"
1141
+ }
1142
+ });
1143
+ }
1144
+ };
1145
+ }
1146
+
1147
+ // src/core/config.ts
1148
+ var DocsConfigError = class extends Error {
1149
+ constructor(message) {
1150
+ super(message);
1151
+ this.name = "DocsConfigError";
1152
+ }
1153
+ };
1154
+ var MIN_PROXY_RATE_LIMIT = 1;
1155
+ var MAX_PROXY_RATE_LIMIT = 6e3;
1156
+ var MIN_SESSION_TTL_SECONDS = 60;
1157
+ var MAX_SESSION_TTL_SECONDS = 2592e3;
1158
+ var PALETTE_KEYS = [
1159
+ "primary",
1160
+ "background",
1161
+ "foreground",
1162
+ "card",
1163
+ "border",
1164
+ "muted"
1165
+ ];
1166
+ function describe2(value) {
1167
+ if (typeof value === "string") {
1168
+ return `'${value}'`;
1169
+ }
1170
+ if (typeof value === "number" || typeof value === "boolean" || value === null) {
1171
+ return String(value);
1172
+ }
1173
+ if (value === void 0) {
1174
+ return "undefined";
1175
+ }
1176
+ return Array.isArray(value) ? "an array" : `a ${typeof value}`;
1177
+ }
1178
+ function isPlainObject(value) {
1179
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1180
+ }
1181
+ function requireObject(value, field, example) {
1182
+ if (!isPlainObject(value)) {
1183
+ throw new DocsConfigError(`${field} must be an object, got ${describe2(value)}. ${example}`);
1184
+ }
1185
+ return value;
1186
+ }
1187
+ function optionalObject(value, field) {
1188
+ if (value === void 0) {
1189
+ return void 0;
1190
+ }
1191
+ if (!isPlainObject(value)) {
1192
+ throw new DocsConfigError(
1193
+ `${field} must be an object, got ${describe2(value)}. Remove the field to leave it unset.`
1194
+ );
1195
+ }
1196
+ return value;
1197
+ }
1198
+ function requireString(value, field, example) {
1199
+ if (typeof value !== "string" || value.trim() === "") {
1200
+ throw new DocsConfigError(
1201
+ `${field} must be a non-empty string, got ${describe2(value)}. ${example}`
1202
+ );
1203
+ }
1204
+ return value;
1205
+ }
1206
+ function optionalString(value, field) {
1207
+ if (value === void 0) {
1208
+ return void 0;
1209
+ }
1210
+ if (typeof value !== "string" || value.trim() === "") {
1211
+ throw new DocsConfigError(
1212
+ `${field} must be a non-empty string, got ${describe2(value)}. Remove the field to use the default.`
1213
+ );
1214
+ }
1215
+ return value;
1216
+ }
1217
+ function optionalBoolean(value, field) {
1218
+ if (value !== void 0 && typeof value !== "boolean") {
1219
+ throw new DocsConfigError(`${field} must be true or false, got ${describe2(value)}.`);
1220
+ }
1221
+ }
1222
+ function optionalIntegerInRange(value, field, min, max) {
1223
+ if (value === void 0) {
1224
+ return;
1225
+ }
1226
+ if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max) {
1227
+ throw new DocsConfigError(
1228
+ `${field} must be an integer between ${String(min)} and ${String(max)}, got ${describe2(value)}.`
1229
+ );
1230
+ }
1231
+ }
1232
+ function optionalPath(value, field) {
1233
+ const path = optionalString(value, field);
1234
+ if (path !== void 0 && !path.startsWith("/")) {
1235
+ throw new DocsConfigError(
1236
+ `${field} must start with '/', got ${describe2(path)}. Example: '/signin'.`
1237
+ );
1238
+ }
1239
+ }
1240
+ function requireStringArray(value, field, options) {
1241
+ if (!Array.isArray(value)) {
1242
+ throw new DocsConfigError(
1243
+ `${field} must be an array, got ${describe2(value)}. ${options.example}`
1244
+ );
1245
+ }
1246
+ if (value.length < options.minLength) {
1247
+ throw new DocsConfigError(
1248
+ `${field} must contain at least ${String(options.minLength)} entry, got an empty array. ${options.example}`
1249
+ );
1250
+ }
1251
+ return value.map(
1252
+ (entry, index) => requireString(entry, `${field}[${String(index)}]`, options.example)
1253
+ );
1254
+ }
1255
+ function validatePalette(value, field) {
1256
+ const palette = optionalObject(value, field);
1257
+ if (palette === void 0) {
1258
+ return;
1259
+ }
1260
+ for (const key of PALETTE_KEYS) {
1261
+ optionalString(palette[key], `${field}.${key}`);
1262
+ }
1263
+ }
1264
+ function validateTheme(value) {
1265
+ const theme = requireObject(value, "theme", "Example: theme: { name: 'Example Docs' }.");
1266
+ requireString(theme["name"], "theme.name", "Example: theme: { name: 'Example Docs' }.");
1267
+ const logo = optionalObject(theme["logo"], "theme.logo");
1268
+ if (logo !== void 0) {
1269
+ requireString(logo["light"], "theme.logo.light", "Example: '/logo-light.png'.");
1270
+ requireString(logo["dark"], "theme.logo.dark", "Example: '/logo-dark.png'.");
1271
+ }
1272
+ optionalString(theme["favicon"], "theme.favicon");
1273
+ validatePalette(theme["colors"], "theme.colors");
1274
+ validatePalette(theme["darkColors"], "theme.darkColors");
1275
+ }
1276
+ function validateWorkspaceProviders(value) {
1277
+ const providers = requireObject(value, "auth.providers", "Example: providers: { email: true }.");
1278
+ optionalBoolean(providers["email"], "auth.providers.email");
1279
+ const secret = optionalObject(providers["secret"], "auth.providers.secret");
1280
+ if (secret !== void 0) {
1281
+ optionalString(secret["secretEnv"], "auth.providers.secret.secretEnv");
1282
+ }
1283
+ const google = optionalObject(providers["google"], "auth.providers.google");
1284
+ if (google !== void 0) {
1285
+ optionalString(google["clientIdEnv"], "auth.providers.google.clientIdEnv");
1286
+ optionalString(google["clientSecretEnv"], "auth.providers.google.clientSecretEnv");
1287
+ optionalPath(google["redirectPath"], "auth.providers.google.redirectPath");
1288
+ }
1289
+ if (providers["email"] !== true && secret === void 0 && google === void 0) {
1290
+ throw new DocsConfigError(
1291
+ "auth.providers must enable at least one provider: 'email', 'secret', or 'google'. Example: providers: { email: true }."
1292
+ );
1293
+ }
1294
+ }
1295
+ function validateAuth(value) {
1296
+ if (value === "public") {
1297
+ return;
1298
+ }
1299
+ if (!isPlainObject(value)) {
1300
+ throw new DocsConfigError(
1301
+ `auth must be 'public' or a workspace configuration object, got ${describe2(value)}. Example: auth: 'public'.`
1302
+ );
1303
+ }
1304
+ const mode = value["mode"];
1305
+ if (mode !== "workspace") {
1306
+ throw new DocsConfigError(`auth.mode must be 'workspace', got ${describe2(mode)}.`);
1307
+ }
1308
+ const domains = requireStringArray(value["allowedDomains"], "auth.allowedDomains", {
1309
+ minLength: 1,
1310
+ example: "Example: allowedDomains: ['example.com']."
1311
+ });
1312
+ for (const [index, domain] of domains.entries()) {
1313
+ if (domain.includes("@") || !domain.includes(".") || /\s/.test(domain)) {
1314
+ throw new DocsConfigError(
1315
+ `auth.allowedDomains[${String(index)}] must be a bare domain like 'example.com', got ${describe2(domain)}.`
1316
+ );
1317
+ }
1318
+ }
1319
+ validateWorkspaceProviders(value["providers"]);
1320
+ optionalPath(value["signInPath"], "auth.signInPath");
1321
+ optionalString(value["sessionSecretEnv"], "auth.sessionSecretEnv");
1322
+ optionalIntegerInRange(
1323
+ value["sessionTtlSeconds"],
1324
+ "auth.sessionTtlSeconds",
1325
+ MIN_SESSION_TTL_SECONDS,
1326
+ MAX_SESSION_TTL_SECONDS
1327
+ );
1328
+ optionalBoolean(value["secureCookies"], "auth.secureCookies");
1329
+ }
1330
+ function validateProxy(value) {
1331
+ const proxy = requireObject(value, "proxy", "Example: proxy: { allowedOrigins: [] }.");
1332
+ const origins = requireStringArray(proxy["allowedOrigins"], "proxy.allowedOrigins", {
1333
+ minLength: 0,
1334
+ example: "Example: allowedOrigins: ['https://api.example.com']."
1335
+ });
1336
+ for (const [index, origin] of origins.entries()) {
1337
+ const field = `proxy.allowedOrigins[${String(index)}]`;
1338
+ let parsed;
1339
+ try {
1340
+ parsed = new URL(origin);
1341
+ } catch {
1342
+ throw new DocsConfigError(
1343
+ `${field} must be an absolute origin like 'https://api.example.com', got ${describe2(origin)}.`
1344
+ );
1345
+ }
1346
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1347
+ throw new DocsConfigError(`${field} must use http or https, got ${describe2(origin)}.`);
1348
+ }
1349
+ }
1350
+ optionalIntegerInRange(
1351
+ proxy["rateLimitPerMinute"],
1352
+ "proxy.rateLimitPerMinute",
1353
+ MIN_PROXY_RATE_LIMIT,
1354
+ MAX_PROXY_RATE_LIMIT
1355
+ );
1356
+ }
1357
+ function validateFeatures(value) {
1358
+ const features = optionalObject(value, "features");
1359
+ if (features === void 0) {
1360
+ return;
1361
+ }
1362
+ optionalBoolean(features["copyPage"], "features.copyPage");
1363
+ }
1364
+ function validateSeo(value) {
1365
+ const seo = optionalObject(value, "seo");
1366
+ if (seo === void 0) {
1367
+ return;
1368
+ }
1369
+ const siteUrl = optionalString(seo["siteUrl"], "seo.siteUrl");
1370
+ if (siteUrl !== void 0) {
1371
+ let parsed;
1372
+ try {
1373
+ parsed = new URL(siteUrl);
1374
+ } catch {
1375
+ throw new DocsConfigError(
1376
+ `seo.siteUrl must be an absolute URL like 'https://docs.example.com', got ${describe2(siteUrl)}.`
1377
+ );
1378
+ }
1379
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1380
+ throw new DocsConfigError(`seo.siteUrl must use http or https, got ${describe2(siteUrl)}.`);
1381
+ }
1382
+ }
1383
+ const template = optionalString(seo["titleTemplate"], "seo.titleTemplate");
1384
+ if (template !== void 0 && !template.includes("%s")) {
1385
+ throw new DocsConfigError(
1386
+ `seo.titleTemplate must contain '%s' for the page title, got ${describe2(template)}. Example: '%s | Example Docs'.`
1387
+ );
1388
+ }
1389
+ optionalString(seo["description"], "seo.description");
1390
+ optionalString(seo["ogImage"], "seo.ogImage");
1391
+ const twitter = optionalString(seo["twitter"], "seo.twitter");
1392
+ if (twitter !== void 0 && !twitter.startsWith("@")) {
1393
+ throw new DocsConfigError(
1394
+ `seo.twitter must start with '@', got ${describe2(twitter)}. Example: '@example'.`
1395
+ );
1396
+ }
1397
+ if (seo["keywords"] !== void 0) {
1398
+ requireStringArray(seo["keywords"], "seo.keywords", {
1399
+ minLength: 0,
1400
+ example: "Example: keywords: ['payments', 'api']."
1401
+ });
1402
+ }
1403
+ optionalBoolean(seo["noindex"], "seo.noindex");
1404
+ }
1405
+ function validateDocsConfig(config) {
1406
+ if (!isPlainObject(config)) {
1407
+ throw new DocsConfigError(
1408
+ `The docs configuration must be an object, got ${describe2(config)}. Export one object from docs.config.ts.`
1409
+ );
1410
+ }
1411
+ validateTheme(config["theme"]);
1412
+ validateAuth(config["auth"]);
1413
+ validateProxy(config["proxy"]);
1414
+ validateFeatures(config["features"]);
1415
+ validateSeo(config["seo"]);
1416
+ if (config["ai"] !== void 0) {
1417
+ resolveAiConfig(config["ai"]);
1418
+ }
1419
+ return config;
1420
+ }
1421
+ function defineDocsConfig(config) {
1422
+ return validateDocsConfig(config);
1423
+ }
1424
+
1425
+ // src/core/theme.ts
1426
+ var TOKEN_TO_VAR = {
1427
+ primary: "--color-fd-primary",
1428
+ background: "--color-fd-background",
1429
+ foreground: "--color-fd-foreground",
1430
+ card: "--color-fd-card",
1431
+ border: "--color-fd-border",
1432
+ muted: "--color-fd-muted-foreground"
1433
+ };
1434
+ function paletteToDeclarations(palette) {
1435
+ const parts = [];
1436
+ for (const key of Object.keys(TOKEN_TO_VAR)) {
1437
+ const value = palette[key];
1438
+ if (typeof value === "string" && value.length > 0) {
1439
+ parts.push(`${TOKEN_TO_VAR[key]}: ${value};`);
1440
+ }
1441
+ }
1442
+ return parts.join(" ");
1443
+ }
1444
+ function buildThemeCss(theme) {
1445
+ const blocks = [];
1446
+ if (theme.colors) {
1447
+ const light = paletteToDeclarations(theme.colors);
1448
+ if (light.length > 0) {
1449
+ blocks.push(`:root { ${light} }`);
1450
+ }
1451
+ }
1452
+ if (theme.darkColors) {
1453
+ const dark = paletteToDeclarations(theme.darkColors);
1454
+ if (dark.length > 0) {
1455
+ blocks.push(`:root.dark, .dark { ${dark} }`);
1456
+ }
1457
+ }
1458
+ return blocks.join("\n");
1459
+ }
1460
+
1461
+ // src/core/seo.ts
1462
+ function trimTrailingSlash(value) {
1463
+ return value.replace(/\/+$/, "");
1464
+ }
1465
+ function buildMetadata(config, page = {}) {
1466
+ const name = config.theme.name;
1467
+ const seo = config.seo ?? {};
1468
+ const template = seo.titleTemplate ?? `%s \xB7 ${name}`;
1469
+ const siteUrl = seo.siteUrl ? trimTrailingSlash(seo.siteUrl) : void 0;
1470
+ const isDefault = page.title === void 0;
1471
+ const displayTitle = isDefault ? name : template.replace("%s", page.title);
1472
+ const description = page.description ?? seo.description;
1473
+ const path = page.path ?? "/";
1474
+ const ogImage = page.ogImage ?? seo.ogImage;
1475
+ const favicon = config.theme.favicon;
1476
+ const indexable = !(page.noindex ?? seo.noindex ?? false);
1477
+ const keywords = [...seo.keywords ?? [], ...page.keywords ?? []];
1478
+ return {
1479
+ ...siteUrl ? { metadataBase: new URL(siteUrl) } : {},
1480
+ title: isDefault ? { default: name, template } : page.title,
1481
+ ...description ? { description } : {},
1482
+ ...keywords.length ? { keywords } : {},
1483
+ alternates: { canonical: path },
1484
+ ...favicon ? { icons: { icon: favicon } } : {},
1485
+ robots: { index: indexable, follow: indexable },
1486
+ openGraph: {
1487
+ type: "website",
1488
+ siteName: name,
1489
+ title: displayTitle,
1490
+ ...description ? { description } : {},
1491
+ url: path,
1492
+ ...ogImage ? { images: [{ url: ogImage }] } : {}
1493
+ },
1494
+ twitter: {
1495
+ card: ogImage ? "summary_large_image" : "summary",
1496
+ title: displayTitle,
1497
+ ...description ? { description } : {},
1498
+ ...seo.twitter ? { site: seo.twitter } : {},
1499
+ ...ogImage ? { images: [ogImage] } : {}
1500
+ }
1501
+ };
1502
+ }
1503
+ function buildRobots(config) {
1504
+ const seo = config.seo ?? {};
1505
+ const siteUrl = seo.siteUrl ? trimTrailingSlash(seo.siteUrl) : void 0;
1506
+ const blocked = seo.noindex ?? false;
1507
+ return {
1508
+ rules: blocked ? { userAgent: "*", disallow: "/" } : { userAgent: "*", allow: "/", disallow: ["/signin", "/api/"] },
1509
+ ...siteUrl && !blocked ? { sitemap: `${siteUrl}/sitemap.xml`, host: siteUrl } : {}
1510
+ };
1511
+ }
1512
+ function buildSitemap(entries, config, options = {}) {
1513
+ const seo = config.seo ?? {};
1514
+ const siteUrl = seo.siteUrl ? trimTrailingSlash(seo.siteUrl) : "";
1515
+ const changeFrequency = options.changeFrequency ?? "weekly";
1516
+ const pagePriority = options.priority ?? 0.7;
1517
+ const homePriority = options.homePriority ?? 1;
1518
+ return entries.map((entry) => {
1519
+ const path = entry.path.startsWith("/") ? entry.path : `/${entry.path}`;
1520
+ return {
1521
+ url: `${siteUrl}${path}`,
1522
+ ...entry.lastModified ? { lastModified: entry.lastModified } : {},
1523
+ changeFrequency,
1524
+ priority: path === "/" ? homePriority : pagePriority
1525
+ };
1526
+ });
1527
+ }
1528
+
1529
+ // src/core/docs-tree.ts
1530
+ function slugify(title) {
1531
+ return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1532
+ }
1533
+ function isOpenApi(item) {
1534
+ return item.kind === "openapi";
1535
+ }
1536
+ function hasOpenApi(items) {
1537
+ return items.some((item) => item.kind === "group" ? hasOpenApi(item.items) : isOpenApi(item));
1538
+ }
1539
+ function basename(file) {
1540
+ const i = file.lastIndexOf("/");
1541
+ return i === -1 ? file : file.slice(i + 1);
1542
+ }
1543
+ function dirname(file) {
1544
+ const i = file.lastIndexOf("/");
1545
+ return i === -1 ? "" : file.slice(0, i);
1546
+ }
1547
+ function planDocsTree(navigation, options = {}) {
1548
+ const apiDir = options.apiDir ?? "api";
1549
+ const specs = [];
1550
+ const apiPages = [];
1551
+ const apiGroups = [];
1552
+ const metas = [];
1553
+ const rootPages = [];
1554
+ for (const tab of navigation) {
1555
+ if (hasOpenApi(tab.items)) {
1556
+ planApiTab(tab);
1557
+ continue;
1558
+ }
1559
+ planDocTab(tab);
1560
+ }
1561
+ metas.unshift({ path: "meta.json", content: { pages: rootPages } });
1562
+ return { specs, apiPages, apiGroups, metas };
1563
+ function planApiTab(tab) {
1564
+ rootPages.push(apiDir);
1565
+ const groupDirs = [];
1566
+ for (const item of tab.items) {
1567
+ if (item.kind !== "group") {
1568
+ continue;
1569
+ }
1570
+ const dir = `${apiDir}/${slugify(item.title)}`;
1571
+ groupDirs.push(slugify(item.title));
1572
+ apiGroups.push({ dir, title: item.title });
1573
+ let order = 0;
1574
+ for (const page of item.items) {
1575
+ if (page.kind !== "openapi") {
1576
+ continue;
1577
+ }
1578
+ if (!specs.includes(page.file)) {
1579
+ specs.push(page.file);
1580
+ }
1581
+ apiPages.push({ file: page.file, method: page.method, apiPath: page.apiPath, dir, order });
1582
+ order += 1;
1583
+ }
1584
+ }
1585
+ metas.push({
1586
+ path: `${apiDir}/meta.json`,
1587
+ content: {
1588
+ title: tab.title,
1589
+ root: true,
1590
+ ...tab.icon ? { icon: tab.icon } : {},
1591
+ pages: groupDirs
1592
+ }
1593
+ });
1594
+ }
1595
+ function planDocTab(tab) {
1596
+ const directPages = tab.items.filter((item) => item.kind === "doc");
1597
+ const groups = tab.items.filter((item) => item.kind === "group");
1598
+ if (groups.length === 0 && directPages.every((page) => !page.file.includes("/"))) {
1599
+ for (const page of directPages) {
1600
+ rootPages.push(page.file);
1601
+ }
1602
+ return;
1603
+ }
1604
+ const firstFile = directPages[0]?.file ?? groups.flatMap((g) => g.items).find((item) => item.kind === "doc")?.file;
1605
+ if (firstFile === void 0) {
1606
+ return;
1607
+ }
1608
+ const tabDir = firstFile.split("/")[0] ?? "";
1609
+ rootPages.push(tabDir);
1610
+ const tabPages = directPages.map((page) => basename(page.file));
1611
+ for (const group of groups) {
1612
+ const pages = group.items.filter((item) => item.kind === "doc");
1613
+ const groupDir = dirname(pages[0]?.file ?? "");
1614
+ const folder = groupDir.split("/").slice(1).join("/");
1615
+ if (folder === "") {
1616
+ tabPages.push(...pages.map((page) => basename(page.file)));
1617
+ continue;
1618
+ }
1619
+ tabPages.push(folder);
1620
+ metas.push({
1621
+ path: `${groupDir}/meta.json`,
1622
+ content: { title: group.title, pages: pages.map((page) => basename(page.file)) }
1623
+ });
1624
+ }
1625
+ metas.push({
1626
+ path: `${tabDir}/meta.json`,
1627
+ content: {
1628
+ title: tab.title,
1629
+ root: true,
1630
+ ...tab.icon ? { icon: tab.icon } : {},
1631
+ pages: tabPages
1632
+ }
1633
+ });
1634
+ }
1635
+ }
1636
+
1637
+ // src/core/openapi-markdown.ts
1638
+ var MAX_FIELD_DEPTH = 3;
1639
+ function isObject(value) {
1640
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1641
+ }
1642
+ function asString(value) {
1643
+ return typeof value === "string" ? value : void 0;
1644
+ }
1645
+ function resolvePointer(document, pointer) {
1646
+ if (!pointer.startsWith("#/")) {
1647
+ return void 0;
1648
+ }
1649
+ let node = document;
1650
+ for (const rawSegment of pointer.slice(2).split("/")) {
1651
+ const segment = rawSegment.replaceAll("~1", "/").replaceAll("~0", "~");
1652
+ if (!isObject(node)) {
1653
+ return void 0;
1654
+ }
1655
+ node = node[segment];
1656
+ }
1657
+ return node;
1658
+ }
1659
+ function withRef(schema, seen) {
1660
+ const ref = isObject(schema) ? asString(schema["$ref"]) : void 0;
1661
+ return ref === void 0 || seen.has(ref) ? seen : /* @__PURE__ */ new Set([...seen, ref]);
1662
+ }
1663
+ function derefNode(document, schema, seen) {
1664
+ if (!isObject(schema)) {
1665
+ return void 0;
1666
+ }
1667
+ const ref = asString(schema["$ref"]);
1668
+ if (ref !== void 0) {
1669
+ if (seen.has(ref)) {
1670
+ return void 0;
1671
+ }
1672
+ return derefNode(document, resolvePointer(document, ref), /* @__PURE__ */ new Set([...seen, ref]));
1673
+ }
1674
+ const allOf = schema["allOf"];
1675
+ if (Array.isArray(allOf)) {
1676
+ const merged = { ...schema };
1677
+ delete merged["allOf"];
1678
+ const properties = {};
1679
+ const required = [];
1680
+ for (const member of allOf) {
1681
+ const resolved = derefNode(document, member, seen);
1682
+ if (resolved === void 0) {
1683
+ continue;
1684
+ }
1685
+ for (const [key, value] of Object.entries(resolved)) {
1686
+ if (key === "properties" && isObject(value)) {
1687
+ Object.assign(properties, value);
1688
+ } else if (key === "required" && Array.isArray(value)) {
1689
+ required.push(...value.filter((entry) => typeof entry === "string"));
1690
+ } else if (!(key in merged)) {
1691
+ merged[key] = value;
1692
+ }
1693
+ }
1694
+ }
1695
+ if (Object.keys(properties).length > 0) {
1696
+ merged["properties"] = {
1697
+ ...properties,
1698
+ ...isObject(merged["properties"]) ? merged["properties"] : {}
1699
+ };
1700
+ }
1701
+ if (required.length > 0) {
1702
+ const own = Array.isArray(merged["required"]) ? merged["required"].filter((entry) => typeof entry === "string") : [];
1703
+ merged["required"] = [.../* @__PURE__ */ new Set([...required, ...own])];
1704
+ }
1705
+ if (merged["type"] === void 0 && isObject(merged["properties"])) {
1706
+ merged["type"] = "object";
1707
+ }
1708
+ return merged;
1709
+ }
1710
+ return schema;
1711
+ }
1712
+ function typeLabel(document, schema, seen) {
1713
+ if (!isObject(schema)) {
1714
+ return "unknown";
1715
+ }
1716
+ const ref = asString(schema["$ref"]);
1717
+ const refName = ref?.split("/").at(-1);
1718
+ const resolved = derefNode(document, schema, seen);
1719
+ if (resolved === void 0) {
1720
+ return refName ?? "unknown";
1721
+ }
1722
+ const type = asString(resolved["type"]);
1723
+ if (refName !== void 0 && (type === "object" || isObject(resolved["properties"]))) {
1724
+ return refName;
1725
+ }
1726
+ if (type === "array") {
1727
+ return `array of ${typeLabel(document, resolved["items"], seen)}`;
1728
+ }
1729
+ if (Array.isArray(resolved["oneOf"]) || Array.isArray(resolved["anyOf"])) {
1730
+ const variants = resolved["oneOf"] ?? resolved["anyOf"];
1731
+ return `one of ${String(variants.length)} shapes`;
1732
+ }
1733
+ if (type === void 0) {
1734
+ return isObject(resolved["properties"]) ? "object" : ref?.split("/").at(-1) ?? "unknown";
1735
+ }
1736
+ const format = asString(resolved["format"]);
1737
+ return format === void 0 ? type : `${type} (${format})`;
1738
+ }
1739
+ function fieldLine(document, name, schema, required, indent, seen, location) {
1740
+ const label = typeLabel(document, schema, seen);
1741
+ const resolved = derefNode(document, schema, seen) ?? schema;
1742
+ const notes = [];
1743
+ const description = asString(resolved["description"]);
1744
+ if (description !== void 0 && description !== "") {
1745
+ notes.push(description.replaceAll(/\s*\n\s*/g, " ").trim());
1746
+ }
1747
+ const enumValues = resolved["enum"];
1748
+ if (Array.isArray(enumValues) && enumValues.length > 0) {
1749
+ notes.push(`One of: ${enumValues.map((value) => `\`${String(value)}\``).join(", ")}.`);
1750
+ }
1751
+ const defaultValue = resolved["default"];
1752
+ if (typeof defaultValue === "string" || typeof defaultValue === "number" || typeof defaultValue === "boolean") {
1753
+ notes.push(`Default: \`${String(defaultValue)}\`.`);
1754
+ }
1755
+ const example = resolved["example"];
1756
+ if ((typeof example === "string" || typeof example === "number" || typeof example === "boolean") && String(example).length <= 60) {
1757
+ notes.push(`Example: \`${String(example)}\`.`);
1758
+ }
1759
+ if (resolved["deprecated"] === true) {
1760
+ notes.push("Deprecated.");
1761
+ }
1762
+ const detail = notes.length > 0 ? ` \u2014 ${notes.join(" ")}` : "";
1763
+ const parenthetical = [
1764
+ ...location === void 0 ? [] : [`in: ${location}`],
1765
+ label,
1766
+ ...required ? ["required"] : []
1767
+ ].join(", ");
1768
+ return `${indent}- \`${name}\` (${parenthetical})${detail}`;
1769
+ }
1770
+ function schemaFieldLines(document, schema, depth, seen) {
1771
+ if (depth >= MAX_FIELD_DEPTH) {
1772
+ return [`${" ".repeat(depth)}- (nested fields omitted at this depth)`];
1773
+ }
1774
+ const scope = withRef(schema, seen);
1775
+ const resolved = derefNode(document, schema, seen);
1776
+ if (resolved === void 0) {
1777
+ return [];
1778
+ }
1779
+ if (asString(resolved["type"]) === "array") {
1780
+ return schemaFieldLines(document, resolved["items"], depth, withRef(resolved["items"], scope));
1781
+ }
1782
+ const properties = resolved["properties"];
1783
+ const variants = resolved["oneOf"] ?? resolved["anyOf"];
1784
+ const variantLines = Array.isArray(variants) ? variants.flatMap((variant, index) => {
1785
+ const inner = schemaFieldLines(document, variant, depth + 1, scope);
1786
+ const title = asString(derefNode(document, variant, scope)?.["title"]);
1787
+ if (inner.length === 0 && title === void 0) {
1788
+ return [];
1789
+ }
1790
+ const label = title ?? typeLabel(document, variant, scope);
1791
+ const head = `${" ".repeat(depth)}- Shape ${String(index + 1)} (${label})${inner.length > 0 ? ":" : ""}`;
1792
+ return [head, ...inner];
1793
+ }) : [];
1794
+ if (!isObject(properties)) {
1795
+ return variantLines;
1796
+ }
1797
+ const required = new Set(
1798
+ Array.isArray(resolved["required"]) ? resolved["required"].filter((entry) => typeof entry === "string") : []
1799
+ );
1800
+ const indent = " ".repeat(depth);
1801
+ const lines = [];
1802
+ for (const [name, property] of Object.entries(properties)) {
1803
+ if (!isObject(property)) {
1804
+ continue;
1805
+ }
1806
+ lines.push(fieldLine(document, name, property, required.has(name), indent, scope));
1807
+ const propertyScope = withRef(property, scope);
1808
+ const child = derefNode(document, property, scope);
1809
+ if (child === void 0) {
1810
+ continue;
1811
+ }
1812
+ const items = asString(child["type"]) === "array" ? child["items"] : void 0;
1813
+ const nestedScope = items === void 0 ? propertyScope : withRef(items, propertyScope);
1814
+ const nested = items === void 0 ? child : derefNode(document, items, propertyScope);
1815
+ const hasNestedFields = nested !== void 0 && (isObject(nested["properties"]) || Array.isArray(nested["oneOf"]) || Array.isArray(nested["anyOf"]));
1816
+ if (!hasNestedFields) {
1817
+ continue;
1818
+ }
1819
+ lines.push(...schemaFieldLines(document, nested, depth + 1, nestedScope));
1820
+ }
1821
+ return lines;
1822
+ }
1823
+ function securityLine(document, operation) {
1824
+ const security = operation["security"] ?? document.security;
1825
+ if (!Array.isArray(security) || security.length === 0) {
1826
+ return void 0;
1827
+ }
1828
+ const requirements = security.filter(isObject).map(
1829
+ (entry) => Object.entries(entry).map(([scheme, scopes]) => {
1830
+ const scopeList = Array.isArray(scopes) ? scopes.filter((scope) => typeof scope === "string") : [];
1831
+ return scopeList.length > 0 ? `${scheme} (scopes: ${scopeList.join(", ")})` : scheme;
1832
+ }).join(" + ")
1833
+ ).filter((entry) => entry !== "");
1834
+ return requirements.length > 0 ? `Auth: ${requirements.join(" or ")}` : void 0;
1835
+ }
1836
+ function bodySchema(content) {
1837
+ if (!isObject(content)) {
1838
+ return void 0;
1839
+ }
1840
+ const mediaType = "application/json" in content ? "application/json" : Object.keys(content)[0];
1841
+ if (mediaType === void 0) {
1842
+ return void 0;
1843
+ }
1844
+ const media = content[mediaType];
1845
+ return isObject(media) ? { mediaType, schema: media["schema"] } : void 0;
1846
+ }
1847
+ function openApiOperationToMarkdown(document, ref) {
1848
+ const pathItem = isObject(document.paths) ? document.paths[ref.path] : void 0;
1849
+ if (!isObject(pathItem)) {
1850
+ return null;
1851
+ }
1852
+ const operation = pathItem[ref.method.toLowerCase()];
1853
+ if (!isObject(operation)) {
1854
+ return null;
1855
+ }
1856
+ const seen = /* @__PURE__ */ new Set();
1857
+ const sections = [];
1858
+ if (operation["deprecated"] === true) {
1859
+ sections.push("**Deprecated.**");
1860
+ }
1861
+ const auth = securityLine(document, operation);
1862
+ if (auth !== void 0) {
1863
+ sections.push(auth);
1864
+ }
1865
+ const parameters = operation["parameters"];
1866
+ if (Array.isArray(parameters)) {
1867
+ const lines = parameters.map((parameter) => derefNode(document, parameter, seen) ?? parameter).filter(isObject).flatMap((parameter) => {
1868
+ const name = asString(parameter["name"]);
1869
+ if (name === void 0) {
1870
+ return [];
1871
+ }
1872
+ const where = asString(parameter["in"]) ?? "query";
1873
+ const schema = isObject(parameter["schema"]) ? parameter["schema"] : {};
1874
+ return [fieldLine(document, name, schema, parameter["required"] === true, "", seen, where)];
1875
+ });
1876
+ if (lines.length > 0) {
1877
+ sections.push(`## Parameters
1878
+
1879
+ ${lines.join("\n")}`);
1880
+ }
1881
+ }
1882
+ const requestBody3 = derefNode(document, operation["requestBody"], seen);
1883
+ if (requestBody3 !== void 0) {
1884
+ const body = bodySchema(requestBody3["content"]);
1885
+ if (body !== void 0) {
1886
+ const lines = schemaFieldLines(document, body.schema, 0, seen);
1887
+ const requiredNote = requestBody3["required"] === true ? ", required" : "";
1888
+ const heading = `## Request body (${body.mediaType}${requiredNote})`;
1889
+ sections.push(lines.length > 0 ? `${heading}
1890
+
1891
+ ${lines.join("\n")}` : heading);
1892
+ }
1893
+ }
1894
+ const responses = operation["responses"];
1895
+ if (isObject(responses)) {
1896
+ const lines = [];
1897
+ for (const [status, rawResponse] of Object.entries(responses)) {
1898
+ const response = derefNode(document, rawResponse, seen) ?? rawResponse;
1899
+ if (!isObject(response)) {
1900
+ continue;
1901
+ }
1902
+ const description = asString(response["description"])?.replaceAll(/\s*\n\s*/g, " ").trim();
1903
+ lines.push(
1904
+ `- \`${status}\`${description !== void 0 && description !== "" ? ` \u2014 ${description}` : ""}`
1905
+ );
1906
+ const body = bodySchema(response["content"]);
1907
+ if (body !== void 0) {
1908
+ lines.push(...schemaFieldLines(document, body.schema, 1, seen));
1909
+ }
1910
+ }
1911
+ if (lines.length > 0) {
1912
+ sections.push(`## Responses
1913
+
1914
+ ${lines.join("\n")}`);
1915
+ }
1916
+ }
1917
+ return sections.length > 0 ? sections.join("\n\n") : null;
1918
+ }
1919
+
1920
+ // src/core/markdown.ts
1921
+ function splitFrontmatter(raw) {
1922
+ const lines = raw.split("\n");
1923
+ if (!/^---\s*$/.test(lines[0] ?? "")) {
1924
+ return { frontmatter: "", body: raw };
1925
+ }
1926
+ for (let i = 1; i < lines.length; i++) {
1927
+ if (/^---\s*$/.test(lines[i] ?? "")) {
1928
+ return { frontmatter: lines.slice(1, i).join("\n"), body: lines.slice(i + 1).join("\n") };
1929
+ }
1930
+ }
1931
+ return { frontmatter: "", body: raw };
1932
+ }
1933
+ function stripQuotes(value) {
1934
+ const quote = value[0];
1935
+ if ((quote === '"' || quote === "'") && value.length >= 2 && value.endsWith(quote)) {
1936
+ return value.slice(1, -1);
1937
+ }
1938
+ return value;
1939
+ }
1940
+ function foldLines(lines) {
1941
+ let out = "";
1942
+ let previous = "start";
1943
+ for (const line of lines) {
1944
+ if (line === "") {
1945
+ out += "\n";
1946
+ previous = "blank";
1947
+ continue;
1948
+ }
1949
+ const literal = /^[ \t]/.test(line);
1950
+ if (previous === "text") {
1951
+ out += literal ? "\n" : " ";
1952
+ } else if (previous === "literal") {
1953
+ out += "\n";
1954
+ }
1955
+ out += line;
1956
+ previous = literal ? "literal" : "text";
1957
+ }
1958
+ return out;
1959
+ }
1960
+ function readBlockScalar(lines, start, style) {
1961
+ const collected = [];
1962
+ for (let i = start; i < lines.length; i++) {
1963
+ const line = lines[i] ?? "";
1964
+ if (line.trim() !== "" && !/^[ \t]/.test(line)) {
1965
+ break;
1966
+ }
1967
+ collected.push(line);
1968
+ }
1969
+ const firstText = collected.find((line) => line.trim() !== "");
1970
+ if (firstText === void 0) {
1971
+ return void 0;
1972
+ }
1973
+ const indent = firstText.length - firstText.trimStart().length;
1974
+ const content = collected.map((line) => line.trim() === "" ? "" : line.slice(indent));
1975
+ return (style === "|" ? content.join("\n") : foldLines(content)).trim();
1976
+ }
1977
+ function stripLineComment(value) {
1978
+ return value.replace(/\s+#.*$/, "");
1979
+ }
1980
+ function frontmatterValue(frontmatter, key) {
1981
+ const lines = frontmatter.split("\n");
1982
+ const keyPattern = new RegExp(`^${key}:(.*)$`);
1983
+ for (let i = 0; i < lines.length; i++) {
1984
+ const match = keyPattern.exec(lines[i] ?? "");
1985
+ if (!match) {
1986
+ continue;
1987
+ }
1988
+ const inline = (match[1] ?? "").trim();
1989
+ const blockHeader = /^([>|])[0-9+-]*$/.exec(inline);
1990
+ if (blockHeader) {
1991
+ return readBlockScalar(lines, i + 1, blockHeader[1]);
1992
+ }
1993
+ if (inline === "") {
1994
+ return void 0;
1995
+ }
1996
+ const quoted = inline.startsWith('"') || inline.startsWith("'");
1997
+ const parts = [quoted ? inline : stripLineComment(inline)];
1998
+ for (let j = i + 1; j < lines.length; j++) {
1999
+ const continuation = lines[j] ?? "";
2000
+ if (continuation.trim() === "" || !/^[ \t]/.test(continuation)) {
2001
+ break;
2002
+ }
2003
+ parts.push(quoted ? continuation.trim() : stripLineComment(continuation.trim()));
2004
+ }
2005
+ return stripQuotes(parts.join(" ")).trim();
2006
+ }
2007
+ return void 0;
2008
+ }
2009
+ function isGeneratedApiPage(frontmatter) {
2010
+ return /^_openapi\s*:/m.test(frontmatter);
2011
+ }
2012
+ function cleanBody(body) {
2013
+ const segments = body.split(/(```[\s\S]*?```)/g);
2014
+ const cleaned = segments.map(
2015
+ (segment) => segment.startsWith("```") ? segment : segment.replace(/^\s*(import|export)\s.*$/gm, "").replace(/\{\/\*[\s\S]*?\*\/\}/g, "").replace(/<\/?[A-Za-z][^>]*>/g, "")
2016
+ );
2017
+ return cleaned.join("").replace(/\n{3,}/g, "\n\n").trim();
2018
+ }
2019
+ function summariseApiPage(title, description, body, documents) {
2020
+ const opMatch = /operations=\{(\[[\s\S]*?\])\}/.exec(body);
2021
+ let ops = [];
2022
+ if (opMatch?.[1]) {
2023
+ try {
2024
+ const parsed = JSON.parse(opMatch[1]);
2025
+ ops = (Array.isArray(parsed) ? parsed : []).filter(
2026
+ (op) => typeof op === "object" && op !== null && typeof op.path === "string" && typeof op.method === "string"
2027
+ );
2028
+ } catch {
2029
+ ops = [];
2030
+ }
2031
+ }
2032
+ const documentName = /document="([^"]+)"/.exec(body)?.[1];
2033
+ const onlyName = Object.keys(documents ?? {}).length === 1 ? Object.keys(documents ?? {})[0] : void 0;
2034
+ const lookupName = documentName ?? onlyName;
2035
+ const document = lookupName === void 0 ? void 0 : documents?.[lookupName];
2036
+ const operationSections = ops.map((op) => {
2037
+ const endpoint = `\`${op.method.toUpperCase()} ${op.path}\``;
2038
+ const detail = document === void 0 ? null : openApiOperationToMarkdown(document, op);
2039
+ return detail === null ? endpoint : `${endpoint}
2040
+
2041
+ ${detail}`;
2042
+ });
2043
+ return `${[`# ${title}`, description ?? "", ...operationSections].filter(Boolean).join("\n\n")}
2044
+ `;
2045
+ }
2046
+ function mdxToMarkdown(raw, options = {}) {
2047
+ if (raw.trim() === "") {
2048
+ return null;
2049
+ }
2050
+ const { frontmatter, body } = splitFrontmatter(raw);
2051
+ const title = frontmatterValue(frontmatter, "title") ?? options.fallbackTitle ?? "Untitled";
2052
+ const description = frontmatterValue(frontmatter, "description");
2053
+ const trimmedBody = body.trim();
2054
+ if (isGeneratedApiPage(frontmatter)) {
2055
+ return summariseApiPage(title, description, trimmedBody, options.openApiDocuments);
2056
+ }
2057
+ return `# ${title}
2058
+
2059
+ ${description ? `${description}
2060
+
2061
+ ` : ""}${cleanBody(trimmedBody)}
2062
+ `;
2063
+ }
2064
+ function createMarkdownRoute(options) {
2065
+ return {
2066
+ async GET(_request, context) {
2067
+ const { slug } = await context.params;
2068
+ const raw = await options.read(slug);
2069
+ if (raw === null) {
2070
+ return new Response("Not found", { status: 404 });
2071
+ }
2072
+ const fallbackTitle = slug?.at(-1);
2073
+ const openApiDocuments = await options.loadOpenApiDocuments?.();
2074
+ const markdown = mdxToMarkdown(raw, {
2075
+ ...fallbackTitle === void 0 ? {} : { fallbackTitle },
2076
+ ...openApiDocuments === void 0 ? {} : { openApiDocuments }
2077
+ });
2078
+ if (markdown === null) {
2079
+ return new Response("Not found", { status: 404 });
2080
+ }
2081
+ return new Response(markdown, {
2082
+ headers: { "content-type": "text/plain; charset=utf-8" }
2083
+ });
2084
+ }
2085
+ };
2086
+ }
2087
+
2088
+ // src/core/auth/domains.ts
2089
+ function isAllowedEmail(email, allowedDomains) {
2090
+ const at = email.lastIndexOf("@");
2091
+ if (at < 1 || at === email.length - 1) {
2092
+ return false;
2093
+ }
2094
+ const domain = email.slice(at + 1).toLowerCase();
2095
+ return allowedDomains.some((allowed) => allowed.toLowerCase() === domain);
2096
+ }
2097
+
2098
+ // src/core/auth/session.ts
2099
+ var SESSION_COOKIE = "docs_session";
2100
+ var encoder = new TextEncoder();
2101
+ var decoder = new TextDecoder();
2102
+ function base64UrlEncode(bytes) {
2103
+ let binary = "";
2104
+ for (const byte of bytes) {
2105
+ binary += String.fromCharCode(byte);
2106
+ }
2107
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
2108
+ }
2109
+ function base64UrlDecode(value) {
2110
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
2111
+ const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
2112
+ const binary = atob(padded);
2113
+ const bytes = new Uint8Array(binary.length);
2114
+ for (let i = 0; i < binary.length; i += 1) {
2115
+ bytes[i] = binary.charCodeAt(i);
2116
+ }
2117
+ return bytes;
2118
+ }
2119
+ async function hmacKey(secret) {
2120
+ return crypto.subtle.importKey(
2121
+ "raw",
2122
+ encoder.encode(secret),
2123
+ { name: "HMAC", hash: "SHA-256" },
2124
+ false,
2125
+ ["sign", "verify"]
2126
+ );
2127
+ }
2128
+ async function signSession(payload, secret) {
2129
+ const data = base64UrlEncode(encoder.encode(JSON.stringify(payload)));
2130
+ const key = await hmacKey(secret);
2131
+ const signature = new Uint8Array(await crypto.subtle.sign("HMAC", key, encoder.encode(data)));
2132
+ return `${data}.${base64UrlEncode(signature)}`;
2133
+ }
2134
+ async function verifySession(token, secret, nowSeconds = Math.floor(Date.now() / 1e3)) {
2135
+ const dot = token.indexOf(".");
2136
+ if (dot <= 0) {
2137
+ return null;
2138
+ }
2139
+ const data = token.slice(0, dot);
2140
+ const signature = token.slice(dot + 1);
2141
+ const key = await hmacKey(secret);
2142
+ let valid;
2143
+ try {
2144
+ valid = await crypto.subtle.verify(
2145
+ "HMAC",
2146
+ key,
2147
+ base64UrlDecode(signature),
2148
+ encoder.encode(data)
2149
+ );
2150
+ } catch {
2151
+ return null;
2152
+ }
2153
+ if (!valid) {
2154
+ return null;
2155
+ }
2156
+ let payload;
2157
+ try {
2158
+ payload = JSON.parse(decoder.decode(base64UrlDecode(data)));
2159
+ } catch {
2160
+ return null;
2161
+ }
2162
+ if (typeof payload.email !== "string" || typeof payload.exp !== "number") {
2163
+ return null;
2164
+ }
2165
+ if (payload.exp < nowSeconds) {
2166
+ return null;
2167
+ }
2168
+ return payload;
2169
+ }
2170
+ function readCookie(cookieHeader, name) {
2171
+ if (cookieHeader === null) {
2172
+ return null;
2173
+ }
2174
+ for (const part of cookieHeader.split(";")) {
2175
+ const [key, ...rest] = part.trim().split("=");
2176
+ if (key === name) {
2177
+ return rest.join("=");
2178
+ }
2179
+ }
2180
+ return null;
2181
+ }
2182
+ function buildSessionCookie(token, options) {
2183
+ const flags = ["Path=/", "HttpOnly", "SameSite=Lax", `Max-Age=${String(options.maxAgeSeconds)}`];
2184
+ if (options.secure !== false) {
2185
+ flags.push("Secure");
2186
+ }
2187
+ return `${SESSION_COOKIE}=${token}; ${flags.join("; ")}`;
2188
+ }
2189
+ function clearSessionCookie(options = {}) {
2190
+ const flags = ["Path=/", "HttpOnly", "SameSite=Lax", "Max-Age=0"];
2191
+ if (options.secure !== false) {
2192
+ flags.push("Secure");
2193
+ }
2194
+ return `${SESSION_COOKIE}=; ${flags.join("; ")}`;
2195
+ }
2196
+
2197
+ // src/core/auth/email-provider.ts
2198
+ var DEFAULT_TTL_SECONDS = 8 * 60 * 60;
2199
+ async function readEmailAndFrom(request) {
2200
+ const contentType = request.headers.get("content-type") ?? "";
2201
+ if (contentType.includes("application/json")) {
2202
+ const body = await request.json();
2203
+ return {
2204
+ email: typeof body["email"] === "string" ? body["email"] : "",
2205
+ from: typeof body["from"] === "string" ? body["from"] : null
2206
+ };
2207
+ }
2208
+ const form = await request.formData();
2209
+ const email = form.get("email");
2210
+ const from = form.get("from");
2211
+ return {
2212
+ email: typeof email === "string" ? email : "",
2213
+ from: typeof from === "string" ? from : null
2214
+ };
2215
+ }
2216
+ function safeRedirectPath(from, fallback) {
2217
+ if (from !== null && from.startsWith("/") && !from.startsWith("//")) {
2218
+ return from;
2219
+ }
2220
+ return fallback;
2221
+ }
2222
+ function createEmailSignIn(options) {
2223
+ const ttl = options.ttlSeconds ?? DEFAULT_TTL_SECONDS;
2224
+ const fallback = options.defaultRedirect ?? "/";
2225
+ const signInPath = options.signInPath ?? "/signin";
2226
+ return async function handle(request) {
2227
+ const isJson = (request.headers.get("content-type") ?? "").includes("application/json");
2228
+ const { email, from } = await readEmailAndFrom(request);
2229
+ if (!isAllowedEmail(email, options.allowedDomains)) {
2230
+ if (isJson) {
2231
+ return new Response(JSON.stringify({ error: "This email domain is not allowed." }), {
2232
+ status: 403,
2233
+ headers: { "content-type": "application/json" }
2234
+ });
2235
+ }
2236
+ const params = new URLSearchParams({ error: "forbidden" });
2237
+ const safeFrom = safeRedirectPath(from, "");
2238
+ if (safeFrom) {
2239
+ params.set("from", safeFrom);
2240
+ }
2241
+ return new Response(null, {
2242
+ status: 303,
2243
+ headers: { location: `${signInPath}?${params.toString()}` }
2244
+ });
2245
+ }
2246
+ const exp = Math.floor(Date.now() / 1e3) + ttl;
2247
+ const token = await signSession({ email, exp }, options.secret);
2248
+ const cookie2 = buildSessionCookie(token, {
2249
+ maxAgeSeconds: ttl,
2250
+ ...options.secureCookies !== void 0 ? { secure: options.secureCookies } : {}
2251
+ });
2252
+ const target = safeRedirectPath(from, fallback);
2253
+ return new Response(null, {
2254
+ status: 303,
2255
+ headers: { location: target, "set-cookie": cookie2 }
2256
+ });
2257
+ };
2258
+ }
2259
+
2260
+ // src/core/auth/google-provider.ts
2261
+ var AUTHORIZE_URL = "https://accounts.google.com/o/oauth2/v2/auth";
2262
+ var TOKEN_URL = "https://oauth2.googleapis.com/token";
2263
+ var STATE_COOKIE = "docs_oauth_state";
2264
+ var FROM_COOKIE = "docs_oauth_from";
2265
+ var DEFAULT_TTL_SECONDS2 = 8 * 60 * 60;
2266
+ function cookie(name, value, secure, maxAge) {
2267
+ const flags = ["Path=/", "HttpOnly", "SameSite=Lax", `Max-Age=${String(maxAge)}`];
2268
+ if (secure) {
2269
+ flags.push("Secure");
2270
+ }
2271
+ return `${name}=${value}; ${flags.join("; ")}`;
2272
+ }
2273
+ function readCookie2(header, name) {
2274
+ if (header === null) {
2275
+ return null;
2276
+ }
2277
+ for (const part of header.split(";")) {
2278
+ const [key, ...rest] = part.trim().split("=");
2279
+ if (key === name) {
2280
+ return rest.join("=");
2281
+ }
2282
+ }
2283
+ return null;
2284
+ }
2285
+ function safeRedirectPath2(from, fallback) {
2286
+ if (from !== null && from.startsWith("/") && !from.startsWith("//")) {
2287
+ return from;
2288
+ }
2289
+ return fallback;
2290
+ }
2291
+ function decodeIdTokenEmail(idToken) {
2292
+ const parts = idToken.split(".");
2293
+ if (parts.length < 2 || parts[1] === void 0) {
2294
+ return null;
2295
+ }
2296
+ try {
2297
+ const normalized = parts[1].replace(/-/g, "+").replace(/_/g, "/");
2298
+ const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
2299
+ const claims = JSON.parse(atob(padded));
2300
+ return typeof claims.email === "string" ? claims.email : null;
2301
+ } catch {
2302
+ return null;
2303
+ }
2304
+ }
2305
+ function createGoogleAuth(options) {
2306
+ const ttl = options.ttlSeconds ?? DEFAULT_TTL_SECONDS2;
2307
+ const secure = options.secureCookies !== false;
2308
+ const fallback = options.defaultRedirect ?? "/";
2309
+ const signInPath = options.signInPath ?? "/signin";
2310
+ const doFetch = options.fetch ?? ((url, init) => fetch(url, init));
2311
+ function bounce(code, from) {
2312
+ const params = new URLSearchParams({ error: code });
2313
+ const safeFrom = safeRedirectPath2(from, "");
2314
+ if (safeFrom) {
2315
+ params.set("from", safeFrom);
2316
+ }
2317
+ const headers = new Headers({ location: `${signInPath}?${params.toString()}` });
2318
+ headers.append("set-cookie", cookie(STATE_COOKIE, "", secure, 0));
2319
+ headers.append("set-cookie", cookie(FROM_COOKIE, "", secure, 0));
2320
+ return new Response(null, { status: 303, headers });
2321
+ }
2322
+ function start(request) {
2323
+ const url = new URL(request.url);
2324
+ const from = url.searchParams.get("from");
2325
+ const state = crypto.randomUUID();
2326
+ const authorize = new URL(AUTHORIZE_URL);
2327
+ authorize.searchParams.set("client_id", options.clientId);
2328
+ authorize.searchParams.set("redirect_uri", options.redirectUri);
2329
+ authorize.searchParams.set("response_type", "code");
2330
+ authorize.searchParams.set("scope", "openid email");
2331
+ authorize.searchParams.set("state", state);
2332
+ if (options.allowedDomains.length === 1 && options.allowedDomains[0] !== void 0) {
2333
+ authorize.searchParams.set("hd", options.allowedDomains[0]);
2334
+ }
2335
+ const headers = new Headers({ location: authorize.toString() });
2336
+ headers.append("set-cookie", cookie(STATE_COOKIE, state, secure, 600));
2337
+ headers.append("set-cookie", cookie(FROM_COOKIE, from ?? fallback, secure, 600));
2338
+ return new Response(null, { status: 302, headers });
2339
+ }
2340
+ async function callback(request) {
2341
+ const url = new URL(request.url);
2342
+ const code = url.searchParams.get("code");
2343
+ const returnedState = url.searchParams.get("state");
2344
+ const cookieHeader = request.headers.get("cookie");
2345
+ const expectedState = readCookie2(cookieHeader, STATE_COOKIE);
2346
+ if (code === null || returnedState === null || expectedState === null || returnedState !== expectedState) {
2347
+ return bounce("state", readCookie2(cookieHeader, FROM_COOKIE));
2348
+ }
2349
+ const tokenResponse = await doFetch(TOKEN_URL, {
2350
+ method: "POST",
2351
+ headers: { "content-type": "application/x-www-form-urlencoded" },
2352
+ body: new URLSearchParams({
2353
+ client_id: options.clientId,
2354
+ client_secret: options.clientSecret,
2355
+ code,
2356
+ grant_type: "authorization_code",
2357
+ redirect_uri: options.redirectUri
2358
+ }).toString()
2359
+ });
2360
+ if (!tokenResponse.ok) {
2361
+ return bounce("oauth", readCookie2(cookieHeader, FROM_COOKIE));
2362
+ }
2363
+ const tokens = await tokenResponse.json();
2364
+ const email = typeof tokens.id_token === "string" ? decodeIdTokenEmail(tokens.id_token) : null;
2365
+ if (email === null || !isAllowedEmail(email, options.allowedDomains)) {
2366
+ return bounce("forbidden", readCookie2(cookieHeader, FROM_COOKIE));
2367
+ }
2368
+ const exp = Math.floor(Date.now() / 1e3) + ttl;
2369
+ const token = await signSession({ email, exp }, options.secret);
2370
+ const target = safeRedirectPath2(readCookie2(cookieHeader, FROM_COOKIE), fallback);
2371
+ const headers = new Headers({ location: target });
2372
+ headers.append("set-cookie", buildSessionCookie(token, { maxAgeSeconds: ttl, secure }));
2373
+ headers.append("set-cookie", cookie(STATE_COOKIE, "", secure, 0));
2374
+ headers.append("set-cookie", cookie(FROM_COOKIE, "", secure, 0));
2375
+ return new Response(null, { status: 303, headers });
2376
+ }
2377
+ return { start, callback };
2378
+ }
2379
+
2380
+ // src/core/auth/gate.ts
2381
+ var ALWAYS_PUBLIC = ["/_next/", "/favicon", "/api/auth/"];
2382
+ function createAuthGate(options) {
2383
+ const signInPath = options.signInPath ?? "/signin";
2384
+ const publicPaths = [...ALWAYS_PUBLIC, ...options.publicPaths ?? []];
2385
+ return async function gate(request) {
2386
+ const url = new URL(request.url);
2387
+ const path = url.pathname;
2388
+ if (path === signInPath || publicPaths.some((prefix) => path.startsWith(prefix))) {
2389
+ return null;
2390
+ }
2391
+ const token = readCookie(request.headers.get("cookie"), SESSION_COOKIE);
2392
+ if (token !== null) {
2393
+ const session = await verifySession(token, options.secret);
2394
+ if (session !== null && isAllowedEmail(session.email, options.allowedDomains)) {
2395
+ return null;
2396
+ }
2397
+ }
2398
+ const redirectUrl = new URL(signInPath, url.origin);
2399
+ redirectUrl.searchParams.set("from", path);
2400
+ return Response.redirect(redirectUrl.toString(), 302);
2401
+ };
2402
+ }
2403
+
2404
+ // src/core/auth/secret-provider.ts
2405
+ var DEFAULT_TTL_SECONDS3 = 8 * 60 * 60;
2406
+ var DEFAULT_PENDING_TTL_SECONDS = 15 * 60;
2407
+ var PENDING_COOKIE = "docs_secret_email";
2408
+ function timingSafeEqual(a, b) {
2409
+ const encoder2 = new TextEncoder();
2410
+ const ea = encoder2.encode(a);
2411
+ const eb = encoder2.encode(b);
2412
+ let diff = ea.length ^ eb.length;
2413
+ const len = Math.max(ea.length, eb.length);
2414
+ for (let i = 0; i < len; i += 1) {
2415
+ diff |= (ea[i] ?? 0) ^ (eb[i] ?? 0);
2416
+ }
2417
+ return diff === 0;
2418
+ }
2419
+ function safeRedirectPath3(from, fallback) {
2420
+ if (from !== null && from.startsWith("/") && !from.startsWith("//")) {
2421
+ return from;
2422
+ }
2423
+ return fallback;
2424
+ }
2425
+ async function readForm(request) {
2426
+ const contentType = request.headers.get("content-type") ?? "";
2427
+ if (contentType.includes("application/json")) {
2428
+ const body = await request.json();
2429
+ return {
2430
+ email: typeof body["email"] === "string" ? body["email"] : "",
2431
+ secret: typeof body["secret"] === "string" ? body["secret"] : null,
2432
+ from: typeof body["from"] === "string" ? body["from"] : null
2433
+ };
2434
+ }
2435
+ const form = await request.formData();
2436
+ const email = form.get("email");
2437
+ const secret = form.get("secret");
2438
+ const from = form.get("from");
2439
+ return {
2440
+ email: typeof email === "string" ? email : "",
2441
+ secret: typeof secret === "string" ? secret : null,
2442
+ from: typeof from === "string" ? from : null
2443
+ };
2444
+ }
2445
+ function signInQuery(parts) {
2446
+ const params = new URLSearchParams();
2447
+ for (const [key, value] of Object.entries(parts)) {
2448
+ if (value) {
2449
+ params.set(key, value);
2450
+ }
2451
+ }
2452
+ const query = params.toString();
2453
+ return query ? `?${query}` : "";
2454
+ }
2455
+ function pendingCookie(email, secure, maxAge) {
2456
+ const flags = ["Path=/", "HttpOnly", "SameSite=Lax", `Max-Age=${String(maxAge)}`];
2457
+ if (secure) {
2458
+ flags.push("Secure");
2459
+ }
2460
+ return `${PENDING_COOKIE}=${encodeURIComponent(email)}; ${flags.join("; ")}`;
2461
+ }
2462
+ function clearPendingCookie(secure) {
2463
+ return pendingCookie("", secure, 0);
2464
+ }
2465
+ function createSecretSignIn(options) {
2466
+ const ttl = options.ttlSeconds ?? DEFAULT_TTL_SECONDS3;
2467
+ const pendingTtl = options.pendingTtlSeconds ?? DEFAULT_PENDING_TTL_SECONDS;
2468
+ const secure = options.secureCookies !== false;
2469
+ const fallback = options.defaultRedirect ?? "/";
2470
+ const signInPath = options.signInPath ?? "/signin";
2471
+ return async function handle(request) {
2472
+ const { email, secret, from } = await readForm(request);
2473
+ const safeFrom = safeRedirectPath3(from, "") || void 0;
2474
+ if (secret === null) {
2475
+ if (!isAllowedEmail(email, options.allowedDomains)) {
2476
+ return new Response(null, {
2477
+ status: 303,
2478
+ headers: {
2479
+ location: `${signInPath}${signInQuery({ error: "forbidden", from: safeFrom })}`
2480
+ }
2481
+ });
2482
+ }
2483
+ const headers2 = new Headers({
2484
+ location: `${signInPath}${signInQuery({ step: "secret", from: safeFrom })}`
2485
+ });
2486
+ headers2.append("set-cookie", pendingCookie(email, secure, pendingTtl));
2487
+ return new Response(null, { status: 303, headers: headers2 });
2488
+ }
2489
+ const pending = readCookie(request.headers.get("cookie"), PENDING_COOKIE);
2490
+ const pendingEmail = pending ? decodeURIComponent(pending) : "";
2491
+ if (!isAllowedEmail(pendingEmail, options.allowedDomains)) {
2492
+ return new Response(null, {
2493
+ status: 303,
2494
+ headers: { location: `${signInPath}${signInQuery({ from: safeFrom })}` }
2495
+ });
2496
+ }
2497
+ if (options.accessSecret === "" || !timingSafeEqual(secret, options.accessSecret)) {
2498
+ return new Response(null, {
2499
+ status: 303,
2500
+ headers: {
2501
+ location: `${signInPath}${signInQuery({ step: "secret", error: "badsecret", from: safeFrom })}`
2502
+ }
2503
+ });
2504
+ }
2505
+ const exp = Math.floor(Date.now() / 1e3) + ttl;
2506
+ const token = await signSession({ email: pendingEmail, exp }, options.secret);
2507
+ const headers = new Headers({ location: safeRedirectPath3(from, fallback) });
2508
+ headers.append("set-cookie", buildSessionCookie(token, { maxAgeSeconds: ttl, secure }));
2509
+ headers.append("set-cookie", clearPendingCookie(secure));
2510
+ return new Response(null, { status: 303, headers });
2511
+ };
2512
+ }
2513
+
2514
+ // src/core/auth/signout.ts
2515
+ function createSignOut(options = {}) {
2516
+ return function handle() {
2517
+ return new Response(null, {
2518
+ status: 303,
2519
+ headers: {
2520
+ location: options.redirectTo ?? "/signin",
2521
+ "set-cookie": clearSessionCookie(
2522
+ options.secureCookies !== void 0 ? { secure: options.secureCookies } : {}
2523
+ )
2524
+ }
2525
+ });
2526
+ };
2527
+ }
2528
+
2529
+ // src/core/proxy.ts
2530
+ var DEFAULT_RATE_LIMIT_PER_MINUTE2 = 60;
2531
+ var ProxyTargetError = class extends Error {
2532
+ constructor(message) {
2533
+ super(message);
2534
+ this.name = "ProxyTargetError";
2535
+ }
2536
+ };
2537
+ var BLOCKED_HOST_PATTERNS = [
2538
+ /^localhost$/i,
2539
+ /\.localhost$/i,
2540
+ /^0\.0\.0\.0$/,
2541
+ /^127\./,
2542
+ /^10\./,
2543
+ /^192\.168\./,
2544
+ /^169\.254\./,
2545
+ /^172\.(1[6-9]|2\d|3[01])\./,
2546
+ /^\[?::1\]?$/,
2547
+ /^\[?fe80:/i,
2548
+ /^\[?fc00:/i,
2549
+ /^\[?fd[0-9a-f]{2}:/i
2550
+ ];
2551
+ function isBlockedHost(hostname) {
2552
+ return BLOCKED_HOST_PATTERNS.some((pattern) => pattern.test(hostname));
2553
+ }
2554
+ function normalizeOrigins(origins) {
2555
+ const set = /* @__PURE__ */ new Set();
2556
+ for (const origin of origins) {
2557
+ let parsed;
2558
+ try {
2559
+ parsed = new URL(origin);
2560
+ } catch {
2561
+ throw new ProxyTargetError(`Invalid allow-listed origin: ${origin}`);
2562
+ }
2563
+ set.add(parsed.origin);
2564
+ }
2565
+ return set;
2566
+ }
2567
+ function assertAllowedTarget(target, allowedOrigins) {
2568
+ const allowed = normalizeOrigins(allowedOrigins);
2569
+ let url;
2570
+ try {
2571
+ url = new URL(target);
2572
+ } catch {
2573
+ throw new ProxyTargetError(`Invalid target URL: ${target}`);
2574
+ }
2575
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
2576
+ throw new ProxyTargetError(`Unsupported protocol: ${url.protocol}`);
2577
+ }
2578
+ if (isBlockedHost(url.hostname)) {
2579
+ throw new ProxyTargetError(`Blocked host: ${url.hostname}`);
2580
+ }
2581
+ if (!allowed.has(url.origin)) {
2582
+ throw new ProxyTargetError(`Target origin is not allow-listed: ${url.origin}`);
2583
+ }
2584
+ return url;
2585
+ }
2586
+ var STRIPPED_REQUEST_HEADERS = /* @__PURE__ */ new Set([
2587
+ "host",
2588
+ "connection",
2589
+ "content-length"
2590
+ ]);
2591
+ function jsonResponse2(status, payload, headers = {}) {
2592
+ return new Response(JSON.stringify(payload), {
2593
+ status,
2594
+ headers: { "content-type": "application/json", ...headers }
2595
+ });
2596
+ }
2597
+ function proxyLimiter(options) {
2598
+ const limit = options.rateLimitPerMinute ?? DEFAULT_RATE_LIMIT_PER_MINUTE2;
2599
+ if (!Number.isInteger(limit) || limit < 1) {
2600
+ throw new ProxyTargetError(
2601
+ `proxy.rateLimitPerMinute must be an integer of 1 or more, got ${String(limit)}.`
2602
+ );
2603
+ }
2604
+ return createRateLimiter({
2605
+ limit,
2606
+ windowMs: RATE_LIMIT_WINDOW_MS,
2607
+ ...options.now ? { now: options.now } : {}
2608
+ });
2609
+ }
2610
+ function tooManyRequests(retryAfterSeconds) {
2611
+ const retryAfter = retryAfterSeconds ?? 60;
2612
+ return jsonResponse2(
2613
+ 429,
2614
+ {
2615
+ error: `Too many requests. Try again in ${String(retryAfter)} seconds.`,
2616
+ retryAfterSeconds: retryAfter
2617
+ },
2618
+ { "retry-after": String(retryAfter) }
2619
+ );
2620
+ }
2621
+ function parseEnvelope(value) {
2622
+ if (typeof value !== "object" || value === null) {
2623
+ throw new ProxyTargetError("Request body must be a JSON object.");
2624
+ }
2625
+ const record = value;
2626
+ if (typeof record["url"] !== "string") {
2627
+ throw new ProxyTargetError('Request body must include a string "url".');
2628
+ }
2629
+ const headers = {};
2630
+ if (typeof record["headers"] === "object" && record["headers"] !== null) {
2631
+ for (const [key, header] of Object.entries(record["headers"])) {
2632
+ if (typeof header === "string" && !STRIPPED_REQUEST_HEADERS.has(key.toLowerCase())) {
2633
+ headers[key] = header;
2634
+ }
2635
+ }
2636
+ }
2637
+ return {
2638
+ url: record["url"],
2639
+ method: typeof record["method"] === "string" ? record["method"] : "GET",
2640
+ headers,
2641
+ body: typeof record["body"] === "string" ? record["body"] : null
2642
+ };
2643
+ }
2644
+ function createTryItProxy(options) {
2645
+ normalizeOrigins(options.allowedOrigins);
2646
+ const doFetch = options.fetch ?? ((url, init) => fetch(url, init));
2647
+ const limiter = proxyLimiter(options);
2648
+ return async function handle(request) {
2649
+ const decision = limiter.allow(clientRateLimitKey(request));
2650
+ if (!decision.allowed) {
2651
+ return tooManyRequests(decision.retryAfterSeconds);
2652
+ }
2653
+ let envelope;
2654
+ try {
2655
+ const parsed = await request.json();
2656
+ envelope = parseEnvelope(parsed);
2657
+ } catch (error) {
2658
+ const message = error instanceof ProxyTargetError ? error.message : "Invalid JSON body.";
2659
+ return jsonResponse2(400, { error: message });
2660
+ }
2661
+ let target;
2662
+ try {
2663
+ target = assertAllowedTarget(envelope.url, options.allowedOrigins);
2664
+ } catch (error) {
2665
+ const message = error instanceof ProxyTargetError ? error.message : "Blocked target.";
2666
+ return jsonResponse2(403, { error: message });
2667
+ }
2668
+ const init = {
2669
+ method: envelope.method ?? "GET",
2670
+ headers: envelope.headers ?? {}
2671
+ };
2672
+ if (envelope.body !== null && envelope.body !== void 0) {
2673
+ init.body = envelope.body;
2674
+ }
2675
+ const upstream = await doFetch(target.toString(), init);
2676
+ return new Response(upstream.body, {
2677
+ status: upstream.status,
2678
+ headers: upstream.headers
2679
+ });
2680
+ };
2681
+ }
2682
+ var STRIPPED_FORWARD_HEADERS = [
2683
+ "host",
2684
+ "connection",
2685
+ "content-length",
2686
+ "origin",
2687
+ "referer",
2688
+ "x-forwarded-for",
2689
+ "x-forwarded-host",
2690
+ "x-forwarded-proto",
2691
+ "x-forwarded-port",
2692
+ "x-real-ip"
2693
+ ];
2694
+ var TARGET_PARAMS = ["url", "scalar_url"];
2695
+ function createTryItProxyRoute(options) {
2696
+ normalizeOrigins(options.allowedOrigins);
2697
+ const doFetch = options.fetch ?? ((url, init) => fetch(url, init));
2698
+ const limiter = proxyLimiter(options);
2699
+ async function handle(request) {
2700
+ const decision = limiter.allow(clientRateLimitKey(request));
2701
+ if (!decision.allowed) {
2702
+ return tooManyRequests(decision.retryAfterSeconds);
2703
+ }
2704
+ const requestUrl = new URL(request.url);
2705
+ const rawTarget = TARGET_PARAMS.map((name) => requestUrl.searchParams.get(name) ?? "").find(
2706
+ (value) => value !== ""
2707
+ );
2708
+ if (rawTarget === void 0) {
2709
+ return jsonResponse2(400, {
2710
+ error: 'Missing target. Pass the upstream URL in the "url" query parameter.'
2711
+ });
2712
+ }
2713
+ let target;
2714
+ try {
2715
+ target = assertAllowedTarget(rawTarget, options.allowedOrigins);
2716
+ } catch (error) {
2717
+ const message = error instanceof ProxyTargetError ? error.message : "Blocked target.";
2718
+ return jsonResponse2(403, { error: message });
2719
+ }
2720
+ const headers = new Headers(request.headers);
2721
+ for (const name of STRIPPED_FORWARD_HEADERS) {
2722
+ headers.delete(name);
2723
+ }
2724
+ const init = { method: request.method, headers, redirect: "manual" };
2725
+ if (request.method !== "GET" && request.method !== "HEAD") {
2726
+ init.body = await request.arrayBuffer();
2727
+ }
2728
+ const upstream = await doFetch(target.toString(), init);
2729
+ const responseHeaders = new Headers(upstream.headers);
2730
+ responseHeaders.set("access-control-allow-origin", "*");
2731
+ return new Response(upstream.body, { status: upstream.status, headers: responseHeaders });
2732
+ }
2733
+ return {
2734
+ GET: handle,
2735
+ POST: handle,
2736
+ PUT: handle,
2737
+ PATCH: handle,
2738
+ DELETE: handle,
2739
+ OPTIONS: () => new Response(null, {
2740
+ status: 204,
2741
+ headers: {
2742
+ "access-control-allow-origin": "*",
2743
+ "access-control-allow-methods": "GET,POST,PUT,PATCH,DELETE,OPTIONS",
2744
+ "access-control-allow-headers": "*"
2745
+ }
2746
+ })
2747
+ };
2748
+ }
2749
+ function unwrapProxyTarget(url) {
2750
+ try {
2751
+ const parsed = new URL(url, "http://localhost");
2752
+ if (!/\/proxy\/?$/.test(parsed.pathname)) {
2753
+ return url;
2754
+ }
2755
+ for (const name of TARGET_PARAMS) {
2756
+ const target = parsed.searchParams.get(name);
2757
+ if (target !== null && target !== "") {
2758
+ return target;
2759
+ }
2760
+ }
2761
+ return url;
2762
+ } catch {
2763
+ return url;
2764
+ }
2765
+ }
2766
+
2767
+ // src/adapters/docs-json/parse-navigation.ts
2768
+ var HTTP_METHODS = /* @__PURE__ */ new Set([
2769
+ "GET",
2770
+ "POST",
2771
+ "PUT",
2772
+ "PATCH",
2773
+ "DELETE",
2774
+ "HEAD",
2775
+ "OPTIONS",
2776
+ "TRACE"
2777
+ ]);
2778
+ var InvalidNavigationError = class extends Error {
2779
+ constructor(message) {
2780
+ super(message);
2781
+ this.name = "InvalidNavigationError";
2782
+ }
2783
+ };
2784
+ function isRecord(value) {
2785
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2786
+ }
2787
+ function parsePageString(entry) {
2788
+ const parts = entry.trim().split(/\s+/);
2789
+ const [file, maybeMethod, ...rest] = parts;
2790
+ if (file === void 0 || file === "") {
2791
+ throw new InvalidNavigationError("Encountered an empty navigation page entry.");
2792
+ }
2793
+ if (maybeMethod !== void 0 && rest.length > 0 && HTTP_METHODS.has(maybeMethod.toUpperCase())) {
2794
+ return {
2795
+ kind: "openapi",
2796
+ file,
2797
+ method: maybeMethod.toUpperCase(),
2798
+ apiPath: rest.join(" ")
2799
+ };
2800
+ }
2801
+ return { kind: "doc", file: entry.trim() };
2802
+ }
2803
+ function parseItem(entry, path) {
2804
+ if (typeof entry === "string") {
2805
+ return parsePageString(entry);
2806
+ }
2807
+ if (isRecord(entry) && typeof entry["group"] === "string") {
2808
+ return parseGroup(entry, path);
2809
+ }
2810
+ throw new InvalidNavigationError(
2811
+ `Navigation entry at ${path} must be a page string or a group object.`
2812
+ );
2813
+ }
2814
+ function parseItems(pages, path) {
2815
+ if (!Array.isArray(pages)) {
2816
+ throw new InvalidNavigationError(`Expected an array of pages at ${path}.`);
2817
+ }
2818
+ return pages.map((entry, index) => parseItem(entry, `${path}[${String(index)}]`));
2819
+ }
2820
+ function parseGroup(group, path) {
2821
+ const title = group["group"];
2822
+ if (typeof title !== "string") {
2823
+ throw new InvalidNavigationError(`Group at ${path} is missing a string "group" title.`);
2824
+ }
2825
+ return {
2826
+ kind: "group",
2827
+ title,
2828
+ items: parseItems(group["pages"], `${path}.pages`)
2829
+ };
2830
+ }
2831
+ function parseTab(tab, path) {
2832
+ if (!isRecord(tab) || typeof tab["tab"] !== "string") {
2833
+ throw new InvalidNavigationError(`Tab at ${path} is missing a string "tab" title.`);
2834
+ }
2835
+ const items = tab["groups"] !== void 0 ? parseGroups(tab["groups"], `${path}.groups`) : parseItems(tab["pages"], `${path}.pages`);
2836
+ const icon = tab["icon"];
2837
+ return {
2838
+ kind: "tab",
2839
+ title: tab["tab"],
2840
+ items,
2841
+ ...typeof icon === "string" ? { icon } : {}
2842
+ };
2843
+ }
2844
+ function parseGroups(groups, path) {
2845
+ if (!Array.isArray(groups)) {
2846
+ throw new InvalidNavigationError(`Expected an array of groups at ${path}.`);
2847
+ }
2848
+ return groups.map((group, index) => {
2849
+ const groupPath = `${path}[${String(index)}]`;
2850
+ if (!isRecord(group)) {
2851
+ throw new InvalidNavigationError(`Group at ${groupPath} must be an object.`);
2852
+ }
2853
+ return parseGroup(group, groupPath);
2854
+ });
2855
+ }
2856
+ function parseNavigation(docsJson) {
2857
+ if (!isRecord(docsJson)) {
2858
+ throw new InvalidNavigationError("docs.json must be an object.");
2859
+ }
2860
+ const navigation = docsJson["navigation"];
2861
+ if (!isRecord(navigation)) {
2862
+ throw new InvalidNavigationError('docs.json must have a "navigation" object.');
2863
+ }
2864
+ const tabs = navigation["tabs"];
2865
+ if (!Array.isArray(tabs)) {
2866
+ throw new InvalidNavigationError('docs.json navigation must have a "tabs" array.');
2867
+ }
2868
+ return tabs.map((tab, index) => parseTab(tab, `navigation.tabs[${String(index)}]`));
2869
+ }
2870
+
2871
+ // src/adapters/openapi/merge-documents.ts
2872
+ var OpenApiMergeError = class extends Error {
2873
+ constructor(message) {
2874
+ super(message);
2875
+ this.name = "OpenApiMergeError";
2876
+ }
2877
+ };
2878
+ function isRecord2(value) {
2879
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2880
+ }
2881
+ function collectServers(inputs, override) {
2882
+ if (override !== void 0) {
2883
+ return [...override];
2884
+ }
2885
+ const byUrl = /* @__PURE__ */ new Map();
2886
+ for (const doc of inputs) {
2887
+ const servers = doc["servers"];
2888
+ if (!Array.isArray(servers)) {
2889
+ continue;
2890
+ }
2891
+ for (const server of servers) {
2892
+ if (isRecord2(server) && typeof server["url"] === "string" && !byUrl.has(server["url"])) {
2893
+ byUrl.set(server["url"], server);
2894
+ }
2895
+ }
2896
+ }
2897
+ return [...byUrl.values()];
2898
+ }
2899
+ function collectTags(inputs) {
2900
+ const byName = /* @__PURE__ */ new Map();
2901
+ for (const doc of inputs) {
2902
+ const tags = doc["tags"];
2903
+ if (!Array.isArray(tags)) {
2904
+ continue;
2905
+ }
2906
+ for (const tag of tags) {
2907
+ if (isRecord2(tag) && typeof tag["name"] === "string" && !byName.has(tag["name"])) {
2908
+ byName.set(tag["name"], tag);
2909
+ }
2910
+ }
2911
+ }
2912
+ return [...byName.values()];
2913
+ }
2914
+ function mergeComponents(inputs) {
2915
+ const merged = {};
2916
+ for (const doc of inputs) {
2917
+ const components = doc["components"];
2918
+ if (!isRecord2(components)) {
2919
+ continue;
2920
+ }
2921
+ for (const [section, value] of Object.entries(components)) {
2922
+ if (!isRecord2(value)) {
2923
+ continue;
2924
+ }
2925
+ const target = merged[section] ?? (merged[section] = {});
2926
+ for (const [key, definition] of Object.entries(value)) {
2927
+ if (!(key in target)) {
2928
+ target[key] = definition;
2929
+ }
2930
+ }
2931
+ }
2932
+ }
2933
+ return merged;
2934
+ }
2935
+ var HTTP_METHODS2 = /* @__PURE__ */ new Set(["get", "put", "post", "delete", "patch", "options", "head", "trace"]);
2936
+ function assemblePaths(inputs) {
2937
+ const paths = {};
2938
+ for (const doc of inputs) {
2939
+ const docPaths = doc["paths"];
2940
+ if (!isRecord2(docPaths)) {
2941
+ throw new OpenApiMergeError('Each OpenAPI input must have a "paths" object.');
2942
+ }
2943
+ const docSecurity = doc["security"];
2944
+ for (const [route, pathItem] of Object.entries(docPaths)) {
2945
+ if (!isRecord2(pathItem)) {
2946
+ throw new OpenApiMergeError(`Path item for "${route}" must be an object.`);
2947
+ }
2948
+ const target = paths[route] ?? (paths[route] = {});
2949
+ for (const [method, operation] of Object.entries(pathItem)) {
2950
+ if (method in target) {
2951
+ throw new OpenApiMergeError(
2952
+ `Duplicate operation ${method.toUpperCase()} ${route} across inputs.`
2953
+ );
2954
+ }
2955
+ let resolved = operation;
2956
+ if (HTTP_METHODS2.has(method.toLowerCase()) && isRecord2(operation) && !("security" in operation) && Array.isArray(docSecurity)) {
2957
+ resolved = { ...operation, security: docSecurity };
2958
+ }
2959
+ target[method] = resolved;
2960
+ }
2961
+ }
2962
+ }
2963
+ return paths;
2964
+ }
2965
+ function mergeOpenApiDocuments(inputs, options) {
2966
+ const records = inputs.map((input, index) => {
2967
+ if (!isRecord2(input)) {
2968
+ throw new OpenApiMergeError(`OpenAPI input at index ${String(index)} must be an object.`);
2969
+ }
2970
+ return input;
2971
+ });
2972
+ return {
2973
+ openapi: options.openapi ?? "3.1.0",
2974
+ info: options.info,
2975
+ servers: collectServers(records, options.servers),
2976
+ tags: collectTags(records),
2977
+ paths: assemblePaths(records),
2978
+ components: mergeComponents(records)
2979
+ };
2980
+ }
2981
+
2982
+ export { AI_PROVIDER_MODELS, AI_PROVIDER_NAMES, AiConfigError, DocsConfigError, InvalidNavigationError, OpenApiMergeError, ProxyTargetError, SESSION_COOKIE, assertAllowedTarget, buildMetadata, buildRobots, buildSearchIndex, buildSessionCookie, buildSitemap, buildThemeCss, clearSessionCookie, createAskAiRoute, createAuthGate, createEmailSignIn, createGoogleAuth, createMarkdownRoute, createSecretSignIn, createSignOut, createTryItProxy, createTryItProxyRoute, defineDocsConfig, isAllowedEmail, mdxToMarkdown, mergeOpenApiDocuments, openApiOperationToMarkdown, parseNavigation, planDocsTree, readCookie, resolveAiConfig, searchIndex, signSession, unwrapProxyTarget, validateDocsConfig, verifySession };
2983
+ //# sourceMappingURL=chunk-3ZX4WIE3.js.map
2984
+ //# sourceMappingURL=chunk-3ZX4WIE3.js.map