@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,295 @@
1
+ import { requireAuth, optionalAuth, extractUserFromToken, requireAdmin } from "../src/auth/middleware";
2
+ import { configureJwt, generateAccessToken } from "../src/auth/jwt";
3
+
4
+ // ── Minimal Hono-context mock ────────────────────────────────────────────
5
+ function createMockContext(opts: {
6
+ authHeader?: string;
7
+ queryToken?: string;
8
+ user?: any;
9
+ } = {}) {
10
+ let capturedStatus: number | undefined;
11
+ let capturedBody: any;
12
+ const variables = new Map<string, any>();
13
+
14
+ if (opts.user !== undefined) variables.set("user", opts.user);
15
+
16
+ const c = {
17
+ req: {
18
+ header: (name: string) => {
19
+ if (name === "authorization") return opts.authHeader;
20
+ return undefined;
21
+ },
22
+ query: (name: string) => {
23
+ if (name === "token") return opts.queryToken;
24
+ return undefined;
25
+ },
26
+ },
27
+ json: (body: any, status?: number) => {
28
+ capturedBody = body;
29
+ capturedStatus = status ?? 200;
30
+ return new Response(JSON.stringify(body), { status: capturedStatus });
31
+ },
32
+ set: (key: string, value: any) => variables.set(key, value),
33
+ get: (key: string) => variables.get(key),
34
+ } as any;
35
+
36
+ return {
37
+ c,
38
+ getStatus: () => capturedStatus,
39
+ getBody: () => capturedBody,
40
+ getUser: () => variables.get("user"),
41
+ };
42
+ }
43
+
44
+ describe("Auth Middleware", () => {
45
+ const testSecret = "test-secret-key-for-middleware-testing";
46
+ const nextFn = jest.fn();
47
+
48
+ beforeAll(() => {
49
+ configureJwt({
50
+ secret: testSecret,
51
+ accessExpiresIn: "1h",
52
+ refreshExpiresIn: "30d"
53
+ });
54
+ });
55
+
56
+ beforeEach(() => {
57
+ nextFn.mockClear();
58
+ });
59
+
60
+ describe("requireAuth", () => {
61
+ it("should call next() and set user for valid token", async () => {
62
+ const token = generateAccessToken("user-123", ["admin", "editor"]);
63
+ const { c, getUser } = createMockContext({ authHeader: `Bearer ${token}` });
64
+
65
+ await requireAuth(c, nextFn);
66
+
67
+ expect(nextFn).toHaveBeenCalled();
68
+ expect(getUser()).toEqual({
69
+ userId: "user-123",
70
+ roles: ["admin", "editor"]
71
+ });
72
+ });
73
+
74
+ it("should return 401 for missing Authorization header", async () => {
75
+ const { c, getStatus, getBody } = createMockContext();
76
+
77
+ await requireAuth(c, nextFn);
78
+
79
+ expect(getStatus()).toBe(401);
80
+ expect(getBody()).toEqual({
81
+ error: {
82
+ message: "Authorization header or token query parameter missing or invalid",
83
+ code: "UNAUTHORIZED"
84
+ }
85
+ });
86
+ expect(nextFn).not.toHaveBeenCalled();
87
+ });
88
+
89
+ it("should return 401 for Authorization header without Bearer prefix", async () => {
90
+ const { c, getStatus } = createMockContext({ authHeader: "token-without-bearer" });
91
+
92
+ await requireAuth(c, nextFn);
93
+
94
+ expect(getStatus()).toBe(401);
95
+ expect(nextFn).not.toHaveBeenCalled();
96
+ });
97
+
98
+ it("should return 401 for invalid token", async () => {
99
+ const { c, getStatus, getBody } = createMockContext({ authHeader: "Bearer invalid-token" });
100
+
101
+ await requireAuth(c, nextFn);
102
+
103
+ expect(getStatus()).toBe(401);
104
+ expect(getBody()).toEqual({
105
+ error: {
106
+ message: "Invalid or expired token",
107
+ code: "UNAUTHORIZED"
108
+ }
109
+ });
110
+ expect(nextFn).not.toHaveBeenCalled();
111
+ });
112
+
113
+ it("should return 401 for token signed with different secret", async () => {
114
+ const token = generateAccessToken("user-123", ["admin"]);
115
+ configureJwt({ secret: "different-secret-that-is-at-least-32-chars-long" });
116
+ const { c, getStatus } = createMockContext({ authHeader: `Bearer ${token}` });
117
+
118
+ await requireAuth(c, nextFn);
119
+
120
+ expect(getStatus()).toBe(401);
121
+ expect(nextFn).not.toHaveBeenCalled();
122
+
123
+ // Reset to original secret
124
+ configureJwt({ secret: testSecret });
125
+ });
126
+
127
+ it("should handle lowercase bearer prefix", async () => {
128
+ const token = generateAccessToken("user-123", ["admin"]);
129
+ const { c, getStatus } = createMockContext({ authHeader: `bearer ${token}` });
130
+
131
+ await requireAuth(c, nextFn);
132
+
133
+ // The implementation requires "Bearer " with capital B
134
+ expect(getStatus()).toBe(401);
135
+ expect(nextFn).not.toHaveBeenCalled();
136
+ });
137
+
138
+ it("should accept token from query parameter", async () => {
139
+ const token = generateAccessToken("user-123", ["viewer"]);
140
+ const { c, getUser } = createMockContext({ queryToken: token });
141
+
142
+ await requireAuth(c, nextFn);
143
+
144
+ expect(nextFn).toHaveBeenCalled();
145
+ expect(getUser()).toEqual({
146
+ userId: "user-123",
147
+ roles: ["viewer"]
148
+ });
149
+ });
150
+ });
151
+
152
+ describe("optionalAuth", () => {
153
+ it("should set user for valid token", async () => {
154
+ const token = generateAccessToken("user-456", ["viewer"]);
155
+ const { c, getUser } = createMockContext({ authHeader: `Bearer ${token}` });
156
+
157
+ await optionalAuth(c, nextFn);
158
+
159
+ expect(nextFn).toHaveBeenCalled();
160
+ expect(getUser()).toEqual({
161
+ userId: "user-456",
162
+ roles: ["viewer"]
163
+ });
164
+ });
165
+
166
+ it("should call next() without setting user when no token provided", async () => {
167
+ const { c, getUser } = createMockContext();
168
+
169
+ await optionalAuth(c, nextFn);
170
+
171
+ expect(nextFn).toHaveBeenCalled();
172
+ expect(getUser()).toBeUndefined();
173
+ });
174
+
175
+ it("should call next() without setting user for invalid token", async () => {
176
+ const { c, getUser } = createMockContext({ authHeader: "Bearer invalid-token" });
177
+
178
+ await optionalAuth(c, nextFn);
179
+
180
+ expect(nextFn).toHaveBeenCalled();
181
+ expect(getUser()).toBeUndefined();
182
+ });
183
+
184
+ it("should call next() without setting user for non-Bearer auth", async () => {
185
+ const { c, getUser } = createMockContext({ authHeader: "Basic dXNlcjpwYXNz" });
186
+
187
+ await optionalAuth(c, nextFn);
188
+
189
+ expect(nextFn).toHaveBeenCalled();
190
+ expect(getUser()).toBeUndefined();
191
+ });
192
+ });
193
+
194
+ describe("requireAdmin", () => {
195
+ it("should return 401 if user is not authenticated", async () => {
196
+ const { c, getStatus, getBody } = createMockContext();
197
+
198
+ await requireAdmin(c, nextFn);
199
+
200
+ expect(getStatus()).toBe(401);
201
+ expect(getBody()).toEqual({
202
+ error: {
203
+ message: "User not authenticated. requireAuth middleware is missing?",
204
+ code: "UNAUTHORIZED"
205
+ }
206
+ });
207
+ expect(nextFn).not.toHaveBeenCalled();
208
+ });
209
+
210
+ it("should return 403 if user has no roles array", async () => {
211
+ const { c, getStatus } = createMockContext({ user: { userId: "user-123" } });
212
+
213
+ await requireAdmin(c, nextFn);
214
+
215
+ expect(getStatus()).toBe(403);
216
+ expect(nextFn).not.toHaveBeenCalled();
217
+ });
218
+
219
+ it("should return 403 if user has empty roles array", async () => {
220
+ const { c, getStatus } = createMockContext({ user: { userId: "user-123", roles: [] } });
221
+
222
+ await requireAdmin(c, nextFn);
223
+
224
+ expect(getStatus()).toBe(403);
225
+ expect(nextFn).not.toHaveBeenCalled();
226
+ });
227
+
228
+ it("should return 403 if user has standard roles (forbidden)", async () => {
229
+ const { c, getStatus } = createMockContext({ user: { userId: "user-123", roles: ["editor", "viewer"] } });
230
+
231
+ await requireAdmin(c, nextFn);
232
+
233
+ expect(getStatus()).toBe(403);
234
+ expect(nextFn).not.toHaveBeenCalled();
235
+ });
236
+
237
+ it("should allow access if user has 'admin' role", async () => {
238
+ const { c, getStatus } = createMockContext({ user: { userId: "user-123", roles: ["editor", "admin"] } });
239
+
240
+ await requireAdmin(c, nextFn);
241
+
242
+ expect(nextFn).toHaveBeenCalled();
243
+ expect(getStatus()).toBeUndefined(); // No error response
244
+ });
245
+
246
+ it("should allow access if user has 'schema-admin' role", async () => {
247
+ const { c } = createMockContext({ user: { userId: "user-123", roles: ["schema-admin"] } });
248
+
249
+ await requireAdmin(c, nextFn);
250
+
251
+ expect(nextFn).toHaveBeenCalled();
252
+ });
253
+
254
+ it("should block access for malformed spoofed string roles", async () => {
255
+ const { c, getStatus } = createMockContext({ user: { userId: "user-123", roles: ["schema-adminstration", "admins", "admin "] } });
256
+
257
+ await requireAdmin(c, nextFn);
258
+
259
+ expect(getStatus()).toBe(403);
260
+ expect(nextFn).not.toHaveBeenCalled();
261
+ });
262
+ });
263
+
264
+ describe("extractUserFromToken", () => {
265
+ it("should extract user from valid token", () => {
266
+ const token = generateAccessToken("ws-user-123", ["admin"]);
267
+ const payload = extractUserFromToken(token);
268
+
269
+ expect(payload).toEqual({
270
+ userId: "ws-user-123",
271
+ roles: ["admin"]
272
+ });
273
+ });
274
+
275
+ it("should return null for invalid token", () => {
276
+ const payload = extractUserFromToken("invalid-token");
277
+ expect(payload).toBeNull();
278
+ });
279
+
280
+ it("should return null for empty token", () => {
281
+ const payload = extractUserFromToken("");
282
+ expect(payload).toBeNull();
283
+ });
284
+
285
+ it("should work with tokens having empty roles", () => {
286
+ const token = generateAccessToken("user-no-roles", []);
287
+ const payload = extractUserFromToken(token);
288
+
289
+ expect(payload).toEqual({
290
+ userId: "user-no-roles",
291
+ roles: []
292
+ });
293
+ });
294
+ });
295
+ });
@@ -0,0 +1,165 @@
1
+ import {
2
+ validatePasswordStrength,
3
+ hashPassword,
4
+ verifyPassword
5
+ } from "../src/auth/password";
6
+
7
+ describe("Password Utilities", () => {
8
+ describe("validatePasswordStrength", () => {
9
+ it("should accept a valid password", () => {
10
+ const result = validatePasswordStrength("ValidPass123");
11
+ expect(result.valid).toBe(true);
12
+ expect(result.errors).toHaveLength(0);
13
+ });
14
+
15
+ it("should reject password shorter than 8 characters", () => {
16
+ const result = validatePasswordStrength("Short1A");
17
+ expect(result.valid).toBe(false);
18
+ expect(result.errors).toContain("Password must be at least 8 characters long");
19
+ });
20
+
21
+ it("should reject password without uppercase letter", () => {
22
+ const result = validatePasswordStrength("lowercase123");
23
+ expect(result.valid).toBe(false);
24
+ expect(result.errors).toContain("Password must contain at least one uppercase letter");
25
+ });
26
+
27
+ it("should reject password without lowercase letter", () => {
28
+ const result = validatePasswordStrength("UPPERCASE123");
29
+ expect(result.valid).toBe(false);
30
+ expect(result.errors).toContain("Password must contain at least one lowercase letter");
31
+ });
32
+
33
+ it("should reject password without number", () => {
34
+ const result = validatePasswordStrength("NoNumbersHere");
35
+ expect(result.valid).toBe(false);
36
+ expect(result.errors).toContain("Password must contain at least one number");
37
+ });
38
+
39
+ it("should return multiple errors for multiple violations", () => {
40
+ const result = validatePasswordStrength("short");
41
+ expect(result.valid).toBe(false);
42
+ expect(result.errors.length).toBeGreaterThan(1);
43
+ });
44
+
45
+ it("should accept password with special characters", () => {
46
+ const result = validatePasswordStrength("Valid@Pass#123!");
47
+ expect(result.valid).toBe(true);
48
+ });
49
+
50
+ it("should accept password at exactly 8 characters", () => {
51
+ const result = validatePasswordStrength("Abcdefg1");
52
+ expect(result.valid).toBe(true);
53
+ });
54
+ });
55
+
56
+ describe("hashPassword", () => {
57
+ it("should return a hash in salt:hash format", async () => {
58
+ const hash = await hashPassword("TestPassword123");
59
+ expect(hash).toContain(":");
60
+ const [salt, hashValue] = hash.split(":");
61
+ expect(salt).toBeTruthy();
62
+ expect(hashValue).toBeTruthy();
63
+ });
64
+
65
+ it("should generate different hashes for the same password (due to random salt)", async () => {
66
+ const hash1 = await hashPassword("SamePassword123");
67
+ const hash2 = await hashPassword("SamePassword123");
68
+ expect(hash1).not.toBe(hash2);
69
+ });
70
+
71
+ it("should generate a 32-byte salt (64 hex chars)", async () => {
72
+ const hash = await hashPassword("TestPassword123");
73
+ const [salt] = hash.split(":");
74
+ expect(salt).toHaveLength(64);
75
+ });
76
+
77
+ it("should generate a 64-byte key (128 hex chars)", async () => {
78
+ const hash = await hashPassword("TestPassword123");
79
+ const [, hashValue] = hash.split(":");
80
+ expect(hashValue).toHaveLength(128);
81
+ });
82
+ });
83
+
84
+ describe("verifyPassword", () => {
85
+ it("should verify a correct password", async () => {
86
+ const password = "CorrectPassword123";
87
+ const hash = await hashPassword(password);
88
+ const isValid = await verifyPassword(password, hash);
89
+ expect(isValid).toBe(true);
90
+ });
91
+
92
+ it("should reject an incorrect password", async () => {
93
+ const hash = await hashPassword("CorrectPassword123");
94
+ const isValid = await verifyPassword("WrongPassword123", hash);
95
+ expect(isValid).toBe(false);
96
+ });
97
+
98
+ it("should return false for malformed hash (no colon)", async () => {
99
+ const isValid = await verifyPassword("AnyPassword", "malformedhash");
100
+ expect(isValid).toBe(false);
101
+ });
102
+
103
+ it("should return false for empty hash parts", async () => {
104
+ const isValid = await verifyPassword("AnyPassword", ":");
105
+ expect(isValid).toBe(false);
106
+ });
107
+
108
+ it("should be case-sensitive", async () => {
109
+ const hash = await hashPassword("CaseSensitive123");
110
+ const isValid = await verifyPassword("casesensitive123", hash);
111
+ expect(isValid).toBe(false);
112
+ });
113
+
114
+ it("should handle empty password", async () => {
115
+ const hash = await hashPassword("");
116
+ const isValid = await verifyPassword("", hash);
117
+ expect(isValid).toBe(true);
118
+ });
119
+
120
+ it("should handle unicode characters", async () => {
121
+ const password = "Pässwörd123日本語";
122
+ const hash = await hashPassword(password);
123
+ const isValid = await verifyPassword(password, hash);
124
+ expect(isValid).toBe(true);
125
+ });
126
+
127
+ it("should handle very long passwords", async () => {
128
+ const password = "A".repeat(1000) + "bcdefg1";
129
+ const hash = await hashPassword(password);
130
+ const isValid = await verifyPassword(password, hash);
131
+ expect(isValid).toBe(true);
132
+ });
133
+ });
134
+
135
+ describe("timing-safe comparison", () => {
136
+ // This test verifies that timing attacks are mitigated
137
+ // by checking that verification time is relatively constant
138
+ it("should take similar time for correct and incorrect passwords", async () => {
139
+ const password = "TestPassword123";
140
+ const hash = await hashPassword(password);
141
+
142
+ // Run multiple iterations to average out noise
143
+ const iterations = 10;
144
+
145
+ // Time correct password
146
+ const correctStart = Date.now();
147
+ for (let i = 0; i < iterations; i++) {
148
+ await verifyPassword(password, hash);
149
+ }
150
+ const correctTime = Date.now() - correctStart;
151
+
152
+ // Time incorrect password (same length)
153
+ const incorrectStart = Date.now();
154
+ for (let i = 0; i < iterations; i++) {
155
+ await verifyPassword("WrongPassword12", hash);
156
+ }
157
+ const incorrectTime = Date.now() - incorrectStart;
158
+
159
+ // Times should be within 50% of each other
160
+ // (allowing for system variance)
161
+ const ratio = Math.abs(correctTime - incorrectTime) / Math.max(correctTime, incorrectTime);
162
+ expect(ratio).toBeLessThan(0.5);
163
+ });
164
+ });
165
+ });