@valentinkolb/cloud 0.1.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 (196) hide show
  1. package/package.json +69 -0
  2. package/public/logo.svg +1 -0
  3. package/scripts/build.ts +113 -0
  4. package/scripts/preload.ts +73 -0
  5. package/src/_internal/define-app.ts +399 -0
  6. package/src/_internal/heartbeat.ts +33 -0
  7. package/src/_internal/registry.ts +100 -0
  8. package/src/_internal/runtime-context.ts +38 -0
  9. package/src/api/accounts-entities.ts +134 -0
  10. package/src/api/admin-lifecycle.ts +210 -0
  11. package/src/api/auth/schemas.ts +28 -0
  12. package/src/api/auth.ts +230 -0
  13. package/src/api/index.ts +66 -0
  14. package/src/api/me.ts +206 -0
  15. package/src/api/search/schemas.ts +43 -0
  16. package/src/api/search.ts +130 -0
  17. package/src/clients/core.ts +19 -0
  18. package/src/config/env.ts +23 -0
  19. package/src/config/index.ts +6 -0
  20. package/src/config/ssr.ts +58 -0
  21. package/src/contracts/app.ts +140 -0
  22. package/src/contracts/index.ts +5 -0
  23. package/src/contracts/profile.ts +67 -0
  24. package/src/contracts/registry.ts +50 -0
  25. package/src/contracts/settings-types.ts +84 -0
  26. package/src/contracts/shared.ts +258 -0
  27. package/src/contracts/widgets.ts +121 -0
  28. package/src/index.ts +6 -0
  29. package/src/server/api/index.ts +1 -0
  30. package/src/server/api/respond.ts +55 -0
  31. package/src/server/api-client.ts +54 -0
  32. package/src/server/app-context.ts +39 -0
  33. package/src/server/index.ts +62 -0
  34. package/src/server/middleware/auth.ts +168 -0
  35. package/src/server/middleware/index.ts +7 -0
  36. package/src/server/middleware/middleware.ts +47 -0
  37. package/src/server/middleware/openapi.ts +126 -0
  38. package/src/server/middleware/rate-limit.ts +126 -0
  39. package/src/server/middleware/request-logger.ts +41 -0
  40. package/src/server/middleware/validator.ts +35 -0
  41. package/src/server/services/access.ts +294 -0
  42. package/src/server/services/freeipa/client.ts +100 -0
  43. package/src/server/services/freeipa/index.ts +9 -0
  44. package/src/server/services/freeipa/session.ts +78 -0
  45. package/src/server/services/freeipa/tls.ts +48 -0
  46. package/src/server/services/freeipa/util.ts +60 -0
  47. package/src/server/services/geo.ts +154 -0
  48. package/src/server/services/index.ts +28 -0
  49. package/src/server/services/services.ts +13 -0
  50. package/src/services/account-lifecycle/audit.ts +41 -0
  51. package/src/services/account-lifecycle/index.ts +907 -0
  52. package/src/services/account-lifecycle/scheduler.ts +347 -0
  53. package/src/services/account-model.ts +21 -0
  54. package/src/services/accounts/app.ts +966 -0
  55. package/src/services/accounts/authz.ts +22 -0
  56. package/src/services/accounts/base-group.ts +11 -0
  57. package/src/services/accounts/base-user.ts +45 -0
  58. package/src/services/accounts/entities.ts +529 -0
  59. package/src/services/accounts/group-sql.ts +106 -0
  60. package/src/services/accounts/groups.ts +246 -0
  61. package/src/services/accounts/index.ts +14 -0
  62. package/src/services/accounts/ipa-data.ts +64 -0
  63. package/src/services/accounts/lifecycle.ts +2 -0
  64. package/src/services/accounts/local-groups.ts +491 -0
  65. package/src/services/accounts/model.ts +135 -0
  66. package/src/services/accounts/switching.ts +117 -0
  67. package/src/services/accounts/users.ts +714 -0
  68. package/src/services/auth-flows/index.ts +6 -0
  69. package/src/services/auth-flows/ipa.ts +128 -0
  70. package/src/services/auth-flows/magic-link.ts +119 -0
  71. package/src/services/freeipa-config.ts +89 -0
  72. package/src/services/index.ts +46 -0
  73. package/src/services/ipa/auth.ts +122 -0
  74. package/src/services/ipa/groups.ts +684 -0
  75. package/src/services/ipa/guard.ts +17 -0
  76. package/src/services/ipa/index.ts +17 -0
  77. package/src/services/ipa/profile.ts +90 -0
  78. package/src/services/ipa/search.ts +154 -0
  79. package/src/services/ipa/sync.ts +740 -0
  80. package/src/services/ipa/users.ts +794 -0
  81. package/src/services/logging/index.ts +294 -0
  82. package/src/services/notifications/email.ts +123 -0
  83. package/src/services/notifications/index.ts +413 -0
  84. package/src/services/postgres.ts +51 -0
  85. package/src/services/providers/index.ts +27 -0
  86. package/src/services/providers/local/auth.ts +13 -0
  87. package/src/services/providers/local/index.ts +4 -0
  88. package/src/services/providers/local/users.ts +255 -0
  89. package/src/services/session/index.ts +137 -0
  90. package/src/services/settings/api.ts +61 -0
  91. package/src/services/settings/app.ts +101 -0
  92. package/src/services/settings/crypto.ts +69 -0
  93. package/src/services/settings/defaults.ts +824 -0
  94. package/src/services/settings/index.ts +203 -0
  95. package/src/services/settings/namespace.ts +9 -0
  96. package/src/services/settings/snapshot.ts +49 -0
  97. package/src/services/settings/store.ts +179 -0
  98. package/src/services/settings/templates.ts +10 -0
  99. package/src/services/weather/forecast.ts +287 -0
  100. package/src/services/weather/geo.ts +110 -0
  101. package/src/services/weather/index.ts +99 -0
  102. package/src/services/weather/location.ts +24 -0
  103. package/src/services/weather/locations.ts +125 -0
  104. package/src/services/weather/migrate.ts +22 -0
  105. package/src/services/weather/types.ts +61 -0
  106. package/src/services/weather/ui.ts +50 -0
  107. package/src/shared/account-display.ts +17 -0
  108. package/src/shared/account-session.ts +15 -0
  109. package/src/shared/icons.ts +109 -0
  110. package/src/shared/index.ts +10 -0
  111. package/src/shared/markdown/client.ts +130 -0
  112. package/src/shared/markdown/extensions/code.ts +58 -0
  113. package/src/shared/markdown/extensions/images.ts +43 -0
  114. package/src/shared/markdown/extensions/info-blocks.ts +93 -0
  115. package/src/shared/markdown/extensions/katex.ts +120 -0
  116. package/src/shared/markdown/extensions/links.ts +34 -0
  117. package/src/shared/markdown/extensions/tables.ts +88 -0
  118. package/src/shared/markdown/extensions/task-list.ts +53 -0
  119. package/src/shared/markdown/index.ts +97 -0
  120. package/src/shared/markdown/shared.ts +36 -0
  121. package/src/ssr/AdminLayout.tsx +42 -0
  122. package/src/ssr/AdminSidebar.tsx +95 -0
  123. package/src/ssr/Footer.island.tsx +62 -0
  124. package/src/ssr/GlobalSearchDialog.tsx +389 -0
  125. package/src/ssr/GlobalSearchHelpDialog.tsx +106 -0
  126. package/src/ssr/GlobalSearchTrigger.island.tsx +42 -0
  127. package/src/ssr/HotkeysHelpRail.island.tsx +99 -0
  128. package/src/ssr/Layout.tsx +326 -0
  129. package/src/ssr/MoreAppsDropdown.island.tsx +61 -0
  130. package/src/ssr/NavMenu.island.tsx +108 -0
  131. package/src/ssr/ThemeToggleRail.island.tsx +27 -0
  132. package/src/ssr/index.ts +5 -0
  133. package/src/ssr/islands/SearchBar.island.tsx +77 -0
  134. package/src/ssr/islands/index.ts +1 -0
  135. package/src/ssr/runtime.ts +22 -0
  136. package/src/styles/base-popover.css +28 -0
  137. package/src/styles/effects.css +65 -0
  138. package/src/styles/global.css +133 -0
  139. package/src/styles/input.css +54 -0
  140. package/src/styles/tokens.css +35 -0
  141. package/src/styles/utilities-buttons.css +125 -0
  142. package/src/styles/utilities-feedback.css +65 -0
  143. package/src/styles/utilities-layout.css +122 -0
  144. package/src/styles/utilities-navigation.css +196 -0
  145. package/src/types/ambient.d.ts +8 -0
  146. package/src/ui/admin-settings.tsx +148 -0
  147. package/src/ui/dialog-core.ts +146 -0
  148. package/src/ui/filter/FilterChip.tsx +196 -0
  149. package/src/ui/filter/index.ts +2 -0
  150. package/src/ui/index.ts +19 -0
  151. package/src/ui/input/Checkbox.tsx +55 -0
  152. package/src/ui/input/ColorInput.tsx +122 -0
  153. package/src/ui/input/DateTimeInput.tsx +86 -0
  154. package/src/ui/input/ImageInput.tsx +170 -0
  155. package/src/ui/input/NumberInput.tsx +113 -0
  156. package/src/ui/input/PinInput.tsx +169 -0
  157. package/src/ui/input/SegmentedControl.tsx +99 -0
  158. package/src/ui/input/Select.tsx +288 -0
  159. package/src/ui/input/SelectChip.tsx +61 -0
  160. package/src/ui/input/Slider.tsx +118 -0
  161. package/src/ui/input/Switch.tsx +62 -0
  162. package/src/ui/input/TagsInput.tsx +115 -0
  163. package/src/ui/input/TextInput.tsx +160 -0
  164. package/src/ui/input/index.ts +13 -0
  165. package/src/ui/input/types.ts +42 -0
  166. package/src/ui/input/util.tsx +105 -0
  167. package/src/ui/ipa/Avatar.tsx +28 -0
  168. package/src/ui/ipa/GroupView.tsx +36 -0
  169. package/src/ui/ipa/LoginBtn.tsx +16 -0
  170. package/src/ui/ipa/UserView.tsx +58 -0
  171. package/src/ui/ipa/index.ts +4 -0
  172. package/src/ui/misc/ContextMenu.tsx +211 -0
  173. package/src/ui/misc/CopyButton.tsx +28 -0
  174. package/src/ui/misc/Dropdown.tsx +194 -0
  175. package/src/ui/misc/EntitySearch.tsx +213 -0
  176. package/src/ui/misc/Lightbox.tsx +194 -0
  177. package/src/ui/misc/LinkCard.tsx +34 -0
  178. package/src/ui/misc/LogEntriesTable.tsx +61 -0
  179. package/src/ui/misc/MarkdownView.tsx +65 -0
  180. package/src/ui/misc/Pagination.tsx +51 -0
  181. package/src/ui/misc/PermissionEditor.tsx +379 -0
  182. package/src/ui/misc/ProgressBar.tsx +47 -0
  183. package/src/ui/misc/RemoveBtn.tsx +27 -0
  184. package/src/ui/misc/StatCell.tsx +90 -0
  185. package/src/ui/misc/index.ts +18 -0
  186. package/src/ui/navigation.ts +32 -0
  187. package/src/ui/prompts.tsx +854 -0
  188. package/src/ui/sidebar.tsx +468 -0
  189. package/src/ui/widgets/Widget.tsx +62 -0
  190. package/src/ui/widgets/WidgetCard.tsx +19 -0
  191. package/src/ui/widgets/WidgetHero.tsx +39 -0
  192. package/src/ui/widgets/WidgetList.tsx +84 -0
  193. package/src/ui/widgets/WidgetPills.tsx +68 -0
  194. package/src/ui/widgets/WidgetStat.tsx +67 -0
  195. package/src/ui/widgets/WidgetStatus.tsx +62 -0
  196. package/src/ui/widgets/index.ts +9 -0
@@ -0,0 +1,48 @@
1
+ /**
2
+ * TLS resolver slot for FreeIPA transport (`client.ts`, `session.ts`).
3
+ *
4
+ * The transport layer cannot directly read settings (would create a cycle:
5
+ * `services/freeipa-config.ts` already depends on `server/services/freeipa/`).
6
+ * Instead, the layer that owns settings registers a resolver via
7
+ * `setFreeIpaTlsResolver()` and the transport calls `getFreeIpaTls()` per fetch.
8
+ *
9
+ * Resolver is async — it reads through the Redis cache-aside settings layer.
10
+ * Returns Bun fetch `tls` options or `undefined` for system trust.
11
+ * Resolution order is the resolver's concern (typically: ca_cert > allow_insecure > undefined).
12
+ */
13
+ // `BunFetchRequestInitTLS` is declared as a global (see bun-types/globals.d.ts)
14
+ // and not exported from the "bun" module. Use `Bun.TLSOptions` — fetch's `tls`
15
+ // option accepts it (the global extends it with `checkServerIdentity` which we
16
+ // don't need).
17
+ type FreeIpaTls = Bun.TLSOptions;
18
+
19
+ let resolver: (() => Promise<FreeIpaTls | undefined>) | null = null;
20
+
21
+ export const setFreeIpaTlsResolver = (
22
+ fn: (() => Promise<FreeIpaTls | undefined>) | null,
23
+ ): void => {
24
+ resolver = fn;
25
+ };
26
+
27
+ export const getFreeIpaTls = async (): Promise<FreeIpaTls | undefined> => {
28
+ return (await resolver?.()) ?? undefined;
29
+ };
30
+
31
+ /**
32
+ * Stable fingerprint of the current TLS config — used as a cache-key suffix
33
+ * (e.g. for the cached service session) so that flipping `allow_insecure` or
34
+ * rotating `ca_cert` forces re-establishment of cached connections.
35
+ */
36
+ export const getFreeIpaTlsFingerprint = async (): Promise<string> => {
37
+ const tls = await getFreeIpaTls();
38
+ if (!tls) return "sys";
39
+ if (tls.ca && typeof tls.ca === "string" && tls.ca.length > 0) {
40
+ // Cheap non-cryptographic hash; collision risk is irrelevant here (only
41
+ // used to detect "did the cert change").
42
+ let h = 0;
43
+ for (let i = 0; i < tls.ca.length; i++) h = ((h << 5) - h + tls.ca.charCodeAt(i)) | 0;
44
+ return `ca:${(h >>> 0).toString(16)}`;
45
+ }
46
+ if (tls.rejectUnauthorized === false) return "insec";
47
+ return "sys";
48
+ };
@@ -0,0 +1,60 @@
1
+ export class IpaError extends Error {
2
+ constructor(
3
+ message: string,
4
+ public readonly code?: number,
5
+ public readonly data?: unknown,
6
+ ) {
7
+ super(message);
8
+ this.name = "IpaError";
9
+ }
10
+ }
11
+
12
+ export type DbRow = Record<string, unknown>;
13
+
14
+ export const str = (val: unknown): string => {
15
+ if (Array.isArray(val)) return String(val[0] ?? "");
16
+ return String(val ?? "");
17
+ };
18
+
19
+ export const num = (val: unknown): number | null => {
20
+ const raw = Array.isArray(val) ? val[0] : val;
21
+ const n = Number(raw);
22
+ return Number.isNaN(n) ? null : n;
23
+ };
24
+
25
+ export const parseGeneralizedTime = (val: unknown): Date | null => {
26
+ let raw = Array.isArray(val) ? val[0] : val;
27
+ if (raw && typeof raw === "object" && "__datetime__" in raw) {
28
+ raw = (raw as Record<string, unknown>).__datetime__;
29
+ }
30
+ const s = typeof raw === "string" ? raw : "";
31
+ if (!s || s.length < 14) return null;
32
+ const iso = `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}T${s.slice(8, 10)}:${s.slice(10, 12)}:${s.slice(12, 14)}Z`;
33
+ const d = new Date(iso);
34
+ return Number.isNaN(d.getTime()) ? null : d;
35
+ };
36
+
37
+ export const toGeneralizedTime = (date: Date): string => {
38
+ const y = date.getUTCFullYear();
39
+ const m = String(date.getUTCMonth() + 1).padStart(2, "0");
40
+ const d = String(date.getUTCDate()).padStart(2, "0");
41
+ const h = String(date.getUTCHours()).padStart(2, "0");
42
+ const min = String(date.getUTCMinutes()).padStart(2, "0");
43
+ const s = String(date.getUTCSeconds()).padStart(2, "0");
44
+ return `${y}${m}${d}${h}${min}${s}Z`;
45
+ };
46
+
47
+ export const escapeLike = (value: string): string => value.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
48
+
49
+ export const toExcludedGroupsSet = (groups: string[]): Set<string> => new Set(groups.map((group) => group.trim()).filter(Boolean));
50
+
51
+ export const mapIpaErrorCode = (code: number): 400 | 401 | 403 => {
52
+ if (code === 4001) return 401;
53
+ if (code === 4301) return 403;
54
+ return 400;
55
+ };
56
+
57
+ export const toPgTextArray = (values: string[] | null | undefined): string => {
58
+ if (!Array.isArray(values) || values.length === 0) return "{}";
59
+ return `{${values.map((value) => `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`).join(",")}}`;
60
+ };
@@ -0,0 +1,154 @@
1
+ import { err, fail, ok, paginate, type PageParams, type Paginated, type Result } from "@valentinkolb/stdlib";
2
+
3
+ type GeoApiPlace = {
4
+ name?: string;
5
+ latitude?: number;
6
+ longitude?: number;
7
+ country_code?: string;
8
+ admin1_code?: string;
9
+ feature_class?: string;
10
+ feature_code?: string;
11
+ };
12
+
13
+ type GeoApiSearchResponse = {
14
+ places?: GeoApiPlace[];
15
+ };
16
+
17
+ export type GeoPlace = {
18
+ name: string;
19
+ lat: number;
20
+ lon: number;
21
+ country?: string;
22
+ state?: string;
23
+ featureClass?: string;
24
+ featureCode?: string;
25
+ };
26
+
27
+ /**
28
+ * Normalizes and validates geo API base URLs before outbound requests.
29
+ */
30
+ const normalizeBaseUrl = (baseUrl: string): Result<string> => {
31
+ const value = baseUrl.trim();
32
+ if (!value) {
33
+ return fail(err.badInput("Geo API base URL is required"));
34
+ }
35
+ return ok(value.replace(/\/$/, ""));
36
+ };
37
+
38
+ /**
39
+ * Maps one geo API place record to the internal place model and drops invalid rows.
40
+ */
41
+ const toPlace = (place: GeoApiPlace): GeoPlace | null => {
42
+ if (typeof place.name !== "string" || typeof place.latitude !== "number" || typeof place.longitude !== "number") {
43
+ return null;
44
+ }
45
+
46
+ return {
47
+ name: place.name,
48
+ lat: place.latitude,
49
+ lon: place.longitude,
50
+ country: place.country_code,
51
+ state: place.admin1_code,
52
+ featureClass: place.feature_class,
53
+ featureCode: place.feature_code,
54
+ };
55
+ };
56
+
57
+ /**
58
+ * Searches places through the configured geo API and returns paginated, normalized matches.
59
+ */
60
+ const list = async (config: {
61
+ baseUrl: string;
62
+ pagination?: PageParams;
63
+ filter: {
64
+ query: string;
65
+ country?: string;
66
+ featureClass?: string;
67
+ featureCode?: string;
68
+ };
69
+ }): Promise<Result<Paginated<GeoPlace>>> => {
70
+ const query = config.filter.query.trim();
71
+ if (!query) {
72
+ const { page, perPage } = paginate(config.pagination);
73
+ return ok({
74
+ items: [],
75
+ page,
76
+ perPage,
77
+ total: 0,
78
+ hasNext: false,
79
+ });
80
+ }
81
+
82
+ const baseUrlResult = normalizeBaseUrl(config.baseUrl);
83
+ if (!baseUrlResult.ok) return baseUrlResult;
84
+
85
+ const params = new URLSearchParams({ q: query });
86
+ if (config.filter.country?.trim()) {
87
+ params.set("country", config.filter.country.trim());
88
+ }
89
+
90
+ try {
91
+ const res = await fetch(`${baseUrlResult.data}/geo/search?${params}`);
92
+ if (!res.ok) {
93
+ return fail(err.internal(`Geo search failed with status ${res.status}`));
94
+ }
95
+
96
+ const body = (await res.json()) as GeoApiSearchResponse;
97
+ const mapped = (body.places ?? []).map(toPlace).filter((place): place is GeoPlace => place !== null);
98
+
99
+ const filtered = mapped.filter((place) => {
100
+ if (config.filter.featureClass && place.featureClass !== config.filter.featureClass) {
101
+ return false;
102
+ }
103
+ if (config.filter.featureCode && place.featureCode !== config.filter.featureCode) {
104
+ return false;
105
+ }
106
+ return true;
107
+ });
108
+
109
+ const { page, perPage, offset } = paginate(config.pagination);
110
+ const items = filtered.slice(offset, offset + perPage);
111
+ return ok({
112
+ items,
113
+ page,
114
+ perPage,
115
+ total: filtered.length,
116
+ hasNext: page * perPage < filtered.length,
117
+ });
118
+ } catch (error) {
119
+ return fail(err.internal(`Geo search request failed: ${error instanceof Error ? error.message : String(error)}`));
120
+ }
121
+ };
122
+
123
+ /**
124
+ * Resolves one place by coordinates via reverse geocoding.
125
+ */
126
+ const get = async (config: { baseUrl: string; lat: number; lon: number }): Promise<Result<GeoPlace | null>> => {
127
+ const baseUrlResult = normalizeBaseUrl(config.baseUrl);
128
+ if (!baseUrlResult.ok) return baseUrlResult;
129
+
130
+ try {
131
+ const res = await fetch(`${baseUrlResult.data}/geo/reverse?lat=${config.lat}&lng=${config.lon}`);
132
+ if (!res.ok) {
133
+ return fail(err.internal(`Geo reverse lookup failed with status ${res.status}`));
134
+ }
135
+
136
+ const body = (await res.json()) as GeoApiSearchResponse;
137
+ const first = (body.places ?? []).map(toPlace).find((place): place is GeoPlace => place !== null);
138
+
139
+ return ok(first ?? null);
140
+ } catch (error) {
141
+ return fail(err.internal(`Geo reverse request failed: ${error instanceof Error ? error.message : String(error)}`));
142
+ }
143
+ };
144
+
145
+ export const geoService = {
146
+ place: {
147
+ list,
148
+ get,
149
+ },
150
+ };
151
+
152
+ export type GeoService = typeof geoService;
153
+
154
+ export const geo = geoService;
@@ -0,0 +1,28 @@
1
+ // Cloud-specific server services
2
+ export { services } from "./services";
3
+ export { freeipa } from "./freeipa";
4
+
5
+ export {
6
+ PERMISSION_LEVELS,
7
+ hasPermission,
8
+ createAccess,
9
+ getAccess,
10
+ updateAccess,
11
+ deleteAccess,
12
+ getEffectivePermission,
13
+ resolveDisplayNames,
14
+ } from "./access";
15
+ export type { AccessEntry, PermissionLevel, PrincipalType, Principal, ResourceAccessAdapter } from "./access";
16
+
17
+ export { geo, geoService } from "./geo";
18
+ export type { GeoService, GeoPlace } from "./geo";
19
+
20
+ // Re-export from stdlib for backward compatibility
21
+ // Prefer importing directly from @valentinkolb/stdlib
22
+ import { svg as _svg, password as _password } from "@valentinkolb/stdlib";
23
+ export { ok, okMany, fail, err, unwrap, paginate, tryCatch, isServiceError, crypto, svg, password } from "@valentinkolb/stdlib";
24
+ export type { Result, Paginated, PageParams, ServiceError, ServiceErrorCode } from "@valentinkolb/stdlib";
25
+
26
+ // Compat aliases for old API names
27
+ export const images = { generateFallback: _svg.generateAvatar, parseWebpDataUrl: _svg.parseWebpDataUrl };
28
+ export const generatePassword = _password.random;
@@ -0,0 +1,13 @@
1
+ import * as access from "./access";
2
+ import { freeipa } from "./freeipa";
3
+ import { geo } from "./geo";
4
+ import { svg, password, ok, okMany, fail, err, unwrap, paginate, tryCatch, isServiceError } from "@valentinkolb/stdlib";
5
+
6
+ export const services = {
7
+ access,
8
+ freeipa,
9
+ geo,
10
+ images: { generateFallback: svg.generateAvatar, parseWebpDataUrl: svg.parseWebpDataUrl },
11
+ password,
12
+ result: { ok, okMany, fail, err, unwrap, paginate, tryCatch, isServiceError },
13
+ } as const;
@@ -0,0 +1,41 @@
1
+ import { sql } from "bun";
2
+ import type { UserProfile, UserProvider } from "../../contracts/shared";
3
+
4
+ export type DeletedAccountReason =
5
+ | "ipa_expired_demoted"
6
+ | "ipa_expired_deleted"
7
+ | "sync_out_of_scope_demoted"
8
+ | "sync_out_of_scope_deleted"
9
+ | "guest_expired_deleted"
10
+ | "local_user_expired_deleted"
11
+ | "manual_delete"
12
+ | "manual_demote";
13
+
14
+ type SqlExecutor = typeof sql;
15
+
16
+ export const writeDeletedAccountAudit = async (config: {
17
+ userId: string;
18
+ uid: string;
19
+ mail: string | null;
20
+ displayName: string | null;
21
+ previousProvider: UserProvider;
22
+ previousProfile: UserProfile;
23
+ reason: DeletedAccountReason;
24
+ meta?: Record<string, unknown>;
25
+ db?: SqlExecutor;
26
+ }): Promise<void> => {
27
+ const db = config.db ?? sql;
28
+ await db`
29
+ INSERT INTO auth.deleted_accounts (deleted_user_id, uid, mail, display_name, previous_provider, previous_profile, reason, meta)
30
+ VALUES (
31
+ ${config.userId}::uuid,
32
+ ${config.uid},
33
+ ${config.mail},
34
+ ${config.displayName},
35
+ ${config.previousProvider},
36
+ ${config.previousProfile},
37
+ ${config.reason},
38
+ ${JSON.stringify(config.meta ?? {})}::jsonb
39
+ )
40
+ `;
41
+ };