@valentinkolb/cloud 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/package.json +18 -8
  2. package/scripts/preload.ts +78 -23
  3. package/src/_internal/define-app.ts +119 -47
  4. package/src/_internal/runtime-context.ts +1 -0
  5. package/src/api/accounts-entities.ts +4 -0
  6. package/src/api/admin-core-settings.ts +98 -0
  7. package/src/api/announcements.ts +131 -0
  8. package/src/api/auth/schemas.ts +24 -0
  9. package/src/api/auth.ts +113 -10
  10. package/src/api/index.ts +15 -25
  11. package/src/api/me.ts +203 -14
  12. package/src/api/search/schemas.ts +1 -0
  13. package/src/api/search.ts +62 -8
  14. package/src/config/ssr.ts +2 -9
  15. package/src/contracts/announcements.test.ts +37 -0
  16. package/src/contracts/announcements.ts +121 -0
  17. package/src/contracts/app.ts +4 -0
  18. package/src/contracts/index.ts +3 -2
  19. package/src/contracts/registry.ts +4 -0
  20. package/src/contracts/shared.ts +108 -1
  21. package/src/desktop/index.ts +704 -0
  22. package/src/desktop/solid.tsx +938 -0
  23. package/src/server/api/index.ts +1 -1
  24. package/src/server/api/respond.ts +50 -10
  25. package/src/server/index.ts +44 -38
  26. package/src/server/middleware/auth.ts +98 -9
  27. package/src/server/middleware/index.ts +2 -1
  28. package/src/server/middleware/settings.ts +26 -0
  29. package/src/server/services/access.test.ts +197 -0
  30. package/src/server/services/access.ts +254 -6
  31. package/src/server/services/index.ts +14 -11
  32. package/src/server/services/pagination.ts +22 -0
  33. package/src/server/time.ts +45 -0
  34. package/src/services/account-lifecycle/index.ts +142 -18
  35. package/src/services/accounts/app.ts +658 -170
  36. package/src/services/accounts/authz.test.ts +77 -0
  37. package/src/services/accounts/authz.ts +22 -0
  38. package/src/services/accounts/entities.ts +84 -5
  39. package/src/services/accounts/groups.ts +30 -24
  40. package/src/services/accounts/model.test.ts +30 -0
  41. package/src/services/accounts/switching.test.ts +14 -0
  42. package/src/services/accounts/switching.ts +15 -6
  43. package/src/services/accounts/users.ts +75 -52
  44. package/src/services/announcements/index.test.ts +32 -0
  45. package/src/services/announcements/index.ts +224 -0
  46. package/src/services/audit/index.test.ts +84 -0
  47. package/src/services/audit/index.ts +431 -0
  48. package/src/services/auth-flows/index.ts +9 -2
  49. package/src/services/auth-flows/ipa.ts +0 -2
  50. package/src/services/auth-flows/magic-link.ts +3 -2
  51. package/src/services/auth-flows/password-reset.ts +284 -0
  52. package/src/services/auth-flows/proxy-return.test.ts +24 -0
  53. package/src/services/auth-flows/proxy-return.ts +49 -0
  54. package/src/services/gateway.ts +162 -0
  55. package/src/services/index.ts +44 -2
  56. package/src/services/ipa/effective-groups.test.ts +33 -0
  57. package/src/services/ipa/effective-groups.ts +70 -0
  58. package/src/services/ipa/profile.ts +45 -3
  59. package/src/services/ipa/search.ts +3 -5
  60. package/src/services/ipa/service-account.ts +15 -0
  61. package/src/services/ipa/sync-planning.test.ts +32 -0
  62. package/src/services/ipa/sync-planning.ts +22 -0
  63. package/src/services/ipa/sync.ts +110 -38
  64. package/src/services/oauth-tokens.ts +104 -0
  65. package/src/services/postgres.ts +21 -6
  66. package/src/services/providers/local/auth.test.ts +22 -0
  67. package/src/services/providers/local/auth.ts +46 -3
  68. package/src/services/secrets.ts +10 -0
  69. package/src/services/service-account-credentials.test.ts +210 -0
  70. package/src/services/service-account-credentials.ts +715 -0
  71. package/src/services/service-accounts.ts +188 -0
  72. package/src/services/session/index.ts +7 -8
  73. package/src/services/settings/app.ts +4 -20
  74. package/src/services/settings/defaults.ts +64 -22
  75. package/src/services/settings/store.ts +47 -0
  76. package/src/services/weather/forecast.ts +40 -7
  77. package/src/services/webauthn.test.ts +36 -0
  78. package/src/services/webauthn.ts +384 -0
  79. package/src/shared/icons.ts +391 -100
  80. package/src/shared/index.ts +7 -0
  81. package/src/shared/markdown/extensions/code.ts +38 -1
  82. package/src/shared/markdown/extensions/images.ts +39 -3
  83. package/src/shared/markdown/extensions/info-blocks.ts +5 -5
  84. package/src/shared/markdown/extensions/mark.ts +48 -0
  85. package/src/shared/markdown/extensions/sub-sup.ts +60 -0
  86. package/src/shared/markdown/extensions/tables.ts +79 -58
  87. package/src/shared/markdown/formula.test.ts +1089 -0
  88. package/src/shared/markdown/formula.ts +1187 -0
  89. package/src/shared/markdown/index.ts +76 -2
  90. package/src/shared/mock-cover.ts +130 -0
  91. package/src/shared/redirect.test.ts +49 -0
  92. package/src/shared/redirect.ts +52 -0
  93. package/src/shared/theme.test.ts +24 -0
  94. package/src/shared/theme.ts +68 -0
  95. package/src/shared/time.ts +13 -0
  96. package/src/ssr/AdminLayout.tsx +7 -3
  97. package/src/ssr/AdminSidebar.tsx +115 -49
  98. package/src/ssr/AppLaunchpad.island.tsx +176 -0
  99. package/src/ssr/Footer.island.tsx +3 -8
  100. package/src/ssr/GlobalAnnouncements.island.tsx +141 -0
  101. package/src/ssr/GlobalSearchDialog.tsx +545 -117
  102. package/src/ssr/HotkeysHelpRail.island.tsx +3 -70
  103. package/src/ssr/Layout.tsx +74 -66
  104. package/src/ssr/LayoutBreadcrumbs.island.tsx +44 -0
  105. package/src/ssr/LayoutHelp.tsx +266 -0
  106. package/src/ssr/NavMenu.island.tsx +0 -39
  107. package/src/ssr/ThemeToggleRail.island.tsx +3 -3
  108. package/src/ssr/TimezoneCookie.island.tsx +23 -0
  109. package/src/ssr/islands/index.ts +13 -0
  110. package/src/styles/base-popover.css +5 -2
  111. package/src/styles/effects.css +87 -6
  112. package/src/styles/global.css +146 -9
  113. package/src/styles/input.css +3 -1
  114. package/src/styles/utilities-buttons.css +133 -27
  115. package/src/styles/utilities-code-display.css +67 -0
  116. package/src/styles/utilities-completion.css +223 -0
  117. package/src/styles/utilities-detail.css +73 -0
  118. package/src/styles/utilities-feedback.css +16 -15
  119. package/src/styles/utilities-layout.css +42 -2
  120. package/src/styles/utilities-markdown-editor.css +472 -0
  121. package/src/styles/utilities-navigation.css +63 -8
  122. package/src/styles/utilities-script.css +84 -0
  123. package/src/styles/utilities-table-tile.css +229 -0
  124. package/src/types/ambient.d.ts +9 -0
  125. package/src/ui/completion/behaviors.test.ts +95 -0
  126. package/src/ui/completion/behaviors.ts +205 -0
  127. package/src/ui/completion/engine.ts +368 -0
  128. package/src/ui/completion/index.ts +40 -0
  129. package/src/ui/completion/overlay.ts +92 -0
  130. package/src/ui/dialog-core.ts +173 -45
  131. package/src/ui/filter/FilterChip.tsx +42 -40
  132. package/src/ui/index.ts +11 -12
  133. package/src/ui/input/AutocompleteEditor.tsx +656 -0
  134. package/src/ui/input/CheckboxCard.tsx +91 -0
  135. package/src/ui/input/Combobox.tsx +375 -0
  136. package/src/ui/input/DatePicker.tsx +846 -0
  137. package/src/ui/input/DateTimeInput.tsx +29 -4
  138. package/src/ui/input/FileDropzone.tsx +116 -0
  139. package/src/ui/input/IconInput.tsx +116 -0
  140. package/src/ui/input/ImageInput.tsx +19 -2
  141. package/src/ui/input/MultiSelectInput.tsx +448 -0
  142. package/src/ui/input/NumberInput.tsx +417 -61
  143. package/src/ui/input/SegmentedControl.tsx +2 -2
  144. package/src/ui/input/Select.tsx +172 -10
  145. package/src/ui/input/Slider.tsx +3 -4
  146. package/src/ui/input/Switch.tsx +3 -2
  147. package/src/ui/input/TemplateEditor.tsx +212 -0
  148. package/src/ui/input/TextInput.tsx +144 -13
  149. package/src/ui/input/index.ts +53 -8
  150. package/src/ui/input/markdown/MarkdownEditor.tsx +774 -0
  151. package/src/ui/input/markdown/Toolbar.tsx +90 -0
  152. package/src/ui/input/markdown/actions.ts +233 -0
  153. package/src/ui/input/markdown/active-formats.ts +94 -0
  154. package/src/ui/input/markdown/behaviors.ts +193 -0
  155. package/src/ui/input/markdown/code-zone.ts +23 -0
  156. package/src/ui/input/markdown/highlight.ts +316 -0
  157. package/src/ui/layout.ts +22 -0
  158. package/src/ui/misc/AppOverview.tsx +105 -0
  159. package/src/ui/misc/AppWorkspace.tsx +607 -0
  160. package/src/ui/misc/Calendar.tsx +1291 -0
  161. package/src/ui/misc/Chart.tsx +162 -0
  162. package/src/ui/misc/CodeDisplay.tsx +54 -0
  163. package/src/ui/misc/ContextMenu.tsx +2 -2
  164. package/src/ui/misc/DataTable.tsx +269 -0
  165. package/src/ui/misc/DockWorkspace.tsx +425 -0
  166. package/src/ui/misc/Docs.tsx +153 -0
  167. package/src/ui/misc/Dropdown.tsx +2 -2
  168. package/src/ui/misc/EntitySearch.tsx +260 -129
  169. package/src/ui/misc/LinkCard.tsx +14 -2
  170. package/src/ui/misc/LogEntriesTable.tsx +34 -31
  171. package/src/ui/misc/Pagination.tsx +31 -12
  172. package/src/ui/misc/PanelDialog.tsx +109 -0
  173. package/src/ui/misc/Panes.tsx +873 -0
  174. package/src/ui/misc/PermissionEditor.tsx +358 -262
  175. package/src/ui/misc/Placeholder.tsx +40 -0
  176. package/src/ui/misc/ProgressBar.tsx +1 -1
  177. package/src/ui/misc/ResourceApiKeys.tsx +260 -0
  178. package/src/ui/misc/SettingsModal.tsx +150 -0
  179. package/src/ui/misc/StatCell.tsx +182 -40
  180. package/src/ui/misc/StatGrid.tsx +149 -0
  181. package/src/ui/misc/StructuredDataPreview.tsx +107 -0
  182. package/src/ui/misc/code-highlight.ts +213 -0
  183. package/src/ui/misc/index.ts +93 -12
  184. package/src/ui/prompts.tsx +362 -312
  185. package/src/ui/toast.ts +384 -0
  186. package/src/ui/widgets/Widget.tsx +12 -4
  187. package/src/ssr/MoreAppsDropdown.island.tsx +0 -61
  188. package/src/ui/ipa/GroupView.tsx +0 -36
  189. package/src/ui/ipa/LoginBtn.tsx +0 -16
  190. package/src/ui/ipa/UserView.tsx +0 -58
  191. package/src/ui/ipa/index.ts +0 -4
  192. package/src/ui/navigation.ts +0 -32
  193. package/src/ui/sidebar.tsx +0 -468
  194. /package/src/ui/{ipa → misc}/Avatar.tsx +0 -0
@@ -0,0 +1,131 @@
1
+ import { ok } from "@valentinkolb/stdlib";
2
+ import { Hono } from "hono";
3
+ import { describeRoute } from "hono-openapi";
4
+ import { z } from "zod";
5
+ import {
6
+ ActiveAnnouncementsResponseSchema,
7
+ AnnouncementEntrySchema,
8
+ AnnouncementListResponseSchema,
9
+ CreateAnnouncementSchema,
10
+ ErrorResponseSchema,
11
+ MessageResponseSchema,
12
+ parseAnnouncementCookieHeader,
13
+ UpdateAnnouncementSchema,
14
+ } from "../contracts";
15
+ import { type AuthContext, auth, jsonResponse, requiresAdmin, requiresAuth, respond, v } from "../server";
16
+ import { announcements } from "../services";
17
+
18
+ const IdParamSchema = z.object({ id: z.uuid() });
19
+
20
+ const withMessage = async <T>(operation: Promise<import("@valentinkolb/stdlib").Result<T>>, message: string) => {
21
+ const result = await operation;
22
+ if (!result.ok) return result;
23
+ return ok({ message });
24
+ };
25
+
26
+ export const announcementRoutes = new Hono<AuthContext>().get(
27
+ "/active",
28
+ auth.requireRole("authenticated"),
29
+ describeRoute({
30
+ tags: ["Announcements"],
31
+ summary: "List active user announcements",
32
+ description: "Returns active banners and unseen announcements for the current request cookie state.",
33
+ ...requiresAuth,
34
+ responses: {
35
+ 200: jsonResponse(ActiveAnnouncementsResponseSchema, "Active announcements"),
36
+ 401: jsonResponse(ErrorResponseSchema, "Authentication required"),
37
+ },
38
+ }),
39
+ async (c) => {
40
+ const state = parseAnnouncementCookieHeader(c.req.header("Cookie"));
41
+ return respond(c, ok(await announcements.active.forState({ state })));
42
+ },
43
+ );
44
+
45
+ export const adminAnnouncementRoutes = new Hono<AuthContext>()
46
+ .use(auth.requireRole("admin"))
47
+ .get(
48
+ "/",
49
+ describeRoute({
50
+ tags: ["Admin Announcements"],
51
+ summary: "List announcements",
52
+ ...requiresAdmin,
53
+ responses: {
54
+ 200: jsonResponse(AnnouncementListResponseSchema, "Announcements"),
55
+ 401: jsonResponse(ErrorResponseSchema, "Authentication required"),
56
+ 403: jsonResponse(ErrorResponseSchema, "Admin access required"),
57
+ },
58
+ }),
59
+ v(
60
+ "query",
61
+ z.object({
62
+ kind: z.enum(["announcement", "banner"]).optional(),
63
+ search: z.string().optional(),
64
+ }),
65
+ ),
66
+ async (c) => {
67
+ const query = c.req.valid("query");
68
+ const items = await announcements.admin.list({
69
+ filter: { kind: query.kind, query: query.search },
70
+ });
71
+ return respond(c, ok({ items }));
72
+ },
73
+ )
74
+ .post(
75
+ "/",
76
+ describeRoute({
77
+ tags: ["Admin Announcements"],
78
+ summary: "Create announcement",
79
+ ...requiresAdmin,
80
+ responses: {
81
+ 201: jsonResponse(AnnouncementEntrySchema, "Created announcement"),
82
+ 400: jsonResponse(ErrorResponseSchema, "Validation error"),
83
+ 401: jsonResponse(ErrorResponseSchema, "Authentication required"),
84
+ 403: jsonResponse(ErrorResponseSchema, "Admin access required"),
85
+ },
86
+ }),
87
+ v("json", CreateAnnouncementSchema),
88
+ async (c) => respond(c, announcements.admin.create({ data: c.req.valid("json"), actorId: c.get("user").id }), 201),
89
+ )
90
+ .patch(
91
+ "/:id",
92
+ describeRoute({
93
+ tags: ["Admin Announcements"],
94
+ summary: "Update announcement",
95
+ ...requiresAdmin,
96
+ responses: {
97
+ 200: jsonResponse(AnnouncementEntrySchema, "Updated announcement"),
98
+ 400: jsonResponse(ErrorResponseSchema, "Validation error"),
99
+ 401: jsonResponse(ErrorResponseSchema, "Authentication required"),
100
+ 403: jsonResponse(ErrorResponseSchema, "Admin access required"),
101
+ 404: jsonResponse(ErrorResponseSchema, "Announcement not found"),
102
+ },
103
+ }),
104
+ v("param", IdParamSchema),
105
+ v("json", UpdateAnnouncementSchema),
106
+ async (c) =>
107
+ respond(
108
+ c,
109
+ announcements.admin.update({
110
+ id: c.req.valid("param").id,
111
+ data: c.req.valid("json"),
112
+ actorId: c.get("user").id,
113
+ }),
114
+ ),
115
+ )
116
+ .delete(
117
+ "/:id",
118
+ describeRoute({
119
+ tags: ["Admin Announcements"],
120
+ summary: "Delete announcement",
121
+ ...requiresAdmin,
122
+ responses: {
123
+ 200: jsonResponse(MessageResponseSchema, "Announcement deleted"),
124
+ 401: jsonResponse(ErrorResponseSchema, "Authentication required"),
125
+ 403: jsonResponse(ErrorResponseSchema, "Admin access required"),
126
+ 404: jsonResponse(ErrorResponseSchema, "Announcement not found"),
127
+ },
128
+ }),
129
+ v("param", IdParamSchema),
130
+ async (c) => respond(c, withMessage(announcements.admin.remove({ id: c.req.valid("param").id }), "Announcement deleted.")),
131
+ );
@@ -11,6 +11,7 @@ export const LoginSchema = z.object({
11
11
  export const EmailLoginSchema = z.object({
12
12
  email: z.email(),
13
13
  acceptedAgb: z.literal(true),
14
+ redirectTo: z.string().max(2048).optional(),
14
15
  });
15
16
 
16
17
  export const VerifyTokenSchema = z.object({
@@ -18,6 +19,24 @@ export const VerifyTokenSchema = z.object({
18
19
  acceptedAgb: z.literal(true),
19
20
  });
20
21
 
22
+ export const PasswordResetRequestSchema = z.object({
23
+ email: z.email(),
24
+ acceptedAgb: z.literal(true),
25
+ redirectTo: z.string().max(2048).optional(),
26
+ });
27
+
28
+ export const PasswordResetCompleteSchema = z
29
+ .object({
30
+ token: z.uuid(),
31
+ newPassword: z.string().min(8),
32
+ confirmPassword: z.string().min(1),
33
+ acceptedAgb: z.literal(true),
34
+ })
35
+ .refine((data) => data.newPassword === data.confirmPassword, {
36
+ message: "Passwords do not match",
37
+ path: ["confirmPassword"],
38
+ });
39
+
21
40
  export const AdminLoginSchema = z.object({
22
41
  token: z.string().min(1),
23
42
  });
@@ -26,3 +45,8 @@ export const AuthResponseSchema = z.object({
26
45
  session_token: z.string(),
27
46
  user: UserSchema,
28
47
  });
48
+
49
+ export const VerifyPasskeyAuthenticationSchema = z.object({
50
+ response: z.unknown(),
51
+ acceptedAgb: z.literal(true),
52
+ });
package/src/api/auth.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import { Hono, type Context } from "hono";
2
2
  import { describeRoute } from "hono-openapi";
3
+ import { z } from "zod";
3
4
  import { v } from "../server";
4
5
  import { jsonResponse } from "../server";
5
6
  import { auth, type AuthContext } from "../server";
6
7
  import { rateLimit } from "../server";
7
- import { authFlows, accounts, getFreeIpaConfig, logger } from "../services";
8
+ import { respond } from "../server";
9
+ import { authFlows, accounts, getFreeIpaConfig, logger, webauthn } from "../services";
8
10
  import { sql } from "bun";
9
11
  import { env } from "../config";
10
12
  import { ChangeExpiredPasswordSchema } from "../contracts";
@@ -14,8 +16,11 @@ import {
14
16
  LoginSchema,
15
17
  EmailLoginSchema,
16
18
  VerifyTokenSchema,
19
+ PasswordResetRequestSchema,
20
+ PasswordResetCompleteSchema,
17
21
  AdminLoginSchema,
18
22
  AuthResponseSchema,
23
+ VerifyPasskeyAuthenticationSchema,
19
24
  } from "./auth/schemas";
20
25
  import { ErrorResponseSchema, MessageResponseSchema } from "../contracts";
21
26
 
@@ -56,7 +61,7 @@ const app = new Hono<AuthContext>()
56
61
  }
57
62
 
58
63
  // Store minimal session in Redis
59
- const sessionToken = await auth.session.create(c, loginResult.userId, loginResult.ipaSession);
64
+ const sessionToken = await auth.session.create(c, loginResult.userId);
60
65
 
61
66
  log.info("Login successful", { uid: username });
62
67
  return c.json({
@@ -65,13 +70,51 @@ const app = new Hono<AuthContext>()
65
70
  });
66
71
  },
67
72
  )
73
+ .post(
74
+ "/passkeys/authentication/start",
75
+ describeRoute({
76
+ tags: ["Auth"],
77
+ summary: "Start passkey login",
78
+ description: "Create WebAuthn authentication options for passkey sign-in.",
79
+ responses: {
80
+ 200: jsonResponse(z.unknown(), "Passkey authentication options"),
81
+ 400: jsonResponse(ErrorResponseSchema, "Passkey login is not available"),
82
+ },
83
+ }),
84
+ async (c) => respond(c, webauthn.beginAuthentication()),
85
+ )
86
+ .post(
87
+ "/passkeys/authentication/verify",
88
+ describeRoute({
89
+ tags: ["Auth"],
90
+ summary: "Verify passkey login",
91
+ description: "Verify a WebAuthn authentication response and create a normal Cloud session.",
92
+ responses: {
93
+ 200: jsonResponse(AuthResponseSchema, "Passkey login successful"),
94
+ 401: jsonResponse(ErrorResponseSchema, "Passkey verification failed"),
95
+ },
96
+ }),
97
+ v("json", VerifyPasskeyAuthenticationSchema),
98
+ async (c) => {
99
+ const result = await webauthn.finishAuthentication({
100
+ response: c.req.valid("json").response as never,
101
+ });
102
+ if (!result.ok) {
103
+ const status = result.error.status;
104
+ return c.json({ message: result.error.message, code: result.error.code }, status as 400 | 401 | 403 | 404 | 409 | 500);
105
+ }
106
+
107
+ const sessionToken = await auth.session.create(c, result.data.user.id);
108
+ log.info("Passkey login successful", { uid: result.data.user.uid });
109
+ return c.json({ session_token: sessionToken, user: result.data.user });
110
+ },
111
+ )
68
112
  .post(
69
113
  "/logout",
70
114
  describeRoute({
71
115
  tags: ["Auth"],
72
116
  summary: "Logout",
73
- description:
74
- "Idempotent: clears the session cookie and deletes the session key if present. No authentication required — logout must always succeed.",
117
+ description: "Idempotent: clears the session cookie and deletes the session key if present. No authentication required — logout must always succeed.",
75
118
  responses: {
76
119
  200: jsonResponse(MessageResponseSchema, "Session invalidated"),
77
120
  },
@@ -116,7 +159,7 @@ const app = new Hono<AuthContext>()
116
159
  return jsonError(c, changeResult.message, changeResult.status === 401 ? 401 : 400);
117
160
  }
118
161
 
119
- const sessionToken = await auth.session.create(c, changeResult.userId, changeResult.ipaSession);
162
+ const sessionToken = await auth.session.create(c, changeResult.userId);
120
163
 
121
164
  log.info("Password changed via expired flow", { uid: username });
122
165
  return c.json({ session_token: sessionToken, user: changeResult.user });
@@ -135,9 +178,12 @@ const app = new Hono<AuthContext>()
135
178
  }),
136
179
  v("json", EmailLoginSchema),
137
180
  async (c) => {
138
- const { email } = c.req.valid("json");
181
+ const { email, redirectTo } = c.req.valid("json");
139
182
 
140
- const requestResult = await authFlows.magicLink.request({ email });
183
+ const requestResult = await authFlows.magicLink.request({
184
+ email,
185
+ redirectTo,
186
+ });
141
187
  if (!requestResult.ok) {
142
188
  return c.json({ message: requestResult.message }, requestResult.status);
143
189
  }
@@ -171,15 +217,72 @@ const app = new Hono<AuthContext>()
171
217
  }
172
218
 
173
219
  // Create session (no IPA session for email-only users)
174
- const sessionToken = await auth.session.create(c, verifyResult.userId, null);
220
+ const sessionToken = await auth.session.create(c, verifyResult.userId);
175
221
 
176
222
  if (verifyResult.createdGuest) {
177
- log.info("Guest user created", { email: verifyResult.email, uid: verifyResult.user.uid });
223
+ log.info("Guest user created", {
224
+ email: verifyResult.email,
225
+ uid: verifyResult.user.uid,
226
+ });
178
227
  }
179
228
  log.info("Token verified", { email: verifyResult.email });
180
229
  return c.json({ session_token: sessionToken, user: verifyResult.user });
181
230
  },
182
231
  )
232
+ .post(
233
+ "/password-reset/request",
234
+ describeRoute({
235
+ tags: ["Auth"],
236
+ summary: "Request password reset",
237
+ description: "Request a one-time password reset email for an IPA-backed account. The response is always generic to avoid account enumeration.",
238
+ responses: {
239
+ 200: jsonResponse(MessageResponseSchema, "Request accepted"),
240
+ },
241
+ }),
242
+ v("json", PasswordResetRequestSchema),
243
+ async (c) => {
244
+ const { email, redirectTo } = c.req.valid("json");
245
+
246
+ const result = await authFlows.passwordReset.request({
247
+ email,
248
+ redirectTo,
249
+ });
250
+ return c.json({ message: result.message });
251
+ },
252
+ )
253
+ .post(
254
+ "/password-reset/complete",
255
+ describeRoute({
256
+ tags: ["Auth"],
257
+ summary: "Complete password reset",
258
+ description: "Set a new password using a one-time reset token. Creates a normal Cloud session only after the password was changed successfully.",
259
+ responses: {
260
+ 200: jsonResponse(AuthResponseSchema, "Password reset completed and session created"),
261
+ 400: jsonResponse(ErrorResponseSchema, "Password reset failed"),
262
+ 401: jsonResponse(ErrorResponseSchema, "Invalid or expired reset token"),
263
+ },
264
+ }),
265
+ v("json", PasswordResetCompleteSchema),
266
+ async (c) => {
267
+ const { token, newPassword } = c.req.valid("json");
268
+
269
+ const result = await authFlows.passwordReset.complete({
270
+ token,
271
+ newPassword,
272
+ });
273
+ if (!result.ok) {
274
+ if (result.reason === "policy_failed") {
275
+ return c.json({ message: result.message }, 400);
276
+ }
277
+ const status = result.status >= 500 ? 500 : result.status === 401 ? 401 : 400;
278
+ return jsonError(c, result.message, status);
279
+ }
280
+
281
+ const sessionToken = await auth.session.create(c, result.userId);
282
+ log.info("Password reset completed", { uid: result.user.uid });
283
+ return c.json({ user: result.user, session_token: sessionToken });
284
+ },
285
+ )
183
286
  .post(
184
287
  "/admin-login",
185
288
  describeRoute({
@@ -221,7 +324,7 @@ const app = new Hono<AuthContext>()
221
324
  const user = await accounts.users.get({ uid: "admin" });
222
325
  if (!user) return jsonError(c, "Failed to resolve admin user.", 500);
223
326
 
224
- const sessionToken = await auth.session.create(c, user.id, null);
327
+ const sessionToken = await auth.session.create(c, user.id);
225
328
  log.info("Admin login successful");
226
329
  return c.json({ session_token: sessionToken, user });
227
330
  },
package/src/api/index.ts CHANGED
@@ -8,17 +8,19 @@
8
8
  * Apps that need a typed client to these routes import from
9
9
  * `@valentinkolb/cloud/clients/core`. The client and the routes share their
10
10
  * type via `CoreApiType` below.
11
+ *
12
+ * The OpenAPI spec for these routes is generated by `defineApp` (driven by
13
+ * core's `openapi: "/api/openapi.json"` opt-in) — this file no longer mounts
14
+ * any docs UI; the api-docs aggregator at `/app/api-docs` is the only consumer.
11
15
  */
12
16
  import { Hono } from "hono";
13
- import { Scalar } from "@scalar/hono-api-reference";
14
- import { generateSpecs } from "hono-openapi";
15
17
  import { prettyJSON } from "hono/pretty-json";
16
- import { createMarkdownFromOpenApi } from "@scalar/openapi-to-markdown";
17
- import { openApiMeta } from "../server";
18
+ import accountsEntitiesRoutes from "./accounts-entities";
19
+ import adminCoreSettingsRoutes from "./admin-core-settings";
20
+ import adminLifecycleRoutes from "./admin-lifecycle";
21
+ import { adminAnnouncementRoutes, announcementRoutes } from "./announcements";
18
22
  import authRoutes from "./auth";
19
23
  import meRoutes from "./me";
20
- import adminLifecycleRoutes from "./admin-lifecycle";
21
- import accountsEntitiesRoutes from "./accounts-entities";
22
24
  import { createSearchRoutes } from "./search";
23
25
 
24
26
  /**
@@ -33,6 +35,9 @@ const buildCoreApi = () => {
33
35
  .route("/auth", authRoutes)
34
36
  .route("/me", meRoutes)
35
37
  .route("/accounts", accountsEntitiesRoutes)
38
+ .route("/announcements", announcementRoutes)
39
+ .route("/admin/core/announcements", adminAnnouncementRoutes)
40
+ .route("/admin/core/settings", adminCoreSettingsRoutes)
36
41
  .route("/admin/lifecycle", adminLifecycleRoutes)
37
42
  .route("/", searchRoutes);
38
43
  };
@@ -41,26 +46,11 @@ const buildCoreApi = () => {
41
46
  export type CoreApiType = ReturnType<typeof buildCoreApi>;
42
47
 
43
48
  /**
44
- * Build the core router and accompanying OpenAPI assets. The core-app calls
45
- * this and mounts the returned router under `/api`.
49
+ * Build the core router. The core-app calls this and mounts the returned
50
+ * router under `/api`.
46
51
  */
47
- export const createCoreApiRouter = async () => {
52
+ export const createCoreApiRouter = () => {
48
53
  const api = buildCoreApi();
49
-
50
- const spec = await generateSpecs(api, openApiMeta);
51
- const llmsTxt = await createMarkdownFromOpenApi(JSON.stringify(spec));
52
-
53
- api.get("/openapi.json", (c) => c.json(spec));
54
- api.get(
55
- "/docs",
56
- Scalar({
57
- theme: "saturn",
58
- url: "/api/openapi.json",
59
- hideClientButton: true,
60
- }),
61
- );
62
-
63
54
  api.all("/*", (c) => c.json({ message: "API route not found" }, 404));
64
-
65
- return { api, llmsTxt };
55
+ return { api };
66
56
  };