alepha 0.19.4 → 0.19.5

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 (104) hide show
  1. package/dist/api/audits/index.d.ts +8 -8
  2. package/dist/api/issues/index.d.ts +810 -0
  3. package/dist/api/issues/index.d.ts.map +1 -0
  4. package/dist/api/issues/index.js +447 -0
  5. package/dist/api/issues/index.js.map +1 -0
  6. package/dist/api/keys/index.d.ts +5 -5
  7. package/dist/api/users/index.d.ts +6 -0
  8. package/dist/api/users/index.d.ts.map +1 -1
  9. package/dist/api/users/index.js +10 -3
  10. package/dist/api/users/index.js.map +1 -1
  11. package/dist/api/workflows/index.d.ts +3 -3
  12. package/dist/captcha/index.d.ts +142 -0
  13. package/dist/captcha/index.d.ts.map +1 -0
  14. package/dist/captcha/index.js +177 -0
  15. package/dist/captcha/index.js.map +1 -0
  16. package/dist/cli/core/index.d.ts +82 -2
  17. package/dist/cli/core/index.d.ts.map +1 -1
  18. package/dist/cli/core/index.js +90 -6
  19. package/dist/cli/core/index.js.map +1 -1
  20. package/dist/cli/platform/index.d.ts +84 -10
  21. package/dist/cli/platform/index.d.ts.map +1 -1
  22. package/dist/cli/platform/index.js +92 -4
  23. package/dist/cli/platform/index.js.map +1 -1
  24. package/dist/cli/vendor/index.d.ts +30 -3
  25. package/dist/cli/vendor/index.d.ts.map +1 -1
  26. package/dist/cli/vendor/index.js +98 -21
  27. package/dist/cli/vendor/index.js.map +1 -1
  28. package/dist/command/index.d.ts.map +1 -1
  29. package/dist/command/index.js +2 -3
  30. package/dist/command/index.js.map +1 -1
  31. package/dist/orm/core/index.bun.js +6 -6
  32. package/dist/orm/core/index.bun.js.map +1 -1
  33. package/dist/orm/core/index.d.ts.map +1 -1
  34. package/dist/orm/core/index.js +6 -6
  35. package/dist/orm/core/index.js.map +1 -1
  36. package/dist/react/i18n/index.d.ts +1 -0
  37. package/dist/react/i18n/index.d.ts.map +1 -1
  38. package/dist/react/i18n/index.js +8 -4
  39. package/dist/react/i18n/index.js.map +1 -1
  40. package/dist/security/index.d.ts.map +1 -1
  41. package/dist/security/index.js.map +1 -1
  42. package/dist/server/auth/index.d.ts +145 -2
  43. package/dist/server/auth/index.d.ts.map +1 -1
  44. package/dist/server/auth/index.js +364 -63
  45. package/dist/server/auth/index.js.map +1 -1
  46. package/dist/server/cookies/index.d.ts.map +1 -1
  47. package/dist/server/cookies/index.js.map +1 -1
  48. package/dist/websocket/index.d.ts.map +1 -1
  49. package/dist/websocket/index.js.map +1 -1
  50. package/package.json +11 -1
  51. package/src/api/issues/__tests__/IssueService.spec.ts +263 -0
  52. package/src/api/issues/controllers/AdminIssueController.ts +149 -0
  53. package/src/api/issues/controllers/IssueController.ts +44 -0
  54. package/src/api/issues/entities/issues.ts +49 -0
  55. package/src/api/issues/index.ts +53 -0
  56. package/src/api/issues/schemas/createIssueSchema.ts +13 -0
  57. package/src/api/issues/schemas/issueConfigAtom.ts +13 -0
  58. package/src/api/issues/schemas/issueQuerySchema.ts +18 -0
  59. package/src/api/issues/schemas/issueResourceSchema.ts +6 -0
  60. package/src/api/issues/schemas/myIssueQuerySchema.ts +10 -0
  61. package/src/api/issues/schemas/updateIssueSchema.ts +13 -0
  62. package/src/api/issues/services/IssueService.ts +264 -0
  63. package/src/api/users/primitives/$realm.ts +24 -0
  64. package/src/api/users/services/CredentialService.ts +6 -3
  65. package/src/api/users/services/RegistrationService.ts +15 -5
  66. package/src/api/users/services/SessionService.ts +2 -0
  67. package/src/captcha/__tests__/MemoryCaptchaProvider.spec.ts +74 -0
  68. package/src/captcha/index.ts +33 -0
  69. package/src/captcha/providers/CaptchaProvider.ts +17 -0
  70. package/src/captcha/providers/MemoryCaptchaProvider.ts +65 -0
  71. package/src/captcha/providers/TurnstileCaptchaProvider.ts +125 -0
  72. package/src/cli/core/atoms/buildOptions.ts +57 -0
  73. package/src/cli/core/commands/build.ts +2 -0
  74. package/src/cli/core/providers/ViteDevServerProvider.ts +1 -1
  75. package/src/cli/core/services/ViteUtils.ts +5 -2
  76. package/src/cli/core/tasks/BuildClientTask.ts +3 -1
  77. package/src/cli/core/tasks/BuildCloudflareTask.ts +4 -0
  78. package/src/cli/core/tasks/BuildPwaTask.ts +81 -0
  79. package/src/cli/platform/adapters/CloudflareAdapter.ts +24 -0
  80. package/src/cli/platform/atoms/platformOptions.ts +19 -3
  81. package/src/cli/platform/hooks/PlatformHook.ts +51 -0
  82. package/src/cli/platform/index.ts +1 -0
  83. package/src/cli/platform/services/CloudflareApi.ts +22 -1
  84. package/src/cli/platform/services/PlatformOrchestrator.ts +67 -2
  85. package/src/cli/vendor/__tests__/VendorService.spec.ts +40 -1
  86. package/src/cli/vendor/commands/VendorCommand.ts +41 -38
  87. package/src/cli/vendor/services/VendorService.ts +108 -4
  88. package/src/command/__tests__/CliProvider.spec.ts +45 -0
  89. package/src/command/providers/CliProvider.ts +3 -4
  90. package/src/orm/core/services/Repository.ts +20 -6
  91. package/src/react/i18n/__tests__/I18nProvider.spec.ts +83 -0
  92. package/src/react/i18n/providers/I18nProvider.ts +12 -10
  93. package/src/security/primitives/$issuer.ts +3 -1
  94. package/src/server/auth/index.ts +7 -0
  95. package/src/server/auth/primitives/$auth.ts +37 -3
  96. package/src/server/auth/primitives/$authApple.ts +114 -4
  97. package/src/server/auth/primitives/$authFacebook.ts +98 -0
  98. package/src/server/auth/primitives/$authFranceConnect.ts +105 -0
  99. package/src/server/auth/primitives/$authGithub.ts +22 -16
  100. package/src/server/auth/primitives/$authMicrosoft.ts +88 -0
  101. package/src/server/auth/providers/ServerAuthProvider.ts +197 -72
  102. package/src/server/cookies/providers/ServerCookiesProvider.ts +3 -0
  103. package/src/server/core/__tests__/ServerRouterProvider-errorHandler.spec.ts +1 -1
  104. package/src/websocket/providers/NodeWebSocketServerProvider.ts +3 -1
@@ -1,4 +1,4 @@
1
- import { $inject, AlephaError } from "alepha";
1
+ import { $inject, Alepha, AlephaError } from "alepha";
2
2
  import type { RunnerMethod } from "alepha/command";
3
3
  import { $logger, ConsoleColorProvider } from "alepha/logger";
4
4
  import { CloudflareAdapter } from "../adapters/CloudflareAdapter.ts";
@@ -9,6 +9,10 @@ import type {
9
9
  PlatformState,
10
10
  } from "../adapters/PlatformAdapter.ts";
11
11
  import { VercelAdapter } from "../adapters/VercelAdapter.ts";
12
+ import {
13
+ PlatformHook,
14
+ type PlatformHookContext,
15
+ } from "../hooks/PlatformHook.ts";
12
16
  import { type NamingContext, NamingService } from "./NamingService.ts";
13
17
  import {
14
18
  PlatformInspector,
@@ -28,6 +32,7 @@ export class PlatformOrchestrator {
28
32
  protected readonly naming = $inject(NamingService);
29
33
  protected readonly cloudflareAdapter = $inject(CloudflareAdapter);
30
34
  protected readonly vercelAdapter = $inject(VercelAdapter);
35
+ protected readonly alepha = $inject(Alepha);
31
36
 
32
37
  // -------------------------------------------------------------------------
33
38
  // Adapter resolution
@@ -104,7 +109,12 @@ export class PlatformOrchestrator {
104
109
  }
105
110
  }
106
111
 
107
- // 7. Secrets (push .env.{env} secrets to deployed workers)
112
+ // 7. Platform hooks (register external resources: Stripe webhooks, etc.)
113
+ // Run before secrets() so any secret a hook writes to .env.<env>
114
+ // gets pushed to the deployed worker in the same up cycle.
115
+ await this.runHooks("register", ctx, urls, run);
116
+
117
+ // 8. Secrets (push .env.{env} secrets to deployed workers)
108
118
  await adapter.secrets(ctx, run);
109
119
 
110
120
  run.end();
@@ -166,6 +176,9 @@ export class PlatformOrchestrator {
166
176
  // Auth
167
177
  await adapter.authenticate(ctx, run);
168
178
 
179
+ // Platform hooks (tear down external resources first, while creds still valid)
180
+ await this.runHooks("unregister", ctx, [], run);
181
+
169
182
  // Teardown
170
183
  await adapter.teardown(ctx, run);
171
184
  run.end();
@@ -173,6 +186,58 @@ export class PlatformOrchestrator {
173
186
  return true;
174
187
  }
175
188
 
189
+ // -------------------------------------------------------------------------
190
+ // Platform hooks
191
+ // -------------------------------------------------------------------------
192
+
193
+ /**
194
+ * Run all registered PlatformHook instances.
195
+ *
196
+ * Discovered dynamically via `alepha.services(PlatformHook)`: any plugin
197
+ * that registers a PlatformHook subclass in its `$module.services`
198
+ * participates automatically, without the core knowing about it.
199
+ */
200
+ protected async runHooks(
201
+ phase: "register" | "unregister",
202
+ ctx: PlatformContext,
203
+ deployUrls: string[],
204
+ run: RunnerMethod,
205
+ ): Promise<void> {
206
+ const hooks = this.alepha.services(PlatformHook);
207
+ if (hooks.length === 0) return;
208
+
209
+ const baseUrl = ctx.envConfig.domain
210
+ ? `https://${ctx.envConfig.domain}`
211
+ : deployUrls[0];
212
+
213
+ if (!baseUrl) {
214
+ this.log.debug("Skipping platform hooks: no base URL available");
215
+ return;
216
+ }
217
+
218
+ const hookCtx: PlatformHookContext = { ...ctx, baseUrl, run };
219
+
220
+ for (const hook of hooks) {
221
+ this.log.info(`Platform hook: ${hook.name} (${phase})`);
222
+ try {
223
+ if (phase === "register") {
224
+ await hook.register(hookCtx);
225
+ } else {
226
+ await hook.unregister(hookCtx);
227
+ }
228
+ } catch (err) {
229
+ // unregister must never block teardown
230
+ if (phase === "unregister") {
231
+ this.log.warn(
232
+ `Platform hook ${hook.name} failed to unregister: ${(err as Error).message}`,
233
+ );
234
+ } else {
235
+ throw err;
236
+ }
237
+ }
238
+ }
239
+ }
240
+
176
241
  // -------------------------------------------------------------------------
177
242
  // plan
178
243
  // -------------------------------------------------------------------------
@@ -443,7 +443,12 @@ describe("VendorService", () => {
443
443
  packages: ["pkg"],
444
444
  });
445
445
 
446
- expect(result.packages[0].modified).toEqual(["file.ts"]);
446
+ expect(result.packages[0].modified).toHaveLength(1);
447
+ expect(result.packages[0].modified[0].file).toBe("file.ts");
448
+ expect(result.packages[0].modified[0].changes).toEqual([
449
+ { line: 1, type: "removed", text: "original" },
450
+ { line: 1, type: "added", text: "modified by user" },
451
+ ]);
447
452
  expect(result.totalChanges).toBe(1);
448
453
  });
449
454
 
@@ -493,6 +498,40 @@ describe("VendorService", () => {
493
498
  expect(result.totalChanges).toBe(0);
494
499
  });
495
500
 
501
+ it("should include line-level changes for multi-line modifications", async ({
502
+ expect,
503
+ }) => {
504
+ const { service, fs } = createDiffTestService();
505
+ await writeVendorLock(fs);
506
+
507
+ await fs.mkdir("/tmp/test-baseline/packages/pkg", { recursive: true });
508
+ await fs.writeFile(
509
+ "/tmp/test-baseline/packages/pkg/config.ts",
510
+ "const a = 1;\nconst b = 2;\nconst c = 3;",
511
+ );
512
+
513
+ await fs.mkdir("/project/packages/pkg", { recursive: true });
514
+ await fs.writeFile(
515
+ "/project/packages/pkg/config.ts",
516
+ "const a = 1;\nconst b = 99;\nconst c = 3;\nconst d = 4;",
517
+ );
518
+
519
+ const result = await service.diff({
520
+ root: "/project",
521
+ remote: "remote",
522
+ branch: "main",
523
+ packages: ["pkg"],
524
+ });
525
+
526
+ const fileDiff = result.packages[0].modified[0];
527
+ expect(fileDiff.file).toBe("config.ts");
528
+ expect(fileDiff.changes).toEqual([
529
+ { line: 2, type: "removed", text: "const b = 2;" },
530
+ { line: 2, type: "added", text: "const b = 99;" },
531
+ { line: 4, type: "added", text: "const d = 4;" },
532
+ ]);
533
+ });
534
+
496
535
  it("should clean up temp directory after diff", async ({ expect }) => {
497
536
  const { service, fs } = createDiffTestService();
498
537
  await writeVendorLock(fs);
@@ -5,6 +5,7 @@ import { $logger, ConsoleColorProvider } from "alepha/logger";
5
5
  import { vendorOptions } from "../atoms/vendorOptions.ts";
6
6
  import type {
7
7
  VendorDiffResult,
8
+ VendorPackageDiff,
8
9
  VendorSyncResult,
9
10
  } from "../services/VendorService.ts";
10
11
  import { VendorService } from "../services/VendorService.ts";
@@ -81,22 +82,7 @@ export class VendorCommand {
81
82
  );
82
83
 
83
84
  for (const pkg of result.aborted.packages) {
84
- const count =
85
- pkg.added.length + pkg.modified.length + pkg.removed.length;
86
- if (count === 0) continue;
87
-
88
- process.stdout.write(
89
- `\n${c.set("CYAN", pkg.name)}: ${count} ${count === 1 ? "file differs" : "files differ"}\n`,
90
- );
91
- for (const file of pkg.added) {
92
- process.stdout.write(` ${c.set("GREEN", "A")} ${file}\n`);
93
- }
94
- for (const file of pkg.modified) {
95
- process.stdout.write(` ${c.set("ORANGE", "M")} ${file}\n`);
96
- }
97
- for (const file of pkg.removed) {
98
- process.stdout.write(` ${c.set("RED", "D")} ${file}\n`);
99
- }
85
+ this.printPackageDiff(pkg);
100
86
  }
101
87
 
102
88
  process.stdout.write("\n");
@@ -160,35 +146,52 @@ export class VendorCommand {
160
146
  return;
161
147
  }
162
148
 
163
- const c = this.color;
164
-
165
149
  for (const pkg of result.packages) {
166
- const count =
167
- pkg.added.length + pkg.modified.length + pkg.removed.length;
168
- if (count === 0) {
169
- process.stdout.write(`\n${c.set("CYAN", pkg.name)}: no changes\n`);
170
- continue;
171
- }
172
-
173
- process.stdout.write(
174
- `\n${c.set("CYAN", pkg.name)}: ${count} ${count === 1 ? "file differs" : "files differ"}\n`,
175
- );
176
-
177
- for (const file of pkg.added) {
178
- process.stdout.write(` ${c.set("GREEN", "A")} ${file}\n`);
179
- }
180
- for (const file of pkg.modified) {
181
- process.stdout.write(` ${c.set("ORANGE", "M")} ${file}\n`);
182
- }
183
- for (const file of pkg.removed) {
184
- process.stdout.write(` ${c.set("RED", "D")} ${file}\n`);
185
- }
150
+ this.printPackageDiff(pkg);
186
151
  }
187
152
 
188
153
  process.stdout.write("\n");
189
154
  },
190
155
  });
191
156
 
157
+ // ─────────────────────────────────────────────────────────────────────────
158
+ // Helpers
159
+ // ─────────────────────────────────────────────────────────────────────────
160
+
161
+ protected printPackageDiff(pkg: VendorPackageDiff) {
162
+ const c = this.color;
163
+ const count = pkg.added.length + pkg.modified.length + pkg.removed.length;
164
+
165
+ if (count === 0) {
166
+ process.stdout.write(`\n${c.set("CYAN", pkg.name)}: no changes\n`);
167
+ return;
168
+ }
169
+
170
+ process.stdout.write(
171
+ `\n${c.set("CYAN", pkg.name)}: ${count} ${count === 1 ? "file differs" : "files differ"}\n`,
172
+ );
173
+
174
+ for (const file of pkg.added) {
175
+ process.stdout.write(` ${c.set("GREEN", "A")} ${file}\n`);
176
+ }
177
+
178
+ for (const fileDiff of pkg.modified) {
179
+ process.stdout.write(` ${c.set("ORANGE", "M")} ${fileDiff.file}\n`);
180
+ for (const change of fileDiff.changes) {
181
+ const prefix = change.type === "removed" ? "-" : "+";
182
+ const color = change.type === "removed" ? "RED" : "GREEN";
183
+ const lineNum = `L${change.line}`;
184
+ process.stdout.write(
185
+ ` ${c.set("DIM", lineNum.padEnd(5))} ${c.set(color, `${prefix} ${change.text}`)}\n`,
186
+ );
187
+ }
188
+ }
189
+
190
+ for (const file of pkg.removed) {
191
+ process.stdout.write(` ${c.set("RED", "D")} ${file}\n`);
192
+ }
193
+ }
194
+
192
195
  // ─────────────────────────────────────────────────────────────────────────
193
196
  // Parent command
194
197
  // ─────────────────────────────────────────────────────────────────────────
@@ -32,13 +32,30 @@ export interface VendorDiffOptions {
32
32
  packages: string[];
33
33
  }
34
34
 
35
+ /**
36
+ * A single line change within a modified file.
37
+ */
38
+ export interface VendorLineDiff {
39
+ line: number;
40
+ type: "added" | "removed";
41
+ text: string;
42
+ }
43
+
44
+ /**
45
+ * A modified file with its line-level changes.
46
+ */
47
+ export interface VendorFileDiff {
48
+ file: string;
49
+ changes: VendorLineDiff[];
50
+ }
51
+
35
52
  /**
36
53
  * Diff result for a single vendored package.
37
54
  */
38
55
  export interface VendorPackageDiff {
39
56
  name: string;
40
57
  added: string[];
41
- modified: string[];
58
+ modified: VendorFileDiff[];
42
59
  removed: string[];
43
60
  }
44
61
 
@@ -388,9 +405,13 @@ export class VendorService {
388
405
  protected async diffDirectories(
389
406
  localDir: string,
390
407
  remoteDir: string,
391
- ): Promise<{ added: string[]; modified: string[]; removed: string[] }> {
408
+ ): Promise<{
409
+ added: string[];
410
+ modified: VendorFileDiff[];
411
+ removed: string[];
412
+ }> {
392
413
  const added: string[] = [];
393
- const modified: string[] = [];
414
+ const modified: VendorFileDiff[] = [];
394
415
  const removed: string[] = [];
395
416
 
396
417
  const [localFiles, remoteFiles] = await Promise.all([
@@ -418,7 +439,11 @@ export class VendorService {
418
439
  ]);
419
440
 
420
441
  if (!localContent.equals(remoteContent)) {
421
- modified.push(file);
442
+ const changes = this.computeLineDiff(
443
+ remoteContent.toString(),
444
+ localContent.toString(),
445
+ );
446
+ modified.push({ file, changes });
422
447
  }
423
448
  } catch {
424
449
  // Skip directories and unreadable entries
@@ -434,4 +459,83 @@ export class VendorService {
434
459
 
435
460
  return { added, modified, removed };
436
461
  }
462
+
463
+ /**
464
+ * Compute line-level differences between two file contents.
465
+ *
466
+ * Uses a longest-common-subsequence algorithm to produce minimal
467
+ * added/removed line changes with accurate line numbers.
468
+ */
469
+ protected computeLineDiff(baseline: string, local: string): VendorLineDiff[] {
470
+ const baseLines = baseline.split("\n");
471
+ const localLines = local.split("\n");
472
+ const lcs = this.longestCommonSubsequence(baseLines, localLines);
473
+ const changes: VendorLineDiff[] = [];
474
+
475
+ let bi = 0;
476
+ let li = 0;
477
+ let ci = 0;
478
+
479
+ while (bi < baseLines.length || li < localLines.length) {
480
+ if (ci < lcs.length && bi < baseLines.length && li < localLines.length) {
481
+ if (baseLines[bi] === lcs[ci] && localLines[li] === lcs[ci]) {
482
+ // Line is unchanged
483
+ bi++;
484
+ li++;
485
+ ci++;
486
+ } else if (baseLines[bi] !== lcs[ci]) {
487
+ changes.push({ line: bi + 1, type: "removed", text: baseLines[bi] });
488
+ bi++;
489
+ } else {
490
+ changes.push({ line: li + 1, type: "added", text: localLines[li] });
491
+ li++;
492
+ }
493
+ } else if (bi < baseLines.length) {
494
+ changes.push({ line: bi + 1, type: "removed", text: baseLines[bi] });
495
+ bi++;
496
+ } else {
497
+ changes.push({ line: li + 1, type: "added", text: localLines[li] });
498
+ li++;
499
+ }
500
+ }
501
+
502
+ return changes;
503
+ }
504
+
505
+ /**
506
+ * Compute the longest common subsequence of two string arrays.
507
+ */
508
+ protected longestCommonSubsequence(a: string[], b: string[]): string[] {
509
+ const m = a.length;
510
+ const n = b.length;
511
+ const dp: number[][] = Array.from({ length: m + 1 }, () =>
512
+ Array(n + 1).fill(0),
513
+ );
514
+
515
+ for (let i = 1; i <= m; i++) {
516
+ for (let j = 1; j <= n; j++) {
517
+ dp[i][j] =
518
+ a[i - 1] === b[j - 1]
519
+ ? dp[i - 1][j - 1] + 1
520
+ : Math.max(dp[i - 1][j], dp[i][j - 1]);
521
+ }
522
+ }
523
+
524
+ const result: string[] = [];
525
+ let i = m;
526
+ let j = n;
527
+ while (i > 0 && j > 0) {
528
+ if (a[i - 1] === b[j - 1]) {
529
+ result.unshift(a[i - 1]);
530
+ i--;
531
+ j--;
532
+ } else if (dp[i - 1][j] > dp[i][j - 1]) {
533
+ i--;
534
+ } else {
535
+ j--;
536
+ }
537
+ }
538
+
539
+ return result;
540
+ }
437
541
  }
@@ -26,6 +26,7 @@ class TestCliProvider extends CliProvider {
26
26
  public testFindPostHooks = this.findPostHooks.bind(this);
27
27
  public testGetEnumValues = this.getEnumValues.bind(this);
28
28
  public testFormatFlagDescription = this.formatFlagDescription.bind(this);
29
+ public testParseCommandEnv = this.parseCommandEnv.bind(this);
29
30
 
30
31
  /**
31
32
  * Extract flag definitions from a command's flags schema (for testing printHelp logic).
@@ -1291,6 +1292,50 @@ describe("CliProvider", () => {
1291
1292
  });
1292
1293
  });
1293
1294
 
1295
+ // ─────────────────────────────────────────────────────────────────────────────
1296
+ // parseCommandEnv
1297
+ // ─────────────────────────────────────────────────────────────────────────────
1298
+
1299
+ describe("parseCommandEnv", () => {
1300
+ it("should use default value for required field with default when env var is missing", () => {
1301
+ const cli = createTestCli();
1302
+ const schema = t.object({
1303
+ MY_VAR: t.text({ default: "fallback" }),
1304
+ });
1305
+
1306
+ delete process.env.MY_VAR;
1307
+ const result = cli.testParseCommandEnv(schema, "test");
1308
+ expect(result.MY_VAR).toBe("fallback");
1309
+ });
1310
+
1311
+ it("should prefer env var over default when both exist", () => {
1312
+ const cli = createTestCli();
1313
+ const schema = t.object({
1314
+ MY_VAR: t.text({ default: "fallback" }),
1315
+ });
1316
+
1317
+ process.env.MY_VAR = "from-env";
1318
+ try {
1319
+ const result = cli.testParseCommandEnv(schema, "test");
1320
+ expect(result.MY_VAR).toBe("from-env");
1321
+ } finally {
1322
+ delete process.env.MY_VAR;
1323
+ }
1324
+ });
1325
+
1326
+ it("should throw for required field without default when env var is missing", () => {
1327
+ const cli = createTestCli();
1328
+ const schema = t.object({
1329
+ MY_VAR: t.text(),
1330
+ });
1331
+
1332
+ delete process.env.MY_VAR;
1333
+ expect(() => cli.testParseCommandEnv(schema, "test")).toThrow(
1334
+ "Missing required environment variable",
1335
+ );
1336
+ });
1337
+ });
1338
+
1294
1339
  // ─────────────────────────────────────────────────────────────────────────────
1295
1340
  // run (test helper)
1296
1341
  // ─────────────────────────────────────────────────────────────────────────────
@@ -594,11 +594,10 @@ export class CliProvider {
594
594
 
595
595
  if (value !== undefined) {
596
596
  result[key] = value;
597
+ } else if ("default" in propSchema) {
598
+ result[key] = propSchema.default;
597
599
  } else if (t.schema.isOptional(propSchema)) {
598
- // Check for default value
599
- if ("default" in propSchema) {
600
- result[key] = propSchema.default;
601
- }
600
+ // Optional with no default — leave undefined
602
601
  } else {
603
602
  missing.push(key);
604
603
  }
@@ -533,6 +533,8 @@ export abstract class Repository<T extends TObject> {
533
533
  } = {},
534
534
  opts: StatementOptions & { count?: boolean } = {},
535
535
  ): Promise<Page<PgStatic<T, R>>> {
536
+ // Overflow-safe: pageQuerySchema constrains size to [1, 100] and page to >= 0.
537
+ // With max size=100, page would need to exceed 2^45 to overflow Number.MAX_SAFE_INTEGER.
536
538
  const limit = query.limit ?? pagination.size ?? 10;
537
539
  const page = pagination.page ?? 0;
538
540
  const offset = query.offset ?? page * limit;
@@ -674,7 +676,9 @@ export abstract class Repository<T extends TObject> {
674
676
  .returning(this.table)
675
677
  .then(([it]) => this.clean(it, this.entity.schema));
676
678
 
677
- this.dbCache.invalidateTable(this.tableName).catch(() => {});
679
+ this.dbCache
680
+ .invalidateTable(this.tableName)
681
+ .catch((err) => this.log.warn("Cache invalidation failed", err));
678
682
 
679
683
  await this.alepha.events.emit("repository:create:after", {
680
684
  tableName: this.tableName,
@@ -728,7 +732,9 @@ export abstract class Repository<T extends TObject> {
728
732
  allEntities.push(...entities);
729
733
  }
730
734
 
731
- this.dbCache.invalidateTable(this.tableName).catch(() => {});
735
+ this.dbCache
736
+ .invalidateTable(this.tableName)
737
+ .catch((err) => this.log.warn("Cache invalidation failed", err));
732
738
 
733
739
  await this.alepha.events.emit("repository:create:after", {
734
740
  tableName: this.tableName,
@@ -825,7 +831,9 @@ export abstract class Repository<T extends TObject> {
825
831
  .returning(this.table)
826
832
  .then(([it]) => this.clean(it, this.entity.schema));
827
833
 
828
- this.dbCache.invalidateTable(this.tableName).catch(() => {});
834
+ this.dbCache
835
+ .invalidateTable(this.tableName)
836
+ .catch((err) => this.log.warn("Cache invalidation failed", err));
829
837
 
830
838
  await this.alepha.events.emit("repository:create:after", {
831
839
  tableName: this.tableName,
@@ -889,7 +897,9 @@ export abstract class Repository<T extends TObject> {
889
897
  try {
890
898
  const entity = this.clean(response[0], this.entity.schema);
891
899
 
892
- this.dbCache.invalidateTable(this.tableName).catch(() => {});
900
+ this.dbCache
901
+ .invalidateTable(this.tableName)
902
+ .catch((err) => this.log.warn("Cache invalidation failed", err));
893
903
 
894
904
  await this.alepha.events.emit("repository:update:after", {
895
905
  tableName: this.tableName,
@@ -1041,7 +1051,9 @@ export abstract class Repository<T extends TObject> {
1041
1051
  .where(this.toSQL(where))
1042
1052
  .returning();
1043
1053
 
1044
- this.dbCache.invalidateTable(this.tableName).catch(() => {});
1054
+ this.dbCache
1055
+ .invalidateTable(this.tableName)
1056
+ .catch((err) => this.log.warn("Cache invalidation failed", err));
1045
1057
 
1046
1058
  await this.alepha.events.emit("repository:update:after", {
1047
1059
  tableName: this.tableName,
@@ -1089,7 +1101,9 @@ export abstract class Repository<T extends TObject> {
1089
1101
  .returning({ id: (this.table as any)[this.id.key] });
1090
1102
  const ids = result.map((row) => row.id);
1091
1103
 
1092
- this.dbCache.invalidateTable(this.tableName).catch(() => {});
1104
+ this.dbCache
1105
+ .invalidateTable(this.tableName)
1106
+ .catch((err) => this.log.warn("Cache invalidation failed", err));
1093
1107
 
1094
1108
  await this.alepha.events.emit("repository:delete:after", {
1095
1109
  tableName: this.tableName,
@@ -366,6 +366,89 @@ describe("I18nProvider", () => {
366
366
  );
367
367
  });
368
368
 
369
+ describe("when only non-English dictionaries are registered", () => {
370
+ test("should default to the first registered language", async ({
371
+ expect,
372
+ }) => {
373
+ class App {
374
+ fr = $dictionary({
375
+ lazy: async () => ({ default: { hello: "Bonjour" } }),
376
+ });
377
+ }
378
+
379
+ const alepha = Alepha.create().with(AlephaReactI18n);
380
+ const app = alepha.inject(App);
381
+ const i18n = alepha.inject(I18nProvider);
382
+
383
+ expect(i18n.lang).toBe("fr");
384
+ });
385
+
386
+ test("should translate using the first registered language", async ({
387
+ expect,
388
+ }) => {
389
+ class App {
390
+ fr = $dictionary({
391
+ lazy: async () => ({
392
+ default: { greeting: "Bonjour!", settings: "Paramètres" },
393
+ }),
394
+ });
395
+ }
396
+
397
+ const alepha = Alepha.create().with(AlephaReactI18n);
398
+ const app = alepha.inject(App);
399
+ const i18n = alepha.inject(I18nProvider);
400
+
401
+ await alepha.start();
402
+
403
+ expect(i18n.tr("greeting")).toBe("Bonjour!");
404
+ expect(i18n.tr("settings")).toBe("Paramètres");
405
+ });
406
+
407
+ test("should not include 'en' in languages list", async ({ expect }) => {
408
+ class App {
409
+ fr = $dictionary({
410
+ lazy: async () => ({ default: { hello: "Bonjour" } }),
411
+ });
412
+
413
+ de = $dictionary({
414
+ lazy: async () => ({ default: { hello: "Hallo" } }),
415
+ });
416
+ }
417
+
418
+ const alepha = Alepha.create().with(AlephaReactI18n);
419
+ const app = alepha.inject(App);
420
+ const i18n = alepha.inject(I18nProvider);
421
+
422
+ expect(i18n.languages).toEqual(["fr", "de"]);
423
+ });
424
+
425
+ test("should fallback to the first registered language for missing keys", async ({
426
+ expect,
427
+ }) => {
428
+ class App {
429
+ fr = $dictionary({
430
+ lazy: async () => ({
431
+ default: { hello: "Bonjour", world: "Monde" },
432
+ }),
433
+ });
434
+
435
+ de = $dictionary({
436
+ lazy: async () => ({ default: { hello: "Hallo" } }),
437
+ });
438
+ }
439
+
440
+ const alepha = Alepha.create().with(AlephaReactI18n);
441
+ const app = alepha.inject(App);
442
+ const i18n = alepha.inject(I18nProvider);
443
+
444
+ await alepha.start();
445
+ await i18n.setLang("de");
446
+
447
+ expect(i18n.tr("hello")).toBe("Hallo");
448
+ expect(i18n.tr("world")).toBe("Monde"); // Falls back to FR
449
+ });
450
+ });
451
+
369
452
  test("should handle partial interpolation args", async ({ expect }) => {
370
453
  class App {
371
454
  en = $dictionary({