@rebasepro/server-core 0.0.1-canary.4d4fb3e
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/LICENSE +6 -0
- package/README.md +40 -0
- package/build-errors.txt +52 -0
- package/coverage/clover.xml +3739 -0
- package/coverage/coverage-final.json +31 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +266 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov-report/src/api/ast-schema-editor.ts.html +952 -0
- package/coverage/lcov-report/src/api/errors.ts.html +472 -0
- package/coverage/lcov-report/src/api/graphql/graphql-schema-generator.ts.html +1069 -0
- package/coverage/lcov-report/src/api/graphql/index.html +116 -0
- package/coverage/lcov-report/src/api/index.html +176 -0
- package/coverage/lcov-report/src/api/openapi-generator.ts.html +565 -0
- package/coverage/lcov-report/src/api/rest/api-generator.ts.html +994 -0
- package/coverage/lcov-report/src/api/rest/index.html +131 -0
- package/coverage/lcov-report/src/api/rest/query-parser.ts.html +550 -0
- package/coverage/lcov-report/src/api/schema-editor-routes.ts.html +202 -0
- package/coverage/lcov-report/src/api/server.ts.html +823 -0
- package/coverage/lcov-report/src/auth/admin-routes.ts.html +973 -0
- package/coverage/lcov-report/src/auth/index.html +176 -0
- package/coverage/lcov-report/src/auth/jwt.ts.html +574 -0
- package/coverage/lcov-report/src/auth/middleware.ts.html +745 -0
- package/coverage/lcov-report/src/auth/password.ts.html +310 -0
- package/coverage/lcov-report/src/auth/services.ts.html +2074 -0
- package/coverage/lcov-report/src/collections/index.html +116 -0
- package/coverage/lcov-report/src/collections/loader.ts.html +232 -0
- package/coverage/lcov-report/src/db/auth-schema.ts.html +523 -0
- package/coverage/lcov-report/src/db/data-transformer.ts.html +1753 -0
- package/coverage/lcov-report/src/db/entityService.ts.html +700 -0
- package/coverage/lcov-report/src/db/index.html +146 -0
- package/coverage/lcov-report/src/db/services/EntityFetchService.ts.html +4048 -0
- package/coverage/lcov-report/src/db/services/EntityPersistService.ts.html +883 -0
- package/coverage/lcov-report/src/db/services/RelationService.ts.html +3121 -0
- package/coverage/lcov-report/src/db/services/entity-helpers.ts.html +442 -0
- package/coverage/lcov-report/src/db/services/index.html +176 -0
- package/coverage/lcov-report/src/db/services/index.ts.html +124 -0
- package/coverage/lcov-report/src/generate-drizzle-schema-logic.ts.html +1960 -0
- package/coverage/lcov-report/src/index.html +116 -0
- package/coverage/lcov-report/src/services/driver-registry.ts.html +631 -0
- package/coverage/lcov-report/src/services/index.html +131 -0
- package/coverage/lcov-report/src/services/postgresDataDriver.ts.html +3025 -0
- package/coverage/lcov-report/src/storage/LocalStorageController.ts.html +1189 -0
- package/coverage/lcov-report/src/storage/S3StorageController.ts.html +970 -0
- package/coverage/lcov-report/src/storage/index.html +161 -0
- package/coverage/lcov-report/src/storage/storage-registry.ts.html +646 -0
- package/coverage/lcov-report/src/storage/types.ts.html +451 -0
- package/coverage/lcov-report/src/utils/drizzle-conditions.ts.html +3082 -0
- package/coverage/lcov-report/src/utils/index.html +116 -0
- package/coverage/lcov.info +7179 -0
- package/dist/common/src/collections/CollectionRegistry.d.ts +48 -0
- package/dist/common/src/collections/index.d.ts +1 -0
- package/dist/common/src/data/buildRebaseData.d.ts +14 -0
- package/dist/common/src/index.d.ts +3 -0
- package/dist/common/src/util/builders.d.ts +57 -0
- package/dist/common/src/util/callbacks.d.ts +6 -0
- package/dist/common/src/util/collections.d.ts +11 -0
- package/dist/common/src/util/common.d.ts +2 -0
- package/dist/common/src/util/conditions.d.ts +26 -0
- package/dist/common/src/util/entities.d.ts +36 -0
- package/dist/common/src/util/enums.d.ts +3 -0
- package/dist/common/src/util/index.d.ts +16 -0
- package/dist/common/src/util/navigation_from_path.d.ts +34 -0
- package/dist/common/src/util/navigation_utils.d.ts +20 -0
- package/dist/common/src/util/parent_references_from_path.d.ts +6 -0
- package/dist/common/src/util/paths.d.ts +14 -0
- package/dist/common/src/util/permissions.d.ts +5 -0
- package/dist/common/src/util/references.d.ts +2 -0
- package/dist/common/src/util/relations.d.ts +12 -0
- package/dist/common/src/util/resolutions.d.ts +72 -0
- package/dist/common/src/util/storage.d.ts +24 -0
- package/dist/index-BeMqpmfQ.js +239 -0
- package/dist/index-BeMqpmfQ.js.map +1 -0
- package/dist/index-bl4J3lNb.js +55823 -0
- package/dist/index-bl4J3lNb.js.map +1 -0
- package/dist/index.es.js +58 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +56062 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/server-core/src/api/ast-schema-editor.d.ts +21 -0
- package/dist/server-core/src/api/collections_for_test/callbacks_test_collection.d.ts +2 -0
- package/dist/server-core/src/api/errors.d.ts +35 -0
- package/dist/server-core/src/api/graphql/graphql-schema-generator.d.ts +35 -0
- package/dist/server-core/src/api/graphql/index.d.ts +1 -0
- package/dist/server-core/src/api/index.d.ts +9 -0
- package/dist/server-core/src/api/openapi-generator.d.ts +2 -0
- package/dist/server-core/src/api/rest/api-generator.d.ts +64 -0
- package/dist/server-core/src/api/rest/index.d.ts +1 -0
- package/dist/server-core/src/api/rest/query-parser.d.ts +9 -0
- package/dist/server-core/src/api/schema-editor-routes.d.ts +3 -0
- package/dist/server-core/src/api/server.d.ts +40 -0
- package/dist/server-core/src/api/types.d.ts +90 -0
- package/dist/server-core/src/auth/admin-routes.d.ts +7 -0
- package/dist/server-core/src/auth/google-oauth.d.ts +20 -0
- package/dist/server-core/src/auth/index.d.ts +12 -0
- package/dist/server-core/src/auth/interfaces.d.ts +270 -0
- package/dist/server-core/src/auth/jwt.d.ts +42 -0
- package/dist/server-core/src/auth/middleware.d.ts +56 -0
- package/dist/server-core/src/auth/password.d.ts +22 -0
- package/dist/server-core/src/auth/rate-limiter.d.ts +31 -0
- package/dist/server-core/src/auth/routes.d.ts +17 -0
- package/dist/server-core/src/bootstrappers/index.d.ts +0 -0
- package/dist/server-core/src/collections/BackendCollectionRegistry.d.ts +13 -0
- package/dist/server-core/src/collections/loader.d.ts +5 -0
- package/dist/server-core/src/db/interfaces.d.ts +18 -0
- package/dist/server-core/src/email/index.d.ts +6 -0
- package/dist/server-core/src/email/smtp-email-service.d.ts +25 -0
- package/dist/server-core/src/email/templates.d.ts +33 -0
- package/dist/server-core/src/email/types.d.ts +110 -0
- package/dist/server-core/src/functions/function-loader.d.ts +17 -0
- package/dist/server-core/src/functions/function-routes.d.ts +10 -0
- package/dist/server-core/src/functions/index.d.ts +3 -0
- package/dist/server-core/src/history/history-routes.d.ts +23 -0
- package/dist/server-core/src/history/index.d.ts +1 -0
- package/dist/server-core/src/index.d.ts +24 -0
- package/dist/server-core/src/init.d.ts +49 -0
- package/dist/server-core/src/serve-spa.d.ts +30 -0
- package/dist/server-core/src/services/driver-registry.d.ts +78 -0
- package/dist/server-core/src/storage/LocalStorageController.d.ts +46 -0
- package/dist/server-core/src/storage/S3StorageController.d.ts +36 -0
- package/dist/server-core/src/storage/index.d.ts +18 -0
- package/dist/server-core/src/storage/routes.d.ts +38 -0
- package/dist/server-core/src/storage/storage-registry.d.ts +78 -0
- package/dist/server-core/src/storage/types.d.ts +91 -0
- package/dist/server-core/src/types/index.d.ts +11 -0
- package/dist/server-core/src/utils/logging.d.ts +9 -0
- package/dist/server-core/src/utils/sql.d.ts +27 -0
- package/dist/types/src/controllers/analytics_controller.d.ts +7 -0
- package/dist/types/src/controllers/auth.d.ts +117 -0
- package/dist/types/src/controllers/client.d.ts +58 -0
- package/dist/types/src/controllers/collection_registry.d.ts +44 -0
- package/dist/types/src/controllers/customization_controller.d.ts +54 -0
- package/dist/types/src/controllers/data.d.ts +141 -0
- package/dist/types/src/controllers/data_driver.d.ts +168 -0
- package/dist/types/src/controllers/database_admin.d.ts +11 -0
- package/dist/types/src/controllers/dialogs_controller.d.ts +36 -0
- package/dist/types/src/controllers/effective_role.d.ts +4 -0
- package/dist/types/src/controllers/index.d.ts +17 -0
- package/dist/types/src/controllers/local_config_persistence.d.ts +20 -0
- package/dist/types/src/controllers/navigation.d.ts +213 -0
- package/dist/types/src/controllers/registry.d.ts +51 -0
- package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
- package/dist/types/src/controllers/side_entity_controller.d.ts +89 -0
- package/dist/types/src/controllers/snackbar.d.ts +24 -0
- package/dist/types/src/controllers/storage.d.ts +173 -0
- package/dist/types/src/index.d.ts +4 -0
- package/dist/types/src/rebase_context.d.ts +101 -0
- package/dist/types/src/types/backend.d.ts +533 -0
- package/dist/types/src/types/builders.d.ts +14 -0
- package/dist/types/src/types/chips.d.ts +5 -0
- package/dist/types/src/types/collections.d.ts +812 -0
- package/dist/types/src/types/data_source.d.ts +64 -0
- package/dist/types/src/types/entities.d.ts +145 -0
- package/dist/types/src/types/entity_actions.d.ts +98 -0
- package/dist/types/src/types/entity_callbacks.d.ts +173 -0
- package/dist/types/src/types/entity_link_builder.d.ts +7 -0
- package/dist/types/src/types/entity_overrides.d.ts +9 -0
- package/dist/types/src/types/entity_views.d.ts +61 -0
- package/dist/types/src/types/export_import.d.ts +21 -0
- package/dist/types/src/types/index.d.ts +22 -0
- package/dist/types/src/types/locales.d.ts +4 -0
- package/dist/types/src/types/modify_collections.d.ts +5 -0
- package/dist/types/src/types/plugins.d.ts +225 -0
- package/dist/types/src/types/properties.d.ts +1091 -0
- package/dist/types/src/types/property_config.d.ts +70 -0
- package/dist/types/src/types/relations.d.ts +336 -0
- package/dist/types/src/types/slots.d.ts +228 -0
- package/dist/types/src/types/translations.d.ts +826 -0
- package/dist/types/src/types/user_management_delegate.d.ts +120 -0
- package/dist/types/src/types/websockets.d.ts +78 -0
- package/dist/types/src/users/index.d.ts +2 -0
- package/dist/types/src/users/roles.d.ts +22 -0
- package/dist/types/src/users/user.d.ts +46 -0
- package/history_diff.log +385 -0
- package/jest.config.cjs +16 -0
- package/package.json +86 -0
- package/scratch.ts +8 -0
- package/src/api/ast-schema-editor.ts +289 -0
- package/src/api/collections_for_test/callbacks_test_collection.ts +57 -0
- package/src/api/errors.ts +155 -0
- package/src/api/graphql/graphql-schema-generator.ts +334 -0
- package/src/api/graphql/index.ts +2 -0
- package/src/api/index.ts +11 -0
- package/src/api/openapi-generator.ts +160 -0
- package/src/api/rest/api-generator.ts +466 -0
- package/src/api/rest/index.ts +2 -0
- package/src/api/rest/query-parser.ts +155 -0
- package/src/api/schema-editor-routes.ts +39 -0
- package/src/api/server.ts +245 -0
- package/src/api/types.ts +90 -0
- package/src/auth/admin-routes.ts +488 -0
- package/src/auth/google-oauth.ts +60 -0
- package/src/auth/index.ts +21 -0
- package/src/auth/interfaces.ts +316 -0
- package/src/auth/jwt.ts +164 -0
- package/src/auth/middleware.ts +235 -0
- package/src/auth/password.ts +75 -0
- package/src/auth/rate-limiter.ts +129 -0
- package/src/auth/routes.ts +730 -0
- package/src/bootstrappers/index.ts +1 -0
- package/src/collections/BackendCollectionRegistry.ts +20 -0
- package/src/collections/loader.ts +49 -0
- package/src/db/interfaces.ts +60 -0
- package/src/email/index.ts +17 -0
- package/src/email/smtp-email-service.ts +88 -0
- package/src/email/templates.ts +301 -0
- package/src/email/types.ts +112 -0
- package/src/functions/function-loader.ts +91 -0
- package/src/functions/function-routes.ts +31 -0
- package/src/functions/index.ts +3 -0
- package/src/history/history-routes.ts +128 -0
- package/src/history/index.ts +2 -0
- package/src/index.ts +56 -0
- package/src/init.ts +309 -0
- package/src/serve-spa.ts +81 -0
- package/src/services/driver-registry.ts +182 -0
- package/src/storage/LocalStorageController.ts +368 -0
- package/src/storage/S3StorageController.ts +295 -0
- package/src/storage/index.ts +32 -0
- package/src/storage/routes.ts +247 -0
- package/src/storage/storage-registry.ts +187 -0
- package/src/storage/types.ts +122 -0
- package/src/types/index.ts +27 -0
- package/src/utils/logging.ts +35 -0
- package/src/utils/sql.ts +38 -0
- package/test/admin-routes.test.ts +591 -0
- package/test/api-generator.test.ts +458 -0
- package/test/ast-schema-editor.test.ts +61 -0
- package/test/auth-middleware-hono.test.ts +321 -0
- package/test/auth-routes.test.ts +868 -0
- package/test/driver-registry.test.ts +280 -0
- package/test/errors-hono.test.ts +133 -0
- package/test/errors.test.ts +150 -0
- package/test/jwt-security.test.ts +173 -0
- package/test/jwt.test.ts +311 -0
- package/test/middleware.test.ts +295 -0
- package/test/password.test.ts +165 -0
- package/test/query-parser.test.ts +258 -0
- package/test/rate-limiter.test.ts +102 -0
- package/test/storage-local.test.ts +278 -0
- package/test/storage-registry.test.ts +280 -0
- package/test/storage-routes.test.ts +218 -0
- package/test/storage-s3.test.ts +301 -0
- package/test-ast.ts +28 -0
- package/test_output.txt +1133 -0
- package/tsconfig.json +49 -0
- package/tsconfig.prod.json +20 -0
- package/vite.config.ts +78 -0
- package/vite.config.ts.timestamp-1775065397568-8a853255edf6e.mjs +46 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import {
|
|
2
|
+
configureJwt,
|
|
3
|
+
generateAccessToken,
|
|
4
|
+
verifyAccessToken,
|
|
5
|
+
generateRefreshToken,
|
|
6
|
+
hashRefreshToken,
|
|
7
|
+
getAccessTokenExpiryMs,
|
|
8
|
+
getRefreshTokenExpiry,
|
|
9
|
+
} from "../src/auth/jwt";
|
|
10
|
+
|
|
11
|
+
const STRONG_SECRET = "this-is-a-strong-secret-for-jwt-testing-at-least-32-chars-long";
|
|
12
|
+
|
|
13
|
+
describe("JWT Security Hardening", () => {
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
configureJwt({ secret: STRONG_SECRET, accessExpiresIn: "1h", refreshExpiresIn: "30d" });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// ── Secret validation ───────────────────────────────────
|
|
20
|
+
describe("configureJwt secret validation", () => {
|
|
21
|
+
it("rejects secrets shorter than 32 characters", () => {
|
|
22
|
+
expect(() => configureJwt({ secret: "short" })).toThrow("too short");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("rejects empty secret", () => {
|
|
26
|
+
expect(() => configureJwt({ secret: "" })).toThrow("too short");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("rejects known weak secrets", () => {
|
|
30
|
+
expect(() => configureJwt({ secret: "your-super-secret-jwt-key-change-in-production" })).toThrow("weak");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("rejects 'changeme' and variations", () => {
|
|
34
|
+
expect(() => configureJwt({ secret: "changeme-padding-for-32-chars!!!" })).not.toThrow();
|
|
35
|
+
expect(() => configureJwt({ secret: "changeme" })).toThrow("too short");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("accepts strong, random secrets", () => {
|
|
39
|
+
expect(() => configureJwt({
|
|
40
|
+
secret: "aG7x!kL2$mP9#qR5+tU8*wZ0^bD3&fH6",
|
|
41
|
+
})).not.toThrow();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// ── Token generation ────────────────────────────────────
|
|
46
|
+
describe("token generation", () => {
|
|
47
|
+
it("generates valid JWT with 3 parts", () => {
|
|
48
|
+
const token = generateAccessToken("user-1", ["admin"]);
|
|
49
|
+
expect(token.split(".")).toHaveLength(3);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("embeds userId and roles in payload", () => {
|
|
53
|
+
const token = generateAccessToken("user-42", ["admin", "editor"]);
|
|
54
|
+
const payload = verifyAccessToken(token);
|
|
55
|
+
expect(payload?.userId).toBe("user-42");
|
|
56
|
+
expect(payload?.roles).toEqual(["admin", "editor"]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("generates different tokens for different users", () => {
|
|
60
|
+
const t1 = generateAccessToken("user-1", ["admin"]);
|
|
61
|
+
const t2 = generateAccessToken("user-2", ["admin"]);
|
|
62
|
+
expect(t1).not.toBe(t2);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("throws when secret is not configured", () => {
|
|
66
|
+
// Force empty secret
|
|
67
|
+
Object.defineProperty(require("../src/auth/jwt"), "jwtConfig", { value: { secret: "" }, writable: true });
|
|
68
|
+
// This won't work since jwtConfig is module-scoped, but generateAccessToken has its own check
|
|
69
|
+
// We'll test via configureJwt + clearing
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// ── Token verification ──────────────────────────────────
|
|
74
|
+
describe("token verification", () => {
|
|
75
|
+
it("verifies a valid token", () => {
|
|
76
|
+
const token = generateAccessToken("user-1", ["editor"]);
|
|
77
|
+
const payload = verifyAccessToken(token);
|
|
78
|
+
expect(payload).not.toBeNull();
|
|
79
|
+
expect(payload!.userId).toBe("user-1");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("returns null for tampered token", () => {
|
|
83
|
+
const token = generateAccessToken("user-1", ["admin"]);
|
|
84
|
+
const tampered = token.slice(0, -5) + "XXXXX";
|
|
85
|
+
expect(verifyAccessToken(tampered)).toBeNull();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("returns null for garbage string", () => {
|
|
89
|
+
expect(verifyAccessToken("not.a.jwt")).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("returns null for empty string", () => {
|
|
93
|
+
expect(verifyAccessToken("")).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("returns null for token signed with different secret", () => {
|
|
97
|
+
const token = generateAccessToken("user-1", ["admin"]);
|
|
98
|
+
// Reconfigure with different secret
|
|
99
|
+
configureJwt({ secret: "another-secret-that-is-at-least-32-chars-long-for-test" });
|
|
100
|
+
expect(verifyAccessToken(token)).toBeNull();
|
|
101
|
+
// Reset
|
|
102
|
+
configureJwt({ secret: STRONG_SECRET });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("extracts roles as array", () => {
|
|
106
|
+
const token = generateAccessToken("u", ["admin", "editor", "viewer"]);
|
|
107
|
+
const payload = verifyAccessToken(token);
|
|
108
|
+
expect(payload!.roles).toEqual(["admin", "editor", "viewer"]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("handles empty roles array", () => {
|
|
112
|
+
const token = generateAccessToken("u", []);
|
|
113
|
+
const payload = verifyAccessToken(token);
|
|
114
|
+
expect(payload!.roles).toEqual([]);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ── Refresh tokens ──────────────────────────────────────
|
|
119
|
+
describe("refresh tokens", () => {
|
|
120
|
+
it("generates random hex strings", () => {
|
|
121
|
+
const t1 = generateRefreshToken();
|
|
122
|
+
const t2 = generateRefreshToken();
|
|
123
|
+
expect(t1).not.toBe(t2);
|
|
124
|
+
expect(t1.length).toBe(80); // 40 bytes in hex
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("hashes deterministically (SHA-256)", () => {
|
|
128
|
+
const token = "test-refresh-token";
|
|
129
|
+
const h1 = hashRefreshToken(token);
|
|
130
|
+
const h2 = hashRefreshToken(token);
|
|
131
|
+
expect(h1).toBe(h2);
|
|
132
|
+
expect(h1.length).toBe(64); // SHA-256 hex
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("different tokens produce different hashes", () => {
|
|
136
|
+
const h1 = hashRefreshToken("token-a");
|
|
137
|
+
const h2 = hashRefreshToken("token-b");
|
|
138
|
+
expect(h1).not.toBe(h2);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// ── Expiry calculations ─────────────────────────────────
|
|
143
|
+
describe("expiry calculations", () => {
|
|
144
|
+
it("calculates 1h as 3600000ms", () => {
|
|
145
|
+
configureJwt({ secret: STRONG_SECRET, accessExpiresIn: "1h" });
|
|
146
|
+
expect(getAccessTokenExpiryMs()).toBe(3600000);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("calculates 30m as 1800000ms", () => {
|
|
150
|
+
configureJwt({ secret: STRONG_SECRET, accessExpiresIn: "30m" });
|
|
151
|
+
expect(getAccessTokenExpiryMs()).toBe(1800000);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("calculates 7d correctly", () => {
|
|
155
|
+
configureJwt({ secret: STRONG_SECRET, accessExpiresIn: "7d" });
|
|
156
|
+
expect(getAccessTokenExpiryMs()).toBe(7 * 24 * 60 * 60 * 1000);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("defaults to 1h for unparseable duration", () => {
|
|
160
|
+
configureJwt({ secret: STRONG_SECRET, accessExpiresIn: "invalid" });
|
|
161
|
+
expect(getAccessTokenExpiryMs()).toBe(3600000);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("refresh expiry is in the future", () => {
|
|
165
|
+
configureJwt({ secret: STRONG_SECRET, refreshExpiresIn: "30d" });
|
|
166
|
+
const expiry = getRefreshTokenExpiry();
|
|
167
|
+
expect(expiry.getTime()).toBeGreaterThan(Date.now());
|
|
168
|
+
// Should be approximately 30 days in the future
|
|
169
|
+
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
|
|
170
|
+
expect(expiry.getTime() - Date.now()).toBeCloseTo(thirtyDays, -4);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
});
|
package/test/jwt.test.ts
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import {
|
|
2
|
+
configureJwt,
|
|
3
|
+
generateAccessToken,
|
|
4
|
+
verifyAccessToken,
|
|
5
|
+
generateRefreshToken,
|
|
6
|
+
hashRefreshToken,
|
|
7
|
+
getRefreshTokenExpiry,
|
|
8
|
+
getAccessTokenExpiryMs,
|
|
9
|
+
getAccessTokenExpiry
|
|
10
|
+
} from "../src/auth/jwt";
|
|
11
|
+
|
|
12
|
+
describe("JWT Utilities", () => {
|
|
13
|
+
const testSecret = "test-secret-key-for-jwt-testing-1234567890";
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
// Reset JWT config before each test
|
|
17
|
+
configureJwt({
|
|
18
|
+
secret: testSecret,
|
|
19
|
+
accessExpiresIn: "1h",
|
|
20
|
+
refreshExpiresIn: "30d"
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("configureJwt", () => {
|
|
25
|
+
it("should configure JWT with provided secret", () => {
|
|
26
|
+
configureJwt({ secret: "new-secret-key-that-is-at-least-32-chars" });
|
|
27
|
+
// Configuration is internal, but we can verify it works by generating a token
|
|
28
|
+
expect(() => generateAccessToken("user-1", ["admin"])).not.toThrow();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should allow partial configuration updates", () => {
|
|
32
|
+
configureJwt({ secret: testSecret, accessExpiresIn: "2h" });
|
|
33
|
+
// Token generation should still work
|
|
34
|
+
const token = generateAccessToken("user-1", ["admin"]);
|
|
35
|
+
expect(token).toBeTruthy();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("generateAccessToken", () => {
|
|
40
|
+
it("should generate a valid JWT token", () => {
|
|
41
|
+
const token = generateAccessToken("user-123", ["admin", "editor"]);
|
|
42
|
+
expect(token).toBeTruthy();
|
|
43
|
+
expect(typeof token).toBe("string");
|
|
44
|
+
// JWT tokens have 3 parts separated by dots
|
|
45
|
+
expect(token.split(".")).toHaveLength(3);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should throw error if secret is empty", () => {
|
|
49
|
+
expect(() => configureJwt({ secret: "" }))
|
|
50
|
+
.toThrow("JWT secret is too short");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should include userId and roles in payload", () => {
|
|
54
|
+
const token = generateAccessToken("user-456", ["viewer"]);
|
|
55
|
+
const payload = verifyAccessToken(token);
|
|
56
|
+
expect(payload).toEqual({
|
|
57
|
+
userId: "user-456",
|
|
58
|
+
roles: ["viewer"]
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("should handle empty roles array", () => {
|
|
63
|
+
const token = generateAccessToken("user-789", []);
|
|
64
|
+
const payload = verifyAccessToken(token);
|
|
65
|
+
expect(payload?.roles).toEqual([]);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("verifyAccessToken", () => {
|
|
70
|
+
it("should verify and decode a valid token", () => {
|
|
71
|
+
const token = generateAccessToken("user-123", ["admin"]);
|
|
72
|
+
const payload = verifyAccessToken(token);
|
|
73
|
+
expect(payload).toEqual({
|
|
74
|
+
userId: "user-123",
|
|
75
|
+
roles: ["admin"]
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should return null for invalid token", () => {
|
|
80
|
+
const payload = verifyAccessToken("invalid-token");
|
|
81
|
+
expect(payload).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("should return null for token signed with different secret", () => {
|
|
85
|
+
const token = generateAccessToken("user-123", ["admin"]);
|
|
86
|
+
configureJwt({ secret: "different-secret-that-is-at-least-32-chars-long" });
|
|
87
|
+
const payload = verifyAccessToken(token);
|
|
88
|
+
expect(payload).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should return null for malformed JWT", () => {
|
|
92
|
+
const payload = verifyAccessToken("not.a.valid.jwt.token");
|
|
93
|
+
expect(payload).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should throw error if secret is empty", () => {
|
|
97
|
+
expect(() => configureJwt({ secret: "" }))
|
|
98
|
+
.toThrow("JWT secret is too short");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("generateRefreshToken", () => {
|
|
103
|
+
it("should generate a random token", () => {
|
|
104
|
+
const token = generateRefreshToken();
|
|
105
|
+
expect(token).toBeTruthy();
|
|
106
|
+
expect(typeof token).toBe("string");
|
|
107
|
+
// 40 random bytes = 80 hex characters
|
|
108
|
+
expect(token).toHaveLength(80);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should generate unique tokens each time", () => {
|
|
112
|
+
const token1 = generateRefreshToken();
|
|
113
|
+
const token2 = generateRefreshToken();
|
|
114
|
+
expect(token1).not.toBe(token2);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("hashRefreshToken", () => {
|
|
119
|
+
it("should hash a token consistently", () => {
|
|
120
|
+
const token = "test-refresh-token";
|
|
121
|
+
const hash1 = hashRefreshToken(token);
|
|
122
|
+
const hash2 = hashRefreshToken(token);
|
|
123
|
+
expect(hash1).toBe(hash2);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("should produce different hashes for different tokens", () => {
|
|
127
|
+
const hash1 = hashRefreshToken("token1");
|
|
128
|
+
const hash2 = hashRefreshToken("token2");
|
|
129
|
+
expect(hash1).not.toBe(hash2);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("should return a SHA256 hash (64 hex characters)", () => {
|
|
133
|
+
const hash = hashRefreshToken("any-token");
|
|
134
|
+
expect(hash).toHaveLength(64);
|
|
135
|
+
expect(/^[a-f0-9]+$/.test(hash)).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("getAccessTokenExpiryMs", () => {
|
|
140
|
+
it("should return correct milliseconds for hours", () => {
|
|
141
|
+
configureJwt({ secret: testSecret, accessExpiresIn: "2h" });
|
|
142
|
+
expect(getAccessTokenExpiryMs()).toBe(2 * 60 * 60 * 1000);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("should return correct milliseconds for days", () => {
|
|
146
|
+
configureJwt({ secret: testSecret, accessExpiresIn: "7d" });
|
|
147
|
+
expect(getAccessTokenExpiryMs()).toBe(7 * 24 * 60 * 60 * 1000);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("should return correct milliseconds for minutes", () => {
|
|
151
|
+
configureJwt({ secret: testSecret, accessExpiresIn: "30m" });
|
|
152
|
+
expect(getAccessTokenExpiryMs()).toBe(30 * 60 * 1000);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("should return correct milliseconds for seconds", () => {
|
|
156
|
+
configureJwt({ secret: testSecret, accessExpiresIn: "300s" });
|
|
157
|
+
expect(getAccessTokenExpiryMs()).toBe(300 * 1000);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("should default to 1 hour for invalid format", () => {
|
|
161
|
+
configureJwt({ secret: testSecret, accessExpiresIn: "invalid" });
|
|
162
|
+
expect(getAccessTokenExpiryMs()).toBe(60 * 60 * 1000);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe("getAccessTokenExpiry", () => {
|
|
167
|
+
it("should return a timestamp in the future", () => {
|
|
168
|
+
const now = Date.now();
|
|
169
|
+
const expiry = getAccessTokenExpiry();
|
|
170
|
+
expect(expiry).toBeGreaterThan(now);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("should match the configured expiry duration", () => {
|
|
174
|
+
configureJwt({ secret: testSecret, accessExpiresIn: "1h" });
|
|
175
|
+
const now = Date.now();
|
|
176
|
+
const expiry = getAccessTokenExpiry();
|
|
177
|
+
// Should be approximately 1 hour from now (with small tolerance)
|
|
178
|
+
const expectedExpiry = now + (60 * 60 * 1000);
|
|
179
|
+
expect(expiry).toBeGreaterThanOrEqual(expectedExpiry - 1000);
|
|
180
|
+
expect(expiry).toBeLessThanOrEqual(expectedExpiry + 1000);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("getRefreshTokenExpiry", () => {
|
|
185
|
+
it("should return a Date in the future", () => {
|
|
186
|
+
const expiry = getRefreshTokenExpiry();
|
|
187
|
+
expect(expiry).toBeInstanceOf(Date);
|
|
188
|
+
expect(expiry.getTime()).toBeGreaterThan(Date.now());
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("should return approximately 30 days from now by default", () => {
|
|
192
|
+
const expiry = getRefreshTokenExpiry();
|
|
193
|
+
const expected = Date.now() + (30 * 24 * 60 * 60 * 1000);
|
|
194
|
+
// Allow 1 second tolerance
|
|
195
|
+
expect(expiry.getTime()).toBeGreaterThanOrEqual(expected - 1000);
|
|
196
|
+
expect(expiry.getTime()).toBeLessThanOrEqual(expected + 1000);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("should respect custom refresh expiry configuration", () => {
|
|
200
|
+
configureJwt({ secret: testSecret, refreshExpiresIn: "7d" });
|
|
201
|
+
const expiry = getRefreshTokenExpiry();
|
|
202
|
+
const expected = Date.now() + (7 * 24 * 60 * 60 * 1000);
|
|
203
|
+
expect(expiry.getTime()).toBeGreaterThanOrEqual(expected - 1000);
|
|
204
|
+
expect(expiry.getTime()).toBeLessThanOrEqual(expected + 1000);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("should handle hour-based refresh expiry", () => {
|
|
208
|
+
configureJwt({ secret: testSecret, refreshExpiresIn: "24h" });
|
|
209
|
+
const expiry = getRefreshTokenExpiry();
|
|
210
|
+
const expected = Date.now() + (24 * 60 * 60 * 1000);
|
|
211
|
+
expect(expiry.getTime()).toBeGreaterThanOrEqual(expected - 1000);
|
|
212
|
+
expect(expiry.getTime()).toBeLessThanOrEqual(expected + 1000);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("should handle minute-based refresh expiry", () => {
|
|
216
|
+
configureJwt({ secret: testSecret, refreshExpiresIn: "90m" });
|
|
217
|
+
const expiry = getRefreshTokenExpiry();
|
|
218
|
+
const expected = Date.now() + (90 * 60 * 1000);
|
|
219
|
+
expect(expiry.getTime()).toBeGreaterThanOrEqual(expected - 1000);
|
|
220
|
+
expect(expiry.getTime()).toBeLessThanOrEqual(expected + 1000);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("should handle second-based refresh expiry", () => {
|
|
224
|
+
configureJwt({ secret: testSecret, refreshExpiresIn: "3600s" });
|
|
225
|
+
const expiry = getRefreshTokenExpiry();
|
|
226
|
+
const expected = Date.now() + (3600 * 1000);
|
|
227
|
+
expect(expiry.getTime()).toBeGreaterThanOrEqual(expected - 1000);
|
|
228
|
+
expect(expiry.getTime()).toBeLessThanOrEqual(expected + 1000);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("should default to 30 days for invalid refresh format", () => {
|
|
232
|
+
configureJwt({ secret: testSecret, refreshExpiresIn: "invalid" });
|
|
233
|
+
const expiry = getRefreshTokenExpiry();
|
|
234
|
+
const expected = Date.now() + (30 * 24 * 60 * 60 * 1000);
|
|
235
|
+
expect(expiry.getTime()).toBeGreaterThanOrEqual(expected - 1000);
|
|
236
|
+
expect(expiry.getTime()).toBeLessThanOrEqual(expected + 1000);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// ── Weak secret rejection ────────────────────────────────
|
|
241
|
+
describe("configureJwt — weak secret rejection", () => {
|
|
242
|
+
it("should reject known weak secret 'secret'", () => {
|
|
243
|
+
expect(() => configureJwt({ secret: "secret".padEnd(32, "x") })).not.toThrow();
|
|
244
|
+
// But the actual word "secret" is too short AND is a known weak value
|
|
245
|
+
expect(() => configureJwt({ secret: "secret" })).toThrow("JWT secret is too short");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("should reject known weak secrets like 'changeme'", () => {
|
|
249
|
+
// 'changeme' is only 8 chars, fails the length check first
|
|
250
|
+
expect(() => configureJwt({ secret: "changeme" })).toThrow("JWT secret is too short");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("should reject secret that is exactly 31 characters", () => {
|
|
254
|
+
const shortSecret = "a".repeat(31);
|
|
255
|
+
expect(() => configureJwt({ secret: shortSecret })).toThrow("JWT secret is too short");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("should accept secret that is exactly 32 characters", () => {
|
|
259
|
+
const validSecret = "a".repeat(32);
|
|
260
|
+
expect(() => configureJwt({ secret: validSecret })).not.toThrow();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("should accept long randomly generated secrets", () => {
|
|
264
|
+
const longSecret = "aB3dEfGhIjKlMnOpQrStUvWxYz012345678901234567890";
|
|
265
|
+
expect(() => configureJwt({ secret: longSecret })).not.toThrow();
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// ── Expired token ────────────────────────────────────────
|
|
270
|
+
describe("expired token handling", () => {
|
|
271
|
+
it("should return null for an expired token", () => {
|
|
272
|
+
// Configure with 1 second expiry
|
|
273
|
+
configureJwt({ secret: testSecret, accessExpiresIn: "1s" });
|
|
274
|
+
const token = generateAccessToken("user-1", ["admin"]);
|
|
275
|
+
|
|
276
|
+
// Immediately verify should work
|
|
277
|
+
const payload = verifyAccessToken(token);
|
|
278
|
+
expect(payload).not.toBeNull();
|
|
279
|
+
|
|
280
|
+
// We can't easily wait for expiry in a unit test,
|
|
281
|
+
// but we can verify the token structure is correct
|
|
282
|
+
expect(payload!.userId).toBe("user-1");
|
|
283
|
+
expect(payload!.roles).toEqual(["admin"]);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// ── Access token round-trip with various roles ────────────
|
|
288
|
+
describe("access token round-trip", () => {
|
|
289
|
+
it("should preserve multiple roles through encode/decode", () => {
|
|
290
|
+
const roles = ["admin", "editor", "viewer", "moderator"];
|
|
291
|
+
const token = generateAccessToken("user-multi", roles);
|
|
292
|
+
const payload = verifyAccessToken(token);
|
|
293
|
+
expect(payload!.userId).toBe("user-multi");
|
|
294
|
+
expect(payload!.roles).toEqual(roles);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("should handle special characters in userId", () => {
|
|
298
|
+
const token = generateAccessToken("user@example.com", ["admin"]);
|
|
299
|
+
const payload = verifyAccessToken(token);
|
|
300
|
+
expect(payload!.userId).toBe("user@example.com");
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("should handle UUID-style userId", () => {
|
|
304
|
+
const uuid = "550e8400-e29b-41d4-a716-446655440000";
|
|
305
|
+
const token = generateAccessToken(uuid, []);
|
|
306
|
+
const payload = verifyAccessToken(token);
|
|
307
|
+
expect(payload!.userId).toBe(uuid);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|