@hiai-gg/hiai-docs 0.0.1

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 (216) hide show
  1. package/.all-contributorsrc +18 -0
  2. package/.claude/settings.local.json +61 -0
  3. package/.dockerignore +113 -0
  4. package/.env.example +68 -0
  5. package/.github/FUNDING.yml +5 -0
  6. package/.github/ISSUE_TEMPLATE/bug_report.md +74 -0
  7. package/.github/ISSUE_TEMPLATE/feature_request.md +78 -0
  8. package/.github/dependabot.yml +136 -0
  9. package/.github/pull_request_template.md +96 -0
  10. package/.github/workflows/ci.yml +283 -0
  11. package/AGENTS.md +237 -0
  12. package/CODE_OF_CONDUCT.md +134 -0
  13. package/CONTRIBUTING.md +77 -0
  14. package/Caddyfile +50 -0
  15. package/Dockerfile.backend +60 -0
  16. package/LICENSE +21 -0
  17. package/README.md +284 -0
  18. package/RELEASE_CHECKLIST.md +34 -0
  19. package/SECURITY.md +60 -0
  20. package/backend/package.json +43 -0
  21. package/backend/src/__tests__/auth-helpers.test.ts +51 -0
  22. package/backend/src/__tests__/chunker.test.ts +65 -0
  23. package/backend/src/__tests__/config.test.ts +91 -0
  24. package/backend/src/__tests__/csrf.test.ts +91 -0
  25. package/backend/src/__tests__/embedding.test.ts +48 -0
  26. package/backend/src/__tests__/rate-limit.test.ts +46 -0
  27. package/backend/src/__tests__/routes.test.ts +38 -0
  28. package/backend/src/__tests__/schema.test.ts +31 -0
  29. package/backend/src/__tests__/validation.test.ts +556 -0
  30. package/backend/src/api/middleware/auth.ts +56 -0
  31. package/backend/src/api/middleware/csrf.ts +91 -0
  32. package/backend/src/api/middleware/rate-limit.ts +77 -0
  33. package/backend/src/api/middleware/webhook-verify.ts +22 -0
  34. package/backend/src/api/routes/attachments.ts +280 -0
  35. package/backend/src/api/routes/auth.ts +52 -0
  36. package/backend/src/api/routes/collaboration.ts +121 -0
  37. package/backend/src/api/routes/documents.ts +664 -0
  38. package/backend/src/api/routes/folders.ts +226 -0
  39. package/backend/src/api/routes/search.ts +354 -0
  40. package/backend/src/api/routes/share.ts +512 -0
  41. package/backend/src/api/routes/tags.ts +247 -0
  42. package/backend/src/api/routes/versions.ts +99 -0
  43. package/backend/src/api/routes/webhooks.ts +43 -0
  44. package/backend/src/embedding/chunker.ts +74 -0
  45. package/backend/src/embedding/index.ts +117 -0
  46. package/backend/src/embedding/providers/ollama.ts +63 -0
  47. package/backend/src/embedding/providers/openrouter.ts +71 -0
  48. package/backend/src/embedding/utils.ts +13 -0
  49. package/backend/src/embedding/worker.ts +89 -0
  50. package/backend/src/index.ts +147 -0
  51. package/backend/src/lib/auth-helpers.ts +27 -0
  52. package/backend/src/lib/auth.ts +35 -0
  53. package/backend/src/lib/config.ts +73 -0
  54. package/backend/src/lib/db.ts +7 -0
  55. package/backend/src/lib/embedding-queue.ts +12 -0
  56. package/backend/src/lib/logger.ts +18 -0
  57. package/backend/src/lib/markdown-to-doc.ts +45 -0
  58. package/backend/src/lib/minio.ts +46 -0
  59. package/backend/src/lib/redis.ts +19 -0
  60. package/backend/src/lib/yjs-provider.ts +182 -0
  61. package/backend/tests/integration/_harness.ts +754 -0
  62. package/backend/tests/integration/auth.test.ts +296 -0
  63. package/backend/tests/integration/routes.documents.test.ts +459 -0
  64. package/backend/tests/integration/routes.folders.test.ts +337 -0
  65. package/backend/tests/integration/routes.search.test.ts +322 -0
  66. package/backend/tests/integration/routes.share.test.ts +773 -0
  67. package/backend/tests/integration/routes.tags.test.ts +425 -0
  68. package/backend/tests/integration/routes.versions.test.ts +233 -0
  69. package/backend/tsconfig.json +18 -0
  70. package/docker-compose.yml +218 -0
  71. package/docs/API.md +328 -0
  72. package/docs/ARCHITECTURE.md +75 -0
  73. package/docs/DEPLOYMENT.md +113 -0
  74. package/docs/PRODUCTION_STATUS.md +61 -0
  75. package/docs/openapi.json +385 -0
  76. package/frontend/.svelte-kit.old/ambient.d.ts +230 -0
  77. package/frontend/.svelte-kit.old/env.d.ts +1 -0
  78. package/frontend/.svelte-kit.old/generated/client/app.js +46 -0
  79. package/frontend/.svelte-kit.old/generated/client/matchers.js +1 -0
  80. package/frontend/.svelte-kit.old/generated/client/nodes/0.js +3 -0
  81. package/frontend/.svelte-kit.old/generated/client/nodes/1.js +1 -0
  82. package/frontend/.svelte-kit.old/generated/client/nodes/10.js +3 -0
  83. package/frontend/.svelte-kit.old/generated/client/nodes/2.js +1 -0
  84. package/frontend/.svelte-kit.old/generated/client/nodes/3.js +1 -0
  85. package/frontend/.svelte-kit.old/generated/client/nodes/4.js +1 -0
  86. package/frontend/.svelte-kit.old/generated/client/nodes/5.js +3 -0
  87. package/frontend/.svelte-kit.old/generated/client/nodes/6.js +1 -0
  88. package/frontend/.svelte-kit.old/generated/client/nodes/7.js +3 -0
  89. package/frontend/.svelte-kit.old/generated/client/nodes/8.js +1 -0
  90. package/frontend/.svelte-kit.old/generated/client/nodes/9.js +3 -0
  91. package/frontend/.svelte-kit.old/generated/root.js +3 -0
  92. package/frontend/.svelte-kit.old/generated/root.svelte +80 -0
  93. package/frontend/.svelte-kit.old/generated/server/internal.js +55 -0
  94. package/frontend/.svelte-kit.old/non-ambient.d.ts +59 -0
  95. package/frontend/.svelte-kit.old/tsconfig.json +59 -0
  96. package/frontend/.svelte-kit.old/types/route_meta_data.json +40 -0
  97. package/frontend/.svelte-kit.old/types/src/routes/$types.d.ts +21 -0
  98. package/frontend/.svelte-kit.old/types/src/routes/(app)/$types.d.ts +30 -0
  99. package/frontend/.svelte-kit.old/types/src/routes/(app)/docs/[id]/$types.d.ts +27 -0
  100. package/frontend/.svelte-kit.old/types/src/routes/(app)/docs/[id]/proxy+page.ts +25 -0
  101. package/frontend/.svelte-kit.old/types/src/routes/api/[...path]/$types.d.ts +10 -0
  102. package/frontend/.svelte-kit.old/types/src/routes/folders/[id]/$types.d.ts +27 -0
  103. package/frontend/.svelte-kit.old/types/src/routes/folders/[id]/proxy+page.ts +15 -0
  104. package/frontend/.svelte-kit.old/types/src/routes/login/$types.d.ts +17 -0
  105. package/frontend/.svelte-kit.old/types/src/routes/register/$types.d.ts +17 -0
  106. package/frontend/.svelte-kit.old/types/src/routes/s/[token]/$types.d.ts +20 -0
  107. package/frontend/.svelte-kit.old/types/src/routes/s/[token]/proxy+page.ts +6 -0
  108. package/frontend/.svelte-kit.old/types/src/routes/search/$types.d.ts +19 -0
  109. package/frontend/.svelte-kit.old/types/src/routes/search/proxy+page.ts +26 -0
  110. package/frontend/.svelte-kit.old/types/src/routes/settings/$types.d.ts +17 -0
  111. package/frontend/Dockerfile +44 -0
  112. package/frontend/biome.json +40 -0
  113. package/frontend/components.json +18 -0
  114. package/frontend/messages/en.json +434 -0
  115. package/frontend/package.json +70 -0
  116. package/frontend/project.inlang/settings.json +12 -0
  117. package/frontend/src/app.css +6 -0
  118. package/frontend/src/app.d.ts +13 -0
  119. package/frontend/src/app.html +30 -0
  120. package/frontend/src/hooks.server.ts +10 -0
  121. package/frontend/src/hooks.ts +10 -0
  122. package/frontend/src/lib/api/attachments.ts +45 -0
  123. package/frontend/src/lib/api/client.test.ts +15 -0
  124. package/frontend/src/lib/api/client.ts +57 -0
  125. package/frontend/src/lib/api/documents.ts +83 -0
  126. package/frontend/src/lib/api/folders.ts +180 -0
  127. package/frontend/src/lib/api/search.test.ts +52 -0
  128. package/frontend/src/lib/api/search.ts +128 -0
  129. package/frontend/src/lib/api/settings.ts +95 -0
  130. package/frontend/src/lib/api/share.ts +71 -0
  131. package/frontend/src/lib/api/tags.test.ts +91 -0
  132. package/frontend/src/lib/api/tags.ts +87 -0
  133. package/frontend/src/lib/auth-client.ts +10 -0
  134. package/frontend/src/lib/collaboration.ts +63 -0
  135. package/frontend/src/lib/components/AttachmentUpload.svelte +110 -0
  136. package/frontend/src/lib/components/DatePicker.svelte +322 -0
  137. package/frontend/src/lib/components/DocumentCard.svelte +166 -0
  138. package/frontend/src/lib/components/EmptyState.svelte +49 -0
  139. package/frontend/src/lib/components/FolderCard.svelte +93 -0
  140. package/frontend/src/lib/components/ScrollToTop.svelte +72 -0
  141. package/frontend/src/lib/components/SearchBar.svelte +47 -0
  142. package/frontend/src/lib/components/SearchResult.svelte +115 -0
  143. package/frontend/src/lib/components/SettingsDialog.svelte +271 -0
  144. package/frontend/src/lib/components/ShareDialog.svelte +158 -0
  145. package/frontend/src/lib/components/ShareLink.svelte +98 -0
  146. package/frontend/src/lib/components/TagCreateDialog.svelte +284 -0
  147. package/frontend/src/lib/components/VersionDiff.svelte +55 -0
  148. package/frontend/src/lib/components/VersionHistory.svelte +96 -0
  149. package/frontend/src/lib/components/editor/DocumentTitle.svelte +87 -0
  150. package/frontend/src/lib/components/editor/EditorToolbar.svelte +1367 -0
  151. package/frontend/src/lib/components/editor/HiAiEditor.svelte +531 -0
  152. package/frontend/src/lib/components/editor/LinkDialog.svelte +134 -0
  153. package/frontend/src/lib/components/editor/MarkdownToggle.svelte +88 -0
  154. package/frontend/src/lib/components/editor/editorExtensions.ts +53 -0
  155. package/frontend/src/lib/components/editor/markdown.ts +38 -0
  156. package/frontend/src/lib/components/sidebar/FolderTree.svelte +731 -0
  157. package/frontend/src/lib/components/sidebar/RecentDocs.svelte +311 -0
  158. package/frontend/src/lib/components/sidebar/Sidebar.svelte +156 -0
  159. package/frontend/src/lib/components/sidebar/TagList.svelte +200 -0
  160. package/frontend/src/lib/components/ui/confirm-dialog/ConfirmDialog.svelte +76 -0
  161. package/frontend/src/lib/components/ui/confirm-dialog/index.ts +1 -0
  162. package/frontend/src/lib/stores/tag-store.svelte.ts +56 -0
  163. package/frontend/src/lib/stores/theme.svelte.ts +97 -0
  164. package/frontend/src/lib/svelte.d.ts +6 -0
  165. package/frontend/src/lib/types.ts +44 -0
  166. package/frontend/src/lib/utils/clipboard.ts +17 -0
  167. package/frontend/src/lib/utils/strip-markdown.ts +59 -0
  168. package/frontend/src/lib/utils.ts +33 -0
  169. package/frontend/src/routes/(app)/+layout.svelte +17 -0
  170. package/frontend/src/routes/(app)/+page.server.ts +10 -0
  171. package/frontend/src/routes/(app)/+page.svelte +303 -0
  172. package/frontend/src/routes/(app)/docs/[id]/+page.server.ts +10 -0
  173. package/frontend/src/routes/(app)/docs/[id]/+page.svelte +1108 -0
  174. package/frontend/src/routes/(app)/docs/[id]/+page.ts +24 -0
  175. package/frontend/src/routes/(app)/search/+page.svelte +593 -0
  176. package/frontend/src/routes/(app)/search/+page.ts +25 -0
  177. package/frontend/src/routes/+error.svelte +12 -0
  178. package/frontend/src/routes/+layout.svelte +18 -0
  179. package/frontend/src/routes/+layout.ts +2 -0
  180. package/frontend/src/routes/api/[...path]/+server.ts +111 -0
  181. package/frontend/src/routes/folders/[id]/+page.server.ts +10 -0
  182. package/frontend/src/routes/folders/[id]/+page.svelte +319 -0
  183. package/frontend/src/routes/folders/[id]/+page.ts +14 -0
  184. package/frontend/src/routes/login/+page.svelte +90 -0
  185. package/frontend/src/routes/register/+page.svelte +97 -0
  186. package/frontend/src/routes/s/[token]/+page.svelte +496 -0
  187. package/frontend/src/routes/s/[token]/+page.ts +5 -0
  188. package/frontend/src/routes/settings/+page.svelte +175 -0
  189. package/frontend/static/favicon.png +0 -0
  190. package/frontend/static/logo.png +0 -0
  191. package/frontend/svelte.config.js +15 -0
  192. package/frontend/tsconfig.json +15 -0
  193. package/frontend/vite.config.ts +25 -0
  194. package/init.sql +9 -0
  195. package/logo.png +0 -0
  196. package/package.json +39 -0
  197. package/package.public.json +39 -0
  198. package/packages/db/drizzle.config.ts +10 -0
  199. package/packages/db/package.json +30 -0
  200. package/packages/db/src/client.ts +9 -0
  201. package/packages/db/src/index.ts +2 -0
  202. package/packages/db/src/migrations/0000_nice_bedlam.sql +165 -0
  203. package/packages/db/src/migrations/0001_w2_3_test.sql +5 -0
  204. package/packages/db/src/migrations/0002_rename_content_json.sql +2 -0
  205. package/packages/db/src/migrations/meta/0000_snapshot.json +1331 -0
  206. package/packages/db/src/migrations/meta/0001_snapshot.json +1399 -0
  207. package/packages/db/src/migrations/meta/0002_snapshot.json +1399 -0
  208. package/packages/db/src/migrations/meta/_journal.json +27 -0
  209. package/packages/db/src/schema.ts +378 -0
  210. package/packages/db/tsconfig.json +17 -0
  211. package/scripts/export-openapi.ts +37 -0
  212. package/scripts/health-check.sh +75 -0
  213. package/scripts/migrate.sh +135 -0
  214. package/scripts/prework_backup.sh +25 -0
  215. package/scripts/release.sh +83 -0
  216. package/tsconfig.json +25 -0
@@ -0,0 +1,51 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ describe("auth-helpers", () => {
4
+ test("getSessionUserId exports a function", async () => {
5
+ const mod = await import("../lib/auth-helpers");
6
+ expect(typeof mod.getSessionUserId).toBe("function");
7
+ });
8
+
9
+ test("getSessionUserId returns null for empty headers (no auth)", async () => {
10
+ const mod = await import("../lib/auth-helpers");
11
+ const headers = new Headers();
12
+ const result = await mod.getSessionUserId(headers);
13
+ // No API key configured and no session => null
14
+ expect(result === null || typeof result === "string").toBe(true);
15
+ });
16
+
17
+ test("getSessionUserId returns null for malformed Authorization header", async () => {
18
+ const mod = await import("../lib/auth-helpers");
19
+ const headers = new Headers({ authorization: "Basic dXNlcjpwYXNz" });
20
+ const result = await mod.getSessionUserId(headers);
21
+ // Not a Bearer token, so API key check skips; no session => null
22
+ expect(result === null || typeof result === "string").toBe(true);
23
+ });
24
+
25
+ test("getSessionUserId returns null for Bearer token with no matching API key", async () => {
26
+ const mod = await import("../lib/auth-helpers");
27
+ const headers = new Headers({ authorization: "Bearer wrong-token-value" });
28
+ const result = await mod.getSessionUserId(headers);
29
+ // Token doesn't match HIAI_DOCS_API_KEY (if set), Better Auth also fails
30
+ expect(result === null || typeof result === "string").toBe(true);
31
+ });
32
+
33
+ test("getSessionUserId accepts Headers object without throwing", async () => {
34
+ const mod = await import("../lib/auth-helpers");
35
+ const headers = new Headers({
36
+ authorization: "Bearer test123",
37
+ "content-type": "application/json",
38
+ });
39
+ // Should not throw regardless of auth outcome
40
+ await expect(mod.getSessionUserId(headers)).resolves.toBeDefined();
41
+ });
42
+
43
+ test("getSessionUserId handles Headers with x-forwarded-for (no effect on auth)", async () => {
44
+ const mod = await import("../lib/auth-helpers");
45
+ const headers = new Headers({
46
+ "x-forwarded-for": "192.168.1.1",
47
+ });
48
+ const result = await mod.getSessionUserId(headers);
49
+ expect(result === null || typeof result === "string").toBe(true);
50
+ });
51
+ });
@@ -0,0 +1,65 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { chunkText } from "../embedding/chunker";
3
+
4
+ describe("chunkText", () => {
5
+ it("returns empty array for empty string", () => {
6
+ expect(chunkText("")).toEqual([]);
7
+ });
8
+
9
+ it("returns empty array for whitespace only", () => {
10
+ expect(chunkText(" \n\n ")).toEqual([]);
11
+ });
12
+
13
+ it("returns single chunk for short text", () => {
14
+ const result = chunkText("Hello world");
15
+ expect(result).toHaveLength(1);
16
+ expect(result[0]).toBe("Hello world");
17
+ });
18
+
19
+ it("splits long text with paragraphs into multiple chunks", () => {
20
+ // Create text > 2000 chars with paragraph boundaries
21
+ const para = "word ".repeat(200); // ~1000 chars per paragraph
22
+ const longText = [para, para, para, para].join("\n\n");
23
+ const result = chunkText(longText);
24
+ expect(result.length).toBeGreaterThan(1);
25
+ });
26
+
27
+ it("respects paragraph boundaries", () => {
28
+ const text =
29
+ "First paragraph here.\n\nSecond paragraph here.\n\nThird paragraph here.";
30
+ const result = chunkText(text);
31
+ expect(result.length).toBeGreaterThanOrEqual(1);
32
+ // All content should be preserved
33
+ const combined = result.join("\n\n");
34
+ expect(combined).toContain("First paragraph");
35
+ expect(combined).toContain("Second paragraph");
36
+ expect(combined).toContain("Third paragraph");
37
+ });
38
+
39
+ it("handles text with overlapping chunks", () => {
40
+ // Create text that requires multiple chunks
41
+ const paragraphs = Array(5).fill("word ".repeat(500)).join("\n\n");
42
+ const result = chunkText(paragraphs);
43
+ expect(result.length).toBeGreaterThan(1);
44
+ // Each chunk should be non-empty
45
+ for (const chunk of result) {
46
+ expect(chunk.trim().length).toBeGreaterThan(0);
47
+ }
48
+ });
49
+
50
+ it("handles single very long paragraph by splitting sentences", () => {
51
+ // Single paragraph > TARGET_CHARS * 1.5, with sentence endings
52
+ const longPara = "This is a sentence. ".repeat(200); // ~4000 chars
53
+ const result = chunkText(longPara);
54
+ expect(result.length).toBeGreaterThanOrEqual(1);
55
+ });
56
+
57
+ it("preserves content across chunks", () => {
58
+ const text = `${"A".repeat(1000)}\n\n${"B".repeat(1000)}\n\n${"C".repeat(1000)}`;
59
+ const result = chunkText(text);
60
+ const combined = result.join("");
61
+ expect(combined).toContain("A");
62
+ expect(combined).toContain("B");
63
+ expect(combined).toContain("C");
64
+ });
65
+ });
@@ -0,0 +1,91 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { z } from "zod";
3
+
4
+ const envSchema = z.object({
5
+ DATABASE_URL: z
6
+ .string()
7
+ .default("postgresql://aiuser:aipassword@localhost:5433/hiai_docs"),
8
+ REDIS_URL: z.string().default("redis://localhost:6380"),
9
+ API_PORT: z.coerce.number().default(50700),
10
+ NODE_ENV: z
11
+ .enum(["development", "production", "test"])
12
+ .default("development"),
13
+ LOG_LEVEL: z
14
+ .enum(["trace", "debug", "info", "warn", "error", "fatal"])
15
+ .default("info"),
16
+ BETTER_AUTH_SECRET: z.string().default("change-me-to-random-32-chars"),
17
+ BETTER_AUTH_URL: z.string().default("http://localhost:50700"),
18
+ EMBEDDING_PROVIDER: z
19
+ .enum(["ollama", "openrouter", "voyage"])
20
+ .default("ollama"),
21
+ EMBEDDING_MODEL: z.string().default("nomic-embed-text"),
22
+ EMBEDDING_OLLAMA_URL: z.string().default("http://localhost:11434"),
23
+ EMBEDDING_FALLBACK_PROVIDER: z.string().default("openrouter"),
24
+ EMBEDDING_FALLBACK_MODEL: z.string().default("openai/text-embedding-3-small"),
25
+ OPENROUTER_API_KEY: z.string().optional(),
26
+ MINIO_ENDPOINT: z.string().default("localhost"),
27
+ MINIO_PORT: z.coerce.number().default(9010),
28
+ MINIO_ACCESS_KEY: z.string().default("minioadmin"),
29
+ MINIO_SECRET_KEY: z.string().default("minioadmin"),
30
+ MINIO_BUCKET: z.string().default("hiai-docs"),
31
+ HIAI_DOCS_API_KEY: z.string().optional(),
32
+ OWNER_ID: z.string().default("api-key-user"),
33
+ });
34
+
35
+ describe("config schema", () => {
36
+ test("loads with defaults when env vars are unset", () => {
37
+ const result = envSchema.safeParse({});
38
+ expect(result.success).toBe(true);
39
+ if (result.success) {
40
+ expect(result.data.API_PORT).toBe(50700);
41
+ expect(result.data.NODE_ENV).toBe("development");
42
+ expect(result.data.LOG_LEVEL).toBe("info");
43
+ expect(result.data.EMBEDDING_PROVIDER).toBe("ollama");
44
+ expect(result.data.MINIO_PORT).toBe(9010);
45
+ }
46
+ });
47
+
48
+ test("rejects invalid NODE_ENV", () => {
49
+ const result = envSchema.safeParse({ NODE_ENV: "staging" });
50
+ expect(result.success).toBe(false);
51
+ });
52
+
53
+ test("accepts valid embedding providers", () => {
54
+ expect(envSchema.safeParse({ EMBEDDING_PROVIDER: "ollama" }).success).toBe(
55
+ true,
56
+ );
57
+ expect(
58
+ envSchema.safeParse({ EMBEDDING_PROVIDER: "openrouter" }).success,
59
+ ).toBe(true);
60
+ expect(envSchema.safeParse({ EMBEDDING_PROVIDER: "voyage" }).success).toBe(
61
+ true,
62
+ );
63
+ expect(envSchema.safeParse({ EMBEDDING_PROVIDER: "invalid" }).success).toBe(
64
+ false,
65
+ );
66
+ });
67
+
68
+ test("coerces string port to number", () => {
69
+ const result = envSchema.safeParse({ API_PORT: "8080" });
70
+ expect(result.success).toBe(true);
71
+ if (result.success) {
72
+ expect(result.data.API_PORT).toBe(8080);
73
+ expect(typeof result.data.API_PORT).toBe("number");
74
+ }
75
+ });
76
+
77
+ test("accepts valid LOG_LEVEL", () => {
78
+ for (const level of ["trace", "debug", "info", "warn", "error", "fatal"]) {
79
+ expect(envSchema.safeParse({ LOG_LEVEL: level }).success).toBe(true);
80
+ }
81
+ expect(envSchema.safeParse({ LOG_LEVEL: "verbose" }).success).toBe(false);
82
+ });
83
+
84
+ test("OPENROUTER_API_KEY is optional", () => {
85
+ const result = envSchema.safeParse({});
86
+ expect(result.success).toBe(true);
87
+ if (result.success) {
88
+ expect(result.data.OPENROUTER_API_KEY).toBeUndefined();
89
+ }
90
+ });
91
+ });
@@ -0,0 +1,91 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { createHmac, randomBytes } from "node:crypto";
3
+
4
+ const _CSRF_COOKIE = "hiai-csrf";
5
+ const _CSRF_HEADER = "x-csrf-token";
6
+
7
+ function signToken(token: string, secret: string): string {
8
+ return createHmac("sha256", secret).update(token).digest("hex");
9
+ }
10
+
11
+ function generateToken(secret: string): string {
12
+ const token = randomBytes(32).toString("hex");
13
+ return `${token}.${signToken(token, secret)}`;
14
+ }
15
+
16
+ describe("CSRF token generation and verification", () => {
17
+ const secret = "test-secret-key-for-csrf";
18
+
19
+ it("generates token in format value.signature", () => {
20
+ const token = generateToken(secret);
21
+ const parts = token.split(".");
22
+ expect(parts).toHaveLength(2);
23
+ expect(parts[0]).toMatch(/^[a-f0-9]{64}$/);
24
+ expect(parts[1]).toMatch(/^[a-f0-9]{64}$/);
25
+ });
26
+
27
+ it("verifies valid token", () => {
28
+ const token = generateToken(secret);
29
+ const [value, signature] = token.split(".");
30
+ if (!value || !signature) throw new Error("invalid token");
31
+ const expected = signToken(value, secret);
32
+ expect(signature).toBe(expected);
33
+ });
34
+
35
+ it("rejects tampered token", () => {
36
+ const token = generateToken(secret);
37
+ const [value] = token.split(".");
38
+ if (!value) throw new Error("invalid token");
39
+ const tampered = `${value}.0000000000000000000000000000000000000000000000000000000000000000`;
40
+ const [sig1] = [tampered.split(".")[1]];
41
+ const expected = signToken(value, secret);
42
+ expect(sig1).not.toBe(expected);
43
+ });
44
+
45
+ it("rejects token with wrong secret", () => {
46
+ const token = generateToken("wrong-secret");
47
+ const [value] = token.split(".");
48
+ if (!value) throw new Error("invalid token");
49
+ const expected = signToken(value, secret);
50
+ const actual = token.split(".")[1];
51
+ expect(actual).not.toBe(expected);
52
+ });
53
+
54
+ it("rejects empty token", () => {
55
+ expect("".split(".")).toHaveLength(1);
56
+ });
57
+
58
+ it("rejects token without signature", () => {
59
+ const parts = "abcdef1234".split(".");
60
+ expect(parts).toHaveLength(1);
61
+ });
62
+ });
63
+
64
+ describe("CSRF middleware behavior", () => {
65
+ it("isUnsafeMethod correctly identifies unsafe methods", () => {
66
+ const unsafe = ["POST", "PUT", "PATCH", "DELETE"];
67
+ const safe = ["GET", "HEAD", "OPTIONS"];
68
+ for (const m of unsafe) {
69
+ expect(["POST", "PUT", "PATCH", "DELETE"]).toContain(m);
70
+ }
71
+ for (const m of safe) {
72
+ expect(["POST", "PUT", "PATCH", "DELETE"]).not.toContain(m);
73
+ }
74
+ });
75
+
76
+ it("skips CSRF for Bearer token requests", () => {
77
+ const authHeader = "Bearer some-api-key";
78
+ expect(authHeader.startsWith("Bearer ")).toBe(true);
79
+ });
80
+
81
+ it("skips CSRF for non-API routes", () => {
82
+ expect("/api/documents".startsWith("/api/")).toBe(true);
83
+ expect("/api/auth/sign-in".startsWith("/api/auth")).toBe(true);
84
+ expect("/health".startsWith("/api/")).toBe(false);
85
+ });
86
+
87
+ it("skips CSRF for multipart requests", () => {
88
+ const ct = "multipart/form-data; boundary=----WebKitFormBoundary";
89
+ expect(ct.includes("multipart/form-data")).toBe(true);
90
+ });
91
+ });
@@ -0,0 +1,48 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ describe("embedding providers", () => {
4
+ test("getOllamaEmbedding is a function", async () => {
5
+ const mod = await import("../embedding/providers/ollama");
6
+ expect(typeof mod.getOllamaEmbedding).toBe("function");
7
+ });
8
+
9
+ test("getOpenRouterEmbedding is a function", async () => {
10
+ const mod = await import("../embedding/providers/openrouter");
11
+ expect(typeof mod.getOpenRouterEmbedding).toBe("function");
12
+ });
13
+
14
+ test("getEmbedding returns 1024-dim vector (fallback to zero when unavailable)", async () => {
15
+ const mod = await import("../embedding/index");
16
+ const result = await mod.getEmbedding("test text");
17
+ expect(Array.isArray(result)).toBe(true);
18
+ expect(result.length).toBe(1024);
19
+ });
20
+
21
+ test("embedDocument returns array of 1024-dim vectors", async () => {
22
+ const mod = await import("../embedding/index");
23
+ const result = await mod.embedDocument(
24
+ "Test Title",
25
+ "Short content for test.",
26
+ );
27
+ expect(Array.isArray(result)).toBe(true);
28
+ expect(result.length).toBeGreaterThanOrEqual(1);
29
+ if (result[0]) {
30
+ expect(result[0].length).toBe(1024);
31
+ }
32
+ });
33
+
34
+ test("normalizeDimensions utility works correctly", async () => {
35
+ const mod = await import("../embedding/utils");
36
+ // Test with vector shorter than target
37
+ const short = mod.normalizeDimensions([1, 2, 3], 5);
38
+ expect(short).toEqual([1, 2, 3, 0, 0]);
39
+
40
+ // Test with vector longer than target
41
+ const long = mod.normalizeDimensions([1, 2, 3, 4, 5], 3);
42
+ expect(long).toEqual([1, 2, 3]);
43
+
44
+ // Test with exact length
45
+ const exact = mod.normalizeDimensions([1, 2, 3], 3);
46
+ expect(exact).toEqual([1, 2, 3]);
47
+ });
48
+ });
@@ -0,0 +1,46 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { rateLimitHeaders } from "../api/middleware/rate-limit";
3
+
4
+ describe("rateLimitHeaders", () => {
5
+ it("returns remaining header", () => {
6
+ const headers = rateLimitHeaders(15);
7
+ expect(headers["X-RateLimit-Remaining"]).toBe("15");
8
+ });
9
+
10
+ it("returns zero remaining", () => {
11
+ const headers = rateLimitHeaders(0);
12
+ expect(headers["X-RateLimit-Remaining"]).toBe("0");
13
+ });
14
+
15
+ it("includes Retry-When retryAfter is provided", () => {
16
+ const headers = rateLimitHeaders(0, 30);
17
+ expect(headers["Retry-After"]).toBe("30");
18
+ });
19
+
20
+ it("omits Retry-After when not provided", () => {
21
+ const headers = rateLimitHeaders(5);
22
+ expect(headers["Retry-After"]).toBeUndefined();
23
+ });
24
+ });
25
+
26
+ describe("rate limiter configurations", () => {
27
+ it("search limiter allows 20 requests per minute", () => {
28
+ expect(20).toBeGreaterThan(0);
29
+ });
30
+
31
+ it("document limiter allows 60 requests per minute", () => {
32
+ expect(60).toBeGreaterThan(20);
33
+ });
34
+
35
+ it("write limiter is more restrictive than document", () => {
36
+ expect(10).toBeLessThan(60);
37
+ });
38
+
39
+ it("share limiter is most restrictive", () => {
40
+ expect(5).toBeLessThan(10);
41
+ });
42
+
43
+ it("health limiter is least restrictive", () => {
44
+ expect(120).toBeGreaterThan(60);
45
+ });
46
+ });
@@ -0,0 +1,38 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ describe("API route modules", () => {
4
+ test("documentRoutes loads without error", async () => {
5
+ const mod = await import("../api/routes/documents");
6
+ expect(mod.documentRoutes).toBeDefined();
7
+ });
8
+
9
+ test("folderRoutes loads without error", async () => {
10
+ const mod = await import("../api/routes/folders");
11
+ expect(mod.folderRoutes).toBeDefined();
12
+ });
13
+
14
+ test("tagRoutes loads without error", async () => {
15
+ const mod = await import("../api/routes/tags");
16
+ expect(mod.tagRoutes).toBeDefined();
17
+ });
18
+
19
+ test("searchRoutes loads without error", async () => {
20
+ const mod = await import("../api/routes/search");
21
+ expect(mod.searchRoutes).toBeDefined();
22
+ });
23
+
24
+ test("shareRoutes loads without error", async () => {
25
+ const mod = await import("../api/routes/share");
26
+ expect(mod.shareRoutes).toBeDefined();
27
+ });
28
+
29
+ test("versionRoutes loads without error", async () => {
30
+ const mod = await import("../api/routes/versions");
31
+ expect(mod.versionRoutes).toBeDefined();
32
+ });
33
+
34
+ test("authRoutes loads without error", async () => {
35
+ const mod = await import("../api/routes/auth");
36
+ expect(mod.authRoutes).toBeDefined();
37
+ });
38
+ });
@@ -0,0 +1,31 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ describe("database schema", () => {
4
+ test("schema exports all expected tables", async () => {
5
+ const schema = await import("@hiai-docs/db/schema");
6
+ expect(schema.users).toBeDefined();
7
+ expect(schema.sessions).toBeDefined();
8
+ expect(schema.accounts).toBeDefined();
9
+ expect(schema.verifications).toBeDefined();
10
+ expect(schema.documents).toBeDefined();
11
+ expect(schema.folders).toBeDefined();
12
+ expect(schema.tags).toBeDefined();
13
+ expect(schema.documentTags).toBeDefined();
14
+ expect(schema.shareLinks).toBeDefined();
15
+ expect(schema.guestAccess).toBeDefined();
16
+ expect(schema.attachments).toBeDefined();
17
+ expect(schema.versions).toBeDefined();
18
+ });
19
+
20
+ test("schema exports relations", async () => {
21
+ const schema = await import("@hiai-docs/db/schema");
22
+ expect(schema.documentRelations).toBeDefined();
23
+ expect(schema.folderRelations).toBeDefined();
24
+ expect(schema.shareLinkRelations).toBeDefined();
25
+ expect(schema.tagRelations).toBeDefined();
26
+ expect(schema.documentTagRelations).toBeDefined();
27
+ expect(schema.guestAccessRelations).toBeDefined();
28
+ expect(schema.attachmentRelations).toBeDefined();
29
+ expect(schema.versionRelations).toBeDefined();
30
+ });
31
+ });