alepha 0.13.2 → 0.13.3

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 (76) hide show
  1. package/dist/api-files/index.browser.js +80 -0
  2. package/dist/api-files/index.browser.js.map +1 -0
  3. package/dist/api-jobs/index.browser.js +56 -0
  4. package/dist/api-jobs/index.browser.js.map +1 -0
  5. package/dist/api-notifications/index.browser.js +382 -0
  6. package/dist/api-notifications/index.browser.js.map +1 -0
  7. package/dist/api-notifications/index.d.ts +124 -69
  8. package/dist/api-notifications/index.js +107 -55
  9. package/dist/api-notifications/index.js.map +1 -1
  10. package/dist/api-parameters/index.browser.js +29 -0
  11. package/dist/api-parameters/index.browser.js.map +1 -0
  12. package/dist/api-users/index.d.ts +16 -3
  13. package/dist/api-users/index.js +75 -28
  14. package/dist/api-users/index.js.map +1 -1
  15. package/dist/api-verifications/index.browser.js +52 -0
  16. package/dist/api-verifications/index.browser.js.map +1 -0
  17. package/dist/api-verifications/index.d.ts +117 -95
  18. package/dist/api-verifications/index.js +1 -1
  19. package/dist/api-verifications/index.js.map +1 -1
  20. package/dist/batch/index.js +0 -5
  21. package/dist/batch/index.js.map +1 -1
  22. package/dist/bucket/index.js +7 -5
  23. package/dist/bucket/index.js.map +1 -1
  24. package/dist/cli/{dist-Dl9Vl7Ur.js → dist-lGnqsKpu.js} +11 -15
  25. package/dist/cli/dist-lGnqsKpu.js.map +1 -0
  26. package/dist/cli/index.d.ts +26 -45
  27. package/dist/cli/index.js +40 -58
  28. package/dist/cli/index.js.map +1 -1
  29. package/dist/command/index.d.ts +1 -0
  30. package/dist/command/index.js +9 -0
  31. package/dist/command/index.js.map +1 -1
  32. package/dist/email/index.js +5 -0
  33. package/dist/email/index.js.map +1 -1
  34. package/dist/orm/index.js +3 -3
  35. package/dist/orm/index.js.map +1 -1
  36. package/dist/redis/index.d.ts +10 -10
  37. package/dist/security/index.d.ts +28 -28
  38. package/dist/security/index.js +3 -3
  39. package/dist/security/index.js.map +1 -1
  40. package/dist/server/index.d.ts +9 -9
  41. package/dist/server-auth/index.d.ts +152 -152
  42. package/dist/server-cookies/index.js +2 -2
  43. package/dist/server-cookies/index.js.map +1 -1
  44. package/dist/server-links/index.d.ts +33 -33
  45. package/dist/server-static/index.js +18 -2
  46. package/dist/server-static/index.js.map +1 -1
  47. package/package.json +16 -6
  48. package/src/api-files/index.browser.ts +17 -0
  49. package/src/api-jobs/index.browser.ts +15 -0
  50. package/src/api-notifications/controllers/NotificationController.ts +26 -1
  51. package/src/api-notifications/index.browser.ts +17 -0
  52. package/src/api-notifications/index.ts +1 -0
  53. package/src/api-notifications/schemas/notificationQuerySchema.ts +13 -0
  54. package/src/api-notifications/services/NotificationService.ts +45 -2
  55. package/src/api-parameters/index.browser.ts +12 -0
  56. package/src/api-users/atoms/realmAuthSettingsAtom.ts +3 -1
  57. package/src/api-users/controllers/UserController.ts +21 -1
  58. package/src/api-users/primitives/$userRealm.ts +33 -10
  59. package/src/api-users/providers/UserRealmProvider.ts +1 -0
  60. package/src/api-users/services/SessionService.ts +2 -0
  61. package/src/api-users/services/UserService.ts +56 -16
  62. package/src/api-verifications/index.browser.ts +15 -0
  63. package/src/api-verifications/index.ts +1 -0
  64. package/src/batch/providers/BatchProvider.ts +0 -7
  65. package/src/bucket/index.ts +7 -5
  66. package/src/cli/apps/AlephaCli.ts +27 -1
  67. package/src/cli/apps/AlephaPackageBuilderCli.ts +3 -0
  68. package/src/cli/commands/CoreCommands.ts +6 -2
  69. package/src/cli/commands/ViteCommands.ts +2 -1
  70. package/src/cli/services/ProjectUtils.ts +40 -75
  71. package/src/command/helpers/Asker.ts +10 -0
  72. package/src/email/index.ts +13 -5
  73. package/src/orm/providers/drivers/NodeSqliteProvider.ts +3 -3
  74. package/src/server-cookies/providers/ServerCookiesProvider.ts +2 -1
  75. package/src/server-static/providers/ServerStaticProvider.ts +18 -3
  76. package/dist/cli/dist-Dl9Vl7Ur.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  import { ServerRouteSecure } from "alepha/server/security";
2
- import * as alepha1 from "alepha";
2
+ import * as alepha21 from "alepha";
3
3
  import { Alepha, Async, KIND, Primitive, Static } from "alepha";
4
4
  import * as alepha_server0 from "alepha/server";
5
5
  import { ActionPrimitive, ClientRequestEntry, ClientRequestOptions, ClientRequestResponse, FetchResponse, HttpClient, RequestConfigSchema, ServerRequest, ServerRequestConfigEntry, ServerResponseBody, ServerTimingProvider } from "alepha/server";
@@ -9,23 +9,23 @@ import { ProxyPrimitiveOptions, ServerProxyProvider } from "alepha/server/proxy"
9
9
  import { ServiceAccountPrimitive, UserAccountToken } from "alepha/security";
10
10
 
11
11
  //#region src/server-links/schemas/apiLinksResponseSchema.d.ts
12
- declare const apiLinkSchema: alepha1.TObject<{
13
- name: alepha1.TString;
14
- group: alepha1.TOptional<alepha1.TString>;
15
- path: alepha1.TString;
16
- method: alepha1.TOptional<alepha1.TString>;
17
- requestBodyType: alepha1.TOptional<alepha1.TString>;
18
- service: alepha1.TOptional<alepha1.TString>;
12
+ declare const apiLinkSchema: alepha21.TObject<{
13
+ name: alepha21.TString;
14
+ group: alepha21.TOptional<alepha21.TString>;
15
+ path: alepha21.TString;
16
+ method: alepha21.TOptional<alepha21.TString>;
17
+ requestBodyType: alepha21.TOptional<alepha21.TString>;
18
+ service: alepha21.TOptional<alepha21.TString>;
19
19
  }>;
20
- declare const apiLinksResponseSchema: alepha1.TObject<{
21
- prefix: alepha1.TOptional<alepha1.TString>;
22
- links: alepha1.TArray<alepha1.TObject<{
23
- name: alepha1.TString;
24
- group: alepha1.TOptional<alepha1.TString>;
25
- path: alepha1.TString;
26
- method: alepha1.TOptional<alepha1.TString>;
27
- requestBodyType: alepha1.TOptional<alepha1.TString>;
28
- service: alepha1.TOptional<alepha1.TString>;
20
+ declare const apiLinksResponseSchema: alepha21.TObject<{
21
+ prefix: alepha21.TOptional<alepha21.TString>;
22
+ links: alepha21.TArray<alepha21.TObject<{
23
+ name: alepha21.TString;
24
+ group: alepha21.TOptional<alepha21.TString>;
25
+ path: alepha21.TString;
26
+ method: alepha21.TOptional<alepha21.TString>;
27
+ requestBodyType: alepha21.TOptional<alepha21.TString>;
28
+ service: alepha21.TOptional<alepha21.TString>;
29
29
  }>>;
30
30
  }>;
31
31
  type ApiLinksResponse = Static<typeof apiLinksResponseSchema>;
@@ -180,8 +180,8 @@ declare class RemotePrimitiveProvider {
180
180
  protected readonly remotes: Array<ServerRemote>;
181
181
  protected readonly log: alepha_logger0.Logger;
182
182
  getRemotes(): ServerRemote[];
183
- readonly configure: alepha1.HookPrimitive<"configure">;
184
- readonly start: alepha1.HookPrimitive<"start">;
183
+ readonly configure: alepha21.HookPrimitive<"configure">;
184
+ readonly start: alepha21.HookPrimitive<"start">;
185
185
  registerRemote(value: RemotePrimitive): Promise<void>;
186
186
  protected readonly fetchLinks: alepha_retry0.RetryPrimitiveFn<(opts: FetchLinksOptions) => Promise<ApiLinksResponse>>;
187
187
  }
@@ -249,22 +249,22 @@ declare class ServerLinksProvider {
249
249
  protected readonly remoteProvider: RemotePrimitiveProvider;
250
250
  protected readonly serverTimingProvider: ServerTimingProvider;
251
251
  get prefix(): string;
252
- readonly onRoute: alepha1.HookPrimitive<"configure">;
252
+ readonly onRoute: alepha21.HookPrimitive<"configure">;
253
253
  /**
254
254
  * First API - Get all API links for the user.
255
255
  *
256
256
  * This is based on the user's permissions.
257
257
  */
258
258
  readonly links: alepha_server0.RoutePrimitive<{
259
- response: alepha1.TObject<{
260
- prefix: alepha1.TOptional<alepha1.TString>;
261
- links: alepha1.TArray<alepha1.TObject<{
262
- name: alepha1.TString;
263
- group: alepha1.TOptional<alepha1.TString>;
264
- path: alepha1.TString;
265
- method: alepha1.TOptional<alepha1.TString>;
266
- requestBodyType: alepha1.TOptional<alepha1.TString>;
267
- service: alepha1.TOptional<alepha1.TString>;
259
+ response: alepha21.TObject<{
260
+ prefix: alepha21.TOptional<alepha21.TString>;
261
+ links: alepha21.TArray<alepha21.TObject<{
262
+ name: alepha21.TString;
263
+ group: alepha21.TOptional<alepha21.TString>;
264
+ path: alepha21.TString;
265
+ method: alepha21.TOptional<alepha21.TString>;
266
+ requestBodyType: alepha21.TOptional<alepha21.TString>;
267
+ service: alepha21.TOptional<alepha21.TString>;
268
268
  }>>;
269
269
  }>;
270
270
  }>;
@@ -275,10 +275,10 @@ declare class ServerLinksProvider {
275
275
  * I mean for 150+ links, you got 50ms of serialization time.
276
276
  */
277
277
  readonly schema: alepha_server0.RoutePrimitive<{
278
- params: alepha1.TObject<{
279
- name: alepha1.TString;
278
+ params: alepha21.TObject<{
279
+ name: alepha21.TString;
280
280
  }>;
281
- response: alepha1.TRecord<string, alepha1.TAny>;
281
+ response: alepha21.TRecord<string, alepha21.TAny>;
282
282
  }>;
283
283
  getSchemaByName(name: string, options?: GetApiLinksOptions): Promise<RequestConfigSchema>;
284
284
  /**
@@ -315,7 +315,7 @@ declare module "alepha" {
315
315
  * @see {@link $client}
316
316
  * @module alepha.server.links
317
317
  */
318
- declare const AlephaServerLinks: alepha1.Service<alepha1.Module>;
318
+ declare const AlephaServerLinks: alepha21.Service<alepha21.Module>;
319
319
  //#endregion
320
320
  export { $client, $remote, AlephaServerLinks, ApiLink, ApiLinksResponse, ClientScope, FetchLinksOptions, GetApiLinksOptions, HttpClientLink, HttpVirtualClient, LinkProvider, RemotePrimitive, RemotePrimitiveOptions, RemotePrimitiveProvider, ServerLinksProvider, ServerRemote, VirtualAction, apiLinkSchema, apiLinksResponseSchema };
321
321
  //# sourceMappingURL=index.d.ts.map
@@ -73,7 +73,15 @@ var ServerStaticProvider = class {
73
73
  }
74
74
  reply.headers["content-type"] = "text/html";
75
75
  reply.status = 200;
76
- return createReadStream(join(root, "index.html"));
76
+ return new Promise((resolve, reject) => {
77
+ const stream = createReadStream(join(root, "index.html"));
78
+ stream.on("open", () => {
79
+ resolve(stream);
80
+ });
81
+ stream.on("error", (err) => {
82
+ reject(err);
83
+ });
84
+ });
77
85
  }
78
86
  });
79
87
  }
@@ -111,7 +119,15 @@ var ServerStaticProvider = class {
111
119
  reply.status = 304;
112
120
  return;
113
121
  }
114
- return createReadStream(path);
122
+ return new Promise((resolve, reject) => {
123
+ const stream = createReadStream(path);
124
+ stream.on("open", () => {
125
+ resolve(stream);
126
+ });
127
+ stream.on("error", (err) => {
128
+ reject(err);
129
+ });
130
+ });
115
131
  };
116
132
  }
117
133
  getCacheFileTypes() {
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/server-static/primitives/$serve.ts","../../src/server-static/providers/ServerStaticProvider.ts","../../src/server-static/index.ts"],"sourcesContent":["import { createPrimitive, KIND, Primitive } from \"alepha\";\nimport type { DurationLike } from \"alepha/datetime\";\n\n/**\n * Create a new static file handler.\n */\nexport const $serve = (options: ServePrimitiveOptions = {}): ServePrimitive => {\n return createPrimitive(ServePrimitive, options);\n};\n\nexport interface ServePrimitiveOptions {\n /**\n * Prefix for the served path.\n *\n * @default \"/\"\n */\n path?: string;\n\n /**\n * Path to the directory to serve.\n *\n * @default process.cwd()\n */\n root?: string;\n\n /**\n * If true, primitive will be ignored.\n *\n * @default false\n */\n disabled?: boolean;\n\n /**\n * Whether to keep dot files (e.g. `.gitignore`, `.env`) in the served directory.\n *\n * @default true\n */\n ignoreDotEnvFiles?: boolean;\n\n /**\n * Whether to use the index.html file when the path is a directory.\n *\n * @default true\n */\n indexFallback?: boolean;\n\n /**\n * Force all requests \"not found\" to be served with the index.html file.\n * This is useful for single-page applications (SPAs) that use client-side only routing.\n */\n historyApiFallback?: boolean;\n\n /**\n * Optional name of the primitive.\n * This is used for logging and debugging purposes.\n *\n * @default Key name.\n */\n name?: string;\n\n /**\n * Whether to use cache control headers.\n *\n * @default {}\n */\n cacheControl?: Partial<CacheControlOptions> | false;\n}\n\nexport interface CacheControlOptions {\n /**\n * Whether to use cache control headers.\n *\n * @default [.js, .css]\n */\n fileTypes: string[];\n\n /**\n * The maximum age of the cache in seconds.\n *\n * @default 60 * 60 * 24 * 2 // 2 days\n */\n maxAge: DurationLike;\n\n /**\n * Whether to use immutable cache control headers.\n *\n * @default true\n */\n immutable: boolean;\n}\n\nexport class ServePrimitive extends Primitive<ServePrimitiveOptions> {}\n\n$serve[KIND] = ServePrimitive;\n","import { createReadStream } from \"node:fs\";\nimport { access, readdir, stat } from \"node:fs/promises\";\nimport { basename, isAbsolute, join } from \"node:path\";\nimport type { Readable as NodeStream } from \"node:stream\";\nimport { $hook, $inject, Alepha } from \"alepha\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { FileDetector } from \"alepha/file\";\nimport { $logger } from \"alepha/logger\";\nimport { type ServerHandler, ServerRouterProvider } from \"alepha/server\";\nimport { $serve, type ServePrimitiveOptions } from \"../primitives/$serve.ts\";\n\nexport class ServerStaticProvider {\n protected readonly alepha = $inject(Alepha);\n protected readonly routerProvider = $inject(ServerRouterProvider);\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n protected readonly fileDetector = $inject(FileDetector);\n protected readonly log = $logger();\n protected readonly directories: ServeDirectory[] = [];\n\n protected readonly configure = $hook({\n on: \"configure\",\n handler: async () => {\n await Promise.all(\n this.alepha\n .primitives($serve)\n .map((it) => this.createStaticServer(it.options)),\n );\n },\n });\n\n public async createStaticServer(\n options: ServePrimitiveOptions,\n ): Promise<void> {\n const prefix = options.path ?? \"/\";\n\n let root = options.root ?? process.cwd();\n if (!isAbsolute(root)) {\n root = join(process.cwd(), root);\n }\n\n this.log.debug(\"Serve static files\", { prefix, root });\n\n await stat(root);\n\n // 1. get all files in the root directory (recursively)\n const files = await this.getAllFiles(root, options.ignoreDotEnvFiles);\n\n // 2. create a $route for each file (yes, this could be a lot of routes)\n const routes = await Promise.all(\n files.map(async (file) => {\n const path = file.replace(root, \"\").replace(/\\\\/g, \"/\");\n this.log.trace(`Mount ${join(prefix, path)} -> ${join(root, path)}`);\n return {\n path: join(prefix, encodeURI(path)),\n handler: await this.createFileHandler(join(root, path), options),\n };\n }),\n );\n\n for (const route of routes) {\n this.routerProvider.createRoute(route);\n\n // if route is for index.html, also create a route without it\n // e.g. /my/path/index.html -> /my/path/\n if (\n options.indexFallback !== false &&\n route.path.endsWith(\"index.html\")\n ) {\n this.routerProvider.createRoute({\n path: route.path.replace(/index\\.html$/, \"\"),\n handler: route.handler,\n });\n }\n }\n\n // 3. store the directory info for reference\n this.directories.push({\n options,\n files: files.map((file) => file.replace(root, \"\").replace(/\\\\/g, \"/\")),\n });\n\n // bonus! for SPAs, handle history API fallback\n if (options.historyApiFallback) {\n // meaning all unmatched routes should serve index.html\n this.routerProvider.createRoute({\n path: join(prefix, \"*\").replace(/\\\\/g, \"/\"),\n handler: async (request) => {\n const { reply } = request;\n\n if (request.url.pathname.includes(\".\")) {\n // If the request is for a file (e.g., /style.css), do not fall back\n reply.headers[\"content-type\"] = \"text/plain\";\n reply.body = \"Not Found\";\n reply.status = 404;\n return;\n }\n\n reply.headers[\"content-type\"] = \"text/html\";\n reply.status = 200;\n\n // Serve index.html for all unmatched routes\n return createReadStream(join(root, \"index.html\"));\n },\n });\n }\n }\n\n public async createFileHandler(\n filepath: string,\n options: ServePrimitiveOptions,\n ): Promise<ServerHandler> {\n const filename = basename(filepath);\n\n const hasGzip = await access(`${filepath}.gz`)\n .then(() => true)\n .catch(() => false);\n\n const hasBr = await access(`${filepath}.br`)\n .then(() => true)\n .catch(() => false);\n\n const fileStat = await stat(filepath);\n const lastModified = fileStat.mtime.toUTCString();\n const etag = `\"${fileStat.size}-${fileStat.mtime.getTime()}\"`;\n const contentType = this.fileDetector.getContentType(filename);\n const cacheControl = this.getCacheControl(filename, options);\n\n return async (request): Promise<NodeStream | undefined> => {\n const { headers, reply } = request;\n let path = filepath;\n\n const encoding = headers[\"accept-encoding\"];\n if (encoding) {\n if (hasBr && encoding.includes(\"br\")) {\n reply.headers[\"content-encoding\"] = \"br\";\n path += \".br\";\n } else if (hasGzip && encoding.includes(\"gzip\")) {\n reply.headers[\"content-encoding\"] = \"gzip\";\n path += \".gz\";\n }\n }\n\n reply.headers[\"content-type\"] = contentType;\n reply.headers[\"accept-ranges\"] = \"bytes\";\n reply.headers[\"last-modified\"] = lastModified;\n\n if (cacheControl) {\n reply.headers[\"cache-control\"] =\n `public, max-age=${cacheControl.maxAge}`;\n if (cacheControl.immutable) {\n reply.headers[\"cache-control\"] += \", immutable\";\n }\n }\n\n reply.headers.etag = etag;\n if (\n headers[\"if-none-match\"] === etag ||\n headers[\"if-modified-since\"] === lastModified\n ) {\n reply.status = 304;\n return;\n }\n\n return createReadStream(path);\n };\n }\n\n protected getCacheFileTypes(): string[] {\n return [\n \".js\",\n \".css\",\n \".woff\",\n \".woff2\",\n \".ttf\",\n \".eot\",\n \".otf\",\n \".jpg\",\n \".jpeg\",\n \".png\",\n \".svg\",\n \".gif\",\n ];\n }\n\n protected getCacheControl(\n filename: string,\n options: ServePrimitiveOptions,\n ): { maxAge: number; immutable: boolean } | undefined {\n if (!options.cacheControl) {\n return;\n }\n\n const fileTypes =\n options.cacheControl.fileTypes ?? this.getCacheFileTypes();\n\n for (const type of fileTypes) {\n if (filename.endsWith(type)) {\n return {\n immutable: options.cacheControl.immutable ?? true,\n maxAge: this.dateTimeProvider\n .duration(options.cacheControl.maxAge ?? [30, \"days\"])\n .as(\"seconds\"),\n };\n }\n }\n }\n\n public async getAllFiles(\n dir: string,\n ignoreDotEnvFiles = true,\n ): Promise<string[]> {\n const entries = await readdir(dir, { withFileTypes: true });\n\n const files = await Promise.all(\n entries.map((dirent) => {\n // skip .env & other dot files\n if (ignoreDotEnvFiles && dirent.name.startsWith(\".\")) {\n return [];\n }\n\n const fullPath = join(dir, dirent.name);\n return dirent.isDirectory() ? this.getAllFiles(fullPath) : fullPath;\n }),\n );\n\n return files.flat();\n }\n}\n\nexport interface ServeDirectory {\n options: ServePrimitiveOptions;\n files: string[];\n}\n","import { $module } from \"alepha\";\nimport { AlephaServer } from \"alepha/server\";\nimport { $serve } from \"./primitives/$serve.ts\";\nimport { ServerStaticProvider } from \"./providers/ServerStaticProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./primitives/$serve.ts\";\nexport * from \"./providers/ServerStaticProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Create static file server with `$static()`.\n *\n * @see {@link ServerStaticProvider}\n * @module alepha.server.static\n */\nexport const AlephaServerStatic = $module({\n name: \"alepha.server.static\",\n primitives: [$serve],\n services: [AlephaServer, ServerStaticProvider],\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/server-static/primitives/$serve.ts","../../src/server-static/providers/ServerStaticProvider.ts","../../src/server-static/index.ts"],"sourcesContent":["import { createPrimitive, KIND, Primitive } from \"alepha\";\nimport type { DurationLike } from \"alepha/datetime\";\n\n/**\n * Create a new static file handler.\n */\nexport const $serve = (options: ServePrimitiveOptions = {}): ServePrimitive => {\n return createPrimitive(ServePrimitive, options);\n};\n\nexport interface ServePrimitiveOptions {\n /**\n * Prefix for the served path.\n *\n * @default \"/\"\n */\n path?: string;\n\n /**\n * Path to the directory to serve.\n *\n * @default process.cwd()\n */\n root?: string;\n\n /**\n * If true, primitive will be ignored.\n *\n * @default false\n */\n disabled?: boolean;\n\n /**\n * Whether to keep dot files (e.g. `.gitignore`, `.env`) in the served directory.\n *\n * @default true\n */\n ignoreDotEnvFiles?: boolean;\n\n /**\n * Whether to use the index.html file when the path is a directory.\n *\n * @default true\n */\n indexFallback?: boolean;\n\n /**\n * Force all requests \"not found\" to be served with the index.html file.\n * This is useful for single-page applications (SPAs) that use client-side only routing.\n */\n historyApiFallback?: boolean;\n\n /**\n * Optional name of the primitive.\n * This is used for logging and debugging purposes.\n *\n * @default Key name.\n */\n name?: string;\n\n /**\n * Whether to use cache control headers.\n *\n * @default {}\n */\n cacheControl?: Partial<CacheControlOptions> | false;\n}\n\nexport interface CacheControlOptions {\n /**\n * Whether to use cache control headers.\n *\n * @default [.js, .css]\n */\n fileTypes: string[];\n\n /**\n * The maximum age of the cache in seconds.\n *\n * @default 60 * 60 * 24 * 2 // 2 days\n */\n maxAge: DurationLike;\n\n /**\n * Whether to use immutable cache control headers.\n *\n * @default true\n */\n immutable: boolean;\n}\n\nexport class ServePrimitive extends Primitive<ServePrimitiveOptions> {}\n\n$serve[KIND] = ServePrimitive;\n","import { createReadStream } from \"node:fs\";\nimport { access, readdir, stat } from \"node:fs/promises\";\nimport { basename, isAbsolute, join } from \"node:path\";\nimport type { Readable as NodeStream } from \"node:stream\";\nimport { $hook, $inject, Alepha } from \"alepha\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { FileDetector } from \"alepha/file\";\nimport { $logger } from \"alepha/logger\";\nimport { type ServerHandler, ServerRouterProvider } from \"alepha/server\";\nimport { $serve, type ServePrimitiveOptions } from \"../primitives/$serve.ts\";\n\nexport class ServerStaticProvider {\n protected readonly alepha = $inject(Alepha);\n protected readonly routerProvider = $inject(ServerRouterProvider);\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n protected readonly fileDetector = $inject(FileDetector);\n protected readonly log = $logger();\n protected readonly directories: ServeDirectory[] = [];\n\n protected readonly configure = $hook({\n on: \"configure\",\n handler: async () => {\n await Promise.all(\n this.alepha\n .primitives($serve)\n .map((it) => this.createStaticServer(it.options)),\n );\n },\n });\n\n public async createStaticServer(\n options: ServePrimitiveOptions,\n ): Promise<void> {\n const prefix = options.path ?? \"/\";\n\n let root = options.root ?? process.cwd();\n if (!isAbsolute(root)) {\n root = join(process.cwd(), root);\n }\n\n this.log.debug(\"Serve static files\", { prefix, root });\n\n await stat(root);\n\n // 1. get all files in the root directory (recursively)\n const files = await this.getAllFiles(root, options.ignoreDotEnvFiles);\n\n // 2. create a $route for each file (yes, this could be a lot of routes)\n const routes = await Promise.all(\n files.map(async (file) => {\n const path = file.replace(root, \"\").replace(/\\\\/g, \"/\");\n this.log.trace(`Mount ${join(prefix, path)} -> ${join(root, path)}`);\n return {\n path: join(prefix, encodeURI(path)),\n handler: await this.createFileHandler(join(root, path), options),\n };\n }),\n );\n\n for (const route of routes) {\n this.routerProvider.createRoute(route);\n\n // if route is for index.html, also create a route without it\n // e.g. /my/path/index.html -> /my/path/\n if (\n options.indexFallback !== false &&\n route.path.endsWith(\"index.html\")\n ) {\n this.routerProvider.createRoute({\n path: route.path.replace(/index\\.html$/, \"\"),\n handler: route.handler,\n });\n }\n }\n\n // 3. store the directory info for reference\n this.directories.push({\n options,\n files: files.map((file) => file.replace(root, \"\").replace(/\\\\/g, \"/\")),\n });\n\n // bonus! for SPAs, handle history API fallback\n if (options.historyApiFallback) {\n // meaning all unmatched routes should serve index.html\n this.routerProvider.createRoute({\n path: join(prefix, \"*\").replace(/\\\\/g, \"/\"),\n handler: async (request) => {\n const { reply } = request;\n\n if (request.url.pathname.includes(\".\")) {\n // If the request is for a file (e.g., /style.css), do not fall back\n reply.headers[\"content-type\"] = \"text/plain\";\n reply.body = \"Not Found\";\n reply.status = 404;\n return;\n }\n\n reply.headers[\"content-type\"] = \"text/html\";\n reply.status = 200;\n\n return new Promise<any>((resolve, reject) => {\n const stream = createReadStream(join(root, \"index.html\"));\n stream.on(\"open\", () => {\n resolve(stream);\n });\n stream.on(\"error\", (err) => {\n reject(err);\n });\n });\n },\n });\n }\n }\n\n public async createFileHandler(\n filepath: string,\n options: ServePrimitiveOptions,\n ): Promise<ServerHandler> {\n const filename = basename(filepath);\n\n const hasGzip = await access(`${filepath}.gz`)\n .then(() => true)\n .catch(() => false);\n\n const hasBr = await access(`${filepath}.br`)\n .then(() => true)\n .catch(() => false);\n\n const fileStat = await stat(filepath);\n const lastModified = fileStat.mtime.toUTCString();\n const etag = `\"${fileStat.size}-${fileStat.mtime.getTime()}\"`;\n const contentType = this.fileDetector.getContentType(filename);\n const cacheControl = this.getCacheControl(filename, options);\n\n return async (request): Promise<NodeStream | undefined> => {\n const { headers, reply } = request;\n let path = filepath;\n\n const encoding = headers[\"accept-encoding\"];\n if (encoding) {\n if (hasBr && encoding.includes(\"br\")) {\n reply.headers[\"content-encoding\"] = \"br\";\n path += \".br\";\n } else if (hasGzip && encoding.includes(\"gzip\")) {\n reply.headers[\"content-encoding\"] = \"gzip\";\n path += \".gz\";\n }\n }\n\n reply.headers[\"content-type\"] = contentType;\n reply.headers[\"accept-ranges\"] = \"bytes\";\n reply.headers[\"last-modified\"] = lastModified;\n\n if (cacheControl) {\n reply.headers[\"cache-control\"] =\n `public, max-age=${cacheControl.maxAge}`;\n if (cacheControl.immutable) {\n reply.headers[\"cache-control\"] += \", immutable\";\n }\n }\n\n reply.headers.etag = etag;\n if (\n headers[\"if-none-match\"] === etag ||\n headers[\"if-modified-since\"] === lastModified\n ) {\n reply.status = 304;\n return;\n }\n\n return new Promise<any>((resolve, reject) => {\n const stream = createReadStream(path);\n stream.on(\"open\", () => {\n resolve(stream);\n });\n stream.on(\"error\", (err) => {\n reject(err);\n });\n });\n };\n }\n\n protected getCacheFileTypes(): string[] {\n return [\n \".js\",\n \".css\",\n \".woff\",\n \".woff2\",\n \".ttf\",\n \".eot\",\n \".otf\",\n \".jpg\",\n \".jpeg\",\n \".png\",\n \".svg\",\n \".gif\",\n ];\n }\n\n protected getCacheControl(\n filename: string,\n options: ServePrimitiveOptions,\n ): { maxAge: number; immutable: boolean } | undefined {\n if (!options.cacheControl) {\n return;\n }\n\n const fileTypes =\n options.cacheControl.fileTypes ?? this.getCacheFileTypes();\n\n for (const type of fileTypes) {\n if (filename.endsWith(type)) {\n return {\n immutable: options.cacheControl.immutable ?? true,\n maxAge: this.dateTimeProvider\n .duration(options.cacheControl.maxAge ?? [30, \"days\"])\n .as(\"seconds\"),\n };\n }\n }\n }\n\n public async getAllFiles(\n dir: string,\n ignoreDotEnvFiles = true,\n ): Promise<string[]> {\n const entries = await readdir(dir, { withFileTypes: true });\n\n const files = await Promise.all(\n entries.map((dirent) => {\n // skip .env & other dot files\n if (ignoreDotEnvFiles && dirent.name.startsWith(\".\")) {\n return [];\n }\n\n const fullPath = join(dir, dirent.name);\n return dirent.isDirectory() ? this.getAllFiles(fullPath) : fullPath;\n }),\n );\n\n return files.flat();\n }\n}\n\nexport interface ServeDirectory {\n options: ServePrimitiveOptions;\n files: string[];\n}\n","import { $module } from \"alepha\";\nimport { AlephaServer } from \"alepha/server\";\nimport { $serve } from \"./primitives/$serve.ts\";\nimport { ServerStaticProvider } from \"./providers/ServerStaticProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./primitives/$serve.ts\";\nexport * from \"./providers/ServerStaticProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Create static file server with `$static()`.\n *\n * @see {@link ServerStaticProvider}\n * @module alepha.server.static\n */\nexport const AlephaServerStatic = $module({\n name: \"alepha.server.static\",\n primitives: [$serve],\n services: [AlephaServer, ServerStaticProvider],\n});\n"],"mappings":"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"}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "alepha",
3
3
  "description": "Easy-to-use modern TypeScript framework for building many kind of applications.",
4
4
  "author": "Nicolas Foures",
5
- "version": "0.13.2",
5
+ "version": "0.13.3",
6
6
  "type": "module",
7
7
  "engines": {
8
8
  "node": ">=22.0.0"
@@ -23,10 +23,10 @@
23
23
  "drizzle-kit": "^0.31.7",
24
24
  "drizzle-orm": "^0.44.7",
25
25
  "postgres": "^3.4.7",
26
- "tsx": "^4.20.6",
27
- "typebox": "^1.0.58",
26
+ "tsx": "^4.21.0",
27
+ "typebox": "^1.0.61",
28
28
  "typescript": "^5.9.3",
29
- "vite": "^7.2.4",
29
+ "vite": "^7.2.6",
30
30
  "vite-bundle-analyzer": "^1.2.3",
31
31
  "ws": "^8.18.3"
32
32
  },
@@ -37,12 +37,12 @@
37
37
  "@types/nodemailer": "^7.0.4",
38
38
  "@types/ws": "^8.18.1",
39
39
  "cron-schedule": "^6.0.0",
40
- "jose": "^6.1.2",
40
+ "jose": "^6.1.3",
41
41
  "nodemailer": "^7.0.11",
42
42
  "openid-client": "^6.8.1",
43
43
  "prom-client": "^15.1.3",
44
44
  "swagger-ui-dist": "^5.30.3",
45
- "vitest": "^4.0.14"
45
+ "vitest": "^4.0.15"
46
46
  },
47
47
  "scarfSettings": {
48
48
  "enabled": false
@@ -70,21 +70,29 @@
70
70
  "exports": {
71
71
  "./api/files": {
72
72
  "types": "./dist/api-files/index.js",
73
+ "react-native": "./dist/api-files/index.browser.js",
74
+ "browser": "./dist/api-files/index.browser.js",
73
75
  "import": "./dist/api-files/index.js",
74
76
  "default": "./dist/api-files/index.js"
75
77
  },
76
78
  "./api/jobs": {
77
79
  "types": "./dist/api-jobs/index.js",
80
+ "react-native": "./dist/api-jobs/index.browser.js",
81
+ "browser": "./dist/api-jobs/index.browser.js",
78
82
  "import": "./dist/api-jobs/index.js",
79
83
  "default": "./dist/api-jobs/index.js"
80
84
  },
81
85
  "./api/notifications": {
82
86
  "types": "./dist/api-notifications/index.js",
87
+ "react-native": "./dist/api-notifications/index.browser.js",
88
+ "browser": "./dist/api-notifications/index.browser.js",
83
89
  "import": "./dist/api-notifications/index.js",
84
90
  "default": "./dist/api-notifications/index.js"
85
91
  },
86
92
  "./api/parameters": {
87
93
  "types": "./dist/api-parameters/index.js",
94
+ "react-native": "./dist/api-parameters/index.browser.js",
95
+ "browser": "./dist/api-parameters/index.browser.js",
88
96
  "import": "./dist/api-parameters/index.js",
89
97
  "default": "./dist/api-parameters/index.js"
90
98
  },
@@ -97,6 +105,8 @@
97
105
  },
98
106
  "./api/verifications": {
99
107
  "types": "./dist/api-verifications/index.js",
108
+ "react-native": "./dist/api-verifications/index.browser.js",
109
+ "browser": "./dist/api-verifications/index.browser.js",
100
110
  "import": "./dist/api-verifications/index.js",
101
111
  "default": "./dist/api-verifications/index.js"
102
112
  },
@@ -0,0 +1,17 @@
1
+ import { $module } from "alepha";
2
+
3
+ // ---------------------------------------------------------------------------------------------------------------------
4
+
5
+ export type * from "./controllers/FileController.ts";
6
+ export type * from "./controllers/StorageStatsController.ts";
7
+ export * from "./entities/files.ts";
8
+ export * from "./schemas/fileQuerySchema.ts";
9
+ export * from "./schemas/fileResourceSchema.ts";
10
+ export * from "./schemas/storageStatsSchema.ts";
11
+
12
+ // ---------------------------------------------------------------------------------------------------------------------
13
+
14
+ export const AlephaApiFiles = $module({
15
+ name: "alepha.api.files",
16
+ services: [],
17
+ });
@@ -0,0 +1,15 @@
1
+ import { $module } from "alepha";
2
+
3
+ // ---------------------------------------------------------------------------------------------------------------------
4
+
5
+ export * from "./entities/jobExecutions.ts";
6
+ export * from "./schemas/jobExecutionQuerySchema.ts";
7
+ export * from "./schemas/jobExecutionResourceSchema.ts";
8
+ export * from "./schemas/triggerJobSchema.ts";
9
+
10
+ // ---------------------------------------------------------------------------------------------------------------------
11
+
12
+ export const AlephaApiJobs = $module({
13
+ name: "alepha.api.jobs",
14
+ services: [],
15
+ });
@@ -1 +1,26 @@
1
- export class NotificationController {}
1
+ import { $inject } from "alepha";
2
+ import { pg } from "alepha/orm";
3
+ import { $action } from "alepha/server";
4
+ import { notifications } from "../entities/notifications.ts";
5
+ import { notificationQuerySchema } from "../schemas/notificationQuerySchema.ts";
6
+ import { NotificationService } from "../services/NotificationService.ts";
7
+
8
+ export class NotificationController {
9
+ protected readonly url = "/notifications";
10
+ protected readonly group = "notifications";
11
+ protected readonly notificationService = $inject(NotificationService);
12
+
13
+ /**
14
+ * Find notifications with pagination and filtering.
15
+ */
16
+ public readonly findNotifications = $action({
17
+ path: this.url,
18
+ group: this.group,
19
+ description: "Find notifications with pagination and filtering",
20
+ schema: {
21
+ query: notificationQuerySchema,
22
+ response: pg.page(notifications.schema),
23
+ },
24
+ handler: ({ query }) => this.notificationService.findNotifications(query),
25
+ });
26
+ }
@@ -0,0 +1,17 @@
1
+ import { $module } from "alepha";
2
+
3
+ // ---------------------------------------------------------------------------------------------------------------------
4
+
5
+ export * from "./controllers/NotificationController.ts";
6
+ export * from "./entities/notifications.ts";
7
+ export * from "./schemas/notificationContactPreferencesSchema.ts";
8
+ export * from "./schemas/notificationContactSchema.ts";
9
+ export * from "./schemas/notificationCreateSchema.ts";
10
+ export * from "./schemas/notificationQuerySchema.ts";
11
+
12
+ // ---------------------------------------------------------------------------------------------------------------------
13
+
14
+ export const AlephaApiNotifications = $module({
15
+ name: "alepha.api.notifications",
16
+ services: [],
17
+ });
@@ -18,6 +18,7 @@ export * from "./primitives/$notification.ts";
18
18
  export * from "./queues/NotificationQueues.ts";
19
19
  export * from "./schemas/notificationContactPreferencesSchema.ts";
20
20
  export * from "./schemas/notificationCreateSchema.ts";
21
+ export * from "./schemas/notificationQuerySchema.ts";
21
22
  export * from "./services/NotificationSenderService.ts";
22
23
  export * from "./services/NotificationService.ts";
23
24
 
@@ -0,0 +1,13 @@
1
+ import type { Static } from "alepha";
2
+ import { t } from "alepha";
3
+ import { pageQuerySchema } from "alepha/orm";
4
+
5
+ export const notificationQuerySchema = t.extend(pageQuerySchema, {
6
+ type: t.optional(t.enum(["email", "sms"])),
7
+ template: t.optional(t.string()),
8
+ contact: t.optional(t.string()),
9
+ category: t.optional(t.string()),
10
+ status: t.optional(t.enum(["pending", "sent", "failed"])),
11
+ });
12
+
13
+ export type NotificationQuery = Static<typeof notificationQuerySchema>;
@@ -2,13 +2,17 @@ import { $env, $inject, Alepha, type Static, t } from "alepha";
2
2
  import { $batch } from "alepha/batch";
3
3
  import { DateTimeProvider } from "alepha/datetime";
4
4
  import { $logger } from "alepha/logger";
5
- import { $repository } from "alepha/orm";
6
- import { notifications } from "../entities/notifications.ts";
5
+ import { $repository, type Page } from "alepha/orm";
6
+ import {
7
+ type NotificationEntity,
8
+ notifications,
9
+ } from "../entities/notifications.ts";
7
10
  import { NotificationQueues } from "../queues/NotificationQueues.ts";
8
11
  import {
9
12
  type NotificationCreate,
10
13
  notificationCreateSchema,
11
14
  } from "../schemas/notificationCreateSchema.ts";
15
+ import type { NotificationQuery } from "../schemas/notificationQuerySchema.ts";
12
16
  import { NotificationSenderService } from "./NotificationSenderService.ts";
13
17
 
14
18
  export const notificationServiceEnvSchema = t.object({
@@ -65,6 +69,45 @@ export class NotificationService {
65
69
  return this.notificationRepository.findOne({ where: { id } });
66
70
  }
67
71
 
72
+ public async findNotifications(
73
+ q: NotificationQuery = {},
74
+ ): Promise<Page<NotificationEntity>> {
75
+ this.log.trace("Finding notifications", { query: q });
76
+ q.sort ??= "-createdAt";
77
+
78
+ const where = this.notificationRepository.createQueryWhere();
79
+
80
+ if (q.type) {
81
+ where.type = { eq: q.type };
82
+ }
83
+
84
+ if (q.template) {
85
+ where.template = { like: `%${q.template}%` };
86
+ }
87
+
88
+ if (q.contact) {
89
+ where.contact = { like: `%${q.contact}%` };
90
+ }
91
+
92
+ if (q.category) {
93
+ where.category = { eq: q.category };
94
+ }
95
+
96
+ if (q.status) {
97
+ if (q.status === "sent") {
98
+ where.sentAt = { isNotNull: true };
99
+ where.error = { isNull: true };
100
+ } else if (q.status === "failed") {
101
+ where.error = { isNotNull: true };
102
+ } else if (q.status === "pending") {
103
+ where.sentAt = { isNull: true };
104
+ where.error = { isNull: true };
105
+ }
106
+ }
107
+
108
+ return this.notificationRepository.paginate(q, { where }, { count: true });
109
+ }
110
+
68
111
  /**
69
112
  * Create a new notification.
70
113
  */
@@ -0,0 +1,12 @@
1
+ import { $module } from "alepha";
2
+
3
+ // ---------------------------------------------------------------------------------------------------------------------
4
+
5
+ export * from "./entities/parameters.ts";
6
+
7
+ // ---------------------------------------------------------------------------------------------------------------------
8
+
9
+ export const AlephaApiParameters = $module({
10
+ name: "alepha.api.parameters",
11
+ services: [],
12
+ });
@@ -60,10 +60,11 @@ export const realmAuthSettingsAtom = $atom({
60
60
  }),
61
61
  }),
62
62
  default: {
63
+ // for a fresh hello world setup, we accept registration and email login
63
64
  registrationAllowed: true,
64
65
  emailEnabled: true,
65
66
  emailRequired: true,
66
- usernameEnabled: true,
67
+ usernameEnabled: false,
67
68
  usernameRequired: false,
68
69
  phoneEnabled: false,
69
70
  phoneRequired: false,
@@ -72,6 +73,7 @@ export const realmAuthSettingsAtom = $atom({
72
73
  resetPasswordAllowed: false,
73
74
  firstNameLastNameEnabled: false,
74
75
  firstNameLastNameRequired: false,
76
+ // TODO: not implemented yet
75
77
  passwordPolicy: {
76
78
  minLength: 8,
77
79
  requireUppercase: true,
@@ -310,6 +310,8 @@ export class UserController {
310
310
  /**
311
311
  * Request email verification.
312
312
  * Generates a verification token using verification service and sends an email to the user.
313
+ * @param method - The verification method: "code" (default) sends a 6-digit code, "link" sends a clickable verification link.
314
+ * @param verifyUrl - Required when method is "link". The base URL for the verification link. Token and email will be appended as query params.
313
315
  */
314
316
  public requestEmailVerification = $action({
315
317
  path: "/users/email-verification/request",
@@ -317,6 +319,19 @@ export class UserController {
317
319
  schema: {
318
320
  query: t.object({
319
321
  userRealmName: t.optional(t.string()),
322
+ method: t.optional(
323
+ t.enum(["code", "link"], {
324
+ default: "code",
325
+ description:
326
+ 'Verification method: "code" sends a 6-digit code, "link" sends a clickable verification link.',
327
+ }),
328
+ ),
329
+ verifyUrl: t.optional(
330
+ t.string({
331
+ description:
332
+ 'Base URL for verification link. Required when method is "link". Token and email will be appended as query params.',
333
+ }),
334
+ ),
320
335
  }),
321
336
  body: t.object({
322
337
  email: t.email(),
@@ -327,15 +342,20 @@ export class UserController {
327
342
  }),
328
343
  },
329
344
  handler: async ({ body, query }) => {
345
+ const method = query.method ?? "code";
330
346
  await this.userService.requestEmailVerification(
331
347
  body.email,
332
348
  query.userRealmName,
349
+ method,
350
+ query.verifyUrl,
333
351
  );
334
352
 
335
353
  return {
336
354
  success: true,
337
355
  message:
338
- "If an account exists with this email, a verification code has been sent.",
356
+ method === "link"
357
+ ? "If an account exists with this email, a verification link has been sent."
358
+ : "If an account exists with this email, a verification code has been sent.",
339
359
  };
340
360
  },
341
361
  });
@@ -1,4 +1,5 @@
1
1
  import { $context } from "alepha";
2
+ import { AlephaApiFiles } from "alepha/api/files";
2
3
  import type { Repository } from "alepha/orm";
3
4
  import {
4
5
  $realm,
@@ -48,7 +49,14 @@ export const $userRealm = (
48
49
  const userRealmProvider = alepha.inject(UserRealmProvider);
49
50
  const name = options.realm?.name ?? DEFAULT_USER_REALM_NAME;
50
51
 
51
- userRealmProvider.register(name, options);
52
+ const userRealm = userRealmProvider.register(name, options);
53
+
54
+ if (options.modules?.audits) {
55
+ }
56
+
57
+ if (options.modules?.files) {
58
+ alepha.with(AlephaApiFiles);
59
+ }
52
60
 
53
61
  const realm: UserRealmPrimitive = $realm({
54
62
  ...options.realm,
@@ -103,18 +111,28 @@ export const $userRealm = (
103
111
  sessionService.login(name, credentials.username, credentials.password);
104
112
  };
105
113
 
106
- if (options.identities) {
107
- const identities: Record<string, AuthPrimitive> = {};
108
- if (options.identities?.credentials) {
109
- identities.credentials = $authCredentials(realm);
114
+ const identities = options.identities ?? {
115
+ credentials: true,
116
+ };
117
+
118
+ if (identities) {
119
+ const auth: Record<string, AuthPrimitive> = {};
120
+ if (identities.credentials) {
121
+ auth.credentials = $authCredentials(realm);
122
+ } else {
123
+ // if credentials auth is disabled, disable registration as well
124
+ userRealm.settings.registrationAllowed = false;
110
125
  }
111
- if (options.identities?.google) {
112
- identities.google = $authGoogle(realm);
126
+
127
+ if (identities.google) {
128
+ auth.google = $authGoogle(realm);
113
129
  }
114
- if (options.identities?.github) {
115
- identities.github = $authGithub(realm);
130
+
131
+ if (identities.github) {
132
+ auth.github = $authGithub(realm);
116
133
  }
117
- alepha.with(() => identities);
134
+
135
+ alepha.with(() => auth);
118
136
  }
119
137
 
120
138
  return realm;
@@ -153,4 +171,9 @@ export interface UserRealmOptions {
153
171
  google?: true;
154
172
  github?: true;
155
173
  };
174
+
175
+ modules?: {
176
+ files?: boolean;
177
+ audits?: boolean;
178
+ };
156
179
  }
@@ -70,6 +70,7 @@ export class UserRealmProvider {
70
70
  },
71
71
  },
72
72
  });
73
+ return this.getRealm(userRealmName);
73
74
  }
74
75
 
75
76
  /**
@@ -290,6 +290,8 @@ export class SessionService {
290
290
  realm: realm.name,
291
291
  username: profile.email.split("@")[0],
292
292
  email: profile.email,
293
+ // we trust the OAuth2 provider
294
+ emailVerified: true,
293
295
  roles: ["user"], // TODO: make default roles configurable via realm settings
294
296
  });
295
297