@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
@@ -1,15 +1,17 @@
1
+ import { describe, it, expect, beforeAll, jest } from "@jest/globals";
1
2
  import { Hono, Context } from "hono";
2
- import { configureJwt, generateAccessToken } from "../src/auth/jwt.js";
3
- import { requireAuth, optionalAuth, requireAdmin, createAuthMiddleware } from "../src/auth/middleware.js";
4
- import type { HonoEnv } from "../src/api/types.js";
5
- import type { DataDriver } from "../../types/src/controllers/data_driver.js";
3
+ import { configureJwt, generateAccessToken } from "../src/auth/jwt";
4
+ import { requireAuth, optionalAuth, requireAdmin, createAuthMiddleware, createRequireAuth } from "../src/auth/middleware";
5
+ import type { HonoEnv } from "../src/api/types";
6
+ import type { DataDriver } from "../../types/src/controllers/data_driver";
6
7
 
7
8
  const TEST_SECRET = "test-secret-key-for-hono-middleware-testing-1234567890";
8
9
 
9
10
  describe("Auth Middleware (Hono)", () => {
10
11
 
11
12
  beforeAll(() => {
12
- configureJwt({ secret: TEST_SECRET, accessExpiresIn: "1h" });
13
+ configureJwt({ secret: TEST_SECRET,
14
+ accessExpiresIn: "1h" });
13
15
  });
14
16
 
15
17
  // ── requireAuth ─────────────────────────────────────────
@@ -28,7 +30,7 @@ describe("Auth Middleware (Hono)", () => {
28
30
  const app = createApp();
29
31
  const token = generateAccessToken("user-1", ["admin"]);
30
32
  const res = await app.request("/protected/resource", {
31
- headers: { Authorization: `Bearer ${token}` },
33
+ headers: { Authorization: `Bearer ${token}` }
32
34
  });
33
35
  expect(res.status).toBe(200);
34
36
  const body = await res.json() as any;
@@ -47,7 +49,7 @@ describe("Auth Middleware (Hono)", () => {
47
49
  it("returns 401 for non-Bearer prefix", async () => {
48
50
  const app = createApp();
49
51
  const res = await app.request("/protected/resource", {
50
- headers: { Authorization: "Basic abc123" },
52
+ headers: { Authorization: "Basic abc123" }
51
53
  });
52
54
  expect(res.status).toBe(401);
53
55
  });
@@ -55,7 +57,7 @@ describe("Auth Middleware (Hono)", () => {
55
57
  it("returns 401 for invalid/expired token", async () => {
56
58
  const app = createApp();
57
59
  const res = await app.request("/protected/resource", {
58
- headers: { Authorization: "Bearer invalid.token.here" },
60
+ headers: { Authorization: "Bearer invalid.token.here" }
59
61
  });
60
62
  expect(res.status).toBe(401);
61
63
  const body = await res.json() as any;
@@ -76,7 +78,7 @@ describe("Auth Middleware (Hono)", () => {
76
78
  const bearerToken = generateAccessToken("bearer-user", ["admin"]);
77
79
  const queryToken = generateAccessToken("query-user", ["viewer"]);
78
80
  const res = await app.request(`/protected/resource?token=${queryToken}`, {
79
- headers: { Authorization: `Bearer ${bearerToken}` },
81
+ headers: { Authorization: `Bearer ${bearerToken}` }
80
82
  });
81
83
  expect(res.status).toBe(200);
82
84
  const body = await res.json() as any;
@@ -97,7 +99,7 @@ describe("Auth Middleware (Hono)", () => {
97
99
  const app = createApp();
98
100
  const token = generateAccessToken("admin-1", ["admin"]);
99
101
  const res = await app.request("/admin/dashboard", {
100
- headers: { Authorization: `Bearer ${token}` },
102
+ headers: { Authorization: `Bearer ${token}` }
101
103
  });
102
104
  expect(res.status).toBe(200);
103
105
  });
@@ -106,7 +108,7 @@ describe("Auth Middleware (Hono)", () => {
106
108
  const app = createApp();
107
109
  const token = generateAccessToken("schema-admin-1", ["schema-admin"]);
108
110
  const res = await app.request("/admin/dashboard", {
109
- headers: { Authorization: `Bearer ${token}` },
111
+ headers: { Authorization: `Bearer ${token}` }
110
112
  });
111
113
  expect(res.status).toBe(200);
112
114
  });
@@ -115,7 +117,7 @@ describe("Auth Middleware (Hono)", () => {
115
117
  const app = createApp();
116
118
  const token = generateAccessToken("user-1", ["editor"]);
117
119
  const res = await app.request("/admin/dashboard", {
118
- headers: { Authorization: `Bearer ${token}` },
120
+ headers: { Authorization: `Bearer ${token}` }
119
121
  });
120
122
  expect(res.status).toBe(403);
121
123
  const body = await res.json() as any;
@@ -139,7 +141,8 @@ describe("Auth Middleware (Hono)", () => {
139
141
  app.use("/public/*", optionalAuth);
140
142
  app.get("/public/feed", (c: Context<HonoEnv>) => {
141
143
  const user = c.get("user");
142
- return c.json({ authenticated: !!user, user: user ?? null });
144
+ return c.json({ authenticated: !!user,
145
+ user: user ?? null });
143
146
  });
144
147
  return app;
145
148
  }
@@ -148,7 +151,7 @@ describe("Auth Middleware (Hono)", () => {
148
151
  const app = createApp();
149
152
  const token = generateAccessToken("opt-user", ["viewer"]);
150
153
  const res = await app.request("/public/feed", {
151
- headers: { Authorization: `Bearer ${token}` },
154
+ headers: { Authorization: `Bearer ${token}` }
152
155
  });
153
156
  const body = await res.json() as any;
154
157
  expect(res.status).toBe(200);
@@ -168,7 +171,7 @@ describe("Auth Middleware (Hono)", () => {
168
171
  it("proceeds without user when token is invalid", async () => {
169
172
  const app = createApp();
170
173
  const res = await app.request("/public/feed", {
171
- headers: { Authorization: "Bearer garbage.token.value" },
174
+ headers: { Authorization: "Bearer garbage.token.value" }
172
175
  });
173
176
  const body = await res.json() as any;
174
177
  expect(res.status).toBe(200);
@@ -182,12 +185,13 @@ describe("Auth Middleware (Hono)", () => {
182
185
  fetchCollection: jest.fn() as any,
183
186
  fetchEntity: jest.fn() as any,
184
187
  saveEntity: jest.fn() as any,
185
- deleteEntity: jest.fn() as any,
188
+ deleteEntity: jest.fn() as any
186
189
  };
187
190
 
188
- it("sets driver in context", async () => {
191
+ it("sets driver in context (requireAuth: false)", async () => {
189
192
  const app = new Hono<HonoEnv>();
190
- app.use("/*", createAuthMiddleware({ driver: mockDriver }));
193
+ app.use("/*", createAuthMiddleware({ driver: mockDriver,
194
+ requireAuth: false }));
191
195
  app.get("/test", (c) => {
192
196
  const driver = c.get("driver");
193
197
  return c.json({ hasDriver: !!driver });
@@ -199,9 +203,19 @@ describe("Auth Middleware (Hono)", () => {
199
203
  expect(body.hasDriver).toBe(true);
200
204
  });
201
205
 
206
+ it("enforces auth by default (requireAuth defaults to true)", async () => {
207
+ const app = new Hono<HonoEnv>();
208
+ app.use("/*", createAuthMiddleware({ driver: mockDriver }));
209
+ app.get("/test", (c) => c.json({ ok: true }));
210
+
211
+ const res = await app.request("/test");
212
+ expect(res.status).toBe(401);
213
+ });
214
+
202
215
  it("enforces auth when requireAuth is true", async () => {
203
216
  const app = new Hono<HonoEnv>();
204
- app.use("/*", createAuthMiddleware({ driver: mockDriver, requireAuth: true }));
217
+ app.use("/*", createAuthMiddleware({ driver: mockDriver,
218
+ requireAuth: true }));
205
219
  app.get("/test", (c) => c.json({ ok: true }));
206
220
 
207
221
  const res = await app.request("/test");
@@ -210,7 +224,8 @@ describe("Auth Middleware (Hono)", () => {
210
224
 
211
225
  it("allows anonymous when requireAuth is false", async () => {
212
226
  const app = new Hono<HonoEnv>();
213
- app.use("/*", createAuthMiddleware({ driver: mockDriver, requireAuth: false }));
227
+ app.use("/*", createAuthMiddleware({ driver: mockDriver,
228
+ requireAuth: false }));
214
229
  app.get("/test", (c) => c.json({ ok: true }));
215
230
 
216
231
  const res = await app.request("/test");
@@ -219,6 +234,7 @@ describe("Auth Middleware (Hono)", () => {
219
234
 
220
235
  it("extracts JWT and sets user in context", async () => {
221
236
  const app = new Hono<HonoEnv>();
237
+ // Default requireAuth=true, but providing a valid token satisfies it
222
238
  app.use("/*", createAuthMiddleware({ driver: mockDriver }));
223
239
  app.get("/test", (c) => {
224
240
  const user = c.get("user");
@@ -227,13 +243,27 @@ describe("Auth Middleware (Hono)", () => {
227
243
 
228
244
  const token = generateAccessToken("jwt-user", ["admin"]);
229
245
  const res = await app.request("/test", {
230
- headers: { Authorization: `Bearer ${token}` },
246
+ headers: { Authorization: `Bearer ${token}` }
231
247
  });
232
248
  const body = await res.json() as any;
233
249
  expect(body.user.userId).toBe("jwt-user");
234
250
  expect(body.user.roles).toEqual(["admin"]);
235
251
  });
236
252
 
253
+ it("rejects invalid tokens even with requireAuth: false (fail closed)", async () => {
254
+ const app = new Hono<HonoEnv>();
255
+ app.use("/*", createAuthMiddleware({ driver: mockDriver,
256
+ requireAuth: false }));
257
+ app.get("/test", (c) => c.json({ ok: true }));
258
+
259
+ const res = await app.request("/test", {
260
+ headers: { Authorization: "Bearer invalid.garbage.token" }
261
+ });
262
+ expect(res.status).toBe(401);
263
+ const body = await res.json() as any;
264
+ expect(body.error.code).toBe("UNAUTHORIZED");
265
+ });
266
+
237
267
  it("uses custom validator when provided", async () => {
238
268
  const app = new Hono<HonoEnv>();
239
269
  app.use("/*", createAuthMiddleware({
@@ -241,10 +271,11 @@ describe("Auth Middleware (Hono)", () => {
241
271
  validator: async (c: Context<HonoEnv>) => {
242
272
  const apiKey = c.req.header("x-api-key");
243
273
  if (apiKey === "valid-key") {
244
- return { userId: "api-user", roles: ["api"] };
274
+ return { userId: "api-user",
275
+ roles: ["api"] };
245
276
  }
246
277
  return false;
247
- },
278
+ }
248
279
  }));
249
280
  app.get("/test", (c) => {
250
281
  const user = c.get("user");
@@ -253,23 +284,24 @@ describe("Auth Middleware (Hono)", () => {
253
284
 
254
285
  // Valid API key
255
286
  const res = await app.request("/test", {
256
- headers: { "x-api-key": "valid-key" },
287
+ headers: { "x-api-key": "valid-key" }
257
288
  });
258
289
  const body = await res.json() as any;
259
290
  expect(body.user.userId).toBe("api-user");
260
291
 
261
- // Invalid API key
292
+ // Invalid API key — requireAuth not set, defaults to true,
293
+ // so unauthenticated requests are rejected
262
294
  const res2 = await app.request("/test", {
263
- headers: { "x-api-key": "bad-key" },
295
+ headers: { "x-api-key": "bad-key" }
264
296
  });
265
- expect(res2.status).toBe(200); // Not enforcing auth
297
+ expect(res2.status).toBe(401); // Secure by default
266
298
  });
267
299
 
268
300
  it("returns 401 when custom validator throws", async () => {
269
301
  const app = new Hono<HonoEnv>();
270
302
  app.use("/*", createAuthMiddleware({
271
303
  driver: mockDriver,
272
- validator: async () => { throw new Error("auth failed"); },
304
+ validator: async () => { throw new Error("auth failed"); }
273
305
  }));
274
306
  app.get("/test", (c) => c.json({ ok: true }));
275
307
 
@@ -278,10 +310,11 @@ describe("Auth Middleware (Hono)", () => {
278
310
  });
279
311
 
280
312
  it("calls withAuth on driver when available", async () => {
281
- const scopedDriver = { ...mockDriver, isScopedDriver: true };
313
+ const scopedDriver = { ...mockDriver,
314
+ isScopedDriver: true };
282
315
  const driverWithAuth = {
283
316
  ...mockDriver,
284
- withAuth: jest.fn().mockResolvedValue(scopedDriver),
317
+ withAuth: jest.fn().mockResolvedValue(scopedDriver)
285
318
  };
286
319
 
287
320
  const app = new Hono<HonoEnv>();
@@ -293,12 +326,13 @@ describe("Auth Middleware (Hono)", () => {
293
326
 
294
327
  const token = generateAccessToken("rls-user", ["editor"]);
295
328
  const res = await app.request("/test", {
296
- headers: { Authorization: `Bearer ${token}` },
329
+ headers: { Authorization: `Bearer ${token}` }
297
330
  });
298
331
  const body = await res.json() as any;
299
332
  expect(body.scoped).toBe(true);
300
333
  expect(driverWithAuth.withAuth).toHaveBeenCalledWith(
301
- expect.objectContaining({ uid: "rls-user", roles: ["editor"] })
334
+ expect.objectContaining({ uid: "rls-user",
335
+ roles: ["editor"] })
302
336
  );
303
337
  });
304
338
 
@@ -306,7 +340,8 @@ describe("Auth Middleware (Hono)", () => {
306
340
  const app = new Hono<HonoEnv>();
307
341
  app.use("/*", createAuthMiddleware({
308
342
  driver: mockDriver,
309
- validator: async () => true,
343
+ requireAuth: false, // Explicit opt-out for this test
344
+ validator: async () => true
310
345
  }));
311
346
  app.get("/test", (c) => {
312
347
  const user = c.get("user");
@@ -318,4 +353,204 @@ describe("Auth Middleware (Hono)", () => {
318
353
  expect(body.user.userId).toBe("default");
319
354
  });
320
355
  });
356
+
357
+ describe("Service Key Authentication", () => {
358
+ const SERVICE_KEY = "a-very-secure-service-key-that-is-at-least-32-characters-long";
359
+ const mockDriver: DataDriver = {
360
+ fetchCollection: jest.fn() as any,
361
+ fetchEntity: jest.fn() as any,
362
+ saveEntity: jest.fn() as any,
363
+ deleteEntity: jest.fn() as any
364
+ };
365
+
366
+ it("grants admin access when Bearer token matches the service key", async () => {
367
+ const app = new Hono<HonoEnv>();
368
+ app.use("/*", createAuthMiddleware({
369
+ driver: mockDriver,
370
+ requireAuth: true,
371
+ serviceKey: SERVICE_KEY
372
+ }));
373
+ app.get("/test", (c) => {
374
+ const user = c.get("user");
375
+ return c.json({ user });
376
+ });
377
+
378
+ const res = await app.request("/test", {
379
+ headers: { Authorization: `Bearer ${SERVICE_KEY}` }
380
+ });
381
+ expect(res.status).toBe(200);
382
+ const body = await res.json() as any;
383
+ expect(body.user.userId).toBe("service");
384
+ expect(body.user.roles).toEqual(["admin"]);
385
+ });
386
+
387
+ it("rejects when Bearer token does not match the service key", async () => {
388
+ const app = new Hono<HonoEnv>();
389
+ app.use("/*", createAuthMiddleware({
390
+ driver: mockDriver,
391
+ requireAuth: true,
392
+ serviceKey: SERVICE_KEY
393
+ }));
394
+ app.get("/test", (c) => c.json({ ok: true }));
395
+
396
+ const res = await app.request("/test", {
397
+ headers: { Authorization: "Bearer wrong-key-that-is-definitely-not-correct" }
398
+ });
399
+ // Wrong key is not a valid service key, and also not a valid JWT → 401
400
+ expect(res.status).toBe(401);
401
+ });
402
+
403
+ it("still accepts valid JWT tokens when service key is configured", async () => {
404
+ const token = generateAccessToken("jwt-user", ["editor"]);
405
+ const app = new Hono<HonoEnv>();
406
+ app.use("/*", createAuthMiddleware({
407
+ driver: mockDriver,
408
+ requireAuth: true,
409
+ serviceKey: SERVICE_KEY
410
+ }));
411
+ app.get("/test", (c) => {
412
+ const user = c.get("user");
413
+ return c.json({ user });
414
+ });
415
+
416
+ const res = await app.request("/test", {
417
+ headers: { Authorization: `Bearer ${token}` }
418
+ });
419
+ expect(res.status).toBe(200);
420
+ const body = await res.json() as any;
421
+ expect(body.user.userId).toBe("jwt-user");
422
+ expect(body.user.roles).toEqual(["editor"]);
423
+ });
424
+
425
+ it("scopes driver via withAuth with service identity", async () => {
426
+ const scopedDriver = { scoped: true } as unknown as DataDriver;
427
+ const driverWithAuth = {
428
+ withAuth: jest.fn().mockResolvedValue(scopedDriver)
429
+ } as unknown as DataDriver;
430
+
431
+ const app = new Hono<HonoEnv>();
432
+ app.use("/*", createAuthMiddleware({
433
+ driver: driverWithAuth,
434
+ requireAuth: true,
435
+ serviceKey: SERVICE_KEY
436
+ }));
437
+ app.get("/test", (c) => {
438
+ const d = c.get("driver") as any;
439
+ return c.json({ scoped: d.scoped });
440
+ });
441
+
442
+ const res = await app.request("/test", {
443
+ headers: { Authorization: `Bearer ${SERVICE_KEY}` }
444
+ });
445
+ const body = await res.json() as any;
446
+ expect(body.scoped).toBe(true);
447
+ expect((driverWithAuth as any).withAuth).toHaveBeenCalledWith(
448
+ expect.objectContaining({ uid: "service",
449
+ roles: ["admin"] })
450
+ );
451
+ });
452
+
453
+ it("does not activate service key path when no serviceKey is configured", async () => {
454
+ const app = new Hono<HonoEnv>();
455
+ app.use("/*", createAuthMiddleware({
456
+ driver: mockDriver,
457
+ requireAuth: true
458
+ // No serviceKey configured
459
+ }));
460
+ app.get("/test", (c) => c.json({ ok: true }));
461
+
462
+ // Sending a random string should fail as invalid JWT
463
+ const res = await app.request("/test", {
464
+ headers: { Authorization: `Bearer ${SERVICE_KEY}` }
465
+ });
466
+ expect(res.status).toBe(401);
467
+ });
468
+ });
469
+
470
+ // ── createRequireAuth (standalone factory) ──────────────
471
+ describe("createRequireAuth", () => {
472
+ const SERVICE_KEY = "a-very-secure-service-key-that-is-at-least-32-characters-long";
473
+ it("returns the default requireAuth when no serviceKey is provided", async () => {
474
+ const middleware = createRequireAuth();
475
+ // The returned function should be the original requireAuth
476
+ expect(middleware).toBe(requireAuth);
477
+ });
478
+
479
+ it("returns the default requireAuth when serviceKey is undefined", async () => {
480
+ const middleware = createRequireAuth({ serviceKey: undefined });
481
+ expect(middleware).toBe(requireAuth);
482
+ });
483
+
484
+ it("authenticates with a valid service key", async () => {
485
+ const app = new Hono<HonoEnv>();
486
+ app.use("/*", createRequireAuth({ serviceKey: SERVICE_KEY }));
487
+ app.get("/admin/users", (c) => {
488
+ const user = c.get("user");
489
+ return c.json({ userId: user?.userId,
490
+ roles: user?.roles });
491
+ });
492
+
493
+ const res = await app.request("/admin/users", {
494
+ headers: { Authorization: `Bearer ${SERVICE_KEY}` }
495
+ });
496
+ expect(res.status).toBe(200);
497
+ const body = await res.json() as any;
498
+ expect(body.userId).toBe("service");
499
+ expect(body.roles).toEqual(["admin"]);
500
+ });
501
+
502
+ it("rejects an incorrect service key and falls back to JWT (which also fails)", async () => {
503
+ const app = new Hono<HonoEnv>();
504
+ app.use("/*", createRequireAuth({ serviceKey: SERVICE_KEY }));
505
+ app.get("/admin/users", (c) => c.json({ ok: true }));
506
+
507
+ const res = await app.request("/admin/users", {
508
+ headers: { Authorization: "Bearer wrong-key" }
509
+ });
510
+ expect(res.status).toBe(401);
511
+ });
512
+
513
+ it("falls back to valid JWT when service key doesn't match", async () => {
514
+ const token = generateAccessToken("user-123", ["admin"]);
515
+ const app = new Hono<HonoEnv>();
516
+ app.use("/*", createRequireAuth({ serviceKey: SERVICE_KEY }));
517
+ app.get("/admin/users", (c) => {
518
+ const user = c.get("user");
519
+ return c.json({ userId: user?.userId,
520
+ roles: user?.roles });
521
+ });
522
+
523
+ const res = await app.request("/admin/users", {
524
+ headers: { Authorization: `Bearer ${token}` }
525
+ });
526
+ expect(res.status).toBe(200);
527
+ const body = await res.json() as any;
528
+ expect(body.userId).toBe("user-123");
529
+ expect(body.roles).toEqual(["admin"]);
530
+ });
531
+
532
+ it("returns 401 when no authorization header is provided", async () => {
533
+ const app = new Hono<HonoEnv>();
534
+ app.use("/*", createRequireAuth({ serviceKey: SERVICE_KEY }));
535
+ app.get("/admin/users", (c) => c.json({ ok: true }));
536
+
537
+ const res = await app.request("/admin/users");
538
+ expect(res.status).toBe(401);
539
+ });
540
+
541
+ it("works with requireAdmin for service key (admin role is granted)", async () => {
542
+ const app = new Hono<HonoEnv>();
543
+ app.use("/*", createRequireAuth({ serviceKey: SERVICE_KEY }));
544
+ app.get("/admin/users", requireAdmin, (c) => {
545
+ return c.json({ authorized: true });
546
+ });
547
+
548
+ const res = await app.request("/admin/users", {
549
+ headers: { Authorization: `Bearer ${SERVICE_KEY}` }
550
+ });
551
+ expect(res.status).toBe(200);
552
+ const body = await res.json() as any;
553
+ expect(body.authorized).toBe(true);
554
+ });
555
+ });
321
556
  });