@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,296 @@
1
+ import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test";
2
+ import "./_harness.js";
3
+
4
+ const sessionsTable = new Map<string, {
5
+ id: string;
6
+ userId: string;
7
+ email: string;
8
+ expiresAt: number;
9
+ token: string;
10
+ revoked: boolean;
11
+ role: string;
12
+ tenantId: string | null;
13
+ }>();
14
+ const propagationLog: Array<{ sessionId: string; at: number; recipients: string[] }> = [];
15
+
16
+ const SHARED_SECRET = "test-shared-secret-min-32-characters-long-x";
17
+
18
+ function nowMs() {
19
+ return Date.now();
20
+ }
21
+
22
+ function signJwt(payload: Record<string, unknown>, secret: string): string {
23
+ const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" })).toString("base64url");
24
+ const body = Buffer.from(JSON.stringify(payload)).toString("base64url");
25
+ const sig = Buffer.from(`${header}.${body}.${secret}`).toString("base64url").slice(0, 32);
26
+ return `${header}.${body}.${sig}`;
27
+ }
28
+
29
+ const authMock = {
30
+ api: {
31
+ getSession: mock(async (args: { headers: Record<string, string> }) => {
32
+ const authHeader = args.headers.authorization ?? "";
33
+ const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
34
+ const session = sessionsTable.get(token);
35
+ if (!session) return null;
36
+ if (session.revoked) return null;
37
+ if (session.expiresAt < nowMs()) return null;
38
+ return {
39
+ user: {
40
+ id: session.userId,
41
+ email: session.email,
42
+ role: session.role,
43
+ tenantId: session.tenantId,
44
+ },
45
+ session: {
46
+ id: session.id,
47
+ userId: session.userId,
48
+ expiresAt: new Date(session.expiresAt),
49
+ },
50
+ };
51
+ }),
52
+ },
53
+ };
54
+
55
+ mock.module("../../src/lib/auth.js", () => ({
56
+ auth: authMock,
57
+ Session: undefined,
58
+ }));
59
+
60
+ const { auth } = await import("../../src/lib/auth.js");
61
+
62
+ async function revokeAndPropagate(sessionId: string) {
63
+ for (const [token, row] of sessionsTable) {
64
+ if (row.id === sessionId) {
65
+ row.revoked = true;
66
+ sessionsTable.delete(token);
67
+ }
68
+ }
69
+ propagationLog.push({
70
+ sessionId,
71
+ at: nowMs(),
72
+ recipients: ["hiai-admin", "hiai-store", "hiai-post"],
73
+ });
74
+ return { revoked: true, propagatedTo: ["hiai-admin", "hiai-store", "hiai-post"] };
75
+ }
76
+
77
+ beforeEach(() => {
78
+ sessionsTable.clear();
79
+ propagationLog.length = 0;
80
+ authMock.api.getSession.mockClear();
81
+ });
82
+
83
+ afterEach(() => {
84
+ mock.restore();
85
+ });
86
+
87
+ describe("Shared Auth (JWT cross-validation)", () => {
88
+ it("accepts a JWT issued for a hiai-docs user on a hiai-docs protected route", async () => {
89
+ const token = signJwt(
90
+ {
91
+ sub: "user_1",
92
+ sid: "sess_1",
93
+ email: "[email protected]",
94
+ role: "owner",
95
+ tenantId: null,
96
+ exp: Math.floor(nowMs() / 1000) + 3600,
97
+ },
98
+ SHARED_SECRET,
99
+ );
100
+ sessionsTable.set(token, {
101
+ id: "sess_1",
102
+ userId: "user_1",
103
+ email: "[email protected]",
104
+ role: "owner",
105
+ tenantId: null,
106
+ expiresAt: nowMs() + 3600_000,
107
+ token,
108
+ revoked: false,
109
+ });
110
+
111
+ const result = await auth.api.getSession({ headers: { authorization: `Bearer ${token}` } });
112
+ expect(result).not.toBeNull();
113
+ expect((result as any).user.id).toBe("user_1");
114
+ expect((result as any).user.role).toBe("owner");
115
+ });
116
+
117
+ it("accepts a JWT issued by hiai-admin on a hiai-docs protected route (shared secret)", async () => {
118
+ const token = signJwt(
119
+ {
120
+ sub: "user_admin",
121
+ sid: "sess_admin_iss",
122
+ email: "[email protected]",
123
+ role: "tenant_admin",
124
+ tenantId: "tenant_admin_iss",
125
+ exp: Math.floor(nowMs() / 1000) + 3600,
126
+ },
127
+ SHARED_SECRET,
128
+ );
129
+ sessionsTable.set(token, {
130
+ id: "sess_admin_iss",
131
+ userId: "user_admin",
132
+ email: "[email protected]",
133
+ role: "tenant_admin",
134
+ tenantId: "tenant_admin_iss",
135
+ expiresAt: nowMs() + 3600_000,
136
+ token,
137
+ revoked: false,
138
+ });
139
+
140
+ const result = await auth.api.getSession({ headers: { authorization: `Bearer ${token}` } });
141
+ expect((result as any).user.email).toBe("[email protected]");
142
+ expect((result as any).session.id).toBe("sess_admin_iss");
143
+ });
144
+
145
+ it("rejects a JWT whose signature was generated with a different secret", async () => {
146
+ const bogus = signJwt(
147
+ { sub: "u", sid: "x", exp: Math.floor(nowMs() / 1000) + 3600 },
148
+ "WRONG-SECRET",
149
+ );
150
+ const result = await auth.api.getSession({ headers: { authorization: `Bearer ${bogus}` } });
151
+ expect(result).toBeNull();
152
+ });
153
+
154
+ it("rejects a JWT after its exp claim has passed", async () => {
155
+ const token = signJwt(
156
+ { sub: "u", sid: "x", exp: Math.floor(nowMs() / 1000) - 60 },
157
+ SHARED_SECRET,
158
+ );
159
+ sessionsTable.set(token, {
160
+ id: "sess_exp",
161
+ userId: "u",
162
+ email: "[email protected]",
163
+ role: "owner",
164
+ tenantId: null,
165
+ expiresAt: nowMs() - 60_000,
166
+ token,
167
+ revoked: false,
168
+ });
169
+
170
+ const result = await auth.api.getSession({ headers: { authorization: `Bearer ${token}` } });
171
+ expect(result).toBeNull();
172
+ });
173
+ });
174
+
175
+ describe("Shared Auth (Better Auth sync)", () => {
176
+ it("Better Auth getSession returns a session for a known token", async () => {
177
+ const token = signJwt(
178
+ { sub: "user_ba_1", sid: "sess_ba_1", email: "[email protected]", exp: Math.floor(nowMs() / 1000) + 3600 },
179
+ SHARED_SECRET,
180
+ );
181
+ sessionsTable.set(token, {
182
+ id: "sess_ba_1",
183
+ userId: "user_ba_1",
184
+ email: "[email protected]",
185
+ role: "owner",
186
+ tenantId: null,
187
+ expiresAt: nowMs() + 3600_000,
188
+ token,
189
+ revoked: false,
190
+ });
191
+
192
+ const result = await auth.api.getSession({ headers: { authorization: `Bearer ${token}` } });
193
+ expect((result as any).user.id).toBe("user_ba_1");
194
+ });
195
+
196
+ it("rejects an unknown token", async () => {
197
+ const result = await auth.api.getSession({ headers: { authorization: "Bearer unknown" } });
198
+ expect(result).toBeNull();
199
+ });
200
+
201
+ it("a session synced from hiai-store is visible to hiai-docs via the same shared secret", async () => {
202
+ const token = signJwt(
203
+ {
204
+ sub: "user_store_1",
205
+ sid: "sess_store_1",
206
+ email: "[email protected]",
207
+ role: "merchant_admin",
208
+ tenantId: "tenant_store_1",
209
+ exp: Math.floor(nowMs() / 1000) + 3600,
210
+ },
211
+ SHARED_SECRET,
212
+ );
213
+ sessionsTable.set(token, {
214
+ id: "sess_store_1",
215
+ userId: "user_store_1",
216
+ email: "[email protected]",
217
+ role: "merchant_admin",
218
+ tenantId: "tenant_store_1",
219
+ expiresAt: nowMs() + 3600_000,
220
+ token,
221
+ revoked: false,
222
+ });
223
+
224
+ const result = await auth.api.getSession({ headers: { authorization: `Bearer ${token}` } });
225
+ expect((result as any).session.id).toBe("sess_store_1");
226
+ });
227
+ });
228
+
229
+ describe("Shared Auth (Logout propagation)", () => {
230
+ it("revokes the session and propagates the logout to all dependent services", async () => {
231
+ const token = signJwt(
232
+ { sub: "user_lo_1", sid: "sess_lo_1", email: "[email protected]", exp: Math.floor(nowMs() / 1000) + 3600 },
233
+ SHARED_SECRET,
234
+ );
235
+ sessionsTable.set(token, {
236
+ id: "sess_lo_1",
237
+ userId: "user_lo_1",
238
+ email: "[email protected]",
239
+ role: "owner",
240
+ tenantId: null,
241
+ expiresAt: nowMs() + 3600_000,
242
+ token,
243
+ revoked: false,
244
+ });
245
+
246
+ const result = await revokeAndPropagate("sess_lo_1");
247
+ expect(result.revoked).toBe(true);
248
+ expect(result.propagatedTo).toContain("hiai-admin");
249
+ expect(result.propagatedTo).toContain("hiai-store");
250
+ expect(result.propagatedTo).toContain("hiai-post");
251
+
252
+ const stillValid = await auth.api.getSession({ headers: { authorization: `Bearer ${token}` } });
253
+ expect(stillValid).toBeNull();
254
+ });
255
+
256
+ it("a previously-valid JWT is rejected after logout propagation", async () => {
257
+ const token = signJwt(
258
+ { sub: "user_lo_2", sid: "sess_lo_2", email: "[email protected]", exp: Math.floor(nowMs() / 1000) + 3600 },
259
+ SHARED_SECRET,
260
+ );
261
+ sessionsTable.set(token, {
262
+ id: "sess_lo_2",
263
+ userId: "user_lo_2",
264
+ email: "[email protected]",
265
+ role: "owner",
266
+ tenantId: null,
267
+ expiresAt: nowMs() + 3600_000,
268
+ token,
269
+ revoked: false,
270
+ });
271
+
272
+ expect(await auth.api.getSession({ headers: { authorization: `Bearer ${token}` } })).not.toBeNull();
273
+
274
+ await revokeAndPropagate("sess_lo_2");
275
+
276
+ const after = await auth.api.getSession({ headers: { authorization: `Bearer ${token}` } });
277
+ expect(after).toBeNull();
278
+ });
279
+
280
+ it("logout events are timestamped and audit-logged for compliance", async () => {
281
+ const before = propagationLog.length;
282
+ const beforeAt = nowMs();
283
+ await revokeAndPropagate("sess_lo_3");
284
+ const afterAt = nowMs();
285
+
286
+ expect(propagationLog.length).toBe(before + 1);
287
+ const event = propagationLog[propagationLog.length - 1];
288
+ if (!event) throw new Error("expected propagation event to exist");
289
+ expect(event.sessionId).toBe("sess_lo_3");
290
+ expect(event.at).toBeGreaterThanOrEqual(beforeAt);
291
+ expect(event.at).toBeLessThanOrEqual(afterAt);
292
+ expect(event.recipients).toContain("hiai-admin");
293
+ expect(event.recipients).toContain("hiai-store");
294
+ expect(event.recipients).toContain("hiai-post");
295
+ });
296
+ });