@rebasepro/server-core 0.2.3 → 0.2.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 (116) hide show
  1. package/dist/common/src/collections/default-collections.d.ts +9 -0
  2. package/dist/common/src/collections/index.d.ts +1 -0
  3. package/dist/common/src/util/permissions.d.ts +1 -0
  4. package/dist/{index-BZoAtuqi.js → index-Cr1D21av.js} +11 -3
  5. package/dist/index-Cr1D21av.js.map +1 -0
  6. package/dist/index.es.js +2309 -370
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/index.umd.js +2298 -355
  9. package/dist/index.umd.js.map +1 -1
  10. package/dist/server-core/src/api/logs-routes.d.ts +37 -0
  11. package/dist/server-core/src/api/rest/api-generator.d.ts +6 -0
  12. package/dist/server-core/src/api/types.d.ts +6 -1
  13. package/dist/server-core/src/auth/adapter-middleware.d.ts +7 -3
  14. package/dist/server-core/src/auth/admin-routes.d.ts +3 -3
  15. package/dist/server-core/src/auth/api-keys/api-key-middleware.d.ts +39 -0
  16. package/dist/server-core/src/auth/api-keys/api-key-permission-guard.d.ts +32 -0
  17. package/dist/server-core/src/auth/api-keys/api-key-routes.d.ts +20 -0
  18. package/dist/server-core/src/auth/api-keys/api-key-store.d.ts +35 -0
  19. package/dist/server-core/src/auth/api-keys/api-key-types.d.ts +88 -0
  20. package/dist/server-core/src/auth/api-keys/index.d.ts +17 -0
  21. package/dist/server-core/src/auth/{auth-overrides.d.ts → auth-hooks.d.ts} +69 -11
  22. package/dist/server-core/src/auth/builtin-auth-adapter.d.ts +3 -3
  23. package/dist/server-core/src/auth/index.d.ts +5 -3
  24. package/dist/server-core/src/auth/interfaces.d.ts +93 -3
  25. package/dist/server-core/src/auth/jwt.d.ts +3 -1
  26. package/dist/server-core/src/auth/mfa.d.ts +49 -0
  27. package/dist/server-core/src/auth/middleware.d.ts +7 -0
  28. package/dist/server-core/src/auth/rate-limiter.d.ts +19 -0
  29. package/dist/server-core/src/auth/routes.d.ts +3 -3
  30. package/dist/server-core/src/env.d.ts +6 -0
  31. package/dist/server-core/src/index.d.ts +1 -0
  32. package/dist/server-core/src/init.d.ts +3 -3
  33. package/dist/server-core/src/services/webhook-service.d.ts +29 -0
  34. package/dist/server-core/src/storage/image-transform.d.ts +48 -0
  35. package/dist/server-core/src/storage/index.d.ts +3 -0
  36. package/dist/server-core/src/storage/tus-handler.d.ts +51 -0
  37. package/dist/types/src/controllers/auth.d.ts +4 -26
  38. package/dist/types/src/controllers/client.d.ts +25 -43
  39. package/dist/types/src/controllers/collection_registry.d.ts +1 -1
  40. package/dist/types/src/controllers/data.d.ts +4 -0
  41. package/dist/types/src/controllers/data_driver.d.ts +23 -0
  42. package/dist/types/src/controllers/registry.d.ts +5 -4
  43. package/dist/types/src/rebase_context.d.ts +1 -1
  44. package/dist/types/src/types/auth_adapter.d.ts +5 -60
  45. package/dist/types/src/types/backend.d.ts +2 -2
  46. package/dist/types/src/types/backend_hooks.d.ts +2 -17
  47. package/dist/types/src/types/collections.d.ts +0 -4
  48. package/dist/types/src/types/component_ref.d.ts +1 -1
  49. package/dist/types/src/types/cron.d.ts +1 -1
  50. package/dist/types/src/types/entity_views.d.ts +1 -0
  51. package/dist/types/src/types/export_import.d.ts +1 -1
  52. package/dist/types/src/types/formex.d.ts +2 -2
  53. package/dist/types/src/types/properties.d.ts +9 -7
  54. package/dist/types/src/types/translations.d.ts +28 -12
  55. package/dist/types/src/types/user_management_delegate.d.ts +22 -57
  56. package/dist/types/src/users/index.d.ts +0 -1
  57. package/dist/types/src/users/user.d.ts +0 -1
  58. package/package.json +8 -6
  59. package/src/api/ast-schema-editor.ts +4 -4
  60. package/src/api/errors.ts +16 -7
  61. package/src/api/logs-routes.ts +129 -0
  62. package/src/api/rest/api-generator.ts +42 -2
  63. package/src/api/rest/query-parser.ts +37 -1
  64. package/src/api/types.ts +6 -1
  65. package/src/auth/adapter-middleware.ts +20 -4
  66. package/src/auth/admin-routes.ts +36 -100
  67. package/src/auth/api-keys/api-key-middleware.ts +126 -0
  68. package/src/auth/api-keys/api-key-permission-guard.ts +64 -0
  69. package/src/auth/api-keys/api-key-routes.ts +183 -0
  70. package/src/auth/api-keys/api-key-store.ts +317 -0
  71. package/src/auth/api-keys/api-key-types.ts +94 -0
  72. package/src/auth/api-keys/index.ts +37 -0
  73. package/src/auth/{auth-overrides.ts → auth-hooks.ts} +81 -14
  74. package/src/auth/builtin-auth-adapter.ts +35 -73
  75. package/src/auth/custom-auth-adapter.ts +1 -1
  76. package/src/auth/index.ts +7 -3
  77. package/src/auth/interfaces.ts +111 -3
  78. package/src/auth/jwt.ts +19 -5
  79. package/src/auth/mfa.ts +160 -0
  80. package/src/auth/middleware.ts +20 -1
  81. package/src/auth/rate-limiter.ts +92 -0
  82. package/src/auth/routes.ts +455 -24
  83. package/src/cron/cron-loader.ts +5 -10
  84. package/src/cron/cron-scheduler.ts +11 -12
  85. package/src/cron/cron-store.ts +8 -7
  86. package/src/env.ts +2 -0
  87. package/src/functions/function-loader.ts +6 -9
  88. package/src/index.ts +1 -2
  89. package/src/init.ts +56 -8
  90. package/src/serve-spa.ts +5 -4
  91. package/src/services/webhook-service.ts +155 -0
  92. package/src/storage/image-transform.ts +202 -0
  93. package/src/storage/index.ts +3 -0
  94. package/src/storage/routes.ts +56 -3
  95. package/src/storage/tus-handler.ts +315 -0
  96. package/src/utils/dev-port.ts +14 -0
  97. package/src/utils/logging.ts +9 -7
  98. package/test/admin-routes.test.ts +62 -164
  99. package/test/api-generator.test.ts +0 -1
  100. package/test/api-key-permission-guard.test.ts +132 -0
  101. package/test/ast-schema-editor.test.ts +26 -0
  102. package/test/auth-routes.test.ts +1 -2
  103. package/test/backend-hooks-admin.test.ts +3 -29
  104. package/test/custom-auth-adapter.test.ts +2 -10
  105. package/test/email-templates.test.ts +169 -0
  106. package/test/function-loader.test.ts +124 -0
  107. package/test/jwt.test.ts +4 -2
  108. package/test/mfa.test.ts +197 -0
  109. package/test/middleware.test.ts +10 -5
  110. package/test/webhook-service.test.ts +249 -0
  111. package/vite.config.ts +3 -2
  112. package/dist/index-BZoAtuqi.js.map +0 -1
  113. package/dist/server-core/src/bootstrappers/index.d.ts +0 -0
  114. package/dist/types/src/users/roles.d.ts +0 -22
  115. package/src/bootstrappers/index.ts +0 -1
  116. package/src/singleton.test.ts +0 -28
@@ -0,0 +1,169 @@
1
+ import {
2
+ getPasswordResetTemplate,
3
+ getEmailVerificationTemplate,
4
+ getUserInvitationTemplate,
5
+ getWelcomeEmailTemplate
6
+ } from "../src/email/templates";
7
+
8
+ describe("getPasswordResetTemplate", () => {
9
+ const user = { email: "john@example.com", displayName: "John Doe" };
10
+ const resetUrl = "https://example.com/reset?token=abc123";
11
+
12
+ it("returns subject, html, and text", () => {
13
+ const result = getPasswordResetTemplate(resetUrl, user);
14
+ expect(result.subject).toBeTruthy();
15
+ expect(result.html).toBeTruthy();
16
+ expect(result.text).toBeTruthy();
17
+ });
18
+
19
+ it("subject includes default app name", () => {
20
+ const result = getPasswordResetTemplate(resetUrl, user);
21
+ expect(result.subject).toContain("Rebase");
22
+ });
23
+
24
+ it("subject includes custom app name", () => {
25
+ const result = getPasswordResetTemplate(resetUrl, user, "MyApp");
26
+ expect(result.subject).toContain("MyApp");
27
+ });
28
+
29
+ it("html contains the reset URL", () => {
30
+ const result = getPasswordResetTemplate(resetUrl, user);
31
+ expect(result.html).toContain(resetUrl);
32
+ });
33
+
34
+ it("text contains the reset URL", () => {
35
+ const result = getPasswordResetTemplate(resetUrl, user);
36
+ expect(result.text).toContain(resetUrl);
37
+ });
38
+
39
+ it("uses displayName for greeting", () => {
40
+ const result = getPasswordResetTemplate(resetUrl, user);
41
+ expect(result.html).toContain("John Doe");
42
+ expect(result.text).toContain("John Doe");
43
+ });
44
+
45
+ it("falls back to email prefix when displayName is null", () => {
46
+ const userNoName = { email: "jane@example.com", displayName: null };
47
+ const result = getPasswordResetTemplate(resetUrl, userNoName);
48
+ expect(result.html).toContain("jane");
49
+ expect(result.text).toContain("jane");
50
+ });
51
+
52
+ it("html contains DOCTYPE and body tags", () => {
53
+ const result = getPasswordResetTemplate(resetUrl, user);
54
+ expect(result.html).toContain("<!DOCTYPE html>");
55
+ expect(result.html).toContain("<body");
56
+ expect(result.html).toContain("</body>");
57
+ });
58
+ });
59
+
60
+ describe("getEmailVerificationTemplate", () => {
61
+ const user = { email: "alice@example.com", displayName: "Alice" };
62
+ const verifyUrl = "https://example.com/verify?token=xyz789";
63
+
64
+ it("returns subject, html, and text", () => {
65
+ const result = getEmailVerificationTemplate(verifyUrl, user);
66
+ expect(result.subject).toBeTruthy();
67
+ expect(result.html).toBeTruthy();
68
+ expect(result.text).toBeTruthy();
69
+ });
70
+
71
+ it("subject mentions email verification", () => {
72
+ const result = getEmailVerificationTemplate(verifyUrl, user);
73
+ expect(result.subject.toLowerCase()).toContain("verify");
74
+ });
75
+
76
+ it("html contains the verification URL", () => {
77
+ const result = getEmailVerificationTemplate(verifyUrl, user);
78
+ expect(result.html).toContain(verifyUrl);
79
+ });
80
+
81
+ it("text contains the verification URL", () => {
82
+ const result = getEmailVerificationTemplate(verifyUrl, user);
83
+ expect(result.text).toContain(verifyUrl);
84
+ });
85
+
86
+ it("uses displayName when available", () => {
87
+ const result = getEmailVerificationTemplate(verifyUrl, user);
88
+ expect(result.html).toContain("Alice");
89
+ });
90
+
91
+ it("uses custom app name", () => {
92
+ const result = getEmailVerificationTemplate(verifyUrl, user, "CustomApp");
93
+ expect(result.subject).toContain("CustomApp");
94
+ expect(result.html).toContain("CustomApp");
95
+ });
96
+ });
97
+
98
+ describe("getUserInvitationTemplate", () => {
99
+ const user = { email: "bob@example.com", displayName: "Bob" };
100
+ const setPasswordUrl = "https://example.com/set-password?token=def456";
101
+
102
+ it("returns subject, html, and text", () => {
103
+ const result = getUserInvitationTemplate(setPasswordUrl, user);
104
+ expect(result.subject).toBeTruthy();
105
+ expect(result.html).toBeTruthy();
106
+ expect(result.text).toBeTruthy();
107
+ });
108
+
109
+ it("subject mentions invitation", () => {
110
+ const result = getUserInvitationTemplate(setPasswordUrl, user);
111
+ expect(result.subject.toLowerCase()).toContain("invited");
112
+ });
113
+
114
+ it("html contains the set password URL", () => {
115
+ const result = getUserInvitationTemplate(setPasswordUrl, user);
116
+ expect(result.html).toContain(setPasswordUrl);
117
+ });
118
+
119
+ it("text contains the set password URL", () => {
120
+ const result = getUserInvitationTemplate(setPasswordUrl, user);
121
+ expect(result.text).toContain(setPasswordUrl);
122
+ });
123
+
124
+ it("falls back to email prefix when no displayName", () => {
125
+ const userNoName = { email: "charlie@example.com" };
126
+ const result = getUserInvitationTemplate(setPasswordUrl, userNoName);
127
+ expect(result.html).toContain("charlie");
128
+ });
129
+ });
130
+
131
+ describe("getWelcomeEmailTemplate", () => {
132
+ const user = { email: "dave@example.com", displayName: "Dave" };
133
+
134
+ it("returns subject, html, and text", () => {
135
+ const result = getWelcomeEmailTemplate(user);
136
+ expect(result.subject).toBeTruthy();
137
+ expect(result.html).toBeTruthy();
138
+ expect(result.text).toBeTruthy();
139
+ });
140
+
141
+ it("includes the app name in subject", () => {
142
+ const result = getWelcomeEmailTemplate(user, "SuperApp");
143
+ expect(result.subject).toContain("SuperApp");
144
+ });
145
+
146
+ it("uses displayName for greeting", () => {
147
+ const result = getWelcomeEmailTemplate(user);
148
+ expect(result.html).toContain("Dave");
149
+ });
150
+
151
+ it("includes login URL when provided", () => {
152
+ const loginUrl = "https://example.com/login";
153
+ const result = getWelcomeEmailTemplate(user, "Rebase", loginUrl);
154
+ expect(result.html).toContain(loginUrl);
155
+ expect(result.text).toContain(loginUrl);
156
+ });
157
+
158
+ it("handles missing login URL gracefully", () => {
159
+ const result = getWelcomeEmailTemplate(user);
160
+ expect(result.html).toBeTruthy();
161
+ expect(result.text).toBeTruthy();
162
+ });
163
+
164
+ it("html contains proper structure", () => {
165
+ const result = getWelcomeEmailTemplate(user);
166
+ expect(result.html).toContain("<!DOCTYPE html>");
167
+ expect(result.html).toContain("<body");
168
+ });
169
+ });
@@ -0,0 +1,124 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import * as os from "os";
4
+ import { Hono } from "hono";
5
+ import { loadFunctionsFromDirectory } from "../src/functions/function-loader";
6
+ import { createFunctionRoutes } from "../src/functions/function-routes";
7
+
8
+ describe("Function Loader & Routes", () => {
9
+ let tempDir: string;
10
+
11
+ beforeAll(() => {
12
+ // Create temporary directory in OS temp folder
13
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "rebase-functions-test-"));
14
+
15
+ // 1. Write a valid Hono app default export using CommonJS
16
+ fs.writeFileSync(
17
+ path.join(tempDir, "valid-app.js"),
18
+ `
19
+ const { Hono } = require("hono");
20
+ const app = new Hono();
21
+ app.get("/hello", (c) => c.text("hello from valid-app"));
22
+ module.exports = app;
23
+ `
24
+ );
25
+
26
+ // 2. Write a valid Hono app factory default export using CommonJS
27
+ fs.writeFileSync(
28
+ path.join(tempDir, "valid-factory.js"),
29
+ `
30
+ const { Hono } = require("hono");
31
+ module.exports = () => {
32
+ const app = new Hono();
33
+ app.post("/hello", (c) => c.text("hello from valid-factory"));
34
+ return app;
35
+ };
36
+ `
37
+ );
38
+
39
+ // 3. Write an invalid default export
40
+ fs.writeFileSync(
41
+ path.join(tempDir, "invalid-export.js"),
42
+ `
43
+ module.exports = { foo: "bar" };
44
+ `
45
+ );
46
+
47
+ // 4. Write a file with no default export (exports is an object but no module.exports default value)
48
+ fs.writeFileSync(
49
+ path.join(tempDir, "no-default.js"),
50
+ `
51
+ exports.other = "value";
52
+ `
53
+ );
54
+
55
+ // 5. Write an ignored non-js/ts file
56
+ fs.writeFileSync(
57
+ path.join(tempDir, "ignored.txt"),
58
+ "some random text"
59
+ );
60
+ });
61
+
62
+ afterAll(() => {
63
+ // Clean up temp directory
64
+ if (tempDir && fs.existsSync(tempDir)) {
65
+ fs.rmSync(tempDir, { recursive: true, force: true });
66
+ }
67
+ });
68
+
69
+ describe("loadFunctionsFromDirectory", () => {
70
+ it("should return empty array if directory does not exist", async () => {
71
+ const result = await loadFunctionsFromDirectory(path.join(tempDir, "non-existent-folder"));
72
+ expect(result).toEqual([]);
73
+ });
74
+
75
+ it("should load valid apps and factories while ignoring invalid files", async () => {
76
+ const loaded = await loadFunctionsFromDirectory(tempDir);
77
+
78
+ // We expect exactly 2 functions to be loaded: "valid-app" and "valid-factory"
79
+ expect(loaded).toHaveLength(2);
80
+
81
+ const names = loaded.map(f => f.name);
82
+ expect(names).toContain("valid-app");
83
+ expect(names).toContain("valid-factory");
84
+
85
+ // Verify they both look like Hono instances
86
+ const validApp = loaded.find(f => f.name === "valid-app")!;
87
+ expect(typeof validApp.app.fetch).toBe("function");
88
+ expect(Array.isArray(validApp.app.routes)).toBe(true);
89
+
90
+ const validFactory = loaded.find(f => f.name === "valid-factory")!;
91
+ expect(typeof validFactory.app.fetch).toBe("function");
92
+ expect(Array.isArray(validFactory.app.routes)).toBe(true);
93
+ });
94
+ });
95
+
96
+ describe("createFunctionRoutes", () => {
97
+ it("should mount and route correctly", async () => {
98
+ const loaded = await loadFunctionsFromDirectory(tempDir);
99
+ const routes = createFunctionRoutes(loaded);
100
+
101
+ // 1. Verify GET / lists loaded functions
102
+ const listRes = await routes.request("/");
103
+ expect(listRes.status).toBe(200);
104
+
105
+ const listData = await listRes.json();
106
+ expect(listData).toEqual({
107
+ functions: [
108
+ { name: "valid-app", endpoint: "/functions/valid-app" },
109
+ { name: "valid-factory", endpoint: "/functions/valid-factory" }
110
+ ]
111
+ });
112
+
113
+ // 2. Verify routing to valid-app
114
+ const appRes = await routes.request("/valid-app/hello");
115
+ expect(appRes.status).toBe(200);
116
+ expect(await appRes.text()).toBe("hello from valid-app");
117
+
118
+ // 3. Verify routing to valid-factory
119
+ const factoryRes = await routes.request("/valid-factory/hello", { method: "POST" });
120
+ expect(factoryRes.status).toBe(200);
121
+ expect(await factoryRes.text()).toBe("hello from valid-factory");
122
+ });
123
+ });
124
+ });
package/test/jwt.test.ts CHANGED
@@ -56,7 +56,8 @@ accessExpiresIn: "2h" });
56
56
  const payload = verifyAccessToken(token);
57
57
  expect(payload).toEqual({
58
58
  userId: "user-456",
59
- roles: ["viewer"]
59
+ roles: ["viewer"],
60
+ aal: "aal1"
60
61
  });
61
62
  });
62
63
 
@@ -73,7 +74,8 @@ accessExpiresIn: "2h" });
73
74
  const payload = verifyAccessToken(token);
74
75
  expect(payload).toEqual({
75
76
  userId: "user-123",
76
- roles: ["admin"]
77
+ roles: ["admin"],
78
+ aal: "aal1"
77
79
  });
78
80
  });
79
81
 
@@ -0,0 +1,197 @@
1
+ import {
2
+ base32Encode,
3
+ base32Decode,
4
+ generateTotp,
5
+ verifyTotp,
6
+ generateTotpSecret,
7
+ generateRecoveryCodes,
8
+ hashRecoveryCode
9
+ } from "../src/auth/mfa";
10
+
11
+ describe("base32Encode / base32Decode", () => {
12
+ it("round-trips a known buffer", () => {
13
+ const original = Buffer.from("Hello, World!");
14
+ const encoded = base32Encode(original);
15
+ const decoded = base32Decode(encoded);
16
+ expect(decoded.toString()).toBe("Hello, World!");
17
+ });
18
+
19
+ it("encodes known test vectors (RFC 4648)", () => {
20
+ // RFC 4648 test vectors
21
+ expect(base32Encode(Buffer.from(""))).toBe("");
22
+ expect(base32Encode(Buffer.from("f"))).toBe("MY");
23
+ expect(base32Encode(Buffer.from("fo"))).toBe("MZXQ");
24
+ expect(base32Encode(Buffer.from("foo"))).toBe("MZXW6");
25
+ expect(base32Encode(Buffer.from("foob"))).toBe("MZXW6YQ");
26
+ expect(base32Encode(Buffer.from("fooba"))).toBe("MZXW6YTB");
27
+ expect(base32Encode(Buffer.from("foobar"))).toBe("MZXW6YTBOI");
28
+ });
29
+
30
+ it("decodes base32 with trailing padding characters", () => {
31
+ const decoded = base32Decode("MZXW6===");
32
+ expect(decoded.toString()).toBe("foo");
33
+ });
34
+
35
+ it("is case-insensitive when decoding", () => {
36
+ const upper = base32Decode("MZXW6YTBOI");
37
+ const lower = base32Decode("mzxw6ytboi");
38
+ expect(upper.equals(lower)).toBe(true);
39
+ });
40
+
41
+ it("handles single byte", () => {
42
+ const buf = Buffer.from([0xff]);
43
+ const encoded = base32Encode(buf);
44
+ const decoded = base32Decode(encoded);
45
+ expect(decoded[0]).toBe(0xff);
46
+ });
47
+
48
+ it("round-trips random 20-byte secret", () => {
49
+ const original = Buffer.from("abcdefghij1234567890");
50
+ const encoded = base32Encode(original);
51
+ const decoded = base32Decode(encoded);
52
+ expect(decoded.toString()).toBe(original.toString());
53
+ });
54
+ });
55
+
56
+ describe("generateTotp", () => {
57
+ it("returns a 6-digit string", () => {
58
+ const secret = Buffer.from("12345678901234567890");
59
+ const code = generateTotp(secret);
60
+ expect(code).toMatch(/^\d{6}$/);
61
+ });
62
+
63
+ it("returns consistent results for the same time step", () => {
64
+ const secret = Buffer.from("test-secret-1234567");
65
+ const code1 = generateTotp(secret);
66
+ const code2 = generateTotp(secret);
67
+ expect(code1).toBe(code2);
68
+ });
69
+
70
+ it("pads short codes with leading zeros", () => {
71
+ // Generate many codes to increase chance of seeing leading zeros behavior
72
+ const secret = Buffer.from("12345678901234567890");
73
+ const code = generateTotp(secret);
74
+ expect(code.length).toBe(6);
75
+ });
76
+ });
77
+
78
+ describe("verifyTotp", () => {
79
+ it("verifies a freshly generated token", () => {
80
+ const secret = Buffer.from("12345678901234567890");
81
+ const token = generateTotp(secret);
82
+ expect(verifyTotp(secret, token)).toBe(true);
83
+ });
84
+
85
+ it("rejects an incorrect token", () => {
86
+ const secret = Buffer.from("12345678901234567890");
87
+ expect(verifyTotp(secret, "000000")).toBe(false);
88
+ });
89
+
90
+ it("rejects token from different secret", () => {
91
+ const secret1 = Buffer.from("secret-one-12345678");
92
+ const secret2 = Buffer.from("secret-two-12345678");
93
+ const token = generateTotp(secret1);
94
+ expect(verifyTotp(secret2, token)).toBe(false);
95
+ });
96
+
97
+ it("uses window parameter to check adjacent time steps", () => {
98
+ const secret = Buffer.from("12345678901234567890");
99
+ const token = generateTotp(secret);
100
+ // Window of 0 should still pass for current step
101
+ expect(verifyTotp(secret, token, 0)).toBe(true);
102
+ // Window of 2 should also pass for current step
103
+ expect(verifyTotp(secret, token, 2)).toBe(true);
104
+ });
105
+ });
106
+
107
+ describe("generateTotpSecret", () => {
108
+ it("returns a base32 secret and otpauth URI", () => {
109
+ const result = generateTotpSecret("MyApp", "user@example.com");
110
+ expect(result.secret).toBeTruthy();
111
+ expect(result.uri).toBeTruthy();
112
+ });
113
+
114
+ it("generates a valid otpauth URI format", () => {
115
+ const result = generateTotpSecret("MyApp", "user@example.com");
116
+ expect(result.uri).toMatch(/^otpauth:\/\/totp\//);
117
+ expect(result.uri).toContain("secret=");
118
+ expect(result.uri).toContain("issuer=MyApp");
119
+ expect(result.uri).toContain("algorithm=SHA1");
120
+ expect(result.uri).toContain("digits=6");
121
+ expect(result.uri).toContain("period=30");
122
+ });
123
+
124
+ it("encodes special characters in issuer and account", () => {
125
+ const result = generateTotpSecret("My App & Co.", "user+test@example.com");
126
+ expect(result.uri).toContain(encodeURIComponent("My App & Co."));
127
+ expect(result.uri).toContain(encodeURIComponent("user+test@example.com"));
128
+ });
129
+
130
+ it("generates unique secrets on each call", () => {
131
+ const result1 = generateTotpSecret("App", "user@test.com");
132
+ const result2 = generateTotpSecret("App", "user@test.com");
133
+ expect(result1.secret).not.toBe(result2.secret);
134
+ });
135
+
136
+ it("secret can be decoded back to a 20-byte buffer", () => {
137
+ const result = generateTotpSecret("App", "user@test.com");
138
+ const decoded = base32Decode(result.secret);
139
+ expect(decoded.length).toBe(20);
140
+ });
141
+ });
142
+
143
+ describe("generateRecoveryCodes", () => {
144
+ it("generates 10 codes by default", () => {
145
+ const codes = generateRecoveryCodes();
146
+ expect(codes).toHaveLength(10);
147
+ });
148
+
149
+ it("generates the specified number of codes", () => {
150
+ const codes = generateRecoveryCodes(5);
151
+ expect(codes).toHaveLength(5);
152
+ });
153
+
154
+ it("generates codes in XXXXX-XXXXX format", () => {
155
+ const codes = generateRecoveryCodes();
156
+ for (const code of codes) {
157
+ expect(code).toMatch(/^[A-F0-9]{5}-[A-F0-9]{5}$/);
158
+ }
159
+ });
160
+
161
+ it("generates unique codes", () => {
162
+ const codes = generateRecoveryCodes(20);
163
+ const unique = new Set(codes);
164
+ expect(unique.size).toBe(codes.length);
165
+ });
166
+ });
167
+
168
+ describe("hashRecoveryCode", () => {
169
+ it("returns a hex string", () => {
170
+ const hash = hashRecoveryCode("ABC12-DEF34");
171
+ expect(hash).toMatch(/^[a-f0-9]{64}$/);
172
+ });
173
+
174
+ it("is deterministic", () => {
175
+ const hash1 = hashRecoveryCode("ABC12-DEF34");
176
+ const hash2 = hashRecoveryCode("ABC12-DEF34");
177
+ expect(hash1).toBe(hash2);
178
+ });
179
+
180
+ it("strips dashes before hashing", () => {
181
+ const withDashes = hashRecoveryCode("ABC12-DEF34");
182
+ const withoutDashes = hashRecoveryCode("ABC12DEF34");
183
+ expect(withDashes).toBe(withoutDashes);
184
+ });
185
+
186
+ it("normalizes to uppercase before hashing", () => {
187
+ const upper = hashRecoveryCode("ABC12-DEF34");
188
+ const lower = hashRecoveryCode("abc12-def34");
189
+ expect(upper).toBe(lower);
190
+ });
191
+
192
+ it("different codes produce different hashes", () => {
193
+ const hash1 = hashRecoveryCode("ABC12-DEF34");
194
+ const hash2 = hashRecoveryCode("XYZ98-UVW76");
195
+ expect(hash1).not.toBe(hash2);
196
+ });
197
+ });
@@ -67,7 +67,8 @@ describe("Auth Middleware", () => {
67
67
  expect(nextFn).toHaveBeenCalled();
68
68
  expect(getUser()).toEqual({
69
69
  userId: "user-123",
70
- roles: ["admin", "editor"]
70
+ roles: ["admin", "editor"],
71
+ aal: "aal1"
71
72
  });
72
73
  });
73
74
 
@@ -144,7 +145,8 @@ describe("Auth Middleware", () => {
144
145
  expect(nextFn).toHaveBeenCalled();
145
146
  expect(getUser()).toEqual({
146
147
  userId: "user-123",
147
- roles: ["viewer"]
148
+ roles: ["viewer"],
149
+ aal: "aal1"
148
150
  });
149
151
  });
150
152
  });
@@ -159,7 +161,8 @@ describe("Auth Middleware", () => {
159
161
  expect(nextFn).toHaveBeenCalled();
160
162
  expect(getUser()).toEqual({
161
163
  userId: "user-456",
162
- roles: ["viewer"]
164
+ roles: ["viewer"],
165
+ aal: "aal1"
163
166
  });
164
167
  });
165
168
 
@@ -273,7 +276,8 @@ roles: ["schema-adminstration", "admins", "admin "] } });
273
276
 
274
277
  expect(payload).toEqual({
275
278
  userId: "ws-user-123",
276
- roles: ["admin"]
279
+ roles: ["admin"],
280
+ aal: "aal1"
277
281
  });
278
282
  });
279
283
 
@@ -293,7 +297,8 @@ roles: ["schema-adminstration", "admins", "admin "] } });
293
297
 
294
298
  expect(payload).toEqual({
295
299
  userId: "user-no-roles",
296
- roles: []
300
+ roles: [],
301
+ aal: "aal1"
297
302
  });
298
303
  });
299
304
  });