@rebasepro/server-core 0.0.1-canary.4d4fb3e → 0.0.1-canary.ca2cb6e

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 (176) hide show
  1. package/dist/common/src/collections/CollectionRegistry.d.ts +8 -0
  2. package/dist/common/src/util/entities.d.ts +22 -0
  3. package/dist/common/src/util/relations.d.ts +14 -4
  4. package/dist/common/src/util/resolutions.d.ts +1 -1
  5. package/dist/index-DXVBFp5V.js +37 -0
  6. package/dist/index-DXVBFp5V.js.map +1 -0
  7. package/dist/index.es.js +49931 -55
  8. package/dist/index.es.js.map +1 -1
  9. package/dist/index.umd.js +28475 -34569
  10. package/dist/index.umd.js.map +1 -1
  11. package/dist/server-core/src/api/openapi-generator.d.ts +15 -1
  12. package/dist/server-core/src/api/types.d.ts +1 -1
  13. package/dist/server-core/src/auth/admin-routes.d.ts +10 -1
  14. package/dist/server-core/src/auth/apple-oauth.d.ts +30 -0
  15. package/dist/server-core/src/auth/bitbucket-oauth.d.ts +11 -0
  16. package/dist/server-core/src/auth/discord-oauth.d.ts +14 -0
  17. package/dist/server-core/src/auth/facebook-oauth.d.ts +14 -0
  18. package/dist/server-core/src/auth/github-oauth.d.ts +15 -0
  19. package/dist/server-core/src/auth/gitlab-oauth.d.ts +13 -0
  20. package/dist/server-core/src/auth/google-oauth.d.ts +5 -11
  21. package/dist/server-core/src/auth/index.d.ts +13 -2
  22. package/dist/server-core/src/auth/interfaces.d.ts +45 -6
  23. package/dist/server-core/src/auth/jwt.d.ts +1 -0
  24. package/dist/server-core/src/auth/linkedin-oauth.d.ts +18 -0
  25. package/dist/server-core/src/auth/microsoft-oauth.d.ts +16 -0
  26. package/dist/server-core/src/auth/middleware.d.ts +36 -11
  27. package/dist/server-core/src/auth/rate-limiter.d.ts +2 -2
  28. package/dist/server-core/src/auth/routes.d.ts +13 -3
  29. package/dist/server-core/src/auth/slack-oauth.d.ts +12 -0
  30. package/dist/server-core/src/auth/spotify-oauth.d.ts +12 -0
  31. package/dist/server-core/src/auth/twitter-oauth.d.ts +18 -0
  32. package/dist/server-core/src/cron/cron-loader.d.ts +17 -0
  33. package/dist/server-core/src/cron/cron-routes.d.ts +14 -0
  34. package/dist/server-core/src/cron/cron-scheduler.d.ts +61 -0
  35. package/dist/server-core/src/cron/cron-store.d.ts +32 -0
  36. package/dist/server-core/src/cron/index.d.ts +6 -0
  37. package/dist/server-core/src/email/index.d.ts +2 -2
  38. package/dist/server-core/src/email/templates.d.ts +9 -0
  39. package/dist/server-core/src/email/types.d.ts +20 -23
  40. package/dist/server-core/src/history/history-routes.d.ts +2 -2
  41. package/dist/server-core/src/index.d.ts +5 -0
  42. package/dist/server-core/src/init.d.ts +112 -2
  43. package/dist/server-core/src/singleton.d.ts +35 -0
  44. package/dist/server-core/src/storage/LocalStorageController.d.ts +9 -9
  45. package/dist/server-core/src/storage/S3StorageController.d.ts +8 -8
  46. package/dist/server-core/src/storage/index.d.ts +18 -11
  47. package/dist/server-core/src/storage/routes.d.ts +3 -3
  48. package/dist/server-core/src/storage/types.d.ts +28 -16
  49. package/dist/server-core/src/utils/dev-port.d.ts +35 -0
  50. package/dist/server-core/src/utils/logger.d.ts +31 -0
  51. package/dist/server-core/src/utils/request-logger.d.ts +19 -0
  52. package/dist/types/src/controllers/auth.d.ts +2 -0
  53. package/dist/types/src/controllers/client.d.ts +119 -7
  54. package/dist/types/src/controllers/collection_registry.d.ts +4 -3
  55. package/dist/types/src/controllers/customization_controller.d.ts +7 -1
  56. package/dist/types/src/controllers/data.d.ts +34 -7
  57. package/dist/types/src/controllers/data_driver.d.ts +20 -28
  58. package/dist/types/src/controllers/database_admin.d.ts +2 -2
  59. package/dist/types/src/controllers/email.d.ts +34 -0
  60. package/dist/types/src/controllers/index.d.ts +1 -0
  61. package/dist/types/src/controllers/local_config_persistence.d.ts +4 -4
  62. package/dist/types/src/controllers/navigation.d.ts +5 -5
  63. package/dist/types/src/controllers/registry.d.ts +6 -3
  64. package/dist/types/src/controllers/side_entity_controller.d.ts +7 -6
  65. package/dist/types/src/controllers/storage.d.ts +24 -26
  66. package/dist/types/src/rebase_context.d.ts +8 -4
  67. package/dist/types/src/types/backend.d.ts +4 -1
  68. package/dist/types/src/types/builders.d.ts +5 -4
  69. package/dist/types/src/types/chips.d.ts +1 -1
  70. package/dist/types/src/types/collections.d.ts +169 -125
  71. package/dist/types/src/types/cron.d.ts +102 -0
  72. package/dist/types/src/types/data_source.d.ts +1 -1
  73. package/dist/types/src/types/entity_actions.d.ts +8 -8
  74. package/dist/types/src/types/entity_callbacks.d.ts +15 -15
  75. package/dist/types/src/types/entity_link_builder.d.ts +1 -1
  76. package/dist/types/src/types/entity_overrides.d.ts +2 -1
  77. package/dist/types/src/types/entity_views.d.ts +8 -8
  78. package/dist/types/src/types/export_import.d.ts +3 -3
  79. package/dist/types/src/types/index.d.ts +1 -0
  80. package/dist/types/src/types/plugins.d.ts +72 -18
  81. package/dist/types/src/types/properties.d.ts +118 -33
  82. package/dist/types/src/types/relations.d.ts +1 -1
  83. package/dist/types/src/types/slots.d.ts +30 -6
  84. package/dist/types/src/types/translations.d.ts +44 -0
  85. package/dist/types/src/types/user_management_delegate.d.ts +1 -0
  86. package/package.json +84 -84
  87. package/scratch.ts +6 -5
  88. package/src/api/ast-schema-editor.ts +11 -11
  89. package/src/api/collections_for_test/callbacks_test_collection.ts +5 -2
  90. package/src/api/errors.ts +38 -14
  91. package/src/api/graphql/graphql-schema-generator.ts +5 -3
  92. package/src/api/openapi-generator.ts +613 -58
  93. package/src/api/rest/api-generator.ts +17 -11
  94. package/src/api/rest/query-parser.ts +1 -1
  95. package/src/api/schema-editor-routes.ts +2 -0
  96. package/src/api/server.ts +8 -5
  97. package/src/api/types.ts +1 -1
  98. package/src/auth/admin-routes.ts +58 -17
  99. package/src/auth/apple-oauth.ts +130 -0
  100. package/src/auth/bitbucket-oauth.ts +82 -0
  101. package/src/auth/discord-oauth.ts +83 -0
  102. package/src/auth/facebook-oauth.ts +72 -0
  103. package/src/auth/github-oauth.ts +110 -0
  104. package/src/auth/gitlab-oauth.ts +70 -0
  105. package/src/auth/google-oauth.ts +34 -46
  106. package/src/auth/index.ts +15 -2
  107. package/src/auth/interfaces.ts +54 -7
  108. package/src/auth/jwt.ts +22 -5
  109. package/src/auth/linkedin-oauth.ts +81 -0
  110. package/src/auth/microsoft-oauth.ts +88 -0
  111. package/src/auth/middleware.ts +194 -45
  112. package/src/auth/password.ts +3 -1
  113. package/src/auth/rate-limiter.ts +4 -4
  114. package/src/auth/routes.ts +197 -139
  115. package/src/auth/slack-oauth.ts +71 -0
  116. package/src/auth/spotify-oauth.ts +67 -0
  117. package/src/auth/twitter-oauth.ts +120 -0
  118. package/src/collections/loader.ts +6 -6
  119. package/src/cron/cron-loader.ts +89 -0
  120. package/src/cron/cron-routes.test.ts +265 -0
  121. package/src/cron/cron-routes.ts +85 -0
  122. package/src/cron/cron-scheduler.test.ts +421 -0
  123. package/src/cron/cron-scheduler.ts +413 -0
  124. package/src/cron/cron-store.ts +163 -0
  125. package/src/cron/index.ts +6 -0
  126. package/src/db/interfaces.ts +2 -2
  127. package/src/email/index.ts +3 -2
  128. package/src/email/smtp-email-service.ts +5 -2
  129. package/src/email/templates.ts +93 -6
  130. package/src/email/types.ts +17 -24
  131. package/src/functions/function-loader.ts +34 -6
  132. package/src/functions/function-routes.ts +2 -2
  133. package/src/history/history-routes.ts +4 -3
  134. package/src/index.ts +10 -0
  135. package/src/init.ts +475 -57
  136. package/src/services/driver-registry.ts +1 -1
  137. package/src/singleton.test.ts +28 -0
  138. package/src/singleton.ts +70 -0
  139. package/src/storage/LocalStorageController.ts +50 -53
  140. package/src/storage/S3StorageController.ts +49 -46
  141. package/src/storage/index.ts +27 -16
  142. package/src/storage/routes.ts +76 -59
  143. package/src/storage/storage-registry.ts +5 -5
  144. package/src/storage/types.ts +44 -32
  145. package/src/types/index.ts +4 -4
  146. package/src/utils/dev-port.ts +176 -0
  147. package/src/utils/logger.ts +143 -0
  148. package/src/utils/logging.ts +5 -2
  149. package/src/utils/request-logger.ts +66 -0
  150. package/test/admin-routes.test.ts +114 -65
  151. package/test/api-generator.test.ts +73 -30
  152. package/test/ast-schema-editor.test.ts +5 -3
  153. package/test/auth-middleware-hono.test.ts +268 -33
  154. package/test/auth-routes.test.ts +325 -146
  155. package/test/driver-registry.test.ts +4 -2
  156. package/test/error-propagation.test.ts +226 -0
  157. package/test/errors.test.ts +12 -7
  158. package/test/jwt-security.test.ts +18 -9
  159. package/test/jwt.test.ts +26 -13
  160. package/test/middleware.test.ts +13 -8
  161. package/test/query-parser.test.ts +13 -8
  162. package/test/rate-limiter.test.ts +7 -7
  163. package/test/safe-compare.test.ts +66 -0
  164. package/test/singleton.test.ts +59 -0
  165. package/test/storage-local.test.ts +36 -43
  166. package/test/storage-registry.test.ts +38 -36
  167. package/test/storage-routes.test.ts +9 -5
  168. package/test/storage-s3.test.ts +51 -48
  169. package/test.ts +6 -0
  170. package/tsconfig.json +1 -1
  171. package/vite.config.ts +7 -5
  172. package/dist/index-BeMqpmfQ.js +0 -239
  173. package/dist/index-BeMqpmfQ.js.map +0 -1
  174. package/dist/index-bl4J3lNb.js +0 -55823
  175. package/dist/index-bl4J3lNb.js.map +0 -1
  176. package/vite.config.ts.timestamp-1775065397568-8a853255edf6e.mjs +0 -46
@@ -14,7 +14,9 @@ function createMockDataDriverDelegate(key: string): DataDriver {
14
14
  initialised: true,
15
15
  fetchCollection: jest.fn().mockResolvedValue([]),
16
16
  fetchEntity: jest.fn().mockResolvedValue(undefined),
17
- saveEntity: jest.fn().mockResolvedValue({ id: "test-id", path: "test", values: {} }),
17
+ saveEntity: jest.fn().mockResolvedValue({ id: "test-id",
18
+ path: "test",
19
+ values: {} }),
18
20
  deleteEntity: jest.fn().mockResolvedValue(undefined),
19
21
  checkUniqueField: jest.fn().mockResolvedValue(true)
20
22
  };
@@ -86,7 +88,7 @@ describe("DriverRegistry", () => {
86
88
  const registry = new DefaultDriverRegistry();
87
89
 
88
90
  expect(() => registry.getDefault()).toThrow(
89
- `[DriverRegistry] No default driver registered.`
91
+ "[DriverRegistry] No default driver registered."
90
92
  );
91
93
  });
92
94
  });
@@ -0,0 +1,226 @@
1
+ import { Hono } from "hono";
2
+ import { ApiError, errorHandler } from "../src/api/errors";
3
+ import { HonoEnv } from "../src/api/types";
4
+
5
+ /**
6
+ * End-to-end tests that verify ApiError thrown inside Hono sub-routers
7
+ * produces the correct HTTP status code and JSON body WHEN the sub-router
8
+ * is mounted on a parent app — exactly simulating the init.ts mounting
9
+ * pattern.
10
+ *
11
+ * This is critical because Hono's `onError` does NOT propagate from parent
12
+ * to child routers. Each sub-router must have its own `router.onError(errorHandler)`.
13
+ */
14
+ describe("Error Propagation through Sub-Routers", () => {
15
+
16
+ // ── Without onError on sub-router (broken behavior) ─────────────
17
+
18
+ describe("Sub-router WITHOUT onError (demonstrates the bug)", () => {
19
+ function createBrokenApp() {
20
+ // This simulates the REAL production scenario: the consumer's Hono app
21
+ // (e.g. SustenTalent's index.ts) does NOT set onError on the parent app.
22
+ // Without onError on the sub-router either, Hono's DEFAULT error handler
23
+ // catches ApiErrors and returns a generic 500 with a plain text body.
24
+ const parentApp = new Hono<HonoEnv>();
25
+ // NOTE: no parentApp.onError(errorHandler) — this is the real scenario
26
+
27
+ const subRouter = new Hono<HonoEnv>();
28
+ // NOTE: no subRouter.onError(errorHandler) — this is the bug
29
+ subRouter.get("/login", () => {
30
+ throw ApiError.unauthorized("Invalid credentials", "INVALID_CREDENTIALS");
31
+ });
32
+
33
+ parentApp.route("/api/auth", subRouter);
34
+ return parentApp;
35
+ }
36
+
37
+ it("ApiError.unauthorized returns 500 plain text without ANY onError handler", async () => {
38
+ const app = createBrokenApp();
39
+ const res = await app.request("/api/auth/login");
40
+ // Without any onError handler, Hono's default handler returns 500
41
+ expect(res.status).toBe(500);
42
+ // And the body is NOT our structured JSON — it's plain text
43
+ const text = await res.text();
44
+ expect(text).toBe("Internal Server Error");
45
+ });
46
+ });
47
+
48
+ // ── With onError on sub-router (fixed behavior) ─────────────────
49
+
50
+ describe("Sub-router WITH onError (correct behavior)", () => {
51
+ function createFixedApp() {
52
+ const parentApp = new Hono<HonoEnv>();
53
+ parentApp.onError(errorHandler);
54
+
55
+ const subRouter = new Hono<HonoEnv>();
56
+ subRouter.onError(errorHandler); // ← THE FIX
57
+
58
+ subRouter.get("/login", () => {
59
+ throw ApiError.unauthorized("Invalid credentials", "INVALID_CREDENTIALS");
60
+ });
61
+ subRouter.get("/register", () => {
62
+ throw ApiError.conflict("Email already exists", "EMAIL_EXISTS");
63
+ });
64
+ subRouter.get("/refresh", () => {
65
+ throw ApiError.unauthorized("Refresh token expired", "TOKEN_EXPIRED");
66
+ });
67
+ subRouter.get("/forbidden", () => {
68
+ throw ApiError.forbidden("Admin only");
69
+ });
70
+ subRouter.get("/not-found", () => {
71
+ throw ApiError.notFound("User not found");
72
+ });
73
+ subRouter.get("/bad-request", () => {
74
+ throw ApiError.badRequest("Missing email", "INVALID_INPUT", { field: "email" });
75
+ });
76
+ subRouter.get("/service-unavailable", () => {
77
+ throw ApiError.serviceUnavailable("Google login not configured", "NOT_CONFIGURED");
78
+ });
79
+ subRouter.get("/generic-error", () => {
80
+ throw new Error("Unexpected failure");
81
+ });
82
+
83
+ parentApp.route("/api/auth", subRouter);
84
+ return parentApp;
85
+ }
86
+
87
+ it("returns 401 for unauthorized errors", async () => {
88
+ const app = createFixedApp();
89
+ const res = await app.request("/api/auth/login");
90
+ expect(res.status).toBe(401);
91
+ const body = await res.json() as { error: { message: string; code: string } };
92
+ expect(body.error.message).toBe("Invalid credentials");
93
+ expect(body.error.code).toBe("INVALID_CREDENTIALS");
94
+ });
95
+
96
+ it("returns 409 for conflict errors", async () => {
97
+ const app = createFixedApp();
98
+ const res = await app.request("/api/auth/register");
99
+ expect(res.status).toBe(409);
100
+ const body = await res.json() as { error: { message: string; code: string } };
101
+ expect(body.error.message).toBe("Email already exists");
102
+ expect(body.error.code).toBe("EMAIL_EXISTS");
103
+ });
104
+
105
+ it("returns 401 for expired refresh token", async () => {
106
+ const app = createFixedApp();
107
+ const res = await app.request("/api/auth/refresh");
108
+ expect(res.status).toBe(401);
109
+ const body = await res.json() as { error: { message: string; code: string } };
110
+ expect(body.error.code).toBe("TOKEN_EXPIRED");
111
+ });
112
+
113
+ it("returns 403 for forbidden errors", async () => {
114
+ const app = createFixedApp();
115
+ const res = await app.request("/api/auth/forbidden");
116
+ expect(res.status).toBe(403);
117
+ });
118
+
119
+ it("returns 404 for not found errors", async () => {
120
+ const app = createFixedApp();
121
+ const res = await app.request("/api/auth/not-found");
122
+ expect(res.status).toBe(404);
123
+ });
124
+
125
+ it("returns 400 with details for bad request errors", async () => {
126
+ const app = createFixedApp();
127
+ const res = await app.request("/api/auth/bad-request");
128
+ expect(res.status).toBe(400);
129
+ const body = await res.json() as { error: { message: string; code: string; details: unknown } };
130
+ expect(body.error.code).toBe("INVALID_INPUT");
131
+ expect(body.error.details).toEqual({ field: "email" });
132
+ });
133
+
134
+ it("returns 503 for service unavailable errors", async () => {
135
+ const app = createFixedApp();
136
+ const res = await app.request("/api/auth/service-unavailable");
137
+ expect(res.status).toBe(503);
138
+ const body = await res.json() as { error: { message: string; code: string } };
139
+ expect(body.error.code).toBe("NOT_CONFIGURED");
140
+ });
141
+
142
+ it("returns 500 with sanitized message for generic errors", async () => {
143
+ const app = createFixedApp();
144
+ const res = await app.request("/api/auth/generic-error");
145
+ expect(res.status).toBe(500);
146
+ const body = await res.json() as { error: { message: string; code: string } };
147
+ expect(body.error.code).toBe("INTERNAL_ERROR");
148
+ // Should NOT leak the raw error message
149
+ expect(body.error.message).toBe("Internal Server Error");
150
+ });
151
+
152
+ it("returns consistent { error: { message, code } } shape for all error types", async () => {
153
+ const app = createFixedApp();
154
+ const paths = ["/api/auth/login", "/api/auth/register", "/api/auth/forbidden", "/api/auth/not-found", "/api/auth/bad-request"];
155
+
156
+ for (const path of paths) {
157
+ const res = await app.request(path);
158
+ const body = await res.json() as Record<string, unknown>;
159
+ expect(body).toHaveProperty("error");
160
+ expect(body.error).toHaveProperty("message");
161
+ expect(body.error).toHaveProperty("code");
162
+ }
163
+ });
164
+ });
165
+
166
+ // ── Multi-level nesting ─────────────────────────────────────────
167
+
168
+ describe("Deeply nested sub-routers", () => {
169
+ it("error handler works through two levels of nesting", async () => {
170
+ const root = new Hono<HonoEnv>();
171
+ root.onError(errorHandler);
172
+
173
+ const level1 = new Hono<HonoEnv>();
174
+ level1.onError(errorHandler);
175
+
176
+ const level2 = new Hono<HonoEnv>();
177
+ level2.onError(errorHandler);
178
+
179
+ level2.get("/deep", () => {
180
+ throw ApiError.unauthorized("Deep error");
181
+ });
182
+
183
+ level1.route("/nested", level2);
184
+ root.route("/api", level1);
185
+
186
+ const res = await root.request("/api/nested/deep");
187
+ expect(res.status).toBe(401);
188
+ const body = await res.json() as { error: { message: string } };
189
+ expect(body.error.message).toBe("Deep error");
190
+ });
191
+ });
192
+
193
+ // ── Data router simulation ──────────────────────────────────────
194
+
195
+ describe("Data router error propagation (simulates init.ts pattern)", () => {
196
+ function createDataApp() {
197
+ const app = new Hono<HonoEnv>();
198
+ app.onError(errorHandler);
199
+
200
+ const dataRouter = new Hono<HonoEnv>();
201
+ dataRouter.onError(errorHandler);
202
+
203
+ dataRouter.get("/posts/:id", () => {
204
+ throw ApiError.notFound("Entity not found");
205
+ });
206
+ dataRouter.post("/posts", () => {
207
+ throw ApiError.badRequest("Validation failed", "INVALID_INPUT");
208
+ });
209
+
210
+ app.route("/api/data", dataRouter);
211
+ return app;
212
+ }
213
+
214
+ it("returns 404 for entity not found", async () => {
215
+ const app = createDataApp();
216
+ const res = await app.request("/api/data/posts/999");
217
+ expect(res.status).toBe(404);
218
+ });
219
+
220
+ it("returns 400 for validation errors", async () => {
221
+ const app = createDataApp();
222
+ const res = await app.request("/api/data/posts", { method: "POST" });
223
+ expect(res.status).toBe(400);
224
+ });
225
+ });
226
+ });
@@ -6,18 +6,19 @@ function createMockContext(method = "GET", path = "/test") {
6
6
  let capturedBody: any;
7
7
 
8
8
  const c = {
9
- req: { method, path },
9
+ req: { method,
10
+ path },
10
11
  json: (body: any, status?: number) => {
11
12
  capturedBody = body;
12
13
  capturedStatus = status ?? 200;
13
14
  return new Response(JSON.stringify(body), { status: capturedStatus });
14
- },
15
+ }
15
16
  } as any;
16
17
 
17
18
  return {
18
19
  c,
19
20
  getStatus: () => capturedStatus,
20
- getBody: () => capturedBody,
21
+ getBody: () => capturedBody
21
22
  };
22
23
  }
23
24
 
@@ -100,7 +101,8 @@ describe("errorHandler", () => {
100
101
 
101
102
  expect(getStatus()).toBe(404);
102
103
  expect(getBody()).toEqual({
103
- error: { message: "User not found", code: "NOT_FOUND" }
104
+ error: { message: "User not found",
105
+ code: "NOT_FOUND" }
104
106
  });
105
107
  });
106
108
 
@@ -125,7 +127,8 @@ describe("errorHandler", () => {
125
127
 
126
128
  expect(getStatus()).toBe(404);
127
129
  expect(getBody()).toEqual({
128
- error: { message: "Not found", code: "NOT_FOUND" }
130
+ error: { message: "Not found",
131
+ code: "NOT_FOUND" }
129
132
  });
130
133
  });
131
134
 
@@ -136,13 +139,15 @@ describe("errorHandler", () => {
136
139
 
137
140
  expect(getStatus()).toBe(500);
138
141
  expect(getBody()).toEqual({
139
- error: { message: "Internal Server Error", code: "INTERNAL_ERROR" }
142
+ error: { message: "Internal Server Error",
143
+ code: "INTERNAL_ERROR" }
140
144
  });
141
145
  });
142
146
 
143
147
  it("should use statusCode from error if present", () => {
144
148
  const { c, getStatus } = createMockContext();
145
- const err = Object.assign(new Error("Rate limited"), { statusCode: 429, code: "RATE_LIMITED" });
149
+ const err = Object.assign(new Error("Rate limited"), { statusCode: 429,
150
+ code: "RATE_LIMITED" });
146
151
  errorHandler(err, c);
147
152
 
148
153
  expect(getStatus()).toBe(429);
@@ -5,7 +5,7 @@ import {
5
5
  generateRefreshToken,
6
6
  hashRefreshToken,
7
7
  getAccessTokenExpiryMs,
8
- getRefreshTokenExpiry,
8
+ getRefreshTokenExpiry
9
9
  } from "../src/auth/jwt";
10
10
 
11
11
  const STRONG_SECRET = "this-is-a-strong-secret-for-jwt-testing-at-least-32-chars-long";
@@ -13,7 +13,9 @@ const STRONG_SECRET = "this-is-a-strong-secret-for-jwt-testing-at-least-32-chars
13
13
  describe("JWT Security Hardening", () => {
14
14
 
15
15
  beforeEach(() => {
16
- configureJwt({ secret: STRONG_SECRET, accessExpiresIn: "1h", refreshExpiresIn: "30d" });
16
+ configureJwt({ secret: STRONG_SECRET,
17
+ accessExpiresIn: "1h",
18
+ refreshExpiresIn: "30d" });
17
19
  });
18
20
 
19
21
  // ── Secret validation ───────────────────────────────────
@@ -37,7 +39,7 @@ describe("JWT Security Hardening", () => {
37
39
 
38
40
  it("accepts strong, random secrets", () => {
39
41
  expect(() => configureJwt({
40
- secret: "aG7x!kL2$mP9#qR5+tU8*wZ0^bD3&fH6",
42
+ secret: "aG7x!kL2$mP9#qR5+tU8*wZ0^bD3&fH6"
41
43
  })).not.toThrow();
42
44
  });
43
45
  });
@@ -64,7 +66,9 @@ describe("JWT Security Hardening", () => {
64
66
 
65
67
  it("throws when secret is not configured", () => {
66
68
  // Force empty secret
67
- Object.defineProperty(require("../src/auth/jwt"), "jwtConfig", { value: { secret: "" }, writable: true });
69
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
70
+ Object.defineProperty(require("../src/auth/jwt"), "jwtConfig", { value: { secret: "" },
71
+ writable: true });
68
72
  // This won't work since jwtConfig is module-scoped, but generateAccessToken has its own check
69
73
  // We'll test via configureJwt + clearing
70
74
  });
@@ -142,27 +146,32 @@ describe("JWT Security Hardening", () => {
142
146
  // ── Expiry calculations ─────────────────────────────────
143
147
  describe("expiry calculations", () => {
144
148
  it("calculates 1h as 3600000ms", () => {
145
- configureJwt({ secret: STRONG_SECRET, accessExpiresIn: "1h" });
149
+ configureJwt({ secret: STRONG_SECRET,
150
+ accessExpiresIn: "1h" });
146
151
  expect(getAccessTokenExpiryMs()).toBe(3600000);
147
152
  });
148
153
 
149
154
  it("calculates 30m as 1800000ms", () => {
150
- configureJwt({ secret: STRONG_SECRET, accessExpiresIn: "30m" });
155
+ configureJwt({ secret: STRONG_SECRET,
156
+ accessExpiresIn: "30m" });
151
157
  expect(getAccessTokenExpiryMs()).toBe(1800000);
152
158
  });
153
159
 
154
160
  it("calculates 7d correctly", () => {
155
- configureJwt({ secret: STRONG_SECRET, accessExpiresIn: "7d" });
161
+ configureJwt({ secret: STRONG_SECRET,
162
+ accessExpiresIn: "7d" });
156
163
  expect(getAccessTokenExpiryMs()).toBe(7 * 24 * 60 * 60 * 1000);
157
164
  });
158
165
 
159
166
  it("defaults to 1h for unparseable duration", () => {
160
- configureJwt({ secret: STRONG_SECRET, accessExpiresIn: "invalid" });
167
+ configureJwt({ secret: STRONG_SECRET,
168
+ accessExpiresIn: "invalid" });
161
169
  expect(getAccessTokenExpiryMs()).toBe(3600000);
162
170
  });
163
171
 
164
172
  it("refresh expiry is in the future", () => {
165
- configureJwt({ secret: STRONG_SECRET, refreshExpiresIn: "30d" });
173
+ configureJwt({ secret: STRONG_SECRET,
174
+ refreshExpiresIn: "30d" });
166
175
  const expiry = getRefreshTokenExpiry();
167
176
  expect(expiry.getTime()).toBeGreaterThan(Date.now());
168
177
  // Should be approximately 30 days in the future
package/test/jwt.test.ts CHANGED
@@ -29,7 +29,8 @@ describe("JWT Utilities", () => {
29
29
  });
30
30
 
31
31
  it("should allow partial configuration updates", () => {
32
- configureJwt({ secret: testSecret, accessExpiresIn: "2h" });
32
+ configureJwt({ secret: testSecret,
33
+ accessExpiresIn: "2h" });
33
34
  // Token generation should still work
34
35
  const token = generateAccessToken("user-1", ["admin"]);
35
36
  expect(token).toBeTruthy();
@@ -138,27 +139,32 @@ describe("JWT Utilities", () => {
138
139
 
139
140
  describe("getAccessTokenExpiryMs", () => {
140
141
  it("should return correct milliseconds for hours", () => {
141
- configureJwt({ secret: testSecret, accessExpiresIn: "2h" });
142
+ configureJwt({ secret: testSecret,
143
+ accessExpiresIn: "2h" });
142
144
  expect(getAccessTokenExpiryMs()).toBe(2 * 60 * 60 * 1000);
143
145
  });
144
146
 
145
147
  it("should return correct milliseconds for days", () => {
146
- configureJwt({ secret: testSecret, accessExpiresIn: "7d" });
148
+ configureJwt({ secret: testSecret,
149
+ accessExpiresIn: "7d" });
147
150
  expect(getAccessTokenExpiryMs()).toBe(7 * 24 * 60 * 60 * 1000);
148
151
  });
149
152
 
150
153
  it("should return correct milliseconds for minutes", () => {
151
- configureJwt({ secret: testSecret, accessExpiresIn: "30m" });
154
+ configureJwt({ secret: testSecret,
155
+ accessExpiresIn: "30m" });
152
156
  expect(getAccessTokenExpiryMs()).toBe(30 * 60 * 1000);
153
157
  });
154
158
 
155
159
  it("should return correct milliseconds for seconds", () => {
156
- configureJwt({ secret: testSecret, accessExpiresIn: "300s" });
160
+ configureJwt({ secret: testSecret,
161
+ accessExpiresIn: "300s" });
157
162
  expect(getAccessTokenExpiryMs()).toBe(300 * 1000);
158
163
  });
159
164
 
160
165
  it("should default to 1 hour for invalid format", () => {
161
- configureJwt({ secret: testSecret, accessExpiresIn: "invalid" });
166
+ configureJwt({ secret: testSecret,
167
+ accessExpiresIn: "invalid" });
162
168
  expect(getAccessTokenExpiryMs()).toBe(60 * 60 * 1000);
163
169
  });
164
170
  });
@@ -171,7 +177,8 @@ describe("JWT Utilities", () => {
171
177
  });
172
178
 
173
179
  it("should match the configured expiry duration", () => {
174
- configureJwt({ secret: testSecret, accessExpiresIn: "1h" });
180
+ configureJwt({ secret: testSecret,
181
+ accessExpiresIn: "1h" });
175
182
  const now = Date.now();
176
183
  const expiry = getAccessTokenExpiry();
177
184
  // Should be approximately 1 hour from now (with small tolerance)
@@ -197,7 +204,8 @@ describe("JWT Utilities", () => {
197
204
  });
198
205
 
199
206
  it("should respect custom refresh expiry configuration", () => {
200
- configureJwt({ secret: testSecret, refreshExpiresIn: "7d" });
207
+ configureJwt({ secret: testSecret,
208
+ refreshExpiresIn: "7d" });
201
209
  const expiry = getRefreshTokenExpiry();
202
210
  const expected = Date.now() + (7 * 24 * 60 * 60 * 1000);
203
211
  expect(expiry.getTime()).toBeGreaterThanOrEqual(expected - 1000);
@@ -205,7 +213,8 @@ describe("JWT Utilities", () => {
205
213
  });
206
214
 
207
215
  it("should handle hour-based refresh expiry", () => {
208
- configureJwt({ secret: testSecret, refreshExpiresIn: "24h" });
216
+ configureJwt({ secret: testSecret,
217
+ refreshExpiresIn: "24h" });
209
218
  const expiry = getRefreshTokenExpiry();
210
219
  const expected = Date.now() + (24 * 60 * 60 * 1000);
211
220
  expect(expiry.getTime()).toBeGreaterThanOrEqual(expected - 1000);
@@ -213,7 +222,8 @@ describe("JWT Utilities", () => {
213
222
  });
214
223
 
215
224
  it("should handle minute-based refresh expiry", () => {
216
- configureJwt({ secret: testSecret, refreshExpiresIn: "90m" });
225
+ configureJwt({ secret: testSecret,
226
+ refreshExpiresIn: "90m" });
217
227
  const expiry = getRefreshTokenExpiry();
218
228
  const expected = Date.now() + (90 * 60 * 1000);
219
229
  expect(expiry.getTime()).toBeGreaterThanOrEqual(expected - 1000);
@@ -221,7 +231,8 @@ describe("JWT Utilities", () => {
221
231
  });
222
232
 
223
233
  it("should handle second-based refresh expiry", () => {
224
- configureJwt({ secret: testSecret, refreshExpiresIn: "3600s" });
234
+ configureJwt({ secret: testSecret,
235
+ refreshExpiresIn: "3600s" });
225
236
  const expiry = getRefreshTokenExpiry();
226
237
  const expected = Date.now() + (3600 * 1000);
227
238
  expect(expiry.getTime()).toBeGreaterThanOrEqual(expected - 1000);
@@ -229,7 +240,8 @@ describe("JWT Utilities", () => {
229
240
  });
230
241
 
231
242
  it("should default to 30 days for invalid refresh format", () => {
232
- configureJwt({ secret: testSecret, refreshExpiresIn: "invalid" });
243
+ configureJwt({ secret: testSecret,
244
+ refreshExpiresIn: "invalid" });
233
245
  const expiry = getRefreshTokenExpiry();
234
246
  const expected = Date.now() + (30 * 24 * 60 * 60 * 1000);
235
247
  expect(expiry.getTime()).toBeGreaterThanOrEqual(expected - 1000);
@@ -270,7 +282,8 @@ describe("JWT Utilities", () => {
270
282
  describe("expired token handling", () => {
271
283
  it("should return null for an expired token", () => {
272
284
  // Configure with 1 second expiry
273
- configureJwt({ secret: testSecret, accessExpiresIn: "1s" });
285
+ configureJwt({ secret: testSecret,
286
+ accessExpiresIn: "1s" });
274
287
  const token = generateAccessToken("user-1", ["admin"]);
275
288
 
276
289
  // Immediately verify should work
@@ -22,7 +22,7 @@ function createMockContext(opts: {
22
22
  query: (name: string) => {
23
23
  if (name === "token") return opts.queryToken;
24
24
  return undefined;
25
- },
25
+ }
26
26
  },
27
27
  json: (body: any, status?: number) => {
28
28
  capturedBody = body;
@@ -30,14 +30,14 @@ function createMockContext(opts: {
30
30
  return new Response(JSON.stringify(body), { status: capturedStatus });
31
31
  },
32
32
  set: (key: string, value: any) => variables.set(key, value),
33
- get: (key: string) => variables.get(key),
33
+ get: (key: string) => variables.get(key)
34
34
  } as any;
35
35
 
36
36
  return {
37
37
  c,
38
38
  getStatus: () => capturedStatus,
39
39
  getBody: () => capturedBody,
40
- getUser: () => variables.get("user"),
40
+ getUser: () => variables.get("user")
41
41
  };
42
42
  }
43
43
 
@@ -217,7 +217,8 @@ describe("Auth Middleware", () => {
217
217
  });
218
218
 
219
219
  it("should return 403 if user has empty roles array", async () => {
220
- const { c, getStatus } = createMockContext({ user: { userId: "user-123", roles: [] } });
220
+ const { c, getStatus } = createMockContext({ user: { userId: "user-123",
221
+ roles: [] } });
221
222
 
222
223
  await requireAdmin(c, nextFn);
223
224
 
@@ -226,7 +227,8 @@ describe("Auth Middleware", () => {
226
227
  });
227
228
 
228
229
  it("should return 403 if user has standard roles (forbidden)", async () => {
229
- const { c, getStatus } = createMockContext({ user: { userId: "user-123", roles: ["editor", "viewer"] } });
230
+ const { c, getStatus } = createMockContext({ user: { userId: "user-123",
231
+ roles: ["editor", "viewer"] } });
230
232
 
231
233
  await requireAdmin(c, nextFn);
232
234
 
@@ -235,7 +237,8 @@ describe("Auth Middleware", () => {
235
237
  });
236
238
 
237
239
  it("should allow access if user has 'admin' role", async () => {
238
- const { c, getStatus } = createMockContext({ user: { userId: "user-123", roles: ["editor", "admin"] } });
240
+ const { c, getStatus } = createMockContext({ user: { userId: "user-123",
241
+ roles: ["editor", "admin"] } });
239
242
 
240
243
  await requireAdmin(c, nextFn);
241
244
 
@@ -244,7 +247,8 @@ describe("Auth Middleware", () => {
244
247
  });
245
248
 
246
249
  it("should allow access if user has 'schema-admin' role", async () => {
247
- const { c } = createMockContext({ user: { userId: "user-123", roles: ["schema-admin"] } });
250
+ const { c } = createMockContext({ user: { userId: "user-123",
251
+ roles: ["schema-admin"] } });
248
252
 
249
253
  await requireAdmin(c, nextFn);
250
254
 
@@ -252,7 +256,8 @@ describe("Auth Middleware", () => {
252
256
  });
253
257
 
254
258
  it("should block access for malformed spoofed string roles", async () => {
255
- const { c, getStatus } = createMockContext({ user: { userId: "user-123", roles: ["schema-adminstration", "admins", "admin "] } });
259
+ const { c, getStatus } = createMockContext({ user: { userId: "user-123",
260
+ roles: ["schema-adminstration", "admins", "admin "] } });
256
261
 
257
262
  await requireAdmin(c, nextFn);
258
263
 
@@ -39,7 +39,8 @@ describe("parseQueryOptions — pagination", () => {
39
39
  });
40
40
 
41
41
  it("calculates offset from page number", () => {
42
- const result = parseQueryOptions({ page: "3", limit: "10" });
42
+ const result = parseQueryOptions({ page: "3",
43
+ limit: "10" });
43
44
  expect(result.offset).toBe(20); // (3-1) * 10
44
45
  });
45
46
 
@@ -134,7 +135,7 @@ describe("parseQueryOptions — PostgREST filters", () => {
134
135
  limit: "10",
135
136
  offset: "0",
136
137
  orderBy: "name:asc",
137
- status: "eq.active",
138
+ status: "eq.active"
138
139
  });
139
140
  // Only status should be in where
140
141
  expect(result.where?.status).toEqual(["==", "active"]);
@@ -161,14 +162,14 @@ describe("parseQueryOptions — PostgREST filters", () => {
161
162
  describe("parseQueryOptions — legacy JSON where", () => {
162
163
  it("parses JSON where string", () => {
163
164
  const result = parseQueryOptions({
164
- where: JSON.stringify({ status: ["==", "published"] }),
165
+ where: JSON.stringify({ status: ["==", "published"] })
165
166
  });
166
167
  expect(result.where?.status).toEqual(["==", "published"]);
167
168
  });
168
169
 
169
170
  it("accepts object where directly", () => {
170
171
  const result = parseQueryOptions({
171
- where: { status: ["==", "draft"] },
172
+ where: { status: ["==", "draft"] }
172
173
  });
173
174
  expect(result.where?.status).toEqual(["==", "draft"]);
174
175
  });
@@ -191,19 +192,23 @@ describe("parseQueryOptions — legacy JSON where", () => {
191
192
  // ─────────────────────────────────────────────────────────────
192
193
  describe("parseQueryOptions — sorting", () => {
193
194
  it("parses JSON orderBy", () => {
194
- const orderBy = JSON.stringify([{ field: "name", direction: "asc" }]);
195
+ const orderBy = JSON.stringify([{ field: "name",
196
+ direction: "asc" }]);
195
197
  const result = parseQueryOptions({ orderBy });
196
- expect(result.orderBy).toEqual([{ field: "name", direction: "asc" }]);
198
+ expect(result.orderBy).toEqual([{ field: "name",
199
+ direction: "asc" }]);
197
200
  });
198
201
 
199
202
  it("parses simple field:direction format", () => {
200
203
  const result = parseQueryOptions({ orderBy: "created_at:desc" });
201
- expect(result.orderBy).toEqual([{ field: "created_at", direction: "desc" }]);
204
+ expect(result.orderBy).toEqual([{ field: "created_at",
205
+ direction: "desc" }]);
202
206
  });
203
207
 
204
208
  it("defaults direction to asc", () => {
205
209
  const result = parseQueryOptions({ orderBy: "name" });
206
- expect(result.orderBy).toEqual([{ field: "name", direction: "asc" }]);
210
+ expect(result.orderBy).toEqual([{ field: "name",
211
+ direction: "asc" }]);
207
212
  });
208
213
 
209
214
  it("handles no orderBy", () => {