@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.
- package/dist/common/src/collections/default-collections.d.ts +9 -0
- package/dist/common/src/collections/index.d.ts +1 -0
- package/dist/common/src/util/permissions.d.ts +1 -0
- package/dist/{index-BZoAtuqi.js → index-Cr1D21av.js} +11 -3
- package/dist/index-Cr1D21av.js.map +1 -0
- package/dist/index.es.js +2309 -370
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +2298 -355
- package/dist/index.umd.js.map +1 -1
- package/dist/server-core/src/api/logs-routes.d.ts +37 -0
- package/dist/server-core/src/api/rest/api-generator.d.ts +6 -0
- package/dist/server-core/src/api/types.d.ts +6 -1
- package/dist/server-core/src/auth/adapter-middleware.d.ts +7 -3
- package/dist/server-core/src/auth/admin-routes.d.ts +3 -3
- package/dist/server-core/src/auth/api-keys/api-key-middleware.d.ts +39 -0
- package/dist/server-core/src/auth/api-keys/api-key-permission-guard.d.ts +32 -0
- package/dist/server-core/src/auth/api-keys/api-key-routes.d.ts +20 -0
- package/dist/server-core/src/auth/api-keys/api-key-store.d.ts +35 -0
- package/dist/server-core/src/auth/api-keys/api-key-types.d.ts +88 -0
- package/dist/server-core/src/auth/api-keys/index.d.ts +17 -0
- package/dist/server-core/src/auth/{auth-overrides.d.ts → auth-hooks.d.ts} +69 -11
- package/dist/server-core/src/auth/builtin-auth-adapter.d.ts +3 -3
- package/dist/server-core/src/auth/index.d.ts +5 -3
- package/dist/server-core/src/auth/interfaces.d.ts +93 -3
- package/dist/server-core/src/auth/jwt.d.ts +3 -1
- package/dist/server-core/src/auth/mfa.d.ts +49 -0
- package/dist/server-core/src/auth/middleware.d.ts +7 -0
- package/dist/server-core/src/auth/rate-limiter.d.ts +19 -0
- package/dist/server-core/src/auth/routes.d.ts +3 -3
- package/dist/server-core/src/env.d.ts +6 -0
- package/dist/server-core/src/index.d.ts +1 -0
- package/dist/server-core/src/init.d.ts +3 -3
- package/dist/server-core/src/services/webhook-service.d.ts +29 -0
- package/dist/server-core/src/storage/image-transform.d.ts +48 -0
- package/dist/server-core/src/storage/index.d.ts +3 -0
- package/dist/server-core/src/storage/tus-handler.d.ts +51 -0
- package/dist/types/src/controllers/auth.d.ts +4 -26
- package/dist/types/src/controllers/client.d.ts +25 -43
- package/dist/types/src/controllers/collection_registry.d.ts +1 -1
- package/dist/types/src/controllers/data.d.ts +4 -0
- package/dist/types/src/controllers/data_driver.d.ts +23 -0
- package/dist/types/src/controllers/registry.d.ts +5 -4
- package/dist/types/src/rebase_context.d.ts +1 -1
- package/dist/types/src/types/auth_adapter.d.ts +5 -60
- package/dist/types/src/types/backend.d.ts +2 -2
- package/dist/types/src/types/backend_hooks.d.ts +2 -17
- package/dist/types/src/types/collections.d.ts +0 -4
- package/dist/types/src/types/component_ref.d.ts +1 -1
- package/dist/types/src/types/cron.d.ts +1 -1
- package/dist/types/src/types/entity_views.d.ts +1 -0
- package/dist/types/src/types/export_import.d.ts +1 -1
- package/dist/types/src/types/formex.d.ts +2 -2
- package/dist/types/src/types/properties.d.ts +9 -7
- package/dist/types/src/types/translations.d.ts +28 -12
- package/dist/types/src/types/user_management_delegate.d.ts +22 -57
- package/dist/types/src/users/index.d.ts +0 -1
- package/dist/types/src/users/user.d.ts +0 -1
- package/package.json +8 -6
- package/src/api/ast-schema-editor.ts +4 -4
- package/src/api/errors.ts +16 -7
- package/src/api/logs-routes.ts +129 -0
- package/src/api/rest/api-generator.ts +42 -2
- package/src/api/rest/query-parser.ts +37 -1
- package/src/api/types.ts +6 -1
- package/src/auth/adapter-middleware.ts +20 -4
- package/src/auth/admin-routes.ts +36 -100
- package/src/auth/api-keys/api-key-middleware.ts +126 -0
- package/src/auth/api-keys/api-key-permission-guard.ts +64 -0
- package/src/auth/api-keys/api-key-routes.ts +183 -0
- package/src/auth/api-keys/api-key-store.ts +317 -0
- package/src/auth/api-keys/api-key-types.ts +94 -0
- package/src/auth/api-keys/index.ts +37 -0
- package/src/auth/{auth-overrides.ts → auth-hooks.ts} +81 -14
- package/src/auth/builtin-auth-adapter.ts +35 -73
- package/src/auth/custom-auth-adapter.ts +1 -1
- package/src/auth/index.ts +7 -3
- package/src/auth/interfaces.ts +111 -3
- package/src/auth/jwt.ts +19 -5
- package/src/auth/mfa.ts +160 -0
- package/src/auth/middleware.ts +20 -1
- package/src/auth/rate-limiter.ts +92 -0
- package/src/auth/routes.ts +455 -24
- package/src/cron/cron-loader.ts +5 -10
- package/src/cron/cron-scheduler.ts +11 -12
- package/src/cron/cron-store.ts +8 -7
- package/src/env.ts +2 -0
- package/src/functions/function-loader.ts +6 -9
- package/src/index.ts +1 -2
- package/src/init.ts +56 -8
- package/src/serve-spa.ts +5 -4
- package/src/services/webhook-service.ts +155 -0
- package/src/storage/image-transform.ts +202 -0
- package/src/storage/index.ts +3 -0
- package/src/storage/routes.ts +56 -3
- package/src/storage/tus-handler.ts +315 -0
- package/src/utils/dev-port.ts +14 -0
- package/src/utils/logging.ts +9 -7
- package/test/admin-routes.test.ts +62 -164
- package/test/api-generator.test.ts +0 -1
- package/test/api-key-permission-guard.test.ts +132 -0
- package/test/ast-schema-editor.test.ts +26 -0
- package/test/auth-routes.test.ts +1 -2
- package/test/backend-hooks-admin.test.ts +3 -29
- package/test/custom-auth-adapter.test.ts +2 -10
- package/test/email-templates.test.ts +169 -0
- package/test/function-loader.test.ts +124 -0
- package/test/jwt.test.ts +4 -2
- package/test/mfa.test.ts +197 -0
- package/test/middleware.test.ts +10 -5
- package/test/webhook-service.test.ts +249 -0
- package/vite.config.ts +3 -2
- package/dist/index-BZoAtuqi.js.map +0 -1
- package/dist/server-core/src/bootstrappers/index.d.ts +0 -0
- package/dist/types/src/users/roles.d.ts +0 -22
- package/src/bootstrappers/index.ts +0 -1
- 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
|
|
package/test/mfa.test.ts
ADDED
|
@@ -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
|
+
});
|
package/test/middleware.test.ts
CHANGED
|
@@ -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
|
});
|