@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,556 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
// ============================================
|
|
5
|
+
// Document schemas (from api/routes/documents.ts)
|
|
6
|
+
// ============================================
|
|
7
|
+
|
|
8
|
+
const createDocumentSchema = z.object({
|
|
9
|
+
title: z.string().min(1).max(500).default("Untitled"),
|
|
10
|
+
content: z.string().optional(),
|
|
11
|
+
folderId: z.string().uuid().optional(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const updateDocumentSchema = z.object({
|
|
15
|
+
title: z.string().min(1).max(500).optional(),
|
|
16
|
+
content: z.string().optional(),
|
|
17
|
+
contentJson: z.unknown().optional(),
|
|
18
|
+
metadata: z.unknown().optional(),
|
|
19
|
+
folderId: z.string().uuid().nullable().optional(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const listQuerySchema = z.object({
|
|
23
|
+
folderId: z.string().uuid().optional(),
|
|
24
|
+
tag: z.string().uuid().optional(),
|
|
25
|
+
page: z.coerce.number().int().min(1).default(1),
|
|
26
|
+
limit: z.coerce.number().int().min(1).max(100).default(20),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// ============================================
|
|
30
|
+
// Folder schemas (from api/routes/folders.ts)
|
|
31
|
+
// ============================================
|
|
32
|
+
|
|
33
|
+
const createFolderSchema = z.object({
|
|
34
|
+
name: z.string().min(1).max(255),
|
|
35
|
+
parentId: z.string().uuid().optional(),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const updateFolderSchema = z.object({
|
|
39
|
+
name: z.string().min(1).max(255).optional(),
|
|
40
|
+
parentId: z.string().uuid().nullable().optional(),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// ============================================
|
|
44
|
+
// Tag schemas (from api/routes/tags.ts)
|
|
45
|
+
// ============================================
|
|
46
|
+
|
|
47
|
+
const createTagSchema = z.object({
|
|
48
|
+
name: z.string().min(1).max(100),
|
|
49
|
+
color: z.string().max(20).optional(),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const updateTagSchema = z.object({
|
|
53
|
+
name: z.string().min(1).max(100).optional(),
|
|
54
|
+
color: z.string().max(20).optional(),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const addTagToDocSchema = z.object({
|
|
58
|
+
tagId: z.string().uuid(),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// ============================================
|
|
62
|
+
// Search schemas (from api/routes/search.ts)
|
|
63
|
+
// ============================================
|
|
64
|
+
|
|
65
|
+
const searchQuerySchema = z.object({
|
|
66
|
+
q: z.string().optional(),
|
|
67
|
+
page: z.coerce.number().int().min(1).default(1),
|
|
68
|
+
limit: z.coerce.number().int().min(1).max(100).default(20),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const suggestQuerySchema = z.object({
|
|
72
|
+
q: z.string().optional(),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ============================================
|
|
76
|
+
// Share schemas (from api/routes/share.ts)
|
|
77
|
+
// ============================================
|
|
78
|
+
|
|
79
|
+
const createShareSchema = z
|
|
80
|
+
.object({
|
|
81
|
+
documentId: z.string().uuid().optional(),
|
|
82
|
+
folderId: z.string().uuid().optional(),
|
|
83
|
+
password: z.string().min(1).optional(),
|
|
84
|
+
expiresIn: z.enum(["1h", "1d", "7d", "30d", "never"]).default("never"),
|
|
85
|
+
})
|
|
86
|
+
.refine((d) => d.documentId || d.folderId, {
|
|
87
|
+
message: "Either documentId or folderId must be provided",
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const addGuestSchema = z.object({
|
|
91
|
+
email: z.string().email("Invalid email address"),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ============================================
|
|
95
|
+
// Tests: Document schemas
|
|
96
|
+
// ============================================
|
|
97
|
+
|
|
98
|
+
describe("createDocumentSchema", () => {
|
|
99
|
+
test("accepts valid input with title and content", () => {
|
|
100
|
+
const result = createDocumentSchema.safeParse({
|
|
101
|
+
title: "Test Doc",
|
|
102
|
+
content: "Hello",
|
|
103
|
+
});
|
|
104
|
+
expect(result.success).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("defaults title to 'Untitled' when omitted", () => {
|
|
108
|
+
const result = createDocumentSchema.safeParse({});
|
|
109
|
+
expect(result.success).toBe(true);
|
|
110
|
+
if (result.success) expect(result.data.title).toBe("Untitled");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("rejects empty title", () => {
|
|
114
|
+
const result = createDocumentSchema.safeParse({ title: "" });
|
|
115
|
+
expect(result.success).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("rejects title over 500 characters", () => {
|
|
119
|
+
const result = createDocumentSchema.safeParse({ title: "x".repeat(501) });
|
|
120
|
+
expect(result.success).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("accepts title at exactly 500 characters", () => {
|
|
124
|
+
const result = createDocumentSchema.safeParse({ title: "x".repeat(500) });
|
|
125
|
+
expect(result.success).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("accepts valid UUID folderId", () => {
|
|
129
|
+
const result = createDocumentSchema.safeParse({
|
|
130
|
+
folderId: "550e8400-e29b-41d4-a716-446655440000",
|
|
131
|
+
});
|
|
132
|
+
expect(result.success).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("rejects invalid UUID folderId", () => {
|
|
136
|
+
const result = createDocumentSchema.safeParse({ folderId: "not-a-uuid" });
|
|
137
|
+
expect(result.success).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("content is optional", () => {
|
|
141
|
+
const result = createDocumentSchema.safeParse({ title: "Doc" });
|
|
142
|
+
expect(result.success).toBe(true);
|
|
143
|
+
if (result.success) expect(result.data.content).toBeUndefined();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("updateDocumentSchema", () => {
|
|
148
|
+
test("accepts partial title update", () => {
|
|
149
|
+
const result = updateDocumentSchema.safeParse({ title: "New Title" });
|
|
150
|
+
expect(result.success).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("accepts partial content update", () => {
|
|
154
|
+
const result = updateDocumentSchema.safeParse({ content: "new body" });
|
|
155
|
+
expect(result.success).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("accepts null folderId (move to root)", () => {
|
|
159
|
+
const result = updateDocumentSchema.safeParse({ folderId: null });
|
|
160
|
+
expect(result.success).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("accepts contentJson (rich editor format)", () => {
|
|
164
|
+
const result = updateDocumentSchema.safeParse({
|
|
165
|
+
contentJson: { type: "doc", content: [{ type: "paragraph" }] },
|
|
166
|
+
});
|
|
167
|
+
expect(result.success).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("accepts metadata object", () => {
|
|
171
|
+
const result = updateDocumentSchema.safeParse({
|
|
172
|
+
metadata: { key: "value" },
|
|
173
|
+
});
|
|
174
|
+
expect(result.success).toBe(true);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("accepts empty object (no fields to update)", () => {
|
|
178
|
+
const result = updateDocumentSchema.safeParse({});
|
|
179
|
+
expect(result.success).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("rejects empty title when provided", () => {
|
|
183
|
+
const result = updateDocumentSchema.safeParse({ title: "" });
|
|
184
|
+
expect(result.success).toBe(false);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("rejects title over 500 characters", () => {
|
|
188
|
+
const result = updateDocumentSchema.safeParse({ title: "x".repeat(501) });
|
|
189
|
+
expect(result.success).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe("listQuerySchema", () => {
|
|
194
|
+
test("defaults page to 1 and limit to 20", () => {
|
|
195
|
+
const result = listQuerySchema.safeParse({});
|
|
196
|
+
expect(result.success).toBe(true);
|
|
197
|
+
if (result.success) {
|
|
198
|
+
expect(result.data.page).toBe(1);
|
|
199
|
+
expect(result.data.limit).toBe(20);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("coerces string page to number", () => {
|
|
204
|
+
const result = listQuerySchema.safeParse({ page: "3" });
|
|
205
|
+
expect(result.success).toBe(true);
|
|
206
|
+
if (result.success) expect(result.data.page).toBe(3);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("coerces string limit to number", () => {
|
|
210
|
+
const result = listQuerySchema.safeParse({ limit: "50" });
|
|
211
|
+
expect(result.success).toBe(true);
|
|
212
|
+
if (result.success) expect(result.data.limit).toBe(50);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("rejects page < 1", () => {
|
|
216
|
+
const result = listQuerySchema.safeParse({ page: 0 });
|
|
217
|
+
expect(result.success).toBe(false);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("rejects limit > 100", () => {
|
|
221
|
+
const result = listQuerySchema.safeParse({ limit: 101 });
|
|
222
|
+
expect(result.success).toBe(false);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("rejects limit < 1", () => {
|
|
226
|
+
const result = listQuerySchema.safeParse({ limit: 0 });
|
|
227
|
+
expect(result.success).toBe(false);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("accepts limit at boundary 100", () => {
|
|
231
|
+
const result = listQuerySchema.safeParse({ limit: 100 });
|
|
232
|
+
expect(result.success).toBe(true);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("accepts valid folderId filter", () => {
|
|
236
|
+
const result = listQuerySchema.safeParse({
|
|
237
|
+
folderId: "550e8400-e29b-41d4-a716-446655440000",
|
|
238
|
+
});
|
|
239
|
+
expect(result.success).toBe(true);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("accepts valid tag filter", () => {
|
|
243
|
+
const result = listQuerySchema.safeParse({
|
|
244
|
+
tag: "550e8400-e29b-41d4-a716-446655440000",
|
|
245
|
+
});
|
|
246
|
+
expect(result.success).toBe(true);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("rejects invalid folderId UUID", () => {
|
|
250
|
+
const result = listQuerySchema.safeParse({ folderId: "bad" });
|
|
251
|
+
expect(result.success).toBe(false);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// ============================================
|
|
256
|
+
// Tests: Folder schemas
|
|
257
|
+
// ============================================
|
|
258
|
+
|
|
259
|
+
describe("createFolderSchema", () => {
|
|
260
|
+
test("requires name", () => {
|
|
261
|
+
const result = createFolderSchema.safeParse({});
|
|
262
|
+
expect(result.success).toBe(false);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("rejects empty name", () => {
|
|
266
|
+
const result = createFolderSchema.safeParse({ name: "" });
|
|
267
|
+
expect(result.success).toBe(false);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("rejects name over 255 characters", () => {
|
|
271
|
+
const result = createFolderSchema.safeParse({ name: "x".repeat(256) });
|
|
272
|
+
expect(result.success).toBe(false);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("accepts name at exactly 255 characters", () => {
|
|
276
|
+
const result = createFolderSchema.safeParse({ name: "x".repeat(255) });
|
|
277
|
+
expect(result.success).toBe(true);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("accepts valid input with name only", () => {
|
|
281
|
+
const result = createFolderSchema.safeParse({ name: "My Folder" });
|
|
282
|
+
expect(result.success).toBe(true);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("accepts valid parentId UUID", () => {
|
|
286
|
+
const result = createFolderSchema.safeParse({
|
|
287
|
+
name: "Subfolder",
|
|
288
|
+
parentId: "550e8400-e29b-41d4-a716-446655440000",
|
|
289
|
+
});
|
|
290
|
+
expect(result.success).toBe(true);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("rejects invalid parentId UUID", () => {
|
|
294
|
+
const result = createFolderSchema.safeParse({
|
|
295
|
+
name: "Sub",
|
|
296
|
+
parentId: "not-uuid",
|
|
297
|
+
});
|
|
298
|
+
expect(result.success).toBe(false);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
describe("updateFolderSchema", () => {
|
|
303
|
+
test("accepts partial name update", () => {
|
|
304
|
+
const result = updateFolderSchema.safeParse({ name: "Renamed" });
|
|
305
|
+
expect(result.success).toBe(true);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("accepts null parentId (move to root)", () => {
|
|
309
|
+
const result = updateFolderSchema.safeParse({ parentId: null });
|
|
310
|
+
expect(result.success).toBe(true);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("accepts empty object (no-op)", () => {
|
|
314
|
+
const result = updateFolderSchema.safeParse({});
|
|
315
|
+
expect(result.success).toBe(true);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("rejects empty name", () => {
|
|
319
|
+
const result = updateFolderSchema.safeParse({ name: "" });
|
|
320
|
+
expect(result.success).toBe(false);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// ============================================
|
|
325
|
+
// Tests: Tag schemas
|
|
326
|
+
// ============================================
|
|
327
|
+
|
|
328
|
+
describe("createTagSchema", () => {
|
|
329
|
+
test("requires name", () => {
|
|
330
|
+
const result = createTagSchema.safeParse({});
|
|
331
|
+
expect(result.success).toBe(false);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test("rejects empty name", () => {
|
|
335
|
+
const result = createTagSchema.safeParse({ name: "" });
|
|
336
|
+
expect(result.success).toBe(false);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("rejects name over 100 characters", () => {
|
|
340
|
+
const result = createTagSchema.safeParse({ name: "x".repeat(101) });
|
|
341
|
+
expect(result.success).toBe(false);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("accepts name at exactly 100 characters", () => {
|
|
345
|
+
const result = createTagSchema.safeParse({ name: "x".repeat(100) });
|
|
346
|
+
expect(result.success).toBe(true);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test("accepts optional color", () => {
|
|
350
|
+
const result = createTagSchema.safeParse({
|
|
351
|
+
name: "important",
|
|
352
|
+
color: "#ff0000",
|
|
353
|
+
});
|
|
354
|
+
expect(result.success).toBe(true);
|
|
355
|
+
if (result.success) expect(result.data.color).toBe("#ff0000");
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test("color is undefined when omitted", () => {
|
|
359
|
+
const result = createTagSchema.safeParse({ name: "tag" });
|
|
360
|
+
expect(result.success).toBe(true);
|
|
361
|
+
if (result.success) expect(result.data.color).toBeUndefined();
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("rejects color over 20 characters", () => {
|
|
365
|
+
const result = createTagSchema.safeParse({
|
|
366
|
+
name: "tag",
|
|
367
|
+
color: "x".repeat(21),
|
|
368
|
+
});
|
|
369
|
+
expect(result.success).toBe(false);
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
describe("updateTagSchema", () => {
|
|
374
|
+
test("accepts partial name update", () => {
|
|
375
|
+
const result = updateTagSchema.safeParse({ name: "new-name" });
|
|
376
|
+
expect(result.success).toBe(true);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test("accepts partial color update", () => {
|
|
380
|
+
const result = updateTagSchema.safeParse({ color: "#00ff00" });
|
|
381
|
+
expect(result.success).toBe(true);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("accepts empty object (no-op)", () => {
|
|
385
|
+
const result = updateTagSchema.safeParse({});
|
|
386
|
+
expect(result.success).toBe(true);
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
describe("addTagToDocSchema", () => {
|
|
391
|
+
test("requires tagId", () => {
|
|
392
|
+
const result = addTagToDocSchema.safeParse({});
|
|
393
|
+
expect(result.success).toBe(false);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test("rejects non-UUID tagId", () => {
|
|
397
|
+
const result = addTagToDocSchema.safeParse({ tagId: "not-uuid" });
|
|
398
|
+
expect(result.success).toBe(false);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("accepts valid UUID tagId", () => {
|
|
402
|
+
const result = addTagToDocSchema.safeParse({
|
|
403
|
+
tagId: "550e8400-e29b-41d4-a716-446655440000",
|
|
404
|
+
});
|
|
405
|
+
expect(result.success).toBe(true);
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// ============================================
|
|
410
|
+
// Tests: Search schemas
|
|
411
|
+
// ============================================
|
|
412
|
+
|
|
413
|
+
describe("searchQuerySchema", () => {
|
|
414
|
+
test("q is optional (defaults to undefined)", () => {
|
|
415
|
+
const result = searchQuerySchema.safeParse({});
|
|
416
|
+
expect(result.success).toBe(true);
|
|
417
|
+
if (result.success) expect(result.data.q).toBeUndefined();
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test("accepts search query string", () => {
|
|
421
|
+
const result = searchQuerySchema.safeParse({ q: "hello world" });
|
|
422
|
+
expect(result.success).toBe(true);
|
|
423
|
+
if (result.success) expect(result.data.q).toBe("hello world");
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test("defaults page and limit", () => {
|
|
427
|
+
const result = searchQuerySchema.safeParse({});
|
|
428
|
+
expect(result.success).toBe(true);
|
|
429
|
+
if (result.success) {
|
|
430
|
+
expect(result.data.page).toBe(1);
|
|
431
|
+
expect(result.data.limit).toBe(20);
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
test("coerces string page to number", () => {
|
|
436
|
+
const result = searchQuerySchema.safeParse({ q: "test", page: "2" });
|
|
437
|
+
expect(result.success).toBe(true);
|
|
438
|
+
if (result.success) expect(result.data.page).toBe(2);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test("rejects page < 1", () => {
|
|
442
|
+
const result = searchQuerySchema.safeParse({ page: 0 });
|
|
443
|
+
expect(result.success).toBe(false);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test("rejects limit > 100", () => {
|
|
447
|
+
const result = searchQuerySchema.safeParse({ limit: 101 });
|
|
448
|
+
expect(result.success).toBe(false);
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
describe("suggestQuerySchema", () => {
|
|
453
|
+
test("q is optional", () => {
|
|
454
|
+
const result = suggestQuerySchema.safeParse({});
|
|
455
|
+
expect(result.success).toBe(true);
|
|
456
|
+
if (result.success) expect(result.data.q).toBeUndefined();
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
test("accepts query string", () => {
|
|
460
|
+
const result = suggestQuerySchema.safeParse({ q: "hello" });
|
|
461
|
+
expect(result.success).toBe(true);
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// ============================================
|
|
466
|
+
// Tests: Share schemas
|
|
467
|
+
// ============================================
|
|
468
|
+
|
|
469
|
+
describe("createShareSchema", () => {
|
|
470
|
+
test("accepts documentId with default expiresIn", () => {
|
|
471
|
+
const result = createShareSchema.safeParse({
|
|
472
|
+
documentId: "550e8400-e29b-41d4-a716-446655440000",
|
|
473
|
+
});
|
|
474
|
+
expect(result.success).toBe(true);
|
|
475
|
+
if (result.success) {
|
|
476
|
+
expect(result.data.expiresIn).toBe("never");
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
test("accepts folderId", () => {
|
|
481
|
+
const result = createShareSchema.safeParse({
|
|
482
|
+
folderId: "550e8400-e29b-41d4-a716-446655440000",
|
|
483
|
+
});
|
|
484
|
+
expect(result.success).toBe(true);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test("accepts both documentId and folderId", () => {
|
|
488
|
+
const result = createShareSchema.safeParse({
|
|
489
|
+
documentId: "550e8400-e29b-41d4-a716-446655440000",
|
|
490
|
+
folderId: "660e8400-e29b-41d4-a716-446655440000",
|
|
491
|
+
});
|
|
492
|
+
expect(result.success).toBe(true);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test("rejects when neither documentId nor folderId provided", () => {
|
|
496
|
+
const result = createShareSchema.safeParse({});
|
|
497
|
+
expect(result.success).toBe(false);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
test("accepts valid expiresIn values", () => {
|
|
501
|
+
for (const exp of ["1h", "1d", "7d", "30d", "never"]) {
|
|
502
|
+
const result = createShareSchema.safeParse({
|
|
503
|
+
documentId: "550e8400-e29b-41d4-a716-446655440000",
|
|
504
|
+
expiresIn: exp,
|
|
505
|
+
});
|
|
506
|
+
expect(result.success).toBe(true);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
test("rejects invalid expiresIn value", () => {
|
|
511
|
+
const result = createShareSchema.safeParse({
|
|
512
|
+
documentId: "550e8400-e29b-41d4-a716-446655440000",
|
|
513
|
+
expiresIn: "2d",
|
|
514
|
+
});
|
|
515
|
+
expect(result.success).toBe(false);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
test("accepts optional password", () => {
|
|
519
|
+
const result = createShareSchema.safeParse({
|
|
520
|
+
documentId: "550e8400-e29b-41d4-a716-446655440000",
|
|
521
|
+
password: "secret123",
|
|
522
|
+
});
|
|
523
|
+
expect(result.success).toBe(true);
|
|
524
|
+
if (result.success) expect(result.data.password).toBe("secret123");
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
test("rejects empty password", () => {
|
|
528
|
+
const result = createShareSchema.safeParse({
|
|
529
|
+
documentId: "550e8400-e29b-41d4-a716-446655440000",
|
|
530
|
+
password: "",
|
|
531
|
+
});
|
|
532
|
+
expect(result.success).toBe(false);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
test("rejects invalid documentId UUID", () => {
|
|
536
|
+
const result = createShareSchema.safeParse({ documentId: "bad-uuid" });
|
|
537
|
+
expect(result.success).toBe(false);
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
describe("addGuestSchema", () => {
|
|
542
|
+
test("accepts valid email", () => {
|
|
543
|
+
const result = addGuestSchema.safeParse({ email: "user@example.com" });
|
|
544
|
+
expect(result.success).toBe(true);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test("rejects invalid email", () => {
|
|
548
|
+
const result = addGuestSchema.safeParse({ email: "not-an-email" });
|
|
549
|
+
expect(result.success).toBe(false);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
test("requires email field", () => {
|
|
553
|
+
const result = addGuestSchema.safeParse({});
|
|
554
|
+
expect(result.success).toBe(false);
|
|
555
|
+
});
|
|
556
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Elysia } from "elysia";
|
|
2
|
+
import { auth } from "../../lib/auth";
|
|
3
|
+
import { config } from "../../lib/config";
|
|
4
|
+
|
|
5
|
+
export const authMiddleware = new Elysia()
|
|
6
|
+
.derive(async ({ request }) => {
|
|
7
|
+
// API key check: if HIAI_DOCS_API_KEY is set and request has matching Bearer token,
|
|
8
|
+
// return a synthetic session (no DB lookup needed)
|
|
9
|
+
const apiKey = config.HIAI_DOCS_API_KEY;
|
|
10
|
+
if (apiKey) {
|
|
11
|
+
const authHeader = request.headers.get("authorization");
|
|
12
|
+
if (authHeader?.startsWith("Bearer ")) {
|
|
13
|
+
const token = authHeader.slice(7);
|
|
14
|
+
if (token === apiKey) {
|
|
15
|
+
const session = {
|
|
16
|
+
session: {
|
|
17
|
+
id: "api-key-session",
|
|
18
|
+
userId: config.OWNER_ID,
|
|
19
|
+
expiresAt: new Date(Date.now() + 60 * 60 * 24 * 365 * 10), // 10 years
|
|
20
|
+
token: "api-key",
|
|
21
|
+
ipAddress: "",
|
|
22
|
+
userAgent: "",
|
|
23
|
+
createdAt: new Date(),
|
|
24
|
+
updatedAt: new Date(),
|
|
25
|
+
},
|
|
26
|
+
user: {
|
|
27
|
+
id: config.OWNER_ID,
|
|
28
|
+
name: "API Key User",
|
|
29
|
+
email: `${config.OWNER_ID}@hiai-docs.local`,
|
|
30
|
+
emailVerified: true,
|
|
31
|
+
createdAt: new Date(),
|
|
32
|
+
updatedAt: new Date(),
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
return { session };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Fall through to Better Auth session check
|
|
41
|
+
const session = await auth.api.getSession({
|
|
42
|
+
headers: request.headers,
|
|
43
|
+
});
|
|
44
|
+
return { session };
|
|
45
|
+
})
|
|
46
|
+
.macro({
|
|
47
|
+
auth: {
|
|
48
|
+
async resolve({ session, set }) {
|
|
49
|
+
if (!session) {
|
|
50
|
+
set.status = 401;
|
|
51
|
+
return { error: "Unauthorized" };
|
|
52
|
+
}
|
|
53
|
+
return { user: session.user };
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { Elysia } from "elysia";
|
|
3
|
+
import { config } from "../../lib/config";
|
|
4
|
+
|
|
5
|
+
const CSRF_SECRET = config.CSRF_SECRET;
|
|
6
|
+
const CSRF_COOKIE = "hiai-csrf";
|
|
7
|
+
const CSRF_HEADER = "x-csrf-token";
|
|
8
|
+
const CSRF_MAX_AGE = 3600;
|
|
9
|
+
|
|
10
|
+
function signToken(token: string): string {
|
|
11
|
+
return createHmac("sha256", CSRF_SECRET).update(token).digest("hex");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function generateToken(): string {
|
|
15
|
+
const token = randomBytes(32).toString("hex");
|
|
16
|
+
return `${token}.${signToken(token)}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function verifyToken(token: string): boolean {
|
|
20
|
+
const [value, signature] = token.split(".");
|
|
21
|
+
if (!value || !signature) return false;
|
|
22
|
+
const expected = signToken(value);
|
|
23
|
+
try {
|
|
24
|
+
return timingSafeEqual(
|
|
25
|
+
Buffer.from(signature, "hex"),
|
|
26
|
+
Buffer.from(expected, "hex"),
|
|
27
|
+
);
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isUnsafeMethod(method: string): boolean {
|
|
34
|
+
return ["POST", "PUT", "PATCH", "DELETE"].includes(method.toUpperCase());
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isApiRoute(url: string): boolean {
|
|
38
|
+
if (!url.startsWith("/api/")) return false;
|
|
39
|
+
if (url.startsWith("/api/auth")) return false;
|
|
40
|
+
if (url.startsWith("/api/webhooks")) return false;
|
|
41
|
+
if (url.startsWith("/api/csrf-token")) return false;
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isMultipart(request: Request): boolean {
|
|
46
|
+
return (
|
|
47
|
+
request.headers.get("content-type")?.includes("multipart/form-data") ===
|
|
48
|
+
true
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const csrfMiddleware = new Elysia()
|
|
53
|
+
.onRequest(({ request, set }) => {
|
|
54
|
+
const url = new URL(request.url);
|
|
55
|
+
|
|
56
|
+
if (!isApiRoute(url.pathname)) return;
|
|
57
|
+
|
|
58
|
+
const apiKey = request.headers.get("authorization")?.startsWith("Bearer ");
|
|
59
|
+
if (apiKey) return;
|
|
60
|
+
|
|
61
|
+
if (!isUnsafeMethod(request.method)) return;
|
|
62
|
+
if (isMultipart(request)) return;
|
|
63
|
+
|
|
64
|
+
const origin = request.headers.get("origin");
|
|
65
|
+
const host = request.headers.get("host");
|
|
66
|
+
if (origin && host) {
|
|
67
|
+
try {
|
|
68
|
+
const originUrl = new URL(origin);
|
|
69
|
+
if (originUrl.host !== host) {
|
|
70
|
+
set.status = 403;
|
|
71
|
+
return { error: "CSRF: origin mismatch" };
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
set.status = 403;
|
|
75
|
+
return { error: "CSRF: invalid origin" };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const token = request.headers.get(CSRF_HEADER);
|
|
80
|
+
if (!token || !verifyToken(token)) {
|
|
81
|
+
set.status = 403;
|
|
82
|
+
return { error: "CSRF: invalid or missing token" };
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
.get("/api/csrf-token", ({ set }) => {
|
|
86
|
+
const token = generateToken();
|
|
87
|
+
const maxAge = CSRF_MAX_AGE;
|
|
88
|
+
set.headers["Set-Cookie"] =
|
|
89
|
+
`${CSRF_COOKIE}=${token}; Path=/; HttpOnly=false; SameSite=Strict; Max-Age=${maxAge}${config.NODE_ENV === "production" ? "; Secure" : ""}`;
|
|
90
|
+
return { token };
|
|
91
|
+
});
|