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
@@ -24,29 +24,21 @@ import type {
24
24
  TUnion,
25
25
  TUnsafe,
26
26
  } from "typebox";
27
- import * as TypeBox from "typebox";
28
27
  import { Type } from "typebox";
29
28
  import type {
30
29
  TLocalizedValidationError,
31
30
  TLocalizedValidationMessageCallback,
32
31
  } from "typebox/error";
33
- import TypeBoxFormat from "typebox/format";
32
+ import Format from "typebox/format";
34
33
  import { Locale } from "typebox/system";
35
- import * as TypeBoxValue from "typebox/value";
34
+ import * as Value from "typebox/value";
36
35
  import { OPTIONS } from "../constants/OPTIONS.ts";
37
36
  import { AlephaError } from "../errors/AlephaError.ts";
38
37
  import type { TypeBoxError } from "../errors/TypeBoxError.ts";
39
38
  import { isTypeFile, type TFile, type TStream } from "../helpers/FileLike.ts";
40
39
 
41
- export { TypeBox, TypeBoxValue, TypeBoxFormat };
42
-
43
- export const isUUID = TypeBoxFormat.IsUuid;
44
- export const isEmail = TypeBoxFormat.IsEmail;
45
- export const isURL = TypeBoxFormat.IsUrl;
46
- export const isDateTime = TypeBoxFormat.IsDateTime;
47
- export const isDate = TypeBoxFormat.IsDate;
48
- export const isTime = TypeBoxFormat.IsTime;
49
- export const isDuration = TypeBoxFormat.IsDuration;
40
+ export { Type, Value, Format };
41
+ export const isUUID = Format.IsUuid;
50
42
 
51
43
  export type {
52
44
  StaticDecode,
@@ -82,26 +74,26 @@ export type {
82
74
  export class TypeGuard {
83
75
  // -------------------------------------------------------------------------------------------------------------------
84
76
  isBigInt = (value: TSchema): value is TString =>
85
- TypeBox.IsString(value) && "format" in value && value.format === "bigint";
77
+ Type.IsString(value) && "format" in value && value.format === "bigint";
86
78
  isUUID = (value: TSchema): value is TString =>
87
- TypeBox.IsString(value) && "format" in value && value.format === "uuid";
88
- isObject = TypeBox.IsObject;
89
- isNumber = TypeBox.IsNumber;
90
- isString = TypeBox.IsString;
91
- isBoolean = TypeBox.IsBoolean;
92
- isAny = TypeBox.IsAny;
93
- isArray = TypeBox.IsArray;
94
- isOptional = TypeBox.IsOptional;
95
- isUnion = TypeBox.IsUnion;
96
- isInteger = TypeBox.IsInteger;
97
- isNull = TypeBox.IsNull;
98
- isUndefined = TypeBox.IsUndefined;
99
- isUnsafe = TypeBox.IsUnsafe;
100
- isRecord = TypeBox.IsRecord;
101
- isTuple = TypeBox.IsTuple;
102
- isVoid = TypeBox.IsVoid;
103
- isLiteral = TypeBox.IsLiteral;
104
- isSchema = TypeBox.IsSchema;
79
+ Type.IsString(value) && "format" in value && value.format === "uuid";
80
+ isObject = Type.IsObject;
81
+ isNumber = Type.IsNumber;
82
+ isString = Type.IsString;
83
+ isBoolean = Type.IsBoolean;
84
+ isAny = Type.IsAny;
85
+ isArray = Type.IsArray;
86
+ isOptional = Type.IsOptional;
87
+ isUnion = Type.IsUnion;
88
+ isInteger = Type.IsInteger;
89
+ isNull = Type.IsNull;
90
+ isUndefined = Type.IsUndefined;
91
+ isUnsafe = Type.IsUnsafe;
92
+ isRecord = Type.IsRecord;
93
+ isTuple = Type.IsTuple;
94
+ isVoid = Type.IsVoid;
95
+ isLiteral = Type.IsLiteral;
96
+ isSchema = Type.IsSchema;
105
97
  // -------------------------------------------------------------------------------------------------------------------
106
98
  isFile = isTypeFile;
107
99
  isDateTime = (schema: unknown) => {
@@ -131,10 +123,10 @@ declare module "typebox" {
131
123
  }
132
124
 
133
125
  export class TypeProvider {
134
- static format = TypeBoxFormat;
126
+ static format = Format;
135
127
 
136
128
  static {
137
- TypeBoxFormat.Set("bigint", (value: string | number) =>
129
+ Format.Set("bigint", (value: string | number) =>
138
130
  TypeProvider.isValidBigInt(value),
139
131
  );
140
132
  }
@@ -398,7 +390,7 @@ export class TypeProvider {
398
390
  * Create a schema for a JSON object.
399
391
  * This is a record with string keys and any values.
400
392
  */
401
- public json(options?: TSchemaOptions): TRecord<string, TAny> {
393
+ public json<T = any>(options?: TSchemaOptions): TRecord<string, TAny> {
402
394
  return t.record(t.text(), t.any(), options);
403
395
  }
404
396
 
@@ -1,12 +1,13 @@
1
1
  import { $module, type Static, t } from "alepha";
2
2
  import { $logger } from "./primitives/$logger.ts";
3
+ import { ConsoleColorProvider } from "./providers/ConsoleColorProvider.ts";
3
4
  import { ConsoleDestinationProvider } from "./providers/ConsoleDestinationProvider.ts";
4
5
  import { JsonFormatterProvider } from "./providers/JsonFormatterProvider.ts";
5
6
  import { LogDestinationProvider } from "./providers/LogDestinationProvider.ts";
6
7
  import { LogFormatterProvider } from "./providers/LogFormatterProvider.ts";
7
8
  import { MemoryDestinationProvider } from "./providers/MemoryDestinationProvider.ts";
9
+ import { PrettyFormatterProvider } from "./providers/PrettyFormatterProvider.ts";
8
10
  import { RawFormatterProvider } from "./providers/RawFormatterProvider.ts";
9
- import { SimpleFormatterProvider } from "./providers/SimpleFormatterProvider.ts";
10
11
  import type { LogEntry } from "./schemas/logEntrySchema.ts";
11
12
  import { Logger } from "./services/Logger.ts";
12
13
 
@@ -19,7 +20,7 @@ export * from "./providers/JsonFormatterProvider.ts";
19
20
  export * from "./providers/LogDestinationProvider.ts";
20
21
  export * from "./providers/LogFormatterProvider.ts";
21
22
  export * from "./providers/MemoryDestinationProvider.ts";
22
- export * from "./providers/SimpleFormatterProvider.ts";
23
+ export * from "./providers/PrettyFormatterProvider.ts";
23
24
  export * from "./schemas/logEntrySchema.ts";
24
25
  export * from "./services/Logger.ts";
25
26
 
@@ -101,8 +102,9 @@ export const AlephaLogger = $module({
101
102
  ConsoleDestinationProvider,
102
103
  MemoryDestinationProvider,
103
104
  JsonFormatterProvider,
104
- SimpleFormatterProvider,
105
+ PrettyFormatterProvider,
105
106
  RawFormatterProvider,
107
+ ConsoleColorProvider,
106
108
  ],
107
109
  register: (alepha) => {
108
110
  const env = alepha.parseEnv(envSchema);
@@ -141,14 +143,14 @@ export const AlephaLogger = $module({
141
143
  if (env.LOG_FORMAT === "raw") {
142
144
  return RawFormatterProvider;
143
145
  }
144
- return SimpleFormatterProvider;
146
+ return PrettyFormatterProvider;
145
147
  }
146
148
 
147
149
  if (alepha.isProduction() && !alepha.isBrowser()) {
148
150
  return JsonFormatterProvider;
149
151
  }
150
152
 
151
- return SimpleFormatterProvider;
153
+ return PrettyFormatterProvider;
152
154
  };
153
155
 
154
156
  alepha.with({
@@ -202,7 +204,7 @@ const envSchema = t.object({
202
204
  /**
203
205
  * Built-in log formats.
204
206
  * - "json" - JSON format, useful for structured logging and log aggregation. {@link JsonFormatterProvider}
205
- * - "pretty" - Simple text format, human-readable, with colors. {@link SimpleFormatterProvider}
207
+ * - "pretty" - Simple text format, human-readable, with colors. {@link PrettyFormatterProvider}
206
208
  * - "raw" - Raw format, no formatting, just the message. {@link RawFormatterProvider}
207
209
  */
208
210
  LOG_FORMAT: t.optional(
@@ -26,7 +26,7 @@ export const $logger = (options: LoggerPrimitiveOptions = {}): Logger => {
26
26
  return $inject(Logger, {
27
27
  lifetime: "transient",
28
28
  args: [
29
- options.name ?? service?.name,
29
+ options.name ?? service?.name ?? "Func",
30
30
  module?.name ?? alepha.env.MODULE_NAME ?? "app",
31
31
  ],
32
32
  });
@@ -3,7 +3,7 @@ import type { LogEntry } from "../schemas/logEntrySchema.ts";
3
3
  import { ConsoleColorProvider } from "./ConsoleColorProvider.ts";
4
4
  import { LogFormatterProvider } from "./LogFormatterProvider.ts";
5
5
 
6
- export class SimpleFormatterProvider extends LogFormatterProvider {
6
+ export class PrettyFormatterProvider extends LogFormatterProvider {
7
7
  protected color = $inject(ConsoleColorProvider);
8
8
  protected alepha = $inject(Alepha);
9
9
 
@@ -107,6 +107,15 @@ export class SimpleFormatterProvider extends LogFormatterProvider {
107
107
  return "";
108
108
  }
109
109
 
110
+ if (this.alepha.isViteDev()) {
111
+ // Node.js - try to fix stack trace with Vite SSR helper
112
+ // Actually, it works only because we have a global helper in viteAlephaDev.ts
113
+ const gl = globalThis as Record<string, unknown>;
114
+ if (typeof gl === "object" && typeof gl.ssrFixStacktrace === "function") {
115
+ gl.ssrFixStacktrace(error);
116
+ }
117
+ }
118
+
110
119
  let str = error.stack ?? error.message;
111
120
 
112
121
  const anyError = error as any;
package/src/orm/index.ts CHANGED
@@ -10,7 +10,10 @@ import { NodePostgresProvider } from "./providers/drivers/NodePostgresProvider.t
10
10
  import { NodeSqliteProvider } from "./providers/drivers/NodeSqliteProvider.ts";
11
11
  import { PglitePostgresProvider } from "./providers/drivers/PglitePostgresProvider.ts";
12
12
  import { RepositoryProvider } from "./providers/RepositoryProvider.ts";
13
+ import { PgJsonQueryManager } from "./services/PgJsonQueryManager.ts";
14
+ import { PgRelationManager } from "./services/PgRelationManager.ts";
13
15
  import { PostgresModelBuilder } from "./services/PostgresModelBuilder.ts";
16
+ import { QueryManager } from "./services/QueryManager.ts";
14
17
  import { Repository } from "./services/Repository.ts";
15
18
  import { SqliteModelBuilder } from "./services/SqliteModelBuilder.ts";
16
19
 
@@ -176,6 +179,9 @@ export const AlephaPostgres = $module({
176
179
  DrizzleKitProvider,
177
180
  RepositoryProvider,
178
181
  Repository,
182
+ PgRelationManager,
183
+ PgJsonQueryManager,
184
+ QueryManager,
179
185
  ],
180
186
  register: (alepha: Alepha) => {
181
187
  const env = alepha.parseEnv(
@@ -1,4 +1,4 @@
1
- import { type TObject, TypeBoxValue, t } from "alepha";
1
+ import { type TObject, t, Value } from "alepha";
2
2
  import { getTableName, type SQL, sql } from "drizzle-orm";
3
3
  import type { PgSelectBase, PgTableWithColumns } from "drizzle-orm/pg-core";
4
4
  import { isSQLWrapper } from "drizzle-orm/sql/sql";
@@ -103,7 +103,7 @@ export class PgRelationManager {
103
103
  joins: PgJoin[],
104
104
  parentPath?: string,
105
105
  ): TObject {
106
- const schema = TypeBoxValue.Clone(baseSchema) as TObject;
106
+ const schema = Value.Clone(baseSchema) as TObject;
107
107
 
108
108
  // Group joins by parent
109
109
  const joinsAtThisLevel = joins.filter((j) => j.parent === parentPath);
@@ -279,6 +279,13 @@ export class PostgresModelBuilder extends ModelBuilder {
279
279
  return schema(key, value);
280
280
  }
281
281
 
282
+ const isTypeEnum = (value: any): value is { enum: any[] } =>
283
+ t.schema.isUnsafe(value) &&
284
+ "type" in value &&
285
+ value.type === "string" &&
286
+ "enum" in value &&
287
+ Array.isArray(value.enum);
288
+
282
289
  if (t.schema.isArray(value)) {
283
290
  if (t.schema.isObject(value.items)) {
284
291
  return schema(key, value);
@@ -298,16 +305,13 @@ export class PostgresModelBuilder extends ModelBuilder {
298
305
  if (t.schema.isBoolean(value.items)) {
299
306
  return pg.boolean(key).array();
300
307
  }
308
+ if (isTypeEnum(value.items)) {
309
+ return pg.text(key).array();
310
+ }
301
311
  }
302
312
 
303
313
  // Enum handling
304
- if (
305
- t.schema.isUnsafe(value) &&
306
- "type" in value &&
307
- value.type === "string" &&
308
- "enum" in value &&
309
- Array.isArray(value.enum)
310
- ) {
314
+ if (isTypeEnum(value)) {
311
315
  if (!value.enum.every((it) => typeof it === "string")) {
312
316
  throw new AlephaError(
313
317
  `Enum for ${fieldName} must be an array of strings, got ${JSON.stringify(
@@ -546,13 +546,15 @@ export abstract class Repository<T extends TObject> {
546
546
  /**
547
547
  * Create many entities.
548
548
  *
549
+ * Inserts are batched in chunks of 1000 to avoid hitting database limits.
550
+ *
549
551
  * @param values The entities to create.
550
552
  * @param opts The statement options.
551
553
  * @returns The created entities.
552
554
  */
553
555
  public async createMany(
554
556
  values: Array<Static<TObjectInsert<T>>>,
555
- opts: StatementOptions = {},
557
+ opts: StatementOptions & { batchSize?: number } = {},
556
558
  ): Promise<Static<T>[]> {
557
559
  if (values.length === 0) {
558
560
  return [];
@@ -563,19 +565,26 @@ export abstract class Repository<T extends TObject> {
563
565
  data: values,
564
566
  });
565
567
 
568
+ const batchSize = opts.batchSize ?? 1000;
569
+ const allEntities: Static<T>[] = [];
570
+
566
571
  try {
567
- const entities = await this.rawInsert(opts)
568
- .values(values.map((data) => this.cast(data, true)))
569
- .returning(this.table)
570
- .then((rows) => rows.map((it) => this.clean(it, this.entity.schema)));
572
+ for (let i = 0; i < values.length; i += batchSize) {
573
+ const batch = values.slice(i, i + batchSize);
574
+ const entities = await this.rawInsert(opts)
575
+ .values(batch.map((data) => this.cast(data, true)))
576
+ .returning(this.table)
577
+ .then((rows) => rows.map((it) => this.clean(it, this.entity.schema)));
578
+ allEntities.push(...entities);
579
+ }
571
580
 
572
581
  await this.alepha.events.emit("repository:create:after", {
573
582
  tableName: this.tableName,
574
583
  data: values,
575
- entity: entities,
584
+ entity: allEntities,
576
585
  });
577
586
 
578
- return entities;
587
+ return allEntities;
579
588
  } catch (error) {
580
589
  throw this.handleError(error, "Insert query has failed");
581
590
  }
@@ -190,6 +190,16 @@ export class SqliteModelBuilder extends ModelBuilder {
190
190
  return pg.integer(key);
191
191
  }
192
192
 
193
+ if (t.schema.isBigInt(value)) {
194
+ if (PG_PRIMARY_KEY in value || PG_IDENTITY in value) {
195
+ return pg
196
+ .integer(key, { mode: "number" })
197
+ .primaryKey({ autoIncrement: true });
198
+ }
199
+
200
+ return pg.integer(key, { mode: "number" });
201
+ }
202
+
193
203
  if (t.schema.isNumber(value)) {
194
204
  if (PG_IDENTITY in value) {
195
205
  return pg
@@ -22,9 +22,12 @@ import { ServerBodyParserProvider } from "./providers/ServerBodyParserProvider.t
22
22
  import { ServerLoggerProvider } from "./providers/ServerLoggerProvider.ts";
23
23
  import { ServerNotReadyProvider } from "./providers/ServerNotReadyProvider.ts";
24
24
  import { ServerProvider } from "./providers/ServerProvider.ts";
25
+ import { ServerRouterProvider } from "./providers/ServerRouterProvider.ts";
25
26
  import { ServerTimingProvider } from "./providers/ServerTimingProvider.ts";
26
27
  import type { FetchOptions, HttpAction } from "./services/HttpClient.ts";
27
28
  import { HttpClient } from "./services/HttpClient.ts";
29
+ import { ServerRequestParser } from "./services/ServerRequestParser.ts";
30
+ import { UserAgentParser } from "./services/UserAgentParser.ts";
28
31
 
29
32
  // ---------------------------------------------------------------------------------------------------------------------
30
33
 
@@ -132,6 +135,9 @@ export const AlephaServer = $module({
132
135
  ServerNotReadyProvider,
133
136
  ServerTimingProvider,
134
137
  HttpClient,
138
+ UserAgentParser,
139
+ ServerRequestParser,
140
+ ServerRouterProvider,
135
141
  ],
136
142
  register: (alepha: Alepha) => {
137
143
  if (!alepha.isServerless() && !alepha.isViteDev()) {
@@ -44,7 +44,16 @@ import {
44
44
  * - Automatic content-type handling (JSON, form-data, plain text)
45
45
  *
46
46
  * **URL Generation**
47
- * Actions are prefixed with `/api` by default (configurable via `SERVER_API_PREFIX`).
47
+ *
48
+ * **Important:** All `$action` paths are automatically prefixed with `/api`.
49
+ *
50
+ * ```ts
51
+ * $action({ path: "/users" }) // → GET /api/users
52
+ * $action({ path: "/users/:id" }) // → GET /api/users/:id
53
+ * $action({ path: "/hello" }) // → GET /api/hello
54
+ * ```
55
+ *
56
+ * This prefix is configurable via the `SERVER_API_PREFIX` environment variable.
48
57
  * HTTP method defaults to GET, or POST if body schema is provided.
49
58
  *
50
59
  * **Common Use Cases**
@@ -1,5 +1,6 @@
1
1
  import { ReadableStream as WebStream } from "node:stream/web";
2
2
  import { createBrotliDecompress, createGunzip, createInflate } from "node:zlib";
3
+ import type { TSchema } from "alepha";
3
4
  import { $env, $hook, $inject, Alepha, t } from "alepha";
4
5
  import { $logger } from "alepha/logger";
5
6
  import { HttpError } from "../errors/HttpError.ts";
@@ -44,7 +45,11 @@ export class ServerBodyParserProvider {
44
45
 
45
46
  if (route.schema?.body) {
46
47
  try {
47
- const body = await this.parse(stream, request.headers);
48
+ const body = await this.parse(
49
+ stream,
50
+ request.headers,
51
+ route.schema.body,
52
+ );
48
53
  if (body) {
49
54
  request.body = body;
50
55
  }
@@ -68,18 +73,19 @@ export class ServerBodyParserProvider {
68
73
  public async parse(
69
74
  stream: ReadableStream,
70
75
  headers: Record<string, string>,
76
+ schema: TSchema,
71
77
  ): Promise<object | string | undefined> {
72
78
  const contentType = headers["content-type"];
73
79
  const contentEncoding = headers["content-encoding"];
74
80
 
75
81
  if (!contentType) return undefined;
76
82
 
77
- if (contentType.startsWith("application/json")) {
78
- return this.parseJson(stream, contentEncoding);
83
+ if (contentType.startsWith("text/plain") || t.schema.isString(schema)) {
84
+ return this.parseText(stream, contentEncoding);
79
85
  }
80
86
 
81
- if (contentType.startsWith("text/plain")) {
82
- return this.parseText(stream, contentEncoding);
87
+ if (contentType.startsWith("application/json")) {
88
+ return this.parseJson(stream, contentEncoding);
83
89
  }
84
90
 
85
91
  if (contentType.startsWith("application/x-www-form-urlencoded")) {
@@ -391,13 +391,19 @@ export class ServerRouterProvider extends RouterProvider<ServerRouteMatcher> {
391
391
  }
392
392
 
393
393
  if (route.schema?.body) {
394
- try {
395
- request.body = this.alepha.codec.decode(
396
- route.schema.body,
397
- request.body,
398
- );
399
- } catch (error) {
400
- throw new ValidationError("Invalid request body", error);
394
+ if (t.schema.isString(route.schema.body)) {
395
+ if (typeof request.body !== "string") {
396
+ throw new ValidationError("Request body is not a string");
397
+ }
398
+ } else {
399
+ try {
400
+ request.body = this.alepha.codec.decode(
401
+ route.schema.body,
402
+ request.body,
403
+ );
404
+ } catch (error) {
405
+ throw new ValidationError("Invalid request body", error);
406
+ }
401
407
  }
402
408
  }
403
409
  }
@@ -261,6 +261,13 @@ export class AuthPrimitive extends Primitive<AuthPrimitiveOptions> {
261
261
  return this.options.name ?? this.config.propertyKey;
262
262
  }
263
263
 
264
+ public get realm(): RealmPrimitive | undefined {
265
+ if ("realm" in this.options) {
266
+ return this.options.realm;
267
+ }
268
+ return undefined;
269
+ }
270
+
264
271
  public get jwks_uri(): string {
265
272
  const jwks = this.oauth?.serverMetadata().jwks_uri;
266
273
  if (!jwks) {
@@ -41,6 +41,7 @@ export class ServerAuthProvider {
41
41
  httpOnly: true,
42
42
  schema: t.object({
43
43
  provider: t.text(),
44
+ realm: t.optional(t.text()),
44
45
  codeVerifier: t.optional(t.text({ size: "long" })),
45
46
  redirectUri: t.optional(t.text({ size: "long" })),
46
47
  state: t.optional(t.text()),
@@ -163,7 +164,7 @@ export class ServerAuthProvider {
163
164
  protected async cookiesToTokens(
164
165
  cookies: Cookies,
165
166
  ): Promise<Tokens | undefined> {
166
- const tokens = this.tokens.get({ cookies });
167
+ const tokens = this.getTokens(cookies);
167
168
  if (!tokens) {
168
169
  // no cookie, no tokens
169
170
  this.log.trace("No tokens found in cookies");
@@ -247,7 +248,7 @@ export class ServerAuthProvider {
247
248
  response: userinfoResponseSchema,
248
249
  },
249
250
  handler: async ({ user, headers, cookies }) => {
250
- const tokens = this.tokens.get({ cookies });
251
+ const tokens = this.getTokens(cookies);
251
252
  if (tokens) {
252
253
  const provider = this.provider(tokens);
253
254
  if (!("realm" in provider.options)) {
@@ -323,6 +324,9 @@ export class ServerAuthProvider {
323
324
  schema: {
324
325
  query: t.object({
325
326
  provider: t.text(),
327
+ realm: t.optional(
328
+ t.text({ description: "Realm name for multi-realm setups" }),
329
+ ),
326
330
  }),
327
331
  body: t.object({
328
332
  username: t.text(),
@@ -331,7 +335,11 @@ export class ServerAuthProvider {
331
335
  response: tokenResponseSchema,
332
336
  },
333
337
  handler: async ({ query, body, cookies }) => {
334
- const provider = this.provider(query);
338
+ const provider = this.provider({
339
+ provider: query.provider,
340
+ realm: query.realm,
341
+ });
342
+
335
343
  const realm = "realm" in provider.options && provider.options.realm;
336
344
  if (!realm) {
337
345
  throw new SecurityError(
@@ -348,6 +356,8 @@ export class ServerAuthProvider {
348
356
  );
349
357
  }
350
358
 
359
+ console.log("->", body);
360
+
351
361
  let user: UserAccount | undefined;
352
362
  try {
353
363
  user = await credentials.account(body);
@@ -392,11 +402,17 @@ export class ServerAuthProvider {
392
402
  schema: {
393
403
  query: t.object({
394
404
  provider: t.text(),
405
+ realm: t.optional(
406
+ t.text({ description: "Realm name for multi-realm setups" }),
407
+ ),
395
408
  redirect_uri: t.optional(t.text({ size: "rich" })),
396
409
  }),
397
410
  },
398
411
  handler: async ({ query, url, reply }) => {
399
- const provider = this.provider(query);
412
+ const provider = this.provider({
413
+ provider: query.provider,
414
+ realm: query.realm,
415
+ });
400
416
  const oauth = provider.oauth;
401
417
  if (!oauth) {
402
418
  throw new SecurityError(
@@ -433,6 +449,7 @@ export class ServerAuthProvider {
433
449
  nonce: parameters.nonce,
434
450
  redirectUri: query.redirect_uri ?? "/",
435
451
  provider: query.provider,
452
+ realm: query.realm,
436
453
  });
437
454
 
438
455
  reply.redirect(buildAuthorizationUrl(oauth, parameters).toString());
@@ -456,6 +473,7 @@ export class ServerAuthProvider {
456
473
  codeVerifier,
457
474
  redirectUri: query.redirect_uri ?? "/",
458
475
  provider: query.provider,
476
+ realm: query.realm,
459
477
  });
460
478
 
461
479
  reply.redirect(buildAuthorizationUrl(oauth, parameters).toString());
@@ -543,7 +561,7 @@ export class ServerAuthProvider {
543
561
  },
544
562
  handler: async ({ query, reply, cookies }) => {
545
563
  const redirect = query.post_logout_redirect_uri ?? "/";
546
- const tokens = this.tokens.get({ cookies });
564
+ const tokens = this.getTokens(cookies);
547
565
  if (!tokens) {
548
566
  reply.redirect(redirect);
549
567
  return;
@@ -603,17 +621,42 @@ export class ServerAuthProvider {
603
621
  },
604
622
  });
605
623
 
606
- protected provider(opts: string | { provider: string }): AuthPrimitive {
624
+ /**
625
+ * Find an auth provider by name and optionally by realm.
626
+ * When realm is specified, it filters providers by both name and realm.
627
+ * This enables multi-realm setups where multiple providers share the same name (e.g., "credentials").
628
+ */
629
+ protected provider(
630
+ opts: string | { provider: string; realm?: string },
631
+ ): AuthPrimitive {
607
632
  const name = typeof opts === "string" ? opts : opts.provider;
608
- const identity = this.identities.find((identity) => identity.name === name);
633
+ const realmName = typeof opts === "string" ? undefined : opts.realm;
634
+
635
+ const identity = this.identities.find((identity) => {
636
+ if (identity.name !== name) {
637
+ return false;
638
+ }
639
+
640
+ // If realm filter is specified, match against provider's realm
641
+ if (realmName && identity.realm?.name !== realmName) {
642
+ return false;
643
+ }
644
+
645
+ return true;
646
+ });
609
647
 
610
648
  if (!identity) {
611
- throw new SecurityError(`Auth provider '${name}' not found`);
649
+ const realmInfo = realmName ? ` for realm '${realmName}'` : "";
650
+ throw new SecurityError(`Auth provider '${name}'${realmInfo} not found`);
612
651
  }
613
652
 
614
653
  return identity;
615
654
  }
616
655
 
656
+ protected getTokens(cookies?: Cookies): Tokens | undefined {
657
+ return this.tokens.get({ cookies });
658
+ }
659
+
617
660
  protected setTokens(tokens: Tokens, cookies?: Cookies): void {
618
661
  const exp =
619
662
  tokens.refresh_token_expires_in ||
@@ -2,6 +2,7 @@ import { $module } from "alepha";
2
2
  import { AlephaServer } from "alepha/server";
3
3
  import { $cookie, type Cookies } from "./primitives/$cookie.ts";
4
4
  import { ServerCookiesProvider } from "./providers/ServerCookiesProvider.ts";
5
+ import { CookieParser } from "./services/CookieParser.ts";
5
6
 
6
7
  // ---------------------------------------------------------------------------------------------------------------------
7
8
 
@@ -29,5 +30,5 @@ declare module "alepha/server" {
29
30
  export const AlephaServerCookies = $module({
30
31
  name: "alepha.server.cookies",
31
32
  primitives: [$cookie],
32
- services: [AlephaServer, ServerCookiesProvider],
33
+ services: [AlephaServer, ServerCookiesProvider, CookieParser],
33
34
  });
@@ -1,7 +1,7 @@
1
1
  import { cpus } from "node:os";
2
2
  import { MessageChannel, type MessagePort, Worker } from "node:worker_threads";
3
3
  import type { TSchema } from "alepha";
4
- import { createPrimitive, KIND, Primitive, TypeBoxValue } from "alepha";
4
+ import { createPrimitive, KIND, Primitive, Value } from "alepha";
5
5
 
6
6
  /**
7
7
  * Creates a worker thread primitive for offloading CPU-intensive tasks to separate threads.
@@ -244,7 +244,7 @@ export class ThreadPrimitive extends Primitive<ThreadPrimitiveOptions> {
244
244
  public async execute<T = any>(data?: any, schema?: TSchema): Promise<T> {
245
245
  if (schema && data) {
246
246
  try {
247
- TypeBoxValue.Decode(schema, data);
247
+ Value.Decode(schema, data);
248
248
  } catch (error) {
249
249
  throw new Error(
250
250
  `Invalid data: ${error instanceof Error ? error.message : error}`,
package/src/vite/index.ts CHANGED
@@ -1,7 +1,5 @@
1
1
  import type { Alepha } from "alepha";
2
2
 
3
- // Vite re-exports
4
- export { defineConfig } from "vite";
5
3
  // Helpers (for advanced use)
6
4
  export * from "./helpers/boot.ts";
7
5
  export * from "./helpers/createBufferedLogger.ts";