@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,347 @@
1
+ import { job, scheduler } from "@valentinkolb/sync";
2
+ import { logger, logging } from "../logging";
3
+ import { providers } from "../providers";
4
+ import { get as getSetting } from "../settings";
5
+ import { accountLifecycle } from "./index";
6
+
7
+ const log = logger("auth:lifecycle:scheduler");
8
+ const ipaSyncLog = logger("auth:ipa:sync");
9
+ const reminderLog = logger("auth:reminder:daily");
10
+ const guestCleanupLog = logger("auth:guest:cleanup");
11
+ const localUserCleanupLog = logger("auth:local-user:cleanup");
12
+ const auditCleanupLog = logger("auth:lifecycle:audit:cleanup");
13
+ const ipaBackfillLog = logger("auth:ipa:backfill");
14
+ const localUserBackfillLog = logger("auth:local-user:backfill");
15
+ const guestBackfillLog = logger("auth:guest:backfill");
16
+ const logCleanupLog = logger("logging");
17
+ const DEFAULT_IPA_SYNC_CRON = "*/5 * * * *";
18
+
19
+ type JobSummary = {
20
+ scanned: number;
21
+ changed: number;
22
+ skipped: number;
23
+ failed: number;
24
+ };
25
+
26
+ const abortedSummary = (): JobSummary => ({ scanned: 0, changed: 0, skipped: 0, failed: 0 });
27
+
28
+ const toDemotionLog = (summary: JobSummary) => ({
29
+ expiredCandidates: summary.scanned,
30
+ demotedToGuest: summary.changed,
31
+ skipped: summary.skipped,
32
+ failed: summary.failed,
33
+ });
34
+
35
+ const toReminderLog = (summary: JobSummary) => ({
36
+ candidates: summary.scanned,
37
+ sent: summary.changed,
38
+ skipped: summary.skipped,
39
+ failed: summary.failed,
40
+ });
41
+
42
+ const toCleanupLog = (summary: JobSummary) => ({
43
+ candidates: summary.scanned,
44
+ deleted: summary.changed,
45
+ skipped: summary.skipped,
46
+ failed: summary.failed,
47
+ });
48
+
49
+ const toBackfillLog = (summary: JobSummary) => ({
50
+ candidates: summary.scanned,
51
+ updated: summary.changed,
52
+ skipped: summary.skipped,
53
+ failed: summary.failed,
54
+ });
55
+
56
+ const getCronSetting = async (key: string, fallback: string): Promise<string> => {
57
+ const value = String((await getSetting<string>(key)) || "").trim();
58
+ return value.length > 0 ? value : fallback;
59
+ };
60
+
61
+ const getTimezoneSetting = async (): Promise<string> => {
62
+ const value = String((await getSetting<string>("app.timezone")) || "").trim();
63
+ return value.length > 0 ? value : "Europe/Berlin";
64
+ };
65
+
66
+ /**
67
+ * Retry policy shared by all lifecycle jobs: on transient failure, reschedule
68
+ * up to `maxAttempts - 1` times with exponential backoff from `baseMs`. Beyond
69
+ * that we go terminal and the next cron slot picks up the work.
70
+ */
71
+ const retryOnError = (cfg: { maxAttempts: number; baseMs: number; maxMs?: number }) =>
72
+ ({ ctx }: { ctx: { error?: Error; failureCount: number; reschedule: (cfg: { delayMs: number }) => void; expBackoff: (cfg: { baseMs: number; maxMs?: number }) => number } }) => {
73
+ if (!ctx.error) return;
74
+ if (ctx.failureCount >= cfg.maxAttempts - 1) return;
75
+ ctx.reschedule({ delayMs: ctx.expBackoff({ baseMs: cfg.baseMs, maxMs: cfg.maxMs }) });
76
+ };
77
+
78
+ // ── Jobs ───────────────────────────────────────────────────────────────
79
+
80
+ const ipaSyncJob = job<void, JobSummary>({
81
+ id: "auth:ipa:sync",
82
+ defaults: { leaseMs: 120_000 },
83
+ process: async ({ ctx }) => {
84
+ if (ctx.signal.aborted) return abortedSummary();
85
+ try {
86
+ await providers.ipa.sync.run();
87
+ } catch (error) {
88
+ ipaSyncLog.error("Sync step failed", { step: "sync", error: error instanceof Error ? error.message : String(error) });
89
+ throw error;
90
+ }
91
+ await ctx.heartbeat();
92
+ try {
93
+ const summary = await accountLifecycle.demoteExpiredIpaUsers();
94
+ ipaSyncLog.info("Expired IPA demotion complete", toDemotionLog(summary));
95
+ return summary;
96
+ } catch (error) {
97
+ ipaSyncLog.error("Expired IPA demotion step failed", { step: "demote-expired", error: error instanceof Error ? error.message : String(error) });
98
+ throw error;
99
+ }
100
+ },
101
+ after: retryOnError({ maxAttempts: 3, baseMs: 1000 }),
102
+ });
103
+
104
+ const reminderJob = job<void, JobSummary>({
105
+ id: "auth:reminder:daily",
106
+ defaults: { leaseMs: 180_000 },
107
+ process: async ({ ctx }) => {
108
+ if (ctx.signal.aborted) return abortedSummary();
109
+ const summary = await accountLifecycle.sendExpiryReminders();
110
+ reminderLog.info("Reminder run complete", toReminderLog(summary));
111
+ return summary;
112
+ },
113
+ after: retryOnError({ maxAttempts: 3, baseMs: 1000 }),
114
+ });
115
+
116
+ const guestCleanupJob = job<void, JobSummary>({
117
+ id: "auth:guest:cleanup",
118
+ defaults: { leaseMs: 120_000 },
119
+ process: async ({ ctx }) => {
120
+ if (ctx.signal.aborted) return abortedSummary();
121
+ const summary = await accountLifecycle.cleanupExpiredGuests();
122
+ guestCleanupLog.info("Expired guest cleanup complete", toCleanupLog(summary));
123
+ return summary;
124
+ },
125
+ after: retryOnError({ maxAttempts: 3, baseMs: 1000 }),
126
+ });
127
+
128
+ const localUserCleanupJob = job<void, JobSummary>({
129
+ id: "auth:local-user:cleanup",
130
+ defaults: { leaseMs: 120_000 },
131
+ process: async ({ ctx }) => {
132
+ if (ctx.signal.aborted) return abortedSummary();
133
+ const summary = await accountLifecycle.cleanupExpiredLocalUsers();
134
+ localUserCleanupLog.info("Expired local user cleanup complete", toCleanupLog(summary));
135
+ return summary;
136
+ },
137
+ after: retryOnError({ maxAttempts: 3, baseMs: 1000 }),
138
+ });
139
+
140
+ const auditCleanupJob = job<void, JobSummary>({
141
+ id: "auth:lifecycle:audit:cleanup",
142
+ defaults: { leaseMs: 120_000 },
143
+ process: async ({ ctx }) => {
144
+ if (ctx.signal.aborted) return abortedSummary();
145
+ const summary = await accountLifecycle.cleanupLifecycleAudit();
146
+ auditCleanupLog.info("Lifecycle audit cleanup complete", toCleanupLog(summary));
147
+ return summary;
148
+ },
149
+ after: retryOnError({ maxAttempts: 3, baseMs: 1000 }),
150
+ });
151
+
152
+ const logCleanupJob = job<void, { deleted: number; retentionDays: number }>({
153
+ id: "app:logs:cleanup",
154
+ defaults: { leaseMs: 120_000 },
155
+ process: async ({ ctx }) => {
156
+ if (ctx.signal.aborted) return { deleted: 0, retentionDays: 0 };
157
+ const configured = Number((await getSetting<number | string | null>("logs.retention_days")) ?? 30);
158
+ const retentionDays = Number.isFinite(configured) ? configured : 30;
159
+ const summary = await logging.cleanup(retentionDays);
160
+ logCleanupLog.info("Log cleanup complete", { deleted: summary.deleted, retentionDays });
161
+ return { deleted: summary.deleted, retentionDays };
162
+ },
163
+ after: retryOnError({ maxAttempts: 3, baseMs: 1000 }),
164
+ });
165
+
166
+ const ipaBackfillJob = job<void, JobSummary>({
167
+ id: "auth:ipa:backfill",
168
+ defaults: { leaseMs: 300_000 },
169
+ process: async ({ ctx }) => {
170
+ if (ctx.signal.aborted) return abortedSummary();
171
+ const summary = await accountLifecycle.runIpaBackfill();
172
+ ipaBackfillLog.info("IPA expiry backfill complete", toBackfillLog(summary));
173
+ return summary;
174
+ },
175
+ after: retryOnError({ maxAttempts: 2, baseMs: 2000 }),
176
+ });
177
+
178
+ const guestBackfillJob = job<void, JobSummary>({
179
+ id: "auth:guest:backfill",
180
+ defaults: { leaseMs: 300_000 },
181
+ process: async ({ ctx }) => {
182
+ if (ctx.signal.aborted) return abortedSummary();
183
+ const summary = await accountLifecycle.runGuestBackfill();
184
+ guestBackfillLog.info("Guest expiry backfill complete", toBackfillLog(summary));
185
+ return summary;
186
+ },
187
+ after: retryOnError({ maxAttempts: 2, baseMs: 2000 }),
188
+ });
189
+
190
+ const localUserBackfillJob = job<void, JobSummary>({
191
+ id: "auth:local-user:backfill",
192
+ defaults: { leaseMs: 300_000 },
193
+ process: async ({ ctx }) => {
194
+ if (ctx.signal.aborted) return abortedSummary();
195
+ const summary = await accountLifecycle.runLocalUserBackfill();
196
+ localUserBackfillLog.info("Local user expiry backfill complete", toBackfillLog(summary));
197
+ return summary;
198
+ },
199
+ after: retryOnError({ maxAttempts: 2, baseMs: 2000 }),
200
+ });
201
+
202
+ // ── Scheduler ──────────────────────────────────────────────────────────
203
+
204
+ const lifecycleScheduler = scheduler({ id: "auth-lifecycle" });
205
+
206
+ let started = false;
207
+ let registered = false;
208
+ let registerPromise: Promise<void> | null = null;
209
+
210
+ /**
211
+ * Register (or update) a cron-triggered schedule that fans out to the given
212
+ * job. `scheduler.create` is idempotent by id — same cron/tz keeps `nextRunAt`
213
+ * intact; a change resets it. We submit one dispatch per slot using the slot
214
+ * timestamp as idempotency key so misfires don't double-run.
215
+ */
216
+ const createSchedule = async (config: {
217
+ id: string;
218
+ cron: string;
219
+ tz: string;
220
+ submit: (key: string) => Promise<string>;
221
+ }): Promise<void> => {
222
+ await lifecycleScheduler.create({
223
+ id: config.id,
224
+ cron: config.cron,
225
+ tz: config.tz,
226
+ process: async ({ ctx }) => {
227
+ await config.submit(`slot:${ctx.slotTs}`);
228
+ },
229
+ });
230
+ };
231
+
232
+ const createScheduleWithFallback = async (config: {
233
+ id: string;
234
+ cron: string;
235
+ fallbackCron: string;
236
+ tz: string;
237
+ submit: (key: string) => Promise<string>;
238
+ settingsKey: string;
239
+ }): Promise<void> => {
240
+ try {
241
+ await createSchedule({ id: config.id, cron: config.cron, tz: config.tz, submit: config.submit });
242
+ } catch (error) {
243
+ if (config.cron === config.fallbackCron) throw error;
244
+ log.warn("Invalid configured cron, falling back to default", {
245
+ key: config.settingsKey,
246
+ configuredCron: config.cron,
247
+ fallbackCron: config.fallbackCron,
248
+ timezone: config.tz,
249
+ error: error instanceof Error ? error.message : String(error),
250
+ });
251
+ await createSchedule({ id: config.id, cron: config.fallbackCron, tz: config.tz, submit: config.submit });
252
+ }
253
+ };
254
+
255
+ const doRegister = async (): Promise<void> => {
256
+ const [scheduleTz, ipaSyncCron, reminderCron, cleanupCron] = await Promise.all([
257
+ getTimezoneSetting(),
258
+ getCronSetting("freeipa.sync_cron", DEFAULT_IPA_SYNC_CRON),
259
+ getCronSetting("user.account.reminder_cron", "0 9 * * *"),
260
+ getCronSetting("app.cleanup_schedule", "0 4 * * *"),
261
+ ]);
262
+
263
+ await createScheduleWithFallback({
264
+ id: "auth:ipa:sync",
265
+ cron: ipaSyncCron,
266
+ fallbackCron: DEFAULT_IPA_SYNC_CRON,
267
+ tz: scheduleTz,
268
+ settingsKey: "freeipa.sync_cron",
269
+ submit: (key) => ipaSyncJob.submit({ key }),
270
+ });
271
+
272
+ await createSchedule({
273
+ id: "auth:reminder:daily",
274
+ cron: reminderCron,
275
+ tz: scheduleTz,
276
+ submit: (key) => reminderJob.submit({ key }),
277
+ });
278
+
279
+ await createSchedule({
280
+ id: "auth:guest:cleanup",
281
+ cron: cleanupCron,
282
+ tz: scheduleTz,
283
+ submit: (key) => guestCleanupJob.submit({ key }),
284
+ });
285
+
286
+ await createSchedule({
287
+ id: "auth:local-user:cleanup",
288
+ cron: cleanupCron,
289
+ tz: scheduleTz,
290
+ submit: (key) => localUserCleanupJob.submit({ key }),
291
+ });
292
+
293
+ await createSchedule({
294
+ id: "auth:lifecycle:audit:cleanup",
295
+ cron: cleanupCron,
296
+ tz: scheduleTz,
297
+ submit: (key) => auditCleanupJob.submit({ key }),
298
+ });
299
+
300
+ await createSchedule({
301
+ id: "app:logs:cleanup",
302
+ cron: cleanupCron,
303
+ tz: scheduleTz,
304
+ submit: (key) => logCleanupJob.submit({ key }),
305
+ });
306
+
307
+ registered = true;
308
+ };
309
+
310
+ const ensureRegistered = async (): Promise<void> => {
311
+ if (registered) return;
312
+ if (!registerPromise) {
313
+ registerPromise = doRegister().finally(() => {
314
+ registerPromise = null;
315
+ });
316
+ }
317
+ await registerPromise;
318
+ };
319
+
320
+ export const lifecycleJobs = {
321
+ start: async (): Promise<void> => {
322
+ if (!started) {
323
+ lifecycleScheduler.start();
324
+ started = true;
325
+ }
326
+ await ensureRegistered();
327
+ },
328
+
329
+ stop: async (): Promise<void> => {
330
+ if (!started) return;
331
+ await lifecycleScheduler.stop();
332
+ started = false;
333
+ registered = false;
334
+ registerPromise = null;
335
+ },
336
+
337
+ // Manual triggers — use a timestamp key so repeated presses within the same
338
+ // millisecond are deduped but subsequent calls always enqueue a new run.
339
+ submitIpaBackfill: (): Promise<string> => ipaBackfillJob.submit({ key: `manual:${Date.now()}` }),
340
+ submitLocalUserBackfill: (): Promise<string> => localUserBackfillJob.submit({ key: `manual:${Date.now()}` }),
341
+ submitGuestBackfill: (): Promise<string> => guestBackfillJob.submit({ key: `manual:${Date.now()}` }),
342
+ submitReminderRun: (): Promise<string> => reminderJob.submit({ key: `manual:${Date.now()}` }),
343
+ submitIpaSync: (): Promise<string> => ipaSyncJob.submit({ key: `manual:${Date.now()}` }),
344
+
345
+ metrics: () => lifecycleScheduler.metric(),
346
+ listSchedules: async () => lifecycleScheduler.list(),
347
+ };
@@ -0,0 +1,21 @@
1
+ export {
2
+ canPersistStoredAdmin,
3
+ calculateIpaProfileFromGroupNames,
4
+ deriveIpaAdminFromGroupNames,
5
+ isGuestProfile,
6
+ isIpaProvider,
7
+ isLocalProvider,
8
+ getConfiguredExpiryDays,
9
+ getDefaultAccountExpiry,
10
+ normalizeManualAccountExpiry,
11
+ parseManualAccountExpiry,
12
+ parseIpaAccountTransitionPolicy,
13
+ parseIpaMatchMode,
14
+ resolveEffectiveAdminState,
15
+ resolveAccountExpires,
16
+ resolveStoredAdminState,
17
+ resolveTargetAccountExpiry,
18
+ type IpaAccountTransitionPolicy,
19
+ type IpaMatchMode,
20
+ } from "./accounts/model";
21
+ export { buildRoles } from "./accounts/authz";