@valentinkolb/cloud 0.4.0 → 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 (193) hide show
  1. package/package.json +18 -6
  2. package/scripts/preload.ts +78 -23
  3. package/src/_internal/define-app.ts +53 -46
  4. package/src/api/accounts-entities.ts +4 -0
  5. package/src/api/admin-core-settings.ts +98 -0
  6. package/src/api/announcements.ts +131 -0
  7. package/src/api/auth/schemas.ts +24 -0
  8. package/src/api/auth.ts +113 -10
  9. package/src/api/index.ts +7 -2
  10. package/src/api/me.ts +203 -14
  11. package/src/api/search/schemas.ts +1 -0
  12. package/src/api/search.ts +62 -8
  13. package/src/config/ssr.ts +2 -9
  14. package/src/contracts/announcements.test.ts +37 -0
  15. package/src/contracts/announcements.ts +121 -0
  16. package/src/contracts/app.ts +2 -0
  17. package/src/contracts/index.ts +3 -2
  18. package/src/contracts/registry.ts +2 -0
  19. package/src/contracts/shared.ts +108 -1
  20. package/src/desktop/index.ts +704 -0
  21. package/src/desktop/solid.tsx +938 -0
  22. package/src/server/api/index.ts +1 -1
  23. package/src/server/api/respond.ts +50 -10
  24. package/src/server/index.ts +44 -38
  25. package/src/server/middleware/auth.ts +98 -9
  26. package/src/server/middleware/index.ts +2 -1
  27. package/src/server/middleware/settings.ts +26 -0
  28. package/src/server/services/access.test.ts +197 -0
  29. package/src/server/services/access.ts +254 -6
  30. package/src/server/services/index.ts +14 -11
  31. package/src/server/services/pagination.ts +22 -0
  32. package/src/server/time.ts +45 -0
  33. package/src/services/account-lifecycle/index.ts +142 -18
  34. package/src/services/accounts/app.ts +658 -170
  35. package/src/services/accounts/authz.test.ts +77 -0
  36. package/src/services/accounts/authz.ts +22 -0
  37. package/src/services/accounts/entities.ts +84 -5
  38. package/src/services/accounts/groups.ts +30 -24
  39. package/src/services/accounts/model.test.ts +30 -0
  40. package/src/services/accounts/switching.test.ts +14 -0
  41. package/src/services/accounts/switching.ts +15 -6
  42. package/src/services/accounts/users.ts +75 -52
  43. package/src/services/announcements/index.test.ts +32 -0
  44. package/src/services/announcements/index.ts +224 -0
  45. package/src/services/audit/index.test.ts +84 -0
  46. package/src/services/audit/index.ts +431 -0
  47. package/src/services/auth-flows/index.ts +9 -2
  48. package/src/services/auth-flows/ipa.ts +0 -2
  49. package/src/services/auth-flows/magic-link.ts +3 -2
  50. package/src/services/auth-flows/password-reset.ts +284 -0
  51. package/src/services/auth-flows/proxy-return.test.ts +24 -0
  52. package/src/services/auth-flows/proxy-return.ts +49 -0
  53. package/src/services/gateway.ts +162 -0
  54. package/src/services/index.ts +44 -2
  55. package/src/services/ipa/effective-groups.test.ts +33 -0
  56. package/src/services/ipa/effective-groups.ts +70 -0
  57. package/src/services/ipa/profile.ts +45 -3
  58. package/src/services/ipa/search.ts +3 -5
  59. package/src/services/ipa/service-account.ts +15 -0
  60. package/src/services/ipa/sync-planning.test.ts +32 -0
  61. package/src/services/ipa/sync-planning.ts +22 -0
  62. package/src/services/ipa/sync.ts +110 -38
  63. package/src/services/oauth-tokens.ts +104 -0
  64. package/src/services/postgres.ts +21 -6
  65. package/src/services/providers/local/auth.test.ts +22 -0
  66. package/src/services/providers/local/auth.ts +46 -3
  67. package/src/services/secrets.ts +10 -0
  68. package/src/services/service-account-credentials.test.ts +210 -0
  69. package/src/services/service-account-credentials.ts +715 -0
  70. package/src/services/service-accounts.ts +188 -0
  71. package/src/services/session/index.ts +7 -8
  72. package/src/services/settings/app.ts +4 -20
  73. package/src/services/settings/defaults.ts +64 -22
  74. package/src/services/settings/store.ts +47 -0
  75. package/src/services/weather/forecast.ts +40 -7
  76. package/src/services/webauthn.test.ts +36 -0
  77. package/src/services/webauthn.ts +384 -0
  78. package/src/shared/icons.ts +391 -100
  79. package/src/shared/index.ts +7 -0
  80. package/src/shared/markdown/extensions/code.ts +38 -1
  81. package/src/shared/markdown/extensions/images.ts +39 -3
  82. package/src/shared/markdown/extensions/info-blocks.ts +5 -5
  83. package/src/shared/markdown/extensions/mark.ts +48 -0
  84. package/src/shared/markdown/extensions/sub-sup.ts +60 -0
  85. package/src/shared/markdown/extensions/tables.ts +79 -58
  86. package/src/shared/markdown/formula.test.ts +1089 -0
  87. package/src/shared/markdown/formula.ts +1187 -0
  88. package/src/shared/markdown/index.ts +76 -2
  89. package/src/shared/mock-cover.ts +130 -0
  90. package/src/shared/redirect.test.ts +49 -0
  91. package/src/shared/redirect.ts +52 -0
  92. package/src/shared/theme.test.ts +24 -0
  93. package/src/shared/theme.ts +68 -0
  94. package/src/shared/time.ts +13 -0
  95. package/src/ssr/AdminLayout.tsx +7 -3
  96. package/src/ssr/AdminSidebar.tsx +115 -49
  97. package/src/ssr/AppLaunchpad.island.tsx +176 -0
  98. package/src/ssr/Footer.island.tsx +3 -8
  99. package/src/ssr/GlobalAnnouncements.island.tsx +141 -0
  100. package/src/ssr/GlobalSearchDialog.tsx +545 -117
  101. package/src/ssr/HotkeysHelpRail.island.tsx +3 -70
  102. package/src/ssr/Layout.tsx +74 -66
  103. package/src/ssr/LayoutBreadcrumbs.island.tsx +44 -0
  104. package/src/ssr/LayoutHelp.tsx +266 -0
  105. package/src/ssr/NavMenu.island.tsx +0 -39
  106. package/src/ssr/ThemeToggleRail.island.tsx +3 -3
  107. package/src/ssr/TimezoneCookie.island.tsx +23 -0
  108. package/src/ssr/islands/index.ts +13 -0
  109. package/src/styles/base-popover.css +5 -2
  110. package/src/styles/effects.css +87 -6
  111. package/src/styles/global.css +146 -9
  112. package/src/styles/input.css +3 -1
  113. package/src/styles/utilities-buttons.css +133 -27
  114. package/src/styles/utilities-code-display.css +67 -0
  115. package/src/styles/utilities-completion.css +223 -0
  116. package/src/styles/utilities-detail.css +73 -0
  117. package/src/styles/utilities-feedback.css +16 -15
  118. package/src/styles/utilities-layout.css +42 -2
  119. package/src/styles/utilities-markdown-editor.css +472 -0
  120. package/src/styles/utilities-navigation.css +63 -8
  121. package/src/styles/utilities-script.css +84 -0
  122. package/src/styles/utilities-table-tile.css +229 -0
  123. package/src/types/ambient.d.ts +9 -0
  124. package/src/ui/completion/behaviors.test.ts +95 -0
  125. package/src/ui/completion/behaviors.ts +205 -0
  126. package/src/ui/completion/engine.ts +368 -0
  127. package/src/ui/completion/index.ts +40 -0
  128. package/src/ui/completion/overlay.ts +92 -0
  129. package/src/ui/dialog-core.ts +173 -45
  130. package/src/ui/filter/FilterChip.tsx +42 -40
  131. package/src/ui/index.ts +11 -12
  132. package/src/ui/input/AutocompleteEditor.tsx +656 -0
  133. package/src/ui/input/CheckboxCard.tsx +91 -0
  134. package/src/ui/input/Combobox.tsx +375 -0
  135. package/src/ui/input/DatePicker.tsx +846 -0
  136. package/src/ui/input/DateTimeInput.tsx +29 -4
  137. package/src/ui/input/FileDropzone.tsx +116 -0
  138. package/src/ui/input/IconInput.tsx +116 -0
  139. package/src/ui/input/ImageInput.tsx +19 -2
  140. package/src/ui/input/MultiSelectInput.tsx +448 -0
  141. package/src/ui/input/NumberInput.tsx +417 -61
  142. package/src/ui/input/SegmentedControl.tsx +2 -2
  143. package/src/ui/input/Select.tsx +172 -10
  144. package/src/ui/input/Slider.tsx +3 -4
  145. package/src/ui/input/Switch.tsx +3 -2
  146. package/src/ui/input/TemplateEditor.tsx +212 -0
  147. package/src/ui/input/TextInput.tsx +144 -13
  148. package/src/ui/input/index.ts +53 -8
  149. package/src/ui/input/markdown/MarkdownEditor.tsx +774 -0
  150. package/src/ui/input/markdown/Toolbar.tsx +90 -0
  151. package/src/ui/input/markdown/actions.ts +233 -0
  152. package/src/ui/input/markdown/active-formats.ts +94 -0
  153. package/src/ui/input/markdown/behaviors.ts +193 -0
  154. package/src/ui/input/markdown/code-zone.ts +23 -0
  155. package/src/ui/input/markdown/highlight.ts +316 -0
  156. package/src/ui/layout.ts +22 -0
  157. package/src/ui/misc/AppOverview.tsx +105 -0
  158. package/src/ui/misc/AppWorkspace.tsx +607 -0
  159. package/src/ui/misc/Calendar.tsx +1291 -0
  160. package/src/ui/misc/Chart.tsx +162 -0
  161. package/src/ui/misc/CodeDisplay.tsx +54 -0
  162. package/src/ui/misc/ContextMenu.tsx +2 -2
  163. package/src/ui/misc/DataTable.tsx +269 -0
  164. package/src/ui/misc/DockWorkspace.tsx +425 -0
  165. package/src/ui/misc/Docs.tsx +153 -0
  166. package/src/ui/misc/Dropdown.tsx +2 -2
  167. package/src/ui/misc/EntitySearch.tsx +260 -129
  168. package/src/ui/misc/LinkCard.tsx +14 -2
  169. package/src/ui/misc/LogEntriesTable.tsx +34 -31
  170. package/src/ui/misc/Pagination.tsx +31 -12
  171. package/src/ui/misc/PanelDialog.tsx +109 -0
  172. package/src/ui/misc/Panes.tsx +873 -0
  173. package/src/ui/misc/PermissionEditor.tsx +358 -262
  174. package/src/ui/misc/Placeholder.tsx +40 -0
  175. package/src/ui/misc/ProgressBar.tsx +1 -1
  176. package/src/ui/misc/ResourceApiKeys.tsx +260 -0
  177. package/src/ui/misc/SettingsModal.tsx +150 -0
  178. package/src/ui/misc/StatCell.tsx +182 -40
  179. package/src/ui/misc/StatGrid.tsx +149 -0
  180. package/src/ui/misc/StructuredDataPreview.tsx +107 -0
  181. package/src/ui/misc/code-highlight.ts +213 -0
  182. package/src/ui/misc/index.ts +93 -12
  183. package/src/ui/prompts.tsx +362 -312
  184. package/src/ui/toast.ts +384 -0
  185. package/src/ui/widgets/Widget.tsx +12 -4
  186. package/src/ssr/MoreAppsDropdown.island.tsx +0 -61
  187. package/src/ui/ipa/GroupView.tsx +0 -36
  188. package/src/ui/ipa/LoginBtn.tsx +0 -16
  189. package/src/ui/ipa/UserView.tsx +0 -58
  190. package/src/ui/ipa/index.ts +0 -4
  191. package/src/ui/navigation.ts +0 -32
  192. package/src/ui/sidebar.tsx +0 -468
  193. /package/src/ui/{ipa → misc}/Avatar.tsx +0 -0
package/src/config/ssr.ts CHANGED
@@ -7,6 +7,7 @@ import { createSSRHandler } from "@valentinkolb/ssr/hono";
7
7
  import { env } from "./env";
8
8
  import { dirname, resolve } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
+ import { themeBootstrapScript } from "../shared/theme";
10
11
 
11
12
  /** Cache-busting version stamp — changes on every server start / rebuild. */
12
13
  const v = Date.now();
@@ -37,15 +38,7 @@ export const { config, plugin, html } = createConfig<PageOptions>({
37
38
  <link rel="icon" href="/branding/favicon">
38
39
  <link rel="stylesheet" href="/public/build.css?v=${v}">
39
40
  <link rel="stylesheet" href="/public/katex.css?v=${v}">
40
- <script>
41
- (function() {
42
- var el = document.documentElement;
43
- if (!el.hasAttribute('data-theme-fixed')) {
44
- var theme = document.cookie.match(/theme=([^;]+)/)?.[1] || 'light';
45
- el.classList.add(theme);
46
- }
47
- })();
48
- </script>
41
+ <script>${themeBootstrapScript}</script>
49
42
  </head>
50
43
  <body>
51
44
  ${body}
@@ -0,0 +1,37 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ ANNOUNCEMENTS_COOKIE,
4
+ mergeAnnouncementCookieState,
5
+ parseAnnouncementCookieHeader,
6
+ parseAnnouncementCookieValue,
7
+ serializeAnnouncementCookieState,
8
+ } from "./announcements";
9
+
10
+ describe("announcement cookie state", () => {
11
+ test("returns defaults for empty or malformed cookies", () => {
12
+ expect(parseAnnouncementCookieValue(null)).toEqual({ seenAnnouncementVersion: 0, dismissedBannerVersions: [] });
13
+ expect(parseAnnouncementCookieValue("%7Bbad")).toEqual({ seenAnnouncementVersion: 0, dismissedBannerVersions: [] });
14
+ expect(parseAnnouncementCookieHeader("theme=dark")).toEqual({ seenAnnouncementVersion: 0, dismissedBannerVersions: [] });
15
+ });
16
+
17
+ test("parses state from a cookie header", () => {
18
+ const value = serializeAnnouncementCookieState({
19
+ seenAnnouncementVersion: 12,
20
+ dismissedBannerVersions: [3, 2, 3],
21
+ });
22
+
23
+ expect(parseAnnouncementCookieHeader(`theme=dark; ${ANNOUNCEMENTS_COOKIE}=${value}; other=1`)).toEqual({
24
+ seenAnnouncementVersion: 12,
25
+ dismissedBannerVersions: [3, 2],
26
+ });
27
+ });
28
+
29
+ test("merges state monotonically", () => {
30
+ expect(
31
+ mergeAnnouncementCookieState(
32
+ { seenAnnouncementVersion: 10, dismissedBannerVersions: [4] },
33
+ { seenAnnouncementVersion: 8, dismissedBannerVersions: [5, 4] },
34
+ ),
35
+ ).toEqual({ seenAnnouncementVersion: 10, dismissedBannerVersions: [5, 4] });
36
+ });
37
+ });
@@ -0,0 +1,121 @@
1
+ import { z } from "zod";
2
+
3
+ export const ANNOUNCEMENTS_COOKIE = "cloud_announcements";
4
+ export const ANNOUNCEMENTS_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 365;
5
+ export const MAX_DISMISSED_BANNER_VERSIONS = 50;
6
+
7
+ export const AnnouncementKindSchema = z.enum(["announcement", "banner"]);
8
+ export type AnnouncementKind = z.infer<typeof AnnouncementKindSchema>;
9
+
10
+ export const AnnouncementToneSchema = z.enum(["info", "success", "warning", "danger"]);
11
+ export type AnnouncementTone = z.infer<typeof AnnouncementToneSchema>;
12
+
13
+ export const AnnouncementCookieStateSchema = z.object({
14
+ seenAnnouncementVersion: z.number().int().nonnegative().default(0),
15
+ dismissedBannerVersions: z.array(z.number().int().positive()).default([]),
16
+ });
17
+ export type AnnouncementCookieState = z.infer<typeof AnnouncementCookieStateSchema>;
18
+
19
+ export const DEFAULT_ANNOUNCEMENT_COOKIE_STATE: AnnouncementCookieState = {
20
+ seenAnnouncementVersion: 0,
21
+ dismissedBannerVersions: [],
22
+ };
23
+
24
+ export const AnnouncementEntrySchema = z.object({
25
+ id: z.uuid(),
26
+ version: z.number().int().positive(),
27
+ kind: AnnouncementKindSchema,
28
+ title: z.string(),
29
+ body: z.string(),
30
+ tone: AnnouncementToneSchema,
31
+ publishedAt: z.string().datetime(),
32
+ expiresAt: z.string().datetime().nullable(),
33
+ createdAt: z.string().datetime(),
34
+ updatedAt: z.string().datetime(),
35
+ createdBy: z.uuid().nullable(),
36
+ updatedBy: z.uuid().nullable(),
37
+ });
38
+ export type AnnouncementEntry = z.infer<typeof AnnouncementEntrySchema>;
39
+
40
+ export const AnnouncementDisplayEntrySchema = AnnouncementEntrySchema.omit({ body: true }).extend({
41
+ bodyHtml: z.string(),
42
+ });
43
+ export type AnnouncementDisplayEntry = z.infer<typeof AnnouncementDisplayEntrySchema>;
44
+
45
+ const DatetimeInputSchema = z.string().datetime();
46
+ const NullableDatetimeInputSchema = z.string().datetime().nullable();
47
+
48
+ export const CreateAnnouncementSchema = z.object({
49
+ kind: AnnouncementKindSchema,
50
+ title: z.string().trim().min(1).max(180),
51
+ body: z.string().trim().min(1).max(20_000),
52
+ tone: AnnouncementToneSchema.default("info"),
53
+ publishedAt: DatetimeInputSchema.optional(),
54
+ expiresAt: NullableDatetimeInputSchema.optional(),
55
+ });
56
+ export type CreateAnnouncement = z.infer<typeof CreateAnnouncementSchema>;
57
+
58
+ export const UpdateAnnouncementSchema = z
59
+ .object({
60
+ kind: AnnouncementKindSchema.optional(),
61
+ title: z.string().trim().min(1).max(180).optional(),
62
+ body: z.string().trim().min(1).max(20_000).optional(),
63
+ tone: AnnouncementToneSchema.optional(),
64
+ publishedAt: DatetimeInputSchema.optional(),
65
+ expiresAt: NullableDatetimeInputSchema.optional(),
66
+ })
67
+ .refine((value) => Object.keys(value).length > 0, "Provide at least one field to update.");
68
+ export type UpdateAnnouncement = z.infer<typeof UpdateAnnouncementSchema>;
69
+
70
+ export const AnnouncementListResponseSchema = z.object({
71
+ items: z.array(AnnouncementEntrySchema),
72
+ });
73
+ export type AnnouncementListResponse = z.infer<typeof AnnouncementListResponseSchema>;
74
+
75
+ export const ActiveAnnouncementsResponseSchema = z.object({
76
+ banners: z.array(AnnouncementDisplayEntrySchema),
77
+ announcements: z.array(AnnouncementDisplayEntrySchema),
78
+ latestAnnouncementVersion: z.number().int().nonnegative(),
79
+ });
80
+ export type ActiveAnnouncementsResponse = z.infer<typeof ActiveAnnouncementsResponseSchema>;
81
+
82
+ const normalizeCookieState = (value: unknown): AnnouncementCookieState => {
83
+ const parsed = AnnouncementCookieStateSchema.safeParse(value);
84
+ if (!parsed.success) return DEFAULT_ANNOUNCEMENT_COOKIE_STATE;
85
+ const dismissed = [...new Set(parsed.data.dismissedBannerVersions)]
86
+ .filter((version) => Number.isInteger(version) && version > 0)
87
+ .sort((a, b) => b - a)
88
+ .slice(0, MAX_DISMISSED_BANNER_VERSIONS);
89
+
90
+ return {
91
+ seenAnnouncementVersion: Math.max(0, parsed.data.seenAnnouncementVersion),
92
+ dismissedBannerVersions: dismissed,
93
+ };
94
+ };
95
+
96
+ export const parseAnnouncementCookieValue = (value: string | null | undefined): AnnouncementCookieState => {
97
+ if (!value) return DEFAULT_ANNOUNCEMENT_COOKIE_STATE;
98
+ try {
99
+ return normalizeCookieState(JSON.parse(decodeURIComponent(value)));
100
+ } catch {
101
+ return DEFAULT_ANNOUNCEMENT_COOKIE_STATE;
102
+ }
103
+ };
104
+
105
+ export const parseAnnouncementCookieHeader = (cookieHeader: string | null | undefined): AnnouncementCookieState => {
106
+ if (!cookieHeader) return DEFAULT_ANNOUNCEMENT_COOKIE_STATE;
107
+ const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${ANNOUNCEMENTS_COOKIE}=([^;]+)`));
108
+ return parseAnnouncementCookieValue(match?.[1]);
109
+ };
110
+
111
+ export const serializeAnnouncementCookieState = (state: AnnouncementCookieState): string =>
112
+ encodeURIComponent(JSON.stringify(normalizeCookieState(state)));
113
+
114
+ export const mergeAnnouncementCookieState = (
115
+ current: AnnouncementCookieState,
116
+ patch: Partial<AnnouncementCookieState>,
117
+ ): AnnouncementCookieState =>
118
+ normalizeCookieState({
119
+ seenAnnouncementVersion: Math.max(current.seenAnnouncementVersion, patch.seenAnnouncementVersion ?? 0),
120
+ dismissedBannerVersions: [...current.dismissedBannerVersions, ...(patch.dismissedBannerVersions ?? [])],
121
+ });
@@ -43,6 +43,8 @@ export type AppMeta = {
43
43
  * silently skip rendering for the current user.
44
44
  */
45
45
  widgets?: WidgetEndpoint[];
46
+ /** Setting keys declared by this app. Used by admin tooling to protect active app-owned settings. */
47
+ settingKeys?: readonly string[];
46
48
  /** Gateway-relative URL where this app's OpenAPI JSON is served, or undefined. */
47
49
  openapi?: string;
48
50
  };
@@ -1,5 +1,6 @@
1
+ export * from "./announcements";
1
2
  export * from "./app";
2
- export * from "./shared";
3
- export * from "./registry";
4
3
  export * from "./profile";
4
+ export * from "./registry";
5
+ export * from "./shared";
5
6
  export * from "./widgets";
@@ -47,6 +47,8 @@ export type AppRegistryEntry = {
47
47
  search?: AppRegistrySearch;
48
48
  legalLinks?: AppRegistryLegalLink[];
49
49
  widgets?: AppRegistryWidget[];
50
+ /** Setting keys declared by this app. Used by admin tooling to avoid treating live app-owned settings as legacy. */
51
+ settingKeys?: readonly string[];
50
52
  /** Gateway-relative URL where this app serves its OpenAPI JSON spec. */
51
53
  openapi?: string;
52
54
  };
@@ -93,7 +93,7 @@ export const BaseGroupSchema = z.object({
93
93
  });
94
94
  export type BaseGroup = z.infer<typeof BaseGroupSchema>;
95
95
 
96
- export const EntityKindSchema = z.enum(["user", "group"]);
96
+ export const EntityKindSchema = z.enum(["user", "group", "service_account"]);
97
97
  export type EntityKind = z.infer<typeof EntityKindSchema>;
98
98
 
99
99
  export const EntityRelationSchema = z.object({
@@ -112,6 +112,22 @@ export const EntityListItemSchema = z.discriminatedUnion("kind", [
112
112
  group: BaseGroupSchema,
113
113
  relation: EntityRelationSchema.optional(),
114
114
  }),
115
+ z.object({
116
+ kind: z.literal("service_account"),
117
+ serviceAccount: z.object({
118
+ id: z.uuid(),
119
+ name: z.string(),
120
+ kind: z.enum(["user_delegated", "resource_bound"]),
121
+ status: z.enum(["active", "disabled"]),
122
+ delegatedUserId: z.uuid().nullable(),
123
+ appId: z.string().nullable(),
124
+ resourceType: z.string().nullable(),
125
+ resourceId: z.string().nullable(),
126
+ createdBy: z.uuid().nullable(),
127
+ createdAt: z.string(),
128
+ }),
129
+ relation: EntityRelationSchema.optional(),
130
+ }),
115
131
  ]);
116
132
  export type EntityListItem = z.infer<typeof EntityListItemSchema>;
117
133
 
@@ -181,9 +197,100 @@ export type MutationResult<T = void> = { ok: true; data: T } | { ok: false; erro
181
197
  export const PermissionLevelSchema = z.enum(["none", "read", "write", "admin"]);
182
198
  export type PermissionLevel = z.infer<typeof PermissionLevelSchema>;
183
199
 
200
+ export const ServiceAccountKindSchema = z.enum(["user_delegated", "resource_bound"]);
201
+ export type ServiceAccountKind = z.infer<typeof ServiceAccountKindSchema>;
202
+
203
+ export const ServiceAccountStatusSchema = z.enum(["active", "disabled"]);
204
+ export type ServiceAccountStatus = z.infer<typeof ServiceAccountStatusSchema>;
205
+
206
+ export const ServiceAccountSchema = z.object({
207
+ id: z.uuid(),
208
+ name: z.string(),
209
+ kind: ServiceAccountKindSchema,
210
+ status: ServiceAccountStatusSchema,
211
+ delegatedUserId: z.uuid().nullable(),
212
+ appId: z.string().nullable(),
213
+ resourceType: z.string().nullable(),
214
+ resourceId: z.string().nullable(),
215
+ createdBy: z.uuid().nullable(),
216
+ createdAt: z.string(),
217
+ });
218
+ export type ServiceAccount = z.infer<typeof ServiceAccountSchema>;
219
+
220
+ export const ServiceAccountCredentialStatusSchema = z.enum(["active", "revoked"]);
221
+ export type ServiceAccountCredentialStatus = z.infer<typeof ServiceAccountCredentialStatusSchema>;
222
+
223
+ export const ServiceAccountCredentialSchema = z.object({
224
+ id: z.uuid(),
225
+ serviceAccountId: z.uuid(),
226
+ name: z.string(),
227
+ kind: z.literal("api_token"),
228
+ status: ServiceAccountCredentialStatusSchema,
229
+ tokenPrefix: z.string(),
230
+ scopes: z.array(z.string()),
231
+ expiresAt: z.string().nullable(),
232
+ lastUsedAt: z.string().nullable(),
233
+ createdBy: z.uuid().nullable(),
234
+ createdAt: z.string(),
235
+ revokedAt: z.string().nullable(),
236
+ revokedBy: z.uuid().nullable(),
237
+ });
238
+ export type ServiceAccountCredential = z.infer<typeof ServiceAccountCredentialSchema>;
239
+
240
+ export const CreateUserApiKeySchema = z.object({
241
+ name: z.string().trim().min(1).max(120),
242
+ expiresAt: z.string().datetime().nullable().optional(),
243
+ });
244
+ export type CreateUserApiKey = z.infer<typeof CreateUserApiKeySchema>;
245
+
246
+ export const CreateUserApiKeyResponseSchema = z.object({
247
+ credential: ServiceAccountCredentialSchema,
248
+ token: z.string(),
249
+ });
250
+ export type CreateUserApiKeyResponse = z.infer<typeof CreateUserApiKeyResponseSchema>;
251
+
252
+ export const WebAuthnPasskeySchema = z.object({
253
+ id: z.uuid(),
254
+ userId: z.uuid(),
255
+ name: z.string(),
256
+ transports: z.array(z.string()),
257
+ deviceType: z.string().nullable(),
258
+ backedUp: z.boolean(),
259
+ createdAt: z.string(),
260
+ lastUsedAt: z.string().nullable(),
261
+ });
262
+ export type WebAuthnPasskey = z.infer<typeof WebAuthnPasskeySchema>;
263
+
264
+ export const CreateWebAuthnPasskeySchema = z.object({
265
+ name: z.string().trim().min(1).max(120),
266
+ response: z.unknown(),
267
+ });
268
+ export type CreateWebAuthnPasskey = z.infer<typeof CreateWebAuthnPasskeySchema>;
269
+
270
+ export const ListWebAuthnPasskeysResponseSchema = z.object({
271
+ items: z.array(WebAuthnPasskeySchema),
272
+ });
273
+ export type ListWebAuthnPasskeysResponse = z.infer<typeof ListWebAuthnPasskeysResponseSchema>;
274
+
275
+ export const AccountActivitySchema = z.object({
276
+ id: z.number(),
277
+ createdAt: z.string(),
278
+ action: z.string(),
279
+ label: z.string(),
280
+ outcome: z.enum(["allowed", "denied", "failed"]),
281
+ context: z.string().nullable(),
282
+ });
283
+ export type AccountActivity = z.infer<typeof AccountActivitySchema>;
284
+
285
+ export const AccountActivityListResponseSchema = z.object({
286
+ items: z.array(AccountActivitySchema),
287
+ });
288
+ export type AccountActivityListResponse = z.infer<typeof AccountActivityListResponseSchema>;
289
+
184
290
  export const PrincipalSchema = z.discriminatedUnion("type", [
185
291
  z.object({ type: z.literal("user"), userId: z.uuid() }),
186
292
  z.object({ type: z.literal("group"), groupId: z.uuid() }),
293
+ z.object({ type: z.literal("service_account"), serviceAccountId: z.uuid() }),
187
294
  z.object({ type: z.literal("authenticated") }),
188
295
  z.object({ type: z.literal("public") }),
189
296
  ]);