alepha 0.13.5 → 0.13.7

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 (136) hide show
  1. package/dist/api-audits/index.browser.js +116 -0
  2. package/dist/api-audits/index.browser.js.map +1 -0
  3. package/dist/api-audits/index.d.ts +1194 -0
  4. package/dist/api-audits/index.js +674 -0
  5. package/dist/api-audits/index.js.map +1 -0
  6. package/dist/api-notifications/index.d.ts +147 -147
  7. package/dist/api-parameters/index.browser.js +36 -5
  8. package/dist/api-parameters/index.browser.js.map +1 -1
  9. package/dist/api-parameters/index.d.ts +711 -33
  10. package/dist/api-parameters/index.js +831 -17
  11. package/dist/api-parameters/index.js.map +1 -1
  12. package/dist/api-users/index.d.ts +16 -3
  13. package/dist/api-users/index.js +699 -19
  14. package/dist/api-users/index.js.map +1 -1
  15. package/dist/api-verifications/index.js +2 -1
  16. package/dist/api-verifications/index.js.map +1 -1
  17. package/dist/bin/index.js +1 -0
  18. package/dist/bin/index.js.map +1 -1
  19. package/dist/cli/index.d.ts +85 -31
  20. package/dist/cli/index.js +205 -33
  21. package/dist/cli/index.js.map +1 -1
  22. package/dist/command/index.d.ts +67 -6
  23. package/dist/command/index.js +30 -3
  24. package/dist/command/index.js.map +1 -1
  25. package/dist/core/index.browser.js +241 -61
  26. package/dist/core/index.browser.js.map +1 -1
  27. package/dist/core/index.d.ts +170 -90
  28. package/dist/core/index.js +264 -67
  29. package/dist/core/index.js.map +1 -1
  30. package/dist/core/index.native.js +248 -65
  31. package/dist/core/index.native.js.map +1 -1
  32. package/dist/email/index.js +15 -10554
  33. package/dist/email/index.js.map +1 -1
  34. package/dist/logger/index.d.ts +4 -4
  35. package/dist/logger/index.js +77 -72
  36. package/dist/logger/index.js.map +1 -1
  37. package/dist/orm/index.d.ts +5 -1
  38. package/dist/orm/index.js +24 -7
  39. package/dist/orm/index.js.map +1 -1
  40. package/dist/queue/index.d.ts +4 -4
  41. package/dist/redis/index.d.ts +10 -10
  42. package/dist/security/index.d.ts +28 -28
  43. package/dist/server/index.d.ts +10 -1
  44. package/dist/server/index.js +20 -6
  45. package/dist/server/index.js.map +1 -1
  46. package/dist/server-auth/index.d.ts +163 -152
  47. package/dist/server-auth/index.js +40 -10
  48. package/dist/server-auth/index.js.map +1 -1
  49. package/dist/server-cookies/index.js +5 -1
  50. package/dist/server-cookies/index.js.map +1 -1
  51. package/dist/server-links/index.d.ts +33 -33
  52. package/dist/server-security/index.d.ts +9 -9
  53. package/dist/thread/index.js +2 -2
  54. package/dist/thread/index.js.map +1 -1
  55. package/dist/vite/index.d.ts +2 -2
  56. package/dist/vite/index.js +102 -45
  57. package/dist/vite/index.js.map +1 -1
  58. package/dist/websocket/index.browser.js +3 -3
  59. package/dist/websocket/index.browser.js.map +1 -1
  60. package/dist/websocket/index.d.ts +7 -7
  61. package/dist/websocket/index.js +4 -4
  62. package/dist/websocket/index.js.map +1 -1
  63. package/package.json +14 -9
  64. package/src/api-audits/controllers/AuditController.ts +186 -0
  65. package/src/api-audits/entities/audits.ts +132 -0
  66. package/src/api-audits/index.browser.ts +18 -0
  67. package/src/api-audits/index.ts +58 -0
  68. package/src/api-audits/primitives/$audit.ts +159 -0
  69. package/src/api-audits/schemas/auditQuerySchema.ts +23 -0
  70. package/src/api-audits/schemas/auditResourceSchema.ts +9 -0
  71. package/src/api-audits/schemas/createAuditSchema.ts +27 -0
  72. package/src/api-audits/services/AuditService.ts +412 -0
  73. package/src/api-parameters/controllers/ConfigController.ts +324 -0
  74. package/src/api-parameters/entities/parameters.ts +93 -10
  75. package/src/api-parameters/index.ts +43 -4
  76. package/src/api-parameters/primitives/$config.ts +291 -19
  77. package/src/api-parameters/schedulers/ConfigActivationScheduler.ts +30 -0
  78. package/src/api-parameters/services/ConfigStore.ts +491 -0
  79. package/src/api-users/atoms/realmAuthSettingsAtom.ts +19 -0
  80. package/src/api-users/controllers/UserRealmController.ts +0 -2
  81. package/src/api-users/index.ts +2 -0
  82. package/src/api-users/primitives/$userRealm.ts +18 -3
  83. package/src/api-users/providers/UserRealmProvider.ts +6 -3
  84. package/src/api-users/services/RegistrationService.ts +2 -1
  85. package/src/api-users/services/SessionService.ts +4 -0
  86. package/src/api-users/services/UserService.ts +3 -0
  87. package/src/api-verifications/index.ts +7 -1
  88. package/src/bin/index.ts +1 -0
  89. package/src/cli/assets/biomeJson.ts +1 -1
  90. package/src/cli/assets/dummySpecTs.ts +7 -0
  91. package/src/cli/assets/editorconfig.ts +13 -0
  92. package/src/cli/assets/mainTs.ts +14 -0
  93. package/src/cli/commands/BiomeCommands.ts +2 -0
  94. package/src/cli/commands/CoreCommands.ts +28 -9
  95. package/src/cli/commands/VerifyCommands.ts +2 -1
  96. package/src/cli/commands/ViteCommands.ts +8 -9
  97. package/src/cli/services/AlephaCliUtils.ts +214 -23
  98. package/src/command/helpers/Asker.ts +0 -1
  99. package/src/command/primitives/$command.ts +67 -0
  100. package/src/command/providers/CliProvider.ts +39 -8
  101. package/src/core/Alepha.ts +40 -30
  102. package/src/core/helpers/jsonSchemaToTypeBox.ts +307 -0
  103. package/src/core/index.shared.ts +1 -0
  104. package/src/core/index.ts +30 -3
  105. package/src/core/providers/EventManager.ts +1 -1
  106. package/src/core/providers/StateManager.ts +23 -12
  107. package/src/core/providers/TypeProvider.ts +26 -34
  108. package/src/logger/index.ts +8 -6
  109. package/src/logger/primitives/$logger.ts +1 -1
  110. package/src/logger/providers/{SimpleFormatterProvider.ts → PrettyFormatterProvider.ts} +10 -1
  111. package/src/orm/index.ts +6 -0
  112. package/src/orm/services/PgRelationManager.ts +2 -2
  113. package/src/orm/services/PostgresModelBuilder.ts +11 -7
  114. package/src/orm/services/Repository.ts +16 -7
  115. package/src/orm/services/SqliteModelBuilder.ts +10 -0
  116. package/src/server/index.ts +6 -0
  117. package/src/server/primitives/$action.ts +10 -1
  118. package/src/server/providers/ServerBodyParserProvider.ts +11 -5
  119. package/src/server/providers/ServerRouterProvider.ts +13 -7
  120. package/src/server-auth/primitives/$auth.ts +7 -0
  121. package/src/server-auth/providers/ServerAuthProvider.ts +51 -8
  122. package/src/server-cookies/index.ts +2 -1
  123. package/src/thread/primitives/$thread.ts +2 -2
  124. package/src/vite/index.ts +0 -2
  125. package/src/vite/tasks/buildServer.ts +3 -4
  126. package/src/vite/tasks/generateCloudflare.ts +35 -19
  127. package/src/vite/tasks/generateDocker.ts +18 -4
  128. package/src/vite/tasks/generateSitemap.ts +5 -7
  129. package/src/vite/tasks/generateVercel.ts +76 -41
  130. package/src/vite/tasks/runAlepha.ts +16 -1
  131. package/src/websocket/providers/NodeWebSocketServerProvider.ts +3 -11
  132. package/src/websocket/services/WebSocketClient.ts +3 -3
  133. package/dist/cli/dist-BlfFtOk2.js +0 -2770
  134. package/dist/cli/dist-BlfFtOk2.js.map +0 -1
  135. package/src/api-parameters/controllers/ParameterController.ts +0 -45
  136. package/src/api-parameters/services/ParameterStore.ts +0 -23
@@ -0,0 +1,491 @@
1
+ import { $inject, type Static, type TObject, t } from "alepha";
2
+ import { DateTimeProvider } from "alepha/datetime";
3
+ import { $logger } from "alepha/logger";
4
+ import { $repository } from "alepha/orm";
5
+ import { $topic } from "alepha/topic";
6
+ import {
7
+ type Parameter,
8
+ type ParameterStatus,
9
+ parameters,
10
+ } from "../entities/parameters.ts";
11
+ import type { ConfigPrimitive } from "../primitives/$config.ts";
12
+
13
+ /**
14
+ * Payload for config sync events across instances.
15
+ */
16
+ export interface ConfigSyncPayload {
17
+ name: string;
18
+ version: number;
19
+ content: unknown;
20
+ status: ParameterStatus;
21
+ instanceId: string;
22
+ }
23
+
24
+ /**
25
+ * ConfigStore manages versioned configuration persistence and synchronization.
26
+ *
27
+ * Features:
28
+ * - Stores all config versions in PostgreSQL
29
+ * - Manages status transitions (future → next → current → expired)
30
+ * - Provides cross-instance sync via topic
31
+ * - Supports schema migrations via hash comparison
32
+ * - Auto-activates scheduled configs
33
+ */
34
+ export class ConfigStore {
35
+ protected readonly log = $logger();
36
+ protected readonly dateTimeProvider = $inject(DateTimeProvider);
37
+ protected readonly repo = $repository(parameters);
38
+
39
+ /** Unique identifier for this instance (to avoid self-updates) */
40
+ protected readonly instanceId = crypto.randomUUID();
41
+
42
+ /** In-memory cache of registered configs */
43
+ protected readonly configs = new Map<string, ConfigPrimitive<any>>();
44
+
45
+ /** Topic for cross-instance synchronization */
46
+ public readonly syncTopic = $topic({
47
+ name: "config:sync",
48
+ schema: {
49
+ payload: t.object({
50
+ name: t.text(),
51
+ version: t.integer(),
52
+ content: t.json(),
53
+ status: t.enum(["expired", "current", "next", "future"]),
54
+ instanceId: t.text(),
55
+ }),
56
+ },
57
+ handler: async ({ payload }) => {
58
+ await this.handleSyncMessage(payload as ConfigSyncPayload);
59
+ },
60
+ });
61
+
62
+ /**
63
+ * Register a config primitive with the store.
64
+ */
65
+ public register(config: ConfigPrimitive<any>): void {
66
+ this.configs.set(config.name, config);
67
+ }
68
+
69
+ /**
70
+ * Load the current config value from database.
71
+ * Returns the current or next version if no current exists.
72
+ */
73
+ public async load<T extends TObject>(
74
+ name: string,
75
+ ): Promise<Static<T> | null> {
76
+ // First try to get CURRENT
77
+ const all = await this.repo.findMany({
78
+ where: { name },
79
+ orderBy: { column: "version", direction: "desc" },
80
+ });
81
+
82
+ let param = all.find((p) => p.status === "current");
83
+
84
+ // If no current, get NEXT (will become current)
85
+ if (!param) {
86
+ param = all
87
+ .filter((p) => p.status === "next")
88
+ .sort((a, b) => {
89
+ return (
90
+ new Date(a.activationDate).getTime() -
91
+ new Date(b.activationDate).getTime()
92
+ );
93
+ })[0];
94
+ }
95
+
96
+ return param?.content as Static<T> | null;
97
+ }
98
+
99
+ /**
100
+ * Save a new config version.
101
+ *
102
+ * @param name - Config name (e.g., "app.features.flags")
103
+ * @param content - New config content
104
+ * @param schemaHash - Hash of the schema for migration detection
105
+ * @param options - Additional options (activation date, creator info, etc.)
106
+ */
107
+ public async save<T extends TObject>(
108
+ name: string,
109
+ content: Static<T>,
110
+ schemaHash: string,
111
+ options: SaveConfigOptions = {},
112
+ ): Promise<Parameter> {
113
+ const now = this.dateTimeProvider.now().toDate();
114
+ const activationDate = options.activationDate ?? now;
115
+ const isImmediate = activationDate <= now;
116
+
117
+ // Get current version number for this config
118
+ const versions = await this.repo.findMany({
119
+ where: { name },
120
+ orderBy: { column: "version", direction: "desc" },
121
+ });
122
+
123
+ const latestVersion = versions[0];
124
+ const newVersion = (latestVersion?.version ?? 0) + 1;
125
+
126
+ // Determine initial status
127
+ let status: ParameterStatus = "future";
128
+ if (isImmediate) {
129
+ status = "current";
130
+ }
131
+
132
+ // Get previous content for rollback reference
133
+ const currentConfig = versions.find((v) => v.status === "current");
134
+ const previousContent = currentConfig?.content;
135
+
136
+ // Check for schema migration
137
+ let migrationLog: string | undefined;
138
+ if (latestVersion && latestVersion.schemaHash !== schemaHash) {
139
+ migrationLog = `Schema changed from ${latestVersion.schemaHash} to ${schemaHash} at version ${newVersion}`;
140
+ this.log.info("Config schema migration detected", { name, migrationLog });
141
+ }
142
+
143
+ // If immediate activation, expire current and transition next to current
144
+ if (isImmediate) {
145
+ await this.transitionStatuses(name, now);
146
+ }
147
+
148
+ // Insert new version - convert Date to ISO string for datetime fields
149
+ const inserted = await this.repo.create({
150
+ name,
151
+ content: content as Record<string, unknown>,
152
+ schemaHash,
153
+ status,
154
+ activationDate: activationDate.toISOString(),
155
+ version: newVersion,
156
+ changeDescription: options.changeDescription,
157
+ tags: options.tags,
158
+ creatorId: options.creatorId,
159
+ creatorName: options.creatorName,
160
+ previousContent: previousContent as Record<string, unknown> | undefined,
161
+ migrationLog,
162
+ });
163
+
164
+ // Recalculate statuses after insert
165
+ await this.recalculateStatuses(name);
166
+
167
+ // Publish sync event
168
+ await this.publishSync(name, newVersion, content, status);
169
+
170
+ this.log.info("Config saved", { name, version: newVersion, status });
171
+
172
+ return inserted;
173
+ }
174
+
175
+ /**
176
+ * Get all versions of a config.
177
+ */
178
+ public async getHistory(name: string): Promise<Parameter[]> {
179
+ return this.repo.findMany({
180
+ where: { name },
181
+ orderBy: { column: "version", direction: "desc" },
182
+ });
183
+ }
184
+
185
+ /**
186
+ * Get a specific version of a config.
187
+ */
188
+ public async getVersion(
189
+ name: string,
190
+ version: number,
191
+ ): Promise<Parameter | null> {
192
+ const versions = await this.repo.findMany({
193
+ where: { name, version },
194
+ });
195
+ return versions[0] ?? null;
196
+ }
197
+
198
+ /**
199
+ * Rollback to a previous version by creating a new version with old content.
200
+ */
201
+ public async rollback(
202
+ name: string,
203
+ targetVersion: number,
204
+ options: SaveConfigOptions = {},
205
+ ): Promise<Parameter> {
206
+ const target = await this.getVersion(name, targetVersion);
207
+
208
+ if (!target) {
209
+ throw new Error(`Config version not found: ${name}@${targetVersion}`);
210
+ }
211
+
212
+ return this.save(
213
+ name,
214
+ target.content as Static<TObject>,
215
+ target.schemaHash,
216
+ {
217
+ ...options,
218
+ changeDescription:
219
+ options.changeDescription ?? `Rollback to version ${targetVersion}`,
220
+ },
221
+ );
222
+ }
223
+
224
+ /**
225
+ * Get all configs by status.
226
+ */
227
+ public async getByStatus(status: ParameterStatus): Promise<Parameter[]> {
228
+ return this.repo.findMany({
229
+ where: { status },
230
+ orderBy: { column: "name", direction: "asc" },
231
+ });
232
+ }
233
+
234
+ /**
235
+ * Get current config value with fallback to default from registered primitive.
236
+ * Returns the in-memory current value which may be the default if never saved.
237
+ */
238
+ public getCurrentValue(
239
+ name: string,
240
+ ): { content: unknown; isDefault: boolean } | null {
241
+ const config = this.configs.get(name);
242
+ if (!config) {
243
+ return null;
244
+ }
245
+ return {
246
+ content: config.current,
247
+ isDefault: true, // Will be updated after checking history
248
+ };
249
+ }
250
+
251
+ /**
252
+ * Get config info including current value with default fallback.
253
+ */
254
+ public async getCurrentWithDefault(name: string): Promise<{
255
+ current: Parameter | null;
256
+ next: Parameter | null;
257
+ defaultValue: unknown | null;
258
+ currentValue: unknown | null;
259
+ schema: TObject | null;
260
+ }> {
261
+ const history = await this.getHistory(name);
262
+ const current = history.find((v) => v.status === "current") ?? null;
263
+ const next = history.find((v) => v.status === "next") ?? null;
264
+
265
+ // Get default and current from registered primitive
266
+ const config = this.configs.get(name);
267
+ const defaultValue = config?.options.default ?? null;
268
+ const currentValue = config?.current ?? null;
269
+ const schema = config?.schema ?? null;
270
+
271
+ return { current, next, defaultValue, currentValue, schema };
272
+ }
273
+
274
+ /**
275
+ * Get all unique config names (for tree view).
276
+ */
277
+ public async getConfigNames(): Promise<string[]> {
278
+ const results = await this.repo.findMany({
279
+ orderBy: { column: "name", direction: "asc" },
280
+ });
281
+
282
+ const names = new Set<string>();
283
+ for (const r of results) {
284
+ names.add(r.name);
285
+ }
286
+
287
+ return Array.from(names);
288
+ }
289
+
290
+ /**
291
+ * Build a tree structure from config names for UI.
292
+ * Includes both database configs and registered (but not yet saved) configs.
293
+ */
294
+ public async getConfigTree(): Promise<ConfigTreeNode[]> {
295
+ const dbNames = await this.getConfigNames();
296
+ const registeredNames = Array.from(this.configs.keys());
297
+ const allNames = [...new Set([...dbNames, ...registeredNames])].sort();
298
+ return this.buildTree(allNames);
299
+ }
300
+
301
+ /**
302
+ * Check and activate scheduled configs that are due.
303
+ * Should be called periodically (e.g., via scheduler).
304
+ */
305
+ public async activateScheduledConfigs(): Promise<void> {
306
+ const now = this.dateTimeProvider.now().toDate();
307
+
308
+ // Find all NEXT configs that should be activated
309
+ const dueConfigs = await this.repo.findMany({
310
+ where: { status: "next" },
311
+ });
312
+
313
+ for (const config of dueConfigs) {
314
+ if (new Date(config.activationDate) <= now) {
315
+ await this.transitionStatuses(config.name, now);
316
+ await this.recalculateStatuses(config.name);
317
+
318
+ // Notify registered config primitives
319
+ const primitive = this.configs.get(config.name);
320
+ if (primitive) {
321
+ await primitive.reload();
322
+ }
323
+
324
+ // Publish sync
325
+ await this.publishSync(
326
+ config.name,
327
+ config.version,
328
+ config.content,
329
+ "current",
330
+ );
331
+ }
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Transition config statuses when a new current is activated.
337
+ */
338
+ protected async transitionStatuses(name: string, now: Date): Promise<void> {
339
+ // Find current configs and expire them
340
+ const currentConfigs = await this.repo.findMany({
341
+ where: { name, status: "current" },
342
+ });
343
+
344
+ for (const config of currentConfigs) {
345
+ await this.repo.updateById(config.id, {
346
+ status: "expired",
347
+ expiredAt: now.toISOString(),
348
+ });
349
+ }
350
+ }
351
+
352
+ /**
353
+ * Recalculate statuses based on activation dates.
354
+ */
355
+ protected async recalculateStatuses(name: string): Promise<void> {
356
+ const now = this.dateTimeProvider.now().toDate();
357
+
358
+ // Get all versions ordered by activation date
359
+ const versions = await this.repo.findMany({
360
+ where: { name },
361
+ orderBy: { column: "activationDate", direction: "asc" },
362
+ });
363
+
364
+ const nonExpired = versions.filter(
365
+ (v: Parameter) => v.status !== "expired",
366
+ );
367
+
368
+ // Find which should be current (latest activated)
369
+ const shouldBeCurrent = nonExpired
370
+ .filter((v: Parameter) => new Date(v.activationDate) <= now)
371
+ .pop();
372
+
373
+ // Find which should be next (closest future)
374
+ const futureVersions = nonExpired.filter(
375
+ (v: Parameter) => new Date(v.activationDate) > now,
376
+ );
377
+ const shouldBeNext = futureVersions[0];
378
+
379
+ for (const v of nonExpired) {
380
+ let newStatus: ParameterStatus;
381
+
382
+ if (shouldBeCurrent && v.id === shouldBeCurrent.id) {
383
+ newStatus = "current";
384
+ } else if (shouldBeNext && v.id === shouldBeNext.id) {
385
+ newStatus = "next";
386
+ } else if (new Date(v.activationDate) > now) {
387
+ newStatus = "future";
388
+ } else {
389
+ newStatus = "expired";
390
+ }
391
+
392
+ if (v.status !== newStatus) {
393
+ await this.repo.updateById(v.id, {
394
+ status: newStatus,
395
+ expiredAt: newStatus === "expired" ? now.toISOString() : undefined,
396
+ });
397
+ }
398
+ }
399
+ }
400
+
401
+ /**
402
+ * Publish sync event to other instances.
403
+ */
404
+ protected async publishSync(
405
+ name: string,
406
+ version: number,
407
+ content: unknown,
408
+ status: ParameterStatus,
409
+ ): Promise<void> {
410
+ await this.syncTopic.publish({
411
+ name,
412
+ version,
413
+ content: content as Record<string, unknown>,
414
+ status,
415
+ instanceId: this.instanceId,
416
+ });
417
+ }
418
+
419
+ /**
420
+ * Handle incoming sync message from other instances.
421
+ */
422
+ protected async handleSyncMessage(payload: ConfigSyncPayload): Promise<void> {
423
+ // Ignore messages from self
424
+ if (payload.instanceId === this.instanceId) {
425
+ return;
426
+ }
427
+
428
+ const config = this.configs.get(payload.name);
429
+ if (!config) {
430
+ return;
431
+ }
432
+
433
+ // Update config with skipEvents to avoid infinite loop
434
+ if (payload.status === "current") {
435
+ await config.updateFromSync(payload.content);
436
+ }
437
+ }
438
+
439
+ /**
440
+ * Build tree structure from dot-notation names.
441
+ */
442
+ protected buildTree(names: string[]): ConfigTreeNode[] {
443
+ const root: ConfigTreeNode[] = [];
444
+
445
+ for (const name of names) {
446
+ const parts = name.split(".");
447
+ let currentLevel = root;
448
+
449
+ for (let i = 0; i < parts.length; i++) {
450
+ const part = parts[i];
451
+ const isLeaf = i === parts.length - 1;
452
+ const path = parts.slice(0, i + 1).join(".");
453
+
454
+ let existing = currentLevel.find((n) => n.name === part);
455
+
456
+ if (!existing) {
457
+ existing = {
458
+ name: part,
459
+ path,
460
+ isLeaf,
461
+ children: [],
462
+ };
463
+ currentLevel.push(existing);
464
+ }
465
+
466
+ if (isLeaf) {
467
+ existing.isLeaf = true;
468
+ }
469
+
470
+ currentLevel = existing.children;
471
+ }
472
+ }
473
+
474
+ return root;
475
+ }
476
+ }
477
+
478
+ export interface SaveConfigOptions {
479
+ activationDate?: Date;
480
+ changeDescription?: string;
481
+ tags?: string[];
482
+ creatorId?: string;
483
+ creatorName?: string;
484
+ }
485
+
486
+ export interface ConfigTreeNode {
487
+ name: string;
488
+ path: string;
489
+ isLeaf: boolean;
490
+ children: ConfigTreeNode[];
491
+ }
@@ -3,6 +3,25 @@ import { $atom, type Static, t } from "alepha";
3
3
  export const realmAuthSettingsAtom = $atom({
4
4
  name: "alepha.api.users.realmAuthSettings",
5
5
  schema: t.object({
6
+ // Branding and display settings
7
+ displayName: t.optional(
8
+ t.string({
9
+ description:
10
+ "Display name shown on auth pages (e.g., 'Customer Portal')",
11
+ }),
12
+ ),
13
+ description: t.optional(
14
+ t.string({
15
+ description: "Description shown on auth pages",
16
+ }),
17
+ ),
18
+ logoUrl: t.optional(
19
+ t.string({
20
+ description: "Logo URL for auth pages",
21
+ }),
22
+ ),
23
+
24
+ // Auth settings
6
25
  registrationAllowed: t.boolean({
7
26
  description: "Enable user self-registration",
8
27
  }),
@@ -1,5 +1,4 @@
1
1
  import { $inject, t } from "alepha";
2
- import { CryptoProvider } from "alepha/security";
3
2
  import { $action } from "alepha/server";
4
3
  import { ServerAuthProvider } from "alepha/server/auth";
5
4
  import { UserRealmProvider } from "../providers/UserRealmProvider.ts";
@@ -13,7 +12,6 @@ export class UserRealmController {
13
12
  protected readonly url = "/realms";
14
13
  protected readonly userRealmProvider = $inject(UserRealmProvider);
15
14
  protected readonly serverAuthProvider = $inject(ServerAuthProvider);
16
- protected readonly cryptoProvider = $inject(CryptoProvider);
17
15
 
18
16
  /**
19
17
  * Get realm configuration settings.
@@ -6,6 +6,7 @@ import { IdentityController } from "./controllers/IdentityController.ts";
6
6
  import { SessionController } from "./controllers/SessionController.ts";
7
7
  import { UserController } from "./controllers/UserController.ts";
8
8
  import { UserRealmController } from "./controllers/UserRealmController.ts";
9
+ import { UserNotifications } from "./notifications/UserNotifications.ts";
9
10
  import { UserRealmProvider } from "./providers/UserRealmProvider.ts";
10
11
  import { CredentialService } from "./services/CredentialService.ts";
11
12
  import { IdentityService } from "./services/IdentityService.ts";
@@ -76,5 +77,6 @@ export const AlephaApiUsers = $module({
76
77
  SessionController,
77
78
  IdentityController,
78
79
  UserRealmController,
80
+ UserNotifications,
79
81
  ],
80
82
  });
@@ -1,5 +1,6 @@
1
1
  import { $context } from "alepha";
2
2
  import { AlephaApiFiles } from "alepha/api/files";
3
+ import { AlephaApiJobs } from "alepha/api/jobs";
3
4
  import type { Repository } from "alepha/orm";
4
5
  import {
5
6
  $realm,
@@ -17,6 +18,7 @@ import {
17
18
  type WithLinkFn,
18
19
  type WithLoginFn,
19
20
  } from "alepha/server/auth";
21
+ import { AlephaApiAudits } from "../../api-audits/index.ts";
20
22
  import type { RealmAuthSettings } from "../atoms/realmAuthSettingsAtom.ts";
21
23
  import type { identities } from "../entities/identities.ts";
22
24
  import type { sessions } from "../entities/sessions.ts";
@@ -52,12 +54,17 @@ export const $userRealm = (
52
54
  const userRealm = userRealmProvider.register(name, options);
53
55
 
54
56
  if (options.modules?.audits) {
57
+ alepha.with(AlephaApiAudits);
55
58
  }
56
59
 
57
60
  if (options.modules?.files) {
58
61
  alepha.with(AlephaApiFiles);
59
62
  }
60
63
 
64
+ if (options.modules?.jobs) {
65
+ alepha.with(AlephaApiJobs);
66
+ }
67
+
61
68
  const realm: UserRealmPrimitive = $realm({
62
69
  ...options.realm,
63
70
  name,
@@ -103,12 +110,19 @@ export const $userRealm = (
103
110
  });
104
111
 
105
112
  realm.link = (name: string) => {
106
- return (ctx: LinkAccountOptions) => sessionService.link(name, ctx.user);
113
+ return (ctx: LinkAccountOptions) =>
114
+ sessionService.link(name, ctx.user, realm.name);
107
115
  };
108
116
 
109
117
  realm.login = (name: string) => {
110
- return (credentials: Credentials) =>
111
- sessionService.login(name, credentials.username, credentials.password);
118
+ return (credentials: Credentials) => {
119
+ return sessionService.login(
120
+ name,
121
+ credentials.username,
122
+ credentials.password,
123
+ realm.name,
124
+ );
125
+ };
112
126
  };
113
127
 
114
128
  const identities = options.identities ?? {
@@ -175,5 +189,6 @@ export interface UserRealmOptions {
175
189
  modules?: {
176
190
  files?: boolean;
177
191
  audits?: boolean;
192
+ jobs?: boolean;
178
193
  };
179
194
  }
@@ -81,9 +81,12 @@ export class UserRealmProvider {
81
81
 
82
82
  if (!realm) {
83
83
  // Auto-register default realm for backward compatibility
84
- if (userRealmName === DEFAULT_USER_REALM_NAME) {
85
- this.register(userRealmName);
86
- realm = this.realms.get(userRealmName)!;
84
+ const realms = Array.from(this.realms.values());
85
+ const firstRealm = realms[0];
86
+ if (userRealmName === DEFAULT_USER_REALM_NAME && firstRealm) {
87
+ realm = firstRealm;
88
+ } else if (this.alepha.isTest()) {
89
+ realm = this.register(userRealmName); // Auto-create default realm in tests
87
90
  } else {
88
91
  throw new AlephaError(
89
92
  `Missing user realm '${userRealmName}', please declare $userRealm in your application.`,
@@ -248,6 +248,7 @@ export class RegistrationService {
248
248
 
249
249
  // Create the user
250
250
  const user = await userRepository.create({
251
+ realm: userRealmName,
251
252
  username: intent.data.username,
252
253
  email: intent.data.email,
253
254
  phoneNumber: intent.data.phoneNumber,
@@ -340,7 +341,7 @@ export class RegistrationService {
340
341
  this.log.debug("Email verification code sent", { email });
341
342
  } catch (error) {
342
343
  // Silent fail - verification service may have rate limiting
343
- this.log.warn("Failed to send email verification code", { email, error });
344
+ this.log.warn("Failed to send email verification code", error);
344
345
  }
345
346
  }
346
347
 
@@ -77,6 +77,7 @@ export class SessionService {
77
77
  this.log.warn("Invalid login identifier format", {
78
78
  provider,
79
79
  username,
80
+ realm: name,
80
81
  });
81
82
  throw new InvalidCredentialsError();
82
83
  }
@@ -86,6 +87,7 @@ export class SessionService {
86
87
  this.log.warn("User not found during login attempt", {
87
88
  provider,
88
89
  username,
90
+ realm: name,
89
91
  });
90
92
  throw new InvalidCredentialsError();
91
93
  }
@@ -103,6 +105,7 @@ export class SessionService {
103
105
  provider,
104
106
  username,
105
107
  identityId: identity.id,
108
+ realm: name,
106
109
  });
107
110
  throw new InvalidCredentialsError();
108
111
  }
@@ -116,6 +119,7 @@ export class SessionService {
116
119
  this.log.warn("Invalid password during login attempt", {
117
120
  provider,
118
121
  username,
122
+ realm: name,
119
123
  });
120
124
  throw new InvalidCredentialsError();
121
125
  }
@@ -243,6 +243,8 @@ export class UserService {
243
243
  userRealmName,
244
244
  });
245
245
 
246
+ const realm = this.userRealmProvider.getRealm(userRealmName);
247
+
246
248
  // TODO: one query instead of 3
247
249
 
248
250
  // Check for existing user based on provided unique fields
@@ -290,6 +292,7 @@ export class UserService {
290
292
  const user = await this.users(userRealmName).create({
291
293
  ...data,
292
294
  roles: data.roles ?? ["user"], // TODO: Default roles from realm settings
295
+ realm: realm.name,
293
296
  });
294
297
 
295
298
  this.log.info("User created", {