@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.
- package/.all-contributorsrc +18 -0
- package/.claude/settings.local.json +61 -0
- package/.dockerignore +113 -0
- package/.env.example +68 -0
- package/.github/FUNDING.yml +5 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +74 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +78 -0
- package/.github/dependabot.yml +136 -0
- package/.github/pull_request_template.md +96 -0
- package/.github/workflows/ci.yml +283 -0
- package/AGENTS.md +237 -0
- package/CODE_OF_CONDUCT.md +134 -0
- package/CONTRIBUTING.md +77 -0
- package/Caddyfile +50 -0
- package/Dockerfile.backend +60 -0
- package/LICENSE +21 -0
- package/README.md +284 -0
- package/RELEASE_CHECKLIST.md +34 -0
- package/SECURITY.md +60 -0
- package/backend/package.json +43 -0
- package/backend/src/__tests__/auth-helpers.test.ts +51 -0
- package/backend/src/__tests__/chunker.test.ts +65 -0
- package/backend/src/__tests__/config.test.ts +91 -0
- package/backend/src/__tests__/csrf.test.ts +91 -0
- package/backend/src/__tests__/embedding.test.ts +48 -0
- package/backend/src/__tests__/rate-limit.test.ts +46 -0
- package/backend/src/__tests__/routes.test.ts +38 -0
- package/backend/src/__tests__/schema.test.ts +31 -0
- package/backend/src/__tests__/validation.test.ts +556 -0
- package/backend/src/api/middleware/auth.ts +56 -0
- package/backend/src/api/middleware/csrf.ts +91 -0
- package/backend/src/api/middleware/rate-limit.ts +77 -0
- package/backend/src/api/middleware/webhook-verify.ts +22 -0
- package/backend/src/api/routes/attachments.ts +280 -0
- package/backend/src/api/routes/auth.ts +52 -0
- package/backend/src/api/routes/collaboration.ts +121 -0
- package/backend/src/api/routes/documents.ts +664 -0
- package/backend/src/api/routes/folders.ts +226 -0
- package/backend/src/api/routes/search.ts +354 -0
- package/backend/src/api/routes/share.ts +512 -0
- package/backend/src/api/routes/tags.ts +247 -0
- package/backend/src/api/routes/versions.ts +99 -0
- package/backend/src/api/routes/webhooks.ts +43 -0
- package/backend/src/embedding/chunker.ts +74 -0
- package/backend/src/embedding/index.ts +117 -0
- package/backend/src/embedding/providers/ollama.ts +63 -0
- package/backend/src/embedding/providers/openrouter.ts +71 -0
- package/backend/src/embedding/utils.ts +13 -0
- package/backend/src/embedding/worker.ts +89 -0
- package/backend/src/index.ts +147 -0
- package/backend/src/lib/auth-helpers.ts +27 -0
- package/backend/src/lib/auth.ts +35 -0
- package/backend/src/lib/config.ts +73 -0
- package/backend/src/lib/db.ts +7 -0
- package/backend/src/lib/embedding-queue.ts +12 -0
- package/backend/src/lib/logger.ts +18 -0
- package/backend/src/lib/markdown-to-doc.ts +45 -0
- package/backend/src/lib/minio.ts +46 -0
- package/backend/src/lib/redis.ts +19 -0
- package/backend/src/lib/yjs-provider.ts +182 -0
- package/backend/tests/integration/_harness.ts +754 -0
- package/backend/tests/integration/auth.test.ts +296 -0
- package/backend/tests/integration/routes.documents.test.ts +459 -0
- package/backend/tests/integration/routes.folders.test.ts +337 -0
- package/backend/tests/integration/routes.search.test.ts +322 -0
- package/backend/tests/integration/routes.share.test.ts +773 -0
- package/backend/tests/integration/routes.tags.test.ts +425 -0
- package/backend/tests/integration/routes.versions.test.ts +233 -0
- package/backend/tsconfig.json +18 -0
- package/docker-compose.yml +218 -0
- package/docs/API.md +328 -0
- package/docs/ARCHITECTURE.md +75 -0
- package/docs/DEPLOYMENT.md +113 -0
- package/docs/PRODUCTION_STATUS.md +61 -0
- package/docs/openapi.json +385 -0
- package/frontend/.svelte-kit.old/ambient.d.ts +230 -0
- package/frontend/.svelte-kit.old/env.d.ts +1 -0
- package/frontend/.svelte-kit.old/generated/client/app.js +46 -0
- package/frontend/.svelte-kit.old/generated/client/matchers.js +1 -0
- package/frontend/.svelte-kit.old/generated/client/nodes/0.js +3 -0
- package/frontend/.svelte-kit.old/generated/client/nodes/1.js +1 -0
- package/frontend/.svelte-kit.old/generated/client/nodes/10.js +3 -0
- package/frontend/.svelte-kit.old/generated/client/nodes/2.js +1 -0
- package/frontend/.svelte-kit.old/generated/client/nodes/3.js +1 -0
- package/frontend/.svelte-kit.old/generated/client/nodes/4.js +1 -0
- package/frontend/.svelte-kit.old/generated/client/nodes/5.js +3 -0
- package/frontend/.svelte-kit.old/generated/client/nodes/6.js +1 -0
- package/frontend/.svelte-kit.old/generated/client/nodes/7.js +3 -0
- package/frontend/.svelte-kit.old/generated/client/nodes/8.js +1 -0
- package/frontend/.svelte-kit.old/generated/client/nodes/9.js +3 -0
- package/frontend/.svelte-kit.old/generated/root.js +3 -0
- package/frontend/.svelte-kit.old/generated/root.svelte +80 -0
- package/frontend/.svelte-kit.old/generated/server/internal.js +55 -0
- package/frontend/.svelte-kit.old/non-ambient.d.ts +59 -0
- package/frontend/.svelte-kit.old/tsconfig.json +59 -0
- package/frontend/.svelte-kit.old/types/route_meta_data.json +40 -0
- package/frontend/.svelte-kit.old/types/src/routes/$types.d.ts +21 -0
- package/frontend/.svelte-kit.old/types/src/routes/(app)/$types.d.ts +30 -0
- package/frontend/.svelte-kit.old/types/src/routes/(app)/docs/[id]/$types.d.ts +27 -0
- package/frontend/.svelte-kit.old/types/src/routes/(app)/docs/[id]/proxy+page.ts +25 -0
- package/frontend/.svelte-kit.old/types/src/routes/api/[...path]/$types.d.ts +10 -0
- package/frontend/.svelte-kit.old/types/src/routes/folders/[id]/$types.d.ts +27 -0
- package/frontend/.svelte-kit.old/types/src/routes/folders/[id]/proxy+page.ts +15 -0
- package/frontend/.svelte-kit.old/types/src/routes/login/$types.d.ts +17 -0
- package/frontend/.svelte-kit.old/types/src/routes/register/$types.d.ts +17 -0
- package/frontend/.svelte-kit.old/types/src/routes/s/[token]/$types.d.ts +20 -0
- package/frontend/.svelte-kit.old/types/src/routes/s/[token]/proxy+page.ts +6 -0
- package/frontend/.svelte-kit.old/types/src/routes/search/$types.d.ts +19 -0
- package/frontend/.svelte-kit.old/types/src/routes/search/proxy+page.ts +26 -0
- package/frontend/.svelte-kit.old/types/src/routes/settings/$types.d.ts +17 -0
- package/frontend/Dockerfile +44 -0
- package/frontend/biome.json +40 -0
- package/frontend/components.json +18 -0
- package/frontend/messages/en.json +434 -0
- package/frontend/package.json +70 -0
- package/frontend/project.inlang/settings.json +12 -0
- package/frontend/src/app.css +6 -0
- package/frontend/src/app.d.ts +13 -0
- package/frontend/src/app.html +30 -0
- package/frontend/src/hooks.server.ts +10 -0
- package/frontend/src/hooks.ts +10 -0
- package/frontend/src/lib/api/attachments.ts +45 -0
- package/frontend/src/lib/api/client.test.ts +15 -0
- package/frontend/src/lib/api/client.ts +57 -0
- package/frontend/src/lib/api/documents.ts +83 -0
- package/frontend/src/lib/api/folders.ts +180 -0
- package/frontend/src/lib/api/search.test.ts +52 -0
- package/frontend/src/lib/api/search.ts +128 -0
- package/frontend/src/lib/api/settings.ts +95 -0
- package/frontend/src/lib/api/share.ts +71 -0
- package/frontend/src/lib/api/tags.test.ts +91 -0
- package/frontend/src/lib/api/tags.ts +87 -0
- package/frontend/src/lib/auth-client.ts +10 -0
- package/frontend/src/lib/collaboration.ts +63 -0
- package/frontend/src/lib/components/AttachmentUpload.svelte +110 -0
- package/frontend/src/lib/components/DatePicker.svelte +322 -0
- package/frontend/src/lib/components/DocumentCard.svelte +166 -0
- package/frontend/src/lib/components/EmptyState.svelte +49 -0
- package/frontend/src/lib/components/FolderCard.svelte +93 -0
- package/frontend/src/lib/components/ScrollToTop.svelte +72 -0
- package/frontend/src/lib/components/SearchBar.svelte +47 -0
- package/frontend/src/lib/components/SearchResult.svelte +115 -0
- package/frontend/src/lib/components/SettingsDialog.svelte +271 -0
- package/frontend/src/lib/components/ShareDialog.svelte +158 -0
- package/frontend/src/lib/components/ShareLink.svelte +98 -0
- package/frontend/src/lib/components/TagCreateDialog.svelte +284 -0
- package/frontend/src/lib/components/VersionDiff.svelte +55 -0
- package/frontend/src/lib/components/VersionHistory.svelte +96 -0
- package/frontend/src/lib/components/editor/DocumentTitle.svelte +87 -0
- package/frontend/src/lib/components/editor/EditorToolbar.svelte +1367 -0
- package/frontend/src/lib/components/editor/HiAiEditor.svelte +531 -0
- package/frontend/src/lib/components/editor/LinkDialog.svelte +134 -0
- package/frontend/src/lib/components/editor/MarkdownToggle.svelte +88 -0
- package/frontend/src/lib/components/editor/editorExtensions.ts +53 -0
- package/frontend/src/lib/components/editor/markdown.ts +38 -0
- package/frontend/src/lib/components/sidebar/FolderTree.svelte +731 -0
- package/frontend/src/lib/components/sidebar/RecentDocs.svelte +311 -0
- package/frontend/src/lib/components/sidebar/Sidebar.svelte +156 -0
- package/frontend/src/lib/components/sidebar/TagList.svelte +200 -0
- package/frontend/src/lib/components/ui/confirm-dialog/ConfirmDialog.svelte +76 -0
- package/frontend/src/lib/components/ui/confirm-dialog/index.ts +1 -0
- package/frontend/src/lib/stores/tag-store.svelte.ts +56 -0
- package/frontend/src/lib/stores/theme.svelte.ts +97 -0
- package/frontend/src/lib/svelte.d.ts +6 -0
- package/frontend/src/lib/types.ts +44 -0
- package/frontend/src/lib/utils/clipboard.ts +17 -0
- package/frontend/src/lib/utils/strip-markdown.ts +59 -0
- package/frontend/src/lib/utils.ts +33 -0
- package/frontend/src/routes/(app)/+layout.svelte +17 -0
- package/frontend/src/routes/(app)/+page.server.ts +10 -0
- package/frontend/src/routes/(app)/+page.svelte +303 -0
- package/frontend/src/routes/(app)/docs/[id]/+page.server.ts +10 -0
- package/frontend/src/routes/(app)/docs/[id]/+page.svelte +1108 -0
- package/frontend/src/routes/(app)/docs/[id]/+page.ts +24 -0
- package/frontend/src/routes/(app)/search/+page.svelte +593 -0
- package/frontend/src/routes/(app)/search/+page.ts +25 -0
- package/frontend/src/routes/+error.svelte +12 -0
- package/frontend/src/routes/+layout.svelte +18 -0
- package/frontend/src/routes/+layout.ts +2 -0
- package/frontend/src/routes/api/[...path]/+server.ts +111 -0
- package/frontend/src/routes/folders/[id]/+page.server.ts +10 -0
- package/frontend/src/routes/folders/[id]/+page.svelte +319 -0
- package/frontend/src/routes/folders/[id]/+page.ts +14 -0
- package/frontend/src/routes/login/+page.svelte +90 -0
- package/frontend/src/routes/register/+page.svelte +97 -0
- package/frontend/src/routes/s/[token]/+page.svelte +496 -0
- package/frontend/src/routes/s/[token]/+page.ts +5 -0
- package/frontend/src/routes/settings/+page.svelte +175 -0
- package/frontend/static/favicon.png +0 -0
- package/frontend/static/logo.png +0 -0
- package/frontend/svelte.config.js +15 -0
- package/frontend/tsconfig.json +15 -0
- package/frontend/vite.config.ts +25 -0
- package/init.sql +9 -0
- package/logo.png +0 -0
- package/package.json +39 -0
- package/package.public.json +39 -0
- package/packages/db/drizzle.config.ts +10 -0
- package/packages/db/package.json +30 -0
- package/packages/db/src/client.ts +9 -0
- package/packages/db/src/index.ts +2 -0
- package/packages/db/src/migrations/0000_nice_bedlam.sql +165 -0
- package/packages/db/src/migrations/0001_w2_3_test.sql +5 -0
- package/packages/db/src/migrations/0002_rename_content_json.sql +2 -0
- package/packages/db/src/migrations/meta/0000_snapshot.json +1331 -0
- package/packages/db/src/migrations/meta/0001_snapshot.json +1399 -0
- package/packages/db/src/migrations/meta/0002_snapshot.json +1399 -0
- package/packages/db/src/migrations/meta/_journal.json +27 -0
- package/packages/db/src/schema.ts +378 -0
- package/packages/db/tsconfig.json +17 -0
- package/scripts/export-openapi.ts +37 -0
- package/scripts/health-check.sh +75 -0
- package/scripts/migrate.sh +135 -0
- package/scripts/prework_backup.sh +25 -0
- package/scripts/release.sh +83 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test harness for HTTP-level route tests.
|
|
3
|
+
*
|
|
4
|
+
* Strategy:
|
|
5
|
+
* - Mock drizzle-orm conditions with marker Symbols so the in-memory DB
|
|
6
|
+
* can interpret `eq`, `and`, `or`, `isNull`, `desc`, `count` without
|
|
7
|
+
* touching a real Postgres connection.
|
|
8
|
+
* - Mock the db, auth, redis, config, logger, embedding-queue, embedding,
|
|
9
|
+
* and webhook-verify modules BEFORE the routes are imported.
|
|
10
|
+
* - Build a minimal Elysia app that mounts the route modules under test
|
|
11
|
+
* so we can call `app.handle(new Request(...))` for true HTTP-level tests.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { mock } from "bun:test";
|
|
15
|
+
|
|
16
|
+
export const API_KEY = "test-api-key-for-routes-32chars-xxx";
|
|
17
|
+
export const OWNER_ID = "00000000-0000-4000-8000-000000000001";
|
|
18
|
+
export const OTHER_USER_ID = "00000000-0000-4000-8000-000000000002";
|
|
19
|
+
export const CSRF_SECRET = "test-csrf-secret-32-characters-long-xxxxx";
|
|
20
|
+
export const WEBHOOK_SECRET = "test-webhook-secret-32-chars-long-xx";
|
|
21
|
+
|
|
22
|
+
export interface TestState {
|
|
23
|
+
users: Map<string, any>;
|
|
24
|
+
folders: Map<string, any>;
|
|
25
|
+
documents: Map<string, any>;
|
|
26
|
+
tags: Map<string, any>;
|
|
27
|
+
documentTags: Array<{ documentId: string; tagId: string }>;
|
|
28
|
+
shareLinks: Map<string, any>;
|
|
29
|
+
guestAccess: any[];
|
|
30
|
+
versions: any[];
|
|
31
|
+
attachments: Map<string, any>;
|
|
32
|
+
documentEmbeddings: any[];
|
|
33
|
+
enqueuedEmbeddings: string[];
|
|
34
|
+
calls: Array<{ kind: string; table: string }>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function uuid4(): string {
|
|
38
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
39
|
+
const r = (Math.random() * 16) | 0;
|
|
40
|
+
const v = c === "x" ? r : (r & 0x3) | 0x8;
|
|
41
|
+
return v.toString(16);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function createState(): TestState {
|
|
46
|
+
const state: TestState = {
|
|
47
|
+
users: new Map(),
|
|
48
|
+
folders: new Map(),
|
|
49
|
+
documents: new Map(),
|
|
50
|
+
tags: new Map(),
|
|
51
|
+
documentTags: [],
|
|
52
|
+
shareLinks: new Map(),
|
|
53
|
+
guestAccess: [],
|
|
54
|
+
versions: [],
|
|
55
|
+
attachments: new Map(),
|
|
56
|
+
documentEmbeddings: [],
|
|
57
|
+
enqueuedEmbeddings: [],
|
|
58
|
+
calls: [],
|
|
59
|
+
};
|
|
60
|
+
state.users.set(OWNER_ID, {
|
|
61
|
+
id: OWNER_ID,
|
|
62
|
+
email: "[email protected]",
|
|
63
|
+
name: "Owner",
|
|
64
|
+
emailVerified: true,
|
|
65
|
+
});
|
|
66
|
+
state.users.set(OTHER_USER_ID, {
|
|
67
|
+
id: OTHER_USER_ID,
|
|
68
|
+
email: "[email protected]",
|
|
69
|
+
name: "Other",
|
|
70
|
+
emailVerified: true,
|
|
71
|
+
});
|
|
72
|
+
return state;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let state: TestState = createState();
|
|
76
|
+
|
|
77
|
+
export function getState(): TestState {
|
|
78
|
+
return state;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function resetState(): void {
|
|
82
|
+
state = createState();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const TAG_EQ = Symbol("eq");
|
|
86
|
+
const TAG_AND = Symbol("and");
|
|
87
|
+
const TAG_OR = Symbol("or");
|
|
88
|
+
const TAG_IS_NULL = Symbol("isNull");
|
|
89
|
+
const TAG_IS_NOT_NULL = Symbol("isNotNull");
|
|
90
|
+
const TAG_IN_ARRAY = Symbol("inArray");
|
|
91
|
+
const TAG_DESC = Symbol("desc");
|
|
92
|
+
const TAG_ASC = Symbol("asc");
|
|
93
|
+
const TAG_SQL = Symbol("sql");
|
|
94
|
+
const TAG_COUNT = Symbol("count");
|
|
95
|
+
const TAG_LIKE = Symbol("like");
|
|
96
|
+
const TAG_NE = Symbol("ne");
|
|
97
|
+
const TAG_GT = Symbol("gt");
|
|
98
|
+
const TAG_LT = Symbol("lt");
|
|
99
|
+
const TAG_GTE = Symbol("gte");
|
|
100
|
+
const TAG_LTE = Symbol("lte");
|
|
101
|
+
|
|
102
|
+
const markEq = (col: any, val: any) => ({ [TAG_EQ]: true, col, val });
|
|
103
|
+
const markAnd = (...conds: any[]) => ({ [TAG_AND]: true, values: conds });
|
|
104
|
+
const markOr = (...conds: any[]) => ({ [TAG_OR]: true, values: conds });
|
|
105
|
+
const markIsNull = (col: any) => ({ [TAG_IS_NULL]: true, col });
|
|
106
|
+
const markIsNotNull = (col: any) => ({ [TAG_IS_NOT_NULL]: true, col });
|
|
107
|
+
const markInArray = (col: any, vals: any[]) => ({
|
|
108
|
+
[TAG_IN_ARRAY]: true,
|
|
109
|
+
col,
|
|
110
|
+
vals,
|
|
111
|
+
});
|
|
112
|
+
const markDesc = (col: any) => ({ [TAG_DESC]: true, col });
|
|
113
|
+
const markAsc = (col: any) => ({ [TAG_ASC]: true, col });
|
|
114
|
+
const markCount = (col: any) => ({ [TAG_COUNT]: true, col });
|
|
115
|
+
const markLike = (col: any, pattern: any) => ({
|
|
116
|
+
[TAG_LIKE]: true,
|
|
117
|
+
col,
|
|
118
|
+
pattern,
|
|
119
|
+
});
|
|
120
|
+
const markNe = (col: any, val: any) => ({ [TAG_NE]: true, col, val });
|
|
121
|
+
const markGt = (col: any, val: any) => ({ [TAG_GT]: true, col, val });
|
|
122
|
+
const markLt = (col: any, val: any) => ({ [TAG_LT]: true, col, val });
|
|
123
|
+
const markGte = (col: any, val: any) => ({ [TAG_GTE]: true, col, val });
|
|
124
|
+
const markLte = (col: any, val: any) => ({ [TAG_LTE]: true, col, val });
|
|
125
|
+
|
|
126
|
+
const sql: any = () => ({ [TAG_SQL]: true });
|
|
127
|
+
sql.raw = () => "RAW";
|
|
128
|
+
|
|
129
|
+
const OVERRIDES: Record<string, any> = {
|
|
130
|
+
eq: markEq,
|
|
131
|
+
and: markAnd,
|
|
132
|
+
or: markOr,
|
|
133
|
+
isNull: markIsNull,
|
|
134
|
+
isNotNull: markIsNotNull,
|
|
135
|
+
inArray: markInArray,
|
|
136
|
+
desc: markDesc,
|
|
137
|
+
asc: markAsc,
|
|
138
|
+
count: markCount,
|
|
139
|
+
like: markLike,
|
|
140
|
+
ne: markNe,
|
|
141
|
+
gt: markGt,
|
|
142
|
+
lt: markLt,
|
|
143
|
+
gte: markGte,
|
|
144
|
+
lte: markLte,
|
|
145
|
+
sql,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
mock.module("drizzle-orm", () => {
|
|
149
|
+
const real = require("drizzle-orm");
|
|
150
|
+
return new Proxy(real, {
|
|
151
|
+
get(target, prop) {
|
|
152
|
+
if (typeof prop === "string" && prop in OVERRIDES) {
|
|
153
|
+
return OVERRIDES[prop];
|
|
154
|
+
}
|
|
155
|
+
return target[prop];
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
function snakeToCamel(s: string): string {
|
|
161
|
+
return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function getColumnName(col: any): string {
|
|
165
|
+
const snake = col?.name ?? "?";
|
|
166
|
+
return snakeToCamel(snake);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function getTableName(table: any): string {
|
|
170
|
+
if (table?._?.name) return table._.name;
|
|
171
|
+
const syms = Object.getOwnPropertySymbols(table ?? {});
|
|
172
|
+
for (const sym of syms) {
|
|
173
|
+
const val = table[sym];
|
|
174
|
+
if (typeof val === "string" && /^[a-z_]+$/.test(val)) return val;
|
|
175
|
+
}
|
|
176
|
+
return table?.name ?? "?";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function getCollection(name: string): any[] | Map<string, any> {
|
|
180
|
+
switch (name) {
|
|
181
|
+
case "users":
|
|
182
|
+
return state.users;
|
|
183
|
+
case "folders":
|
|
184
|
+
return state.folders;
|
|
185
|
+
case "documents":
|
|
186
|
+
return state.documents;
|
|
187
|
+
case "tags":
|
|
188
|
+
return state.tags;
|
|
189
|
+
case "document_tags":
|
|
190
|
+
return state.documentTags;
|
|
191
|
+
case "share_links":
|
|
192
|
+
return state.shareLinks;
|
|
193
|
+
case "guest_access":
|
|
194
|
+
return state.guestAccess;
|
|
195
|
+
case "versions":
|
|
196
|
+
return state.versions;
|
|
197
|
+
case "attachments":
|
|
198
|
+
return state.attachments;
|
|
199
|
+
case "document_embeddings":
|
|
200
|
+
return state.documentEmbeddings;
|
|
201
|
+
default:
|
|
202
|
+
throw new Error(`Unknown table in mock DB: ${name}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function getRows(table: any): any[] {
|
|
207
|
+
const col = getCollection(getTableName(table));
|
|
208
|
+
if (col instanceof Map) return Array.from(col.values());
|
|
209
|
+
return col as any[];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function evaluateCondition(row: any, cond: any): boolean {
|
|
213
|
+
if (cond == null) return true;
|
|
214
|
+
if (cond[TAG_EQ]) return row[getColumnName(cond.col)] === cond.val;
|
|
215
|
+
if (cond[TAG_NE]) return row[getColumnName(cond.col)] !== cond.val;
|
|
216
|
+
if (cond[TAG_GT]) return row[getColumnName(cond.col)] > cond.val;
|
|
217
|
+
if (cond[TAG_LT]) return row[getColumnName(cond.col)] < cond.val;
|
|
218
|
+
if (cond[TAG_GTE]) return row[getColumnName(cond.col)] >= cond.val;
|
|
219
|
+
if (cond[TAG_LTE]) return row[getColumnName(cond.col)] <= cond.val;
|
|
220
|
+
if (cond[TAG_AND]) return cond.values.every((c: any) => evaluateCondition(row, c));
|
|
221
|
+
if (cond[TAG_OR]) return cond.values.some((c: any) => evaluateCondition(row, c));
|
|
222
|
+
if (cond[TAG_IS_NULL]) return row[getColumnName(cond.col)] === null;
|
|
223
|
+
if (cond[TAG_IS_NOT_NULL]) return row[getColumnName(cond.col)] !== null;
|
|
224
|
+
if (cond[TAG_IN_ARRAY]) return cond.vals.includes(row[getColumnName(cond.col)]);
|
|
225
|
+
if (cond[TAG_LIKE]) {
|
|
226
|
+
const v = row[getColumnName(cond.col)];
|
|
227
|
+
if (typeof v !== "string") return false;
|
|
228
|
+
const pattern = String(cond.pattern).replace(/%/g, ".*");
|
|
229
|
+
return new RegExp(`^${pattern}$`).test(v);
|
|
230
|
+
}
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function applyFieldSelection(rows: any[], fields: any): any[] {
|
|
235
|
+
if (!fields || typeof fields !== "object") return rows;
|
|
236
|
+
const keys = Object.keys(fields);
|
|
237
|
+
const aggregateKeys = keys.filter((k) => fields[k]?.[TAG_COUNT]);
|
|
238
|
+
const columnKeys = keys.filter((k) => !aggregateKeys.includes(k));
|
|
239
|
+
|
|
240
|
+
if (aggregateKeys.length === 0) {
|
|
241
|
+
return rows.map((row) => {
|
|
242
|
+
const out: any = {};
|
|
243
|
+
for (const k of columnKeys) {
|
|
244
|
+
out[k] = row[getColumnName(fields[k])];
|
|
245
|
+
}
|
|
246
|
+
return out;
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return rows.map((row) => {
|
|
251
|
+
const out: any = {};
|
|
252
|
+
for (const k of columnKeys) {
|
|
253
|
+
out[k] = row[getColumnName(fields[k])];
|
|
254
|
+
}
|
|
255
|
+
for (const k of aggregateKeys) {
|
|
256
|
+
if (row.id != null) {
|
|
257
|
+
out[k] = state.documentTags.filter((dt) => dt.tagId === row.id).length;
|
|
258
|
+
} else {
|
|
259
|
+
out[k] = 0;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return out;
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
interface SelectCtx {
|
|
267
|
+
type: "select";
|
|
268
|
+
fields: any;
|
|
269
|
+
from: any;
|
|
270
|
+
joins: Array<{ type: string; table: any; cond: any }>;
|
|
271
|
+
where: any;
|
|
272
|
+
limit: number | null;
|
|
273
|
+
offset: number | null;
|
|
274
|
+
orderBy: any;
|
|
275
|
+
groupBy: any[] | null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function buildSelectProxy(ctx: SelectCtx): any {
|
|
279
|
+
const handler: ProxyHandler<any> = {
|
|
280
|
+
get(_target, prop) {
|
|
281
|
+
if (prop === "then") {
|
|
282
|
+
return (resolve: any, reject: any) => {
|
|
283
|
+
try {
|
|
284
|
+
return Promise.resolve(executeSelect(ctx)).then(resolve, reject);
|
|
285
|
+
} catch (err) {
|
|
286
|
+
return Promise.reject(err).then(reject, reject);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
if (prop === "from")
|
|
291
|
+
return (table: any) => {
|
|
292
|
+
ctx.from = table;
|
|
293
|
+
return buildSelectProxy(ctx);
|
|
294
|
+
};
|
|
295
|
+
if (prop === "where")
|
|
296
|
+
return (cond: any) => {
|
|
297
|
+
ctx.where = cond;
|
|
298
|
+
return buildSelectProxy(ctx);
|
|
299
|
+
};
|
|
300
|
+
if (prop === "limit")
|
|
301
|
+
return (n: number) => {
|
|
302
|
+
ctx.limit = n;
|
|
303
|
+
return buildSelectProxy(ctx);
|
|
304
|
+
};
|
|
305
|
+
if (prop === "offset")
|
|
306
|
+
return (n: number) => {
|
|
307
|
+
ctx.offset = n;
|
|
308
|
+
return buildSelectProxy(ctx);
|
|
309
|
+
};
|
|
310
|
+
if (prop === "orderBy")
|
|
311
|
+
return (col: any) => {
|
|
312
|
+
ctx.orderBy = col;
|
|
313
|
+
return buildSelectProxy(ctx);
|
|
314
|
+
};
|
|
315
|
+
if (prop === "groupBy")
|
|
316
|
+
return (...cols: any[]) => {
|
|
317
|
+
ctx.groupBy = cols;
|
|
318
|
+
return buildSelectProxy(ctx);
|
|
319
|
+
};
|
|
320
|
+
if (prop === "leftJoin")
|
|
321
|
+
return (table: any, cond: any) => {
|
|
322
|
+
ctx.joins.push({ type: "left", table, cond });
|
|
323
|
+
return buildSelectProxy(ctx);
|
|
324
|
+
};
|
|
325
|
+
if (prop === "innerJoin")
|
|
326
|
+
return (table: any, cond: any) => {
|
|
327
|
+
ctx.joins.push({ type: "inner", table, cond });
|
|
328
|
+
return buildSelectProxy(ctx);
|
|
329
|
+
};
|
|
330
|
+
return undefined;
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
return new Proxy({}, handler);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function executeSelect(ctx: SelectCtx): any[] {
|
|
337
|
+
if (!ctx.from) return [];
|
|
338
|
+
const tableName = getTableName(ctx.from);
|
|
339
|
+
state.calls.push({ kind: "select", table: tableName });
|
|
340
|
+
let rows = getRows(ctx.from);
|
|
341
|
+
if (ctx.where) rows = rows.filter((r) => evaluateCondition(r, ctx.where));
|
|
342
|
+
if (ctx.orderBy) {
|
|
343
|
+
const isDesc = ctx.orderBy[TAG_DESC] === true;
|
|
344
|
+
const colName = getColumnName(ctx.orderBy.col ?? ctx.orderBy);
|
|
345
|
+
rows = [...rows].sort((a, b) => {
|
|
346
|
+
const av = a[colName];
|
|
347
|
+
const bv = b[colName];
|
|
348
|
+
if (av === bv) return 0;
|
|
349
|
+
if (av == null) return 1;
|
|
350
|
+
if (bv == null) return -1;
|
|
351
|
+
const cmp = av < bv ? -1 : 1;
|
|
352
|
+
return isDesc ? -cmp : cmp;
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
if (ctx.groupBy && ctx.groupBy.length > 0) {
|
|
356
|
+
const seen = new Set<string>();
|
|
357
|
+
const grouped: any[] = [];
|
|
358
|
+
for (const row of rows) {
|
|
359
|
+
const key = ctx.groupBy
|
|
360
|
+
.map((g) => String(row[getColumnName(g)] ?? ""))
|
|
361
|
+
.join("|");
|
|
362
|
+
if (!seen.has(key)) {
|
|
363
|
+
seen.add(key);
|
|
364
|
+
grouped.push(row);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
rows = grouped;
|
|
368
|
+
}
|
|
369
|
+
if (ctx.offset != null) rows = rows.slice(ctx.offset);
|
|
370
|
+
if (ctx.limit != null) rows = rows.slice(0, ctx.limit);
|
|
371
|
+
return applyFieldSelection(rows, ctx.fields);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
interface InsertCtx {
|
|
375
|
+
type: "insert";
|
|
376
|
+
table: any;
|
|
377
|
+
values: any[];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function buildInsertProxy(ctx: InsertCtx): any {
|
|
381
|
+
const handler: ProxyHandler<any> = {
|
|
382
|
+
get(_target, prop) {
|
|
383
|
+
if (prop === "values")
|
|
384
|
+
return (v: any) => {
|
|
385
|
+
ctx.values = Array.isArray(v) ? v : [v];
|
|
386
|
+
return buildInsertProxy(ctx);
|
|
387
|
+
};
|
|
388
|
+
if (prop === "onConflictDoNothing")
|
|
389
|
+
return buildInsertProxy(ctx);
|
|
390
|
+
if (prop === "returning")
|
|
391
|
+
return () => {
|
|
392
|
+
state.calls.push({ kind: "insert", table: getTableName(ctx.table) });
|
|
393
|
+
const tableName = getTableName(ctx.table);
|
|
394
|
+
const collection = getCollection(tableName);
|
|
395
|
+
const returned: any[] = [];
|
|
396
|
+
for (const row of ctx.values) {
|
|
397
|
+
const newRow: any = { ...row };
|
|
398
|
+
if (newRow.id == null) newRow.id = uuid4();
|
|
399
|
+
if (newRow.createdAt == null) newRow.createdAt = new Date();
|
|
400
|
+
if (newRow.updatedAt == null) newRow.updatedAt = new Date();
|
|
401
|
+
if (collection instanceof Map) {
|
|
402
|
+
collection.set(newRow.id, newRow);
|
|
403
|
+
} else {
|
|
404
|
+
(collection as any[]).push(newRow);
|
|
405
|
+
}
|
|
406
|
+
returned.push(newRow);
|
|
407
|
+
}
|
|
408
|
+
return Promise.resolve(returned);
|
|
409
|
+
};
|
|
410
|
+
return undefined;
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
return new Proxy({}, handler);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
interface UpdateCtx {
|
|
417
|
+
type: "update";
|
|
418
|
+
table: any;
|
|
419
|
+
set: any;
|
|
420
|
+
where: any;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function buildUpdateProxy(ctx: UpdateCtx): any {
|
|
424
|
+
const handler: ProxyHandler<any> = {
|
|
425
|
+
get(_target, prop) {
|
|
426
|
+
if (prop === "set")
|
|
427
|
+
return (s: any) => {
|
|
428
|
+
ctx.set = s;
|
|
429
|
+
return buildUpdateProxy(ctx);
|
|
430
|
+
};
|
|
431
|
+
if (prop === "where")
|
|
432
|
+
return (cond: any) => {
|
|
433
|
+
ctx.where = cond;
|
|
434
|
+
return buildUpdateProxy(ctx);
|
|
435
|
+
};
|
|
436
|
+
if (prop === "returning")
|
|
437
|
+
return () => {
|
|
438
|
+
state.calls.push({ kind: "update", table: getTableName(ctx.table) });
|
|
439
|
+
const tableName = getTableName(ctx.table);
|
|
440
|
+
const collection = getCollection(tableName);
|
|
441
|
+
const returned: any[] = [];
|
|
442
|
+
const rows =
|
|
443
|
+
collection instanceof Map
|
|
444
|
+
? Array.from(collection.values())
|
|
445
|
+
: (collection as any[]);
|
|
446
|
+
for (const row of rows) {
|
|
447
|
+
if (!evaluateCondition(row, ctx.where)) continue;
|
|
448
|
+
Object.assign(row, ctx.set);
|
|
449
|
+
row.updatedAt = new Date();
|
|
450
|
+
returned.push({ ...row });
|
|
451
|
+
}
|
|
452
|
+
return Promise.resolve(returned);
|
|
453
|
+
};
|
|
454
|
+
return undefined;
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
return new Proxy({}, handler);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
interface DeleteCtx {
|
|
461
|
+
type: "delete";
|
|
462
|
+
table: any;
|
|
463
|
+
where: any;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function buildDeleteProxy(ctx: DeleteCtx): any {
|
|
467
|
+
const handler: ProxyHandler<any> = {
|
|
468
|
+
get(_target, prop) {
|
|
469
|
+
if (prop === "where")
|
|
470
|
+
return (cond: any) => {
|
|
471
|
+
ctx.where = cond;
|
|
472
|
+
return buildDeleteProxy(ctx);
|
|
473
|
+
};
|
|
474
|
+
if (prop === "returning")
|
|
475
|
+
return () => {
|
|
476
|
+
state.calls.push({ kind: "delete", table: getTableName(ctx.table) });
|
|
477
|
+
const tableName = getTableName(ctx.table);
|
|
478
|
+
const collection = getCollection(tableName);
|
|
479
|
+
const returned: any[] = [];
|
|
480
|
+
const items =
|
|
481
|
+
collection instanceof Map
|
|
482
|
+
? Array.from(collection.entries())
|
|
483
|
+
: (collection as any[]).map((r, i) => [i, r]);
|
|
484
|
+
const kept: any[] = [];
|
|
485
|
+
for (const [, row] of items as Array<[any, any]>) {
|
|
486
|
+
if (evaluateCondition(row, ctx.where)) {
|
|
487
|
+
returned.push({ ...row });
|
|
488
|
+
} else {
|
|
489
|
+
kept.push(row);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (collection instanceof Map) {
|
|
493
|
+
for (const r of returned) collection.delete(r.id);
|
|
494
|
+
} else {
|
|
495
|
+
(collection as any[]).length = 0;
|
|
496
|
+
(collection as any[]).push(...kept);
|
|
497
|
+
}
|
|
498
|
+
return Promise.resolve(returned);
|
|
499
|
+
};
|
|
500
|
+
if (prop === "then")
|
|
501
|
+
return (resolve: any, reject: any) => {
|
|
502
|
+
try {
|
|
503
|
+
state.calls.push({ kind: "delete", table: getTableName(ctx.table) });
|
|
504
|
+
const tableName = getTableName(ctx.table);
|
|
505
|
+
const collection = getCollection(tableName);
|
|
506
|
+
const items =
|
|
507
|
+
collection instanceof Map
|
|
508
|
+
? Array.from(collection.entries())
|
|
509
|
+
: (collection as any[]).map((r, i) => [i, r]);
|
|
510
|
+
const kept: any[] = [];
|
|
511
|
+
for (const [, row] of items as Array<[any, any]>) {
|
|
512
|
+
if (!evaluateCondition(row, ctx.where)) kept.push(row);
|
|
513
|
+
}
|
|
514
|
+
if (collection instanceof Map) {
|
|
515
|
+
for (const r of Array.from(collection.values())) {
|
|
516
|
+
if (!kept.includes(r)) collection.delete(r.id);
|
|
517
|
+
}
|
|
518
|
+
} else {
|
|
519
|
+
(collection as any[]).length = 0;
|
|
520
|
+
(collection as any[]).push(...kept);
|
|
521
|
+
}
|
|
522
|
+
return Promise.resolve(undefined).then(resolve, reject);
|
|
523
|
+
} catch (err) {
|
|
524
|
+
return Promise.reject(err).then(reject, reject);
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
return undefined;
|
|
528
|
+
},
|
|
529
|
+
};
|
|
530
|
+
return new Proxy({}, handler);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function buildMockDb() {
|
|
534
|
+
return {
|
|
535
|
+
select(fields?: any) {
|
|
536
|
+
const ctx: SelectCtx = {
|
|
537
|
+
type: "select",
|
|
538
|
+
fields,
|
|
539
|
+
from: null,
|
|
540
|
+
joins: [],
|
|
541
|
+
where: null,
|
|
542
|
+
limit: null,
|
|
543
|
+
offset: null,
|
|
544
|
+
orderBy: null,
|
|
545
|
+
groupBy: null,
|
|
546
|
+
};
|
|
547
|
+
return buildSelectProxy(ctx);
|
|
548
|
+
},
|
|
549
|
+
insert(table: any) {
|
|
550
|
+
return buildInsertProxy({ type: "insert", table, values: [] });
|
|
551
|
+
},
|
|
552
|
+
update(table: any) {
|
|
553
|
+
return buildUpdateProxy({ type: "update", table, set: {}, where: null });
|
|
554
|
+
},
|
|
555
|
+
delete(table: any) {
|
|
556
|
+
return buildDeleteProxy({ type: "delete", table, where: null });
|
|
557
|
+
},
|
|
558
|
+
execute(_sql: any) {
|
|
559
|
+
return Promise.resolve([]);
|
|
560
|
+
},
|
|
561
|
+
transaction(fn: any) {
|
|
562
|
+
return fn(this);
|
|
563
|
+
},
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const mockDb = buildMockDb();
|
|
568
|
+
|
|
569
|
+
mock.module("../../src/lib/config.js", () => ({
|
|
570
|
+
config: {
|
|
571
|
+
API_KEY,
|
|
572
|
+
HIAI_DOCS_API_KEY: API_KEY,
|
|
573
|
+
OWNER_ID,
|
|
574
|
+
CSRF_SECRET,
|
|
575
|
+
WEBHOOK_SECRET,
|
|
576
|
+
BETTER_AUTH_SECRET: "test-shared-secret-min-32-characters-long-x",
|
|
577
|
+
BETTER_AUTH_URL: "http://localhost:50700",
|
|
578
|
+
DATABASE_URL: "postgresql://test:test@localhost:5432/test",
|
|
579
|
+
REDIS_URL: "redis://localhost:6379",
|
|
580
|
+
EMBEDDING_PROVIDER: "ollama",
|
|
581
|
+
EMBEDDING_MODEL: "nomic-embed-text",
|
|
582
|
+
EMBEDDING_OLLAMA_URL: "http://localhost:11434",
|
|
583
|
+
EMBEDDING_FALLBACK_PROVIDER: "openrouter",
|
|
584
|
+
EMBEDDING_FALLBACK_MODEL: "openai/text-embedding-3-small",
|
|
585
|
+
OPENROUTER_API_KEY: "",
|
|
586
|
+
MINIO_ENDPOINT: "localhost",
|
|
587
|
+
MINIO_PORT: 9000,
|
|
588
|
+
MINIO_ACCESS_KEY: "minioadmin",
|
|
589
|
+
MINIO_SECRET_KEY: "minioadmin",
|
|
590
|
+
MINIO_BUCKET: "hiai-docs",
|
|
591
|
+
MINIO_USE_SSL: false,
|
|
592
|
+
API_PORT: 50700,
|
|
593
|
+
FRONTEND_PORT: 50701,
|
|
594
|
+
CORS_ORIGINS: undefined,
|
|
595
|
+
NODE_ENV: "test",
|
|
596
|
+
LOG_LEVEL: "fatal",
|
|
597
|
+
},
|
|
598
|
+
}));
|
|
599
|
+
|
|
600
|
+
mock.module("../../src/lib/auth.js", () => ({
|
|
601
|
+
auth: {
|
|
602
|
+
api: {
|
|
603
|
+
getSession: async () => null,
|
|
604
|
+
},
|
|
605
|
+
handler: async () =>
|
|
606
|
+
new Response("auth handler not used in tests", { status: 500 }),
|
|
607
|
+
},
|
|
608
|
+
Session: undefined,
|
|
609
|
+
}));
|
|
610
|
+
|
|
611
|
+
mock.module("../../src/lib/db.js", () => ({
|
|
612
|
+
db: mockDb,
|
|
613
|
+
withTransaction: (fn: any) => fn(mockDb),
|
|
614
|
+
}));
|
|
615
|
+
|
|
616
|
+
mock.module("../../src/lib/redis.js", () => ({
|
|
617
|
+
redis: {
|
|
618
|
+
incr: async () => 1,
|
|
619
|
+
expire: async () => 1,
|
|
620
|
+
ttl: async () => 60,
|
|
621
|
+
lpush: async () => 1,
|
|
622
|
+
brpop: async () => null,
|
|
623
|
+
get: async () => null,
|
|
624
|
+
set: async () => "OK",
|
|
625
|
+
del: async () => 1,
|
|
626
|
+
},
|
|
627
|
+
redisHealthCheck: async () => true,
|
|
628
|
+
}));
|
|
629
|
+
|
|
630
|
+
mock.module("../../src/lib/logger.js", () => ({
|
|
631
|
+
logger: {
|
|
632
|
+
info: () => {},
|
|
633
|
+
warn: () => {},
|
|
634
|
+
error: () => {},
|
|
635
|
+
debug: () => {},
|
|
636
|
+
fatal: () => {},
|
|
637
|
+
trace: () => {},
|
|
638
|
+
},
|
|
639
|
+
createChildLogger: () => ({
|
|
640
|
+
info: () => {},
|
|
641
|
+
warn: () => {},
|
|
642
|
+
error: () => {},
|
|
643
|
+
debug: () => {},
|
|
644
|
+
}),
|
|
645
|
+
}));
|
|
646
|
+
|
|
647
|
+
mock.module("../../src/lib/minio.js", () => ({
|
|
648
|
+
minio: {
|
|
649
|
+
putObject: async () => "etag",
|
|
650
|
+
removeObject: async () => {},
|
|
651
|
+
},
|
|
652
|
+
BUCKET: "hiai-docs",
|
|
653
|
+
}));
|
|
654
|
+
|
|
655
|
+
mock.module("../../src/lib/embedding-queue.js", () => ({
|
|
656
|
+
enqueueEmbedding: (id: string) => {
|
|
657
|
+
state.enqueuedEmbeddings.push(id);
|
|
658
|
+
},
|
|
659
|
+
startEmbeddingWorker: () => {},
|
|
660
|
+
}));
|
|
661
|
+
|
|
662
|
+
mock.module("../../src/embedding/index.js", () => ({
|
|
663
|
+
getEmbedding: async () => new Array(1024).fill(0),
|
|
664
|
+
embedDocument: async () => [new Array(1024).fill(0)],
|
|
665
|
+
}));
|
|
666
|
+
|
|
667
|
+
mock.module("../../src/api/middleware/webhook-verify.js", () => ({
|
|
668
|
+
verifyWebhookSignature: (body: string, sig: string | null) => {
|
|
669
|
+
if (!sig) return false;
|
|
670
|
+
return /^[a-f0-9]{64}$/i.test(sig);
|
|
671
|
+
},
|
|
672
|
+
}));
|
|
673
|
+
|
|
674
|
+
export interface BuiltApp {
|
|
675
|
+
app: any;
|
|
676
|
+
csrfToken: string;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
let cachedApp: BuiltApp | null = null;
|
|
680
|
+
|
|
681
|
+
export async function setupHarness(): Promise<BuiltApp> {
|
|
682
|
+
if (cachedApp) return cachedApp;
|
|
683
|
+
|
|
684
|
+
const { Elysia } = await import("elysia");
|
|
685
|
+
const { csrfMiddleware } = await import("../../src/api/middleware/csrf");
|
|
686
|
+
const { authMiddleware } = await import("../../src/api/middleware/auth");
|
|
687
|
+
const { folderRoutes } = await import("../../src/api/routes/folders");
|
|
688
|
+
const { tagRoutes } = await import("../../src/api/routes/tags");
|
|
689
|
+
const { searchRoutes } = await import("../../src/api/routes/search");
|
|
690
|
+
const { shareRoutes } = await import("../../src/api/routes/share");
|
|
691
|
+
const { documentRoutes } = await import("../../src/api/routes/documents");
|
|
692
|
+
const { versionRoutes } = await import("../../src/api/routes/versions");
|
|
693
|
+
const { webhookRoutes } = await import("../../src/api/routes/webhooks");
|
|
694
|
+
|
|
695
|
+
const app = new Elysia()
|
|
696
|
+
.use(csrfMiddleware)
|
|
697
|
+
.use(authMiddleware)
|
|
698
|
+
.use(folderRoutes)
|
|
699
|
+
.use(tagRoutes)
|
|
700
|
+
.use(shareRoutes)
|
|
701
|
+
.use(searchRoutes)
|
|
702
|
+
.use(documentRoutes)
|
|
703
|
+
.use(versionRoutes)
|
|
704
|
+
.use(webhookRoutes);
|
|
705
|
+
|
|
706
|
+
const { createHmac, randomBytes } = await import("node:crypto");
|
|
707
|
+
function signToken(token: string): string {
|
|
708
|
+
return createHmac("sha256", CSRF_SECRET).update(token).digest("hex");
|
|
709
|
+
}
|
|
710
|
+
const raw = randomBytes(32).toString("hex");
|
|
711
|
+
const csrfToken = `${raw}.${signToken(raw)}`;
|
|
712
|
+
|
|
713
|
+
cachedApp = { app, csrfToken };
|
|
714
|
+
return cachedApp;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
export interface ApiResponse<T = any> {
|
|
718
|
+
status: number;
|
|
719
|
+
body: T;
|
|
720
|
+
headers: Headers;
|
|
721
|
+
raw: Response;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
export async function request<T = any>(
|
|
725
|
+
app: any,
|
|
726
|
+
path: string,
|
|
727
|
+
init: RequestInit = {},
|
|
728
|
+
): Promise<ApiResponse<T>> {
|
|
729
|
+
const req = new Request(`http://localhost${path}`, init);
|
|
730
|
+
const res = await app.handle(req);
|
|
731
|
+
const text = await res.text();
|
|
732
|
+
let body: T;
|
|
733
|
+
try {
|
|
734
|
+
body = text ? (JSON.parse(text) as T) : (undefined as T);
|
|
735
|
+
} catch {
|
|
736
|
+
body = text as unknown as T;
|
|
737
|
+
}
|
|
738
|
+
return { status: res.status, body, headers: res.headers, raw: res };
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
export function ownerHeaders(extra: Record<string, string> = {}): Record<string, string> {
|
|
742
|
+
return {
|
|
743
|
+
authorization: `Bearer ${API_KEY}`,
|
|
744
|
+
"content-type": "application/json",
|
|
745
|
+
...extra,
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
export function noAuthHeaders(extra: Record<string, string> = {}): Record<string, string> {
|
|
750
|
+
return {
|
|
751
|
+
"content-type": "application/json",
|
|
752
|
+
...extra,
|
|
753
|
+
};
|
|
754
|
+
}
|