@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.
Files changed (254) hide show
  1. package/LICENSE +6 -0
  2. package/README.md +40 -0
  3. package/build-errors.txt +52 -0
  4. package/coverage/clover.xml +3739 -0
  5. package/coverage/coverage-final.json +31 -0
  6. package/coverage/lcov-report/base.css +224 -0
  7. package/coverage/lcov-report/block-navigation.js +87 -0
  8. package/coverage/lcov-report/favicon.png +0 -0
  9. package/coverage/lcov-report/index.html +266 -0
  10. package/coverage/lcov-report/prettify.css +1 -0
  11. package/coverage/lcov-report/prettify.js +2 -0
  12. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  13. package/coverage/lcov-report/sorter.js +210 -0
  14. package/coverage/lcov-report/src/api/ast-schema-editor.ts.html +952 -0
  15. package/coverage/lcov-report/src/api/errors.ts.html +472 -0
  16. package/coverage/lcov-report/src/api/graphql/graphql-schema-generator.ts.html +1069 -0
  17. package/coverage/lcov-report/src/api/graphql/index.html +116 -0
  18. package/coverage/lcov-report/src/api/index.html +176 -0
  19. package/coverage/lcov-report/src/api/openapi-generator.ts.html +565 -0
  20. package/coverage/lcov-report/src/api/rest/api-generator.ts.html +994 -0
  21. package/coverage/lcov-report/src/api/rest/index.html +131 -0
  22. package/coverage/lcov-report/src/api/rest/query-parser.ts.html +550 -0
  23. package/coverage/lcov-report/src/api/schema-editor-routes.ts.html +202 -0
  24. package/coverage/lcov-report/src/api/server.ts.html +823 -0
  25. package/coverage/lcov-report/src/auth/admin-routes.ts.html +973 -0
  26. package/coverage/lcov-report/src/auth/index.html +176 -0
  27. package/coverage/lcov-report/src/auth/jwt.ts.html +574 -0
  28. package/coverage/lcov-report/src/auth/middleware.ts.html +745 -0
  29. package/coverage/lcov-report/src/auth/password.ts.html +310 -0
  30. package/coverage/lcov-report/src/auth/services.ts.html +2074 -0
  31. package/coverage/lcov-report/src/collections/index.html +116 -0
  32. package/coverage/lcov-report/src/collections/loader.ts.html +232 -0
  33. package/coverage/lcov-report/src/db/auth-schema.ts.html +523 -0
  34. package/coverage/lcov-report/src/db/data-transformer.ts.html +1753 -0
  35. package/coverage/lcov-report/src/db/entityService.ts.html +700 -0
  36. package/coverage/lcov-report/src/db/index.html +146 -0
  37. package/coverage/lcov-report/src/db/services/EntityFetchService.ts.html +4048 -0
  38. package/coverage/lcov-report/src/db/services/EntityPersistService.ts.html +883 -0
  39. package/coverage/lcov-report/src/db/services/RelationService.ts.html +3121 -0
  40. package/coverage/lcov-report/src/db/services/entity-helpers.ts.html +442 -0
  41. package/coverage/lcov-report/src/db/services/index.html +176 -0
  42. package/coverage/lcov-report/src/db/services/index.ts.html +124 -0
  43. package/coverage/lcov-report/src/generate-drizzle-schema-logic.ts.html +1960 -0
  44. package/coverage/lcov-report/src/index.html +116 -0
  45. package/coverage/lcov-report/src/services/driver-registry.ts.html +631 -0
  46. package/coverage/lcov-report/src/services/index.html +131 -0
  47. package/coverage/lcov-report/src/services/postgresDataDriver.ts.html +3025 -0
  48. package/coverage/lcov-report/src/storage/LocalStorageController.ts.html +1189 -0
  49. package/coverage/lcov-report/src/storage/S3StorageController.ts.html +970 -0
  50. package/coverage/lcov-report/src/storage/index.html +161 -0
  51. package/coverage/lcov-report/src/storage/storage-registry.ts.html +646 -0
  52. package/coverage/lcov-report/src/storage/types.ts.html +451 -0
  53. package/coverage/lcov-report/src/utils/drizzle-conditions.ts.html +3082 -0
  54. package/coverage/lcov-report/src/utils/index.html +116 -0
  55. package/coverage/lcov.info +7179 -0
  56. package/dist/common/src/collections/CollectionRegistry.d.ts +48 -0
  57. package/dist/common/src/collections/index.d.ts +1 -0
  58. package/dist/common/src/data/buildRebaseData.d.ts +14 -0
  59. package/dist/common/src/index.d.ts +3 -0
  60. package/dist/common/src/util/builders.d.ts +57 -0
  61. package/dist/common/src/util/callbacks.d.ts +6 -0
  62. package/dist/common/src/util/collections.d.ts +11 -0
  63. package/dist/common/src/util/common.d.ts +2 -0
  64. package/dist/common/src/util/conditions.d.ts +26 -0
  65. package/dist/common/src/util/entities.d.ts +36 -0
  66. package/dist/common/src/util/enums.d.ts +3 -0
  67. package/dist/common/src/util/index.d.ts +16 -0
  68. package/dist/common/src/util/navigation_from_path.d.ts +34 -0
  69. package/dist/common/src/util/navigation_utils.d.ts +20 -0
  70. package/dist/common/src/util/parent_references_from_path.d.ts +6 -0
  71. package/dist/common/src/util/paths.d.ts +14 -0
  72. package/dist/common/src/util/permissions.d.ts +5 -0
  73. package/dist/common/src/util/references.d.ts +2 -0
  74. package/dist/common/src/util/relations.d.ts +12 -0
  75. package/dist/common/src/util/resolutions.d.ts +72 -0
  76. package/dist/common/src/util/storage.d.ts +24 -0
  77. package/dist/index-BeMqpmfQ.js +239 -0
  78. package/dist/index-BeMqpmfQ.js.map +1 -0
  79. package/dist/index-bl4J3lNb.js +55823 -0
  80. package/dist/index-bl4J3lNb.js.map +1 -0
  81. package/dist/index.es.js +58 -0
  82. package/dist/index.es.js.map +1 -0
  83. package/dist/index.umd.js +56062 -0
  84. package/dist/index.umd.js.map +1 -0
  85. package/dist/server-core/src/api/ast-schema-editor.d.ts +21 -0
  86. package/dist/server-core/src/api/collections_for_test/callbacks_test_collection.d.ts +2 -0
  87. package/dist/server-core/src/api/errors.d.ts +35 -0
  88. package/dist/server-core/src/api/graphql/graphql-schema-generator.d.ts +35 -0
  89. package/dist/server-core/src/api/graphql/index.d.ts +1 -0
  90. package/dist/server-core/src/api/index.d.ts +9 -0
  91. package/dist/server-core/src/api/openapi-generator.d.ts +2 -0
  92. package/dist/server-core/src/api/rest/api-generator.d.ts +64 -0
  93. package/dist/server-core/src/api/rest/index.d.ts +1 -0
  94. package/dist/server-core/src/api/rest/query-parser.d.ts +9 -0
  95. package/dist/server-core/src/api/schema-editor-routes.d.ts +3 -0
  96. package/dist/server-core/src/api/server.d.ts +40 -0
  97. package/dist/server-core/src/api/types.d.ts +90 -0
  98. package/dist/server-core/src/auth/admin-routes.d.ts +7 -0
  99. package/dist/server-core/src/auth/google-oauth.d.ts +20 -0
  100. package/dist/server-core/src/auth/index.d.ts +12 -0
  101. package/dist/server-core/src/auth/interfaces.d.ts +270 -0
  102. package/dist/server-core/src/auth/jwt.d.ts +42 -0
  103. package/dist/server-core/src/auth/middleware.d.ts +56 -0
  104. package/dist/server-core/src/auth/password.d.ts +22 -0
  105. package/dist/server-core/src/auth/rate-limiter.d.ts +31 -0
  106. package/dist/server-core/src/auth/routes.d.ts +17 -0
  107. package/dist/server-core/src/bootstrappers/index.d.ts +0 -0
  108. package/dist/server-core/src/collections/BackendCollectionRegistry.d.ts +13 -0
  109. package/dist/server-core/src/collections/loader.d.ts +5 -0
  110. package/dist/server-core/src/db/interfaces.d.ts +18 -0
  111. package/dist/server-core/src/email/index.d.ts +6 -0
  112. package/dist/server-core/src/email/smtp-email-service.d.ts +25 -0
  113. package/dist/server-core/src/email/templates.d.ts +33 -0
  114. package/dist/server-core/src/email/types.d.ts +110 -0
  115. package/dist/server-core/src/functions/function-loader.d.ts +17 -0
  116. package/dist/server-core/src/functions/function-routes.d.ts +10 -0
  117. package/dist/server-core/src/functions/index.d.ts +3 -0
  118. package/dist/server-core/src/history/history-routes.d.ts +23 -0
  119. package/dist/server-core/src/history/index.d.ts +1 -0
  120. package/dist/server-core/src/index.d.ts +24 -0
  121. package/dist/server-core/src/init.d.ts +49 -0
  122. package/dist/server-core/src/serve-spa.d.ts +30 -0
  123. package/dist/server-core/src/services/driver-registry.d.ts +78 -0
  124. package/dist/server-core/src/storage/LocalStorageController.d.ts +46 -0
  125. package/dist/server-core/src/storage/S3StorageController.d.ts +36 -0
  126. package/dist/server-core/src/storage/index.d.ts +18 -0
  127. package/dist/server-core/src/storage/routes.d.ts +38 -0
  128. package/dist/server-core/src/storage/storage-registry.d.ts +78 -0
  129. package/dist/server-core/src/storage/types.d.ts +91 -0
  130. package/dist/server-core/src/types/index.d.ts +11 -0
  131. package/dist/server-core/src/utils/logging.d.ts +9 -0
  132. package/dist/server-core/src/utils/sql.d.ts +27 -0
  133. package/dist/types/src/controllers/analytics_controller.d.ts +7 -0
  134. package/dist/types/src/controllers/auth.d.ts +117 -0
  135. package/dist/types/src/controllers/client.d.ts +58 -0
  136. package/dist/types/src/controllers/collection_registry.d.ts +44 -0
  137. package/dist/types/src/controllers/customization_controller.d.ts +54 -0
  138. package/dist/types/src/controllers/data.d.ts +141 -0
  139. package/dist/types/src/controllers/data_driver.d.ts +168 -0
  140. package/dist/types/src/controllers/database_admin.d.ts +11 -0
  141. package/dist/types/src/controllers/dialogs_controller.d.ts +36 -0
  142. package/dist/types/src/controllers/effective_role.d.ts +4 -0
  143. package/dist/types/src/controllers/index.d.ts +17 -0
  144. package/dist/types/src/controllers/local_config_persistence.d.ts +20 -0
  145. package/dist/types/src/controllers/navigation.d.ts +213 -0
  146. package/dist/types/src/controllers/registry.d.ts +51 -0
  147. package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
  148. package/dist/types/src/controllers/side_entity_controller.d.ts +89 -0
  149. package/dist/types/src/controllers/snackbar.d.ts +24 -0
  150. package/dist/types/src/controllers/storage.d.ts +173 -0
  151. package/dist/types/src/index.d.ts +4 -0
  152. package/dist/types/src/rebase_context.d.ts +101 -0
  153. package/dist/types/src/types/backend.d.ts +533 -0
  154. package/dist/types/src/types/builders.d.ts +14 -0
  155. package/dist/types/src/types/chips.d.ts +5 -0
  156. package/dist/types/src/types/collections.d.ts +812 -0
  157. package/dist/types/src/types/data_source.d.ts +64 -0
  158. package/dist/types/src/types/entities.d.ts +145 -0
  159. package/dist/types/src/types/entity_actions.d.ts +98 -0
  160. package/dist/types/src/types/entity_callbacks.d.ts +173 -0
  161. package/dist/types/src/types/entity_link_builder.d.ts +7 -0
  162. package/dist/types/src/types/entity_overrides.d.ts +9 -0
  163. package/dist/types/src/types/entity_views.d.ts +61 -0
  164. package/dist/types/src/types/export_import.d.ts +21 -0
  165. package/dist/types/src/types/index.d.ts +22 -0
  166. package/dist/types/src/types/locales.d.ts +4 -0
  167. package/dist/types/src/types/modify_collections.d.ts +5 -0
  168. package/dist/types/src/types/plugins.d.ts +225 -0
  169. package/dist/types/src/types/properties.d.ts +1091 -0
  170. package/dist/types/src/types/property_config.d.ts +70 -0
  171. package/dist/types/src/types/relations.d.ts +336 -0
  172. package/dist/types/src/types/slots.d.ts +228 -0
  173. package/dist/types/src/types/translations.d.ts +826 -0
  174. package/dist/types/src/types/user_management_delegate.d.ts +120 -0
  175. package/dist/types/src/types/websockets.d.ts +78 -0
  176. package/dist/types/src/users/index.d.ts +2 -0
  177. package/dist/types/src/users/roles.d.ts +22 -0
  178. package/dist/types/src/users/user.d.ts +46 -0
  179. package/history_diff.log +385 -0
  180. package/jest.config.cjs +16 -0
  181. package/package.json +86 -0
  182. package/scratch.ts +8 -0
  183. package/src/api/ast-schema-editor.ts +289 -0
  184. package/src/api/collections_for_test/callbacks_test_collection.ts +57 -0
  185. package/src/api/errors.ts +155 -0
  186. package/src/api/graphql/graphql-schema-generator.ts +334 -0
  187. package/src/api/graphql/index.ts +2 -0
  188. package/src/api/index.ts +11 -0
  189. package/src/api/openapi-generator.ts +160 -0
  190. package/src/api/rest/api-generator.ts +466 -0
  191. package/src/api/rest/index.ts +2 -0
  192. package/src/api/rest/query-parser.ts +155 -0
  193. package/src/api/schema-editor-routes.ts +39 -0
  194. package/src/api/server.ts +245 -0
  195. package/src/api/types.ts +90 -0
  196. package/src/auth/admin-routes.ts +488 -0
  197. package/src/auth/google-oauth.ts +60 -0
  198. package/src/auth/index.ts +21 -0
  199. package/src/auth/interfaces.ts +316 -0
  200. package/src/auth/jwt.ts +164 -0
  201. package/src/auth/middleware.ts +235 -0
  202. package/src/auth/password.ts +75 -0
  203. package/src/auth/rate-limiter.ts +129 -0
  204. package/src/auth/routes.ts +730 -0
  205. package/src/bootstrappers/index.ts +1 -0
  206. package/src/collections/BackendCollectionRegistry.ts +20 -0
  207. package/src/collections/loader.ts +49 -0
  208. package/src/db/interfaces.ts +60 -0
  209. package/src/email/index.ts +17 -0
  210. package/src/email/smtp-email-service.ts +88 -0
  211. package/src/email/templates.ts +301 -0
  212. package/src/email/types.ts +112 -0
  213. package/src/functions/function-loader.ts +91 -0
  214. package/src/functions/function-routes.ts +31 -0
  215. package/src/functions/index.ts +3 -0
  216. package/src/history/history-routes.ts +128 -0
  217. package/src/history/index.ts +2 -0
  218. package/src/index.ts +56 -0
  219. package/src/init.ts +309 -0
  220. package/src/serve-spa.ts +81 -0
  221. package/src/services/driver-registry.ts +182 -0
  222. package/src/storage/LocalStorageController.ts +368 -0
  223. package/src/storage/S3StorageController.ts +295 -0
  224. package/src/storage/index.ts +32 -0
  225. package/src/storage/routes.ts +247 -0
  226. package/src/storage/storage-registry.ts +187 -0
  227. package/src/storage/types.ts +122 -0
  228. package/src/types/index.ts +27 -0
  229. package/src/utils/logging.ts +35 -0
  230. package/src/utils/sql.ts +38 -0
  231. package/test/admin-routes.test.ts +591 -0
  232. package/test/api-generator.test.ts +458 -0
  233. package/test/ast-schema-editor.test.ts +61 -0
  234. package/test/auth-middleware-hono.test.ts +321 -0
  235. package/test/auth-routes.test.ts +868 -0
  236. package/test/driver-registry.test.ts +280 -0
  237. package/test/errors-hono.test.ts +133 -0
  238. package/test/errors.test.ts +150 -0
  239. package/test/jwt-security.test.ts +173 -0
  240. package/test/jwt.test.ts +311 -0
  241. package/test/middleware.test.ts +295 -0
  242. package/test/password.test.ts +165 -0
  243. package/test/query-parser.test.ts +258 -0
  244. package/test/rate-limiter.test.ts +102 -0
  245. package/test/storage-local.test.ts +278 -0
  246. package/test/storage-registry.test.ts +280 -0
  247. package/test/storage-routes.test.ts +218 -0
  248. package/test/storage-s3.test.ts +301 -0
  249. package/test-ast.ts +28 -0
  250. package/test_output.txt +1133 -0
  251. package/tsconfig.json +49 -0
  252. package/tsconfig.prod.json +20 -0
  253. package/vite.config.ts +78 -0
  254. 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
+ });
@@ -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
+