@rebasepro/server-core 0.6.1 → 0.8.0

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 (116) hide show
  1. package/dist/api/graphql/graphql-schema-generator.d.ts +9 -0
  2. package/dist/api/rest/api-generator.d.ts +2 -14
  3. package/dist/api/rest/query-parser.d.ts +2 -5
  4. package/dist/api/types.d.ts +8 -2
  5. package/dist/auth/adapter-middleware.d.ts +7 -1
  6. package/dist/auth/admin-roles-route.d.ts +18 -0
  7. package/dist/auth/admin-users-route.d.ts +28 -0
  8. package/dist/auth/api-keys/api-key-types.d.ts +8 -0
  9. package/dist/auth/auth-hooks.d.ts +17 -0
  10. package/dist/auth/builtin-auth-adapter.d.ts +3 -1
  11. package/dist/auth/index.d.ts +1 -0
  12. package/dist/auth/interfaces.d.ts +32 -2
  13. package/dist/auth/magic-link-routes.d.ts +30 -0
  14. package/dist/auth/mfa-crypto.d.ts +23 -0
  15. package/dist/auth/mfa-routes.d.ts +2 -1
  16. package/dist/auth/middleware.d.ts +9 -2
  17. package/dist/auth/routes.d.ts +3 -1
  18. package/dist/auth/session-routes.d.ts +2 -0
  19. package/dist/backend-CIxN4FVm.js.map +1 -1
  20. package/dist/base64-js-C_frYBkI.js +1687 -0
  21. package/dist/base64-js-C_frYBkI.js.map +1 -0
  22. package/dist/{dist-CZKP-Xz4.js → dist-DMO-zF6D.js} +2 -2
  23. package/dist/{dist-CZKP-Xz4.js.map → dist-DMO-zF6D.js.map} +1 -1
  24. package/dist/email/index.d.ts +2 -2
  25. package/dist/email/templates.d.ts +8 -0
  26. package/dist/email/types.d.ts +18 -0
  27. package/dist/functions/define-function.d.ts +50 -0
  28. package/dist/functions/index.d.ts +2 -0
  29. package/dist/index.d.ts +16 -18
  30. package/dist/index.es.js +1947 -2356
  31. package/dist/index.es.js.map +1 -1
  32. package/dist/index.umd.js +40615 -10203
  33. package/dist/index.umd.js.map +1 -1
  34. package/dist/init.d.ts +68 -12
  35. package/dist/jws-BqRRaK11.js +616 -0
  36. package/dist/jws-BqRRaK11.js.map +1 -0
  37. package/dist/{jwt-DHcQRGC3.js → jwt-BETC8a1J.js} +3 -611
  38. package/dist/jwt-BETC8a1J.js.map +1 -0
  39. package/dist/services/routed-realtime-service.d.ts +43 -0
  40. package/dist/{src-COaj0G3P.js → src-CB2PIpBe.js} +2 -2
  41. package/dist/{src-COaj0G3P.js.map → src-CB2PIpBe.js.map} +1 -1
  42. package/dist/src-DjzOT1kG.js +29746 -0
  43. package/dist/src-DjzOT1kG.js.map +1 -0
  44. package/dist/storage/GCSStorageController.d.ts +43 -0
  45. package/dist/storage/index.d.ts +5 -2
  46. package/dist/storage/routes.d.ts +33 -1
  47. package/dist/storage/tus-handler.d.ts +3 -1
  48. package/dist/storage/types.d.ts +28 -3
  49. package/dist/utils/dev-port.d.ts +2 -0
  50. package/package.json +13 -5
  51. package/src/api/errors.ts +16 -2
  52. package/src/api/graphql/graphql-schema-generator.ts +50 -15
  53. package/src/api/openapi-generator.ts +2 -2
  54. package/src/api/rest/api-generator.ts +44 -123
  55. package/src/api/rest/query-parser.ts +6 -23
  56. package/src/api/server.ts +10 -2
  57. package/src/api/types.ts +8 -2
  58. package/src/auth/adapter-middleware.ts +10 -2
  59. package/src/auth/admin-roles-route.ts +36 -0
  60. package/src/auth/admin-users-route.ts +302 -0
  61. package/src/auth/api-keys/api-key-middleware.ts +4 -3
  62. package/src/auth/api-keys/api-key-routes.ts +12 -2
  63. package/src/auth/api-keys/api-key-store.ts +83 -66
  64. package/src/auth/api-keys/api-key-types.ts +8 -0
  65. package/src/auth/apple-oauth.ts +2 -1
  66. package/src/auth/auth-hooks.ts +21 -0
  67. package/src/auth/bitbucket-oauth.ts +2 -1
  68. package/src/auth/builtin-auth-adapter.ts +29 -6
  69. package/src/auth/custom-auth-adapter.ts +2 -0
  70. package/src/auth/discord-oauth.ts +2 -1
  71. package/src/auth/facebook-oauth.ts +2 -1
  72. package/src/auth/github-oauth.ts +2 -1
  73. package/src/auth/gitlab-oauth.ts +2 -1
  74. package/src/auth/google-oauth.ts +8 -4
  75. package/src/auth/index.ts +2 -0
  76. package/src/auth/interfaces.ts +38 -2
  77. package/src/auth/linkedin-oauth.ts +2 -1
  78. package/src/auth/magic-link-routes.ts +167 -0
  79. package/src/auth/mfa-crypto.ts +91 -0
  80. package/src/auth/mfa-routes.ts +34 -10
  81. package/src/auth/microsoft-oauth.ts +2 -1
  82. package/src/auth/middleware.ts +14 -5
  83. package/src/auth/reset-password-admin.ts +17 -1
  84. package/src/auth/routes.ts +78 -10
  85. package/src/auth/session-routes.ts +15 -3
  86. package/src/auth/slack-oauth.ts +2 -1
  87. package/src/auth/spotify-oauth.ts +2 -1
  88. package/src/auth/twitter-oauth.ts +8 -1
  89. package/src/cron/cron-store.ts +25 -23
  90. package/src/email/index.ts +3 -2
  91. package/src/email/templates.ts +82 -0
  92. package/src/email/types.ts +16 -0
  93. package/src/functions/define-function.ts +59 -0
  94. package/src/functions/function-loader.ts +16 -0
  95. package/src/functions/index.ts +2 -0
  96. package/src/index.ts +70 -37
  97. package/src/init.ts +214 -25
  98. package/src/services/routed-realtime-service.ts +113 -0
  99. package/src/storage/GCSStorageController.ts +334 -0
  100. package/src/storage/index.ts +9 -3
  101. package/src/storage/routes.ts +191 -23
  102. package/src/storage/tus-handler.ts +16 -4
  103. package/src/storage/types.ts +25 -3
  104. package/src/utils/dev-port.ts +13 -7
  105. package/test/api-generator.test.ts +1 -1
  106. package/test/auth-config-types.test.ts +40 -0
  107. package/test/auth-routes.test.ts +54 -4
  108. package/test/custom-auth-adapter.test.ts +20 -1
  109. package/test/define-function.test.ts +45 -0
  110. package/test/env.test.ts +9 -19
  111. package/test/multi-datasource-routing.test.ts +113 -0
  112. package/test/routed-realtime-service.test.ts +86 -0
  113. package/test/storage-routes.test.ts +160 -0
  114. package/test/transform-auth-response.test.ts +305 -0
  115. package/dist/jwt-DHcQRGC3.js.map +0 -1
  116. package/test/backend-hooks-data.test.ts +0 -477
@@ -0,0 +1,305 @@
1
+ /**
2
+ * transformAuthResponse Hook — Integration Tests
3
+ *
4
+ * Tests the `transformAuthResponse` hook across login, register, refresh,
5
+ * and error handling scenarios. Services are mocked following the same
6
+ * pattern as auth-routes.test.ts.
7
+ */
8
+
9
+ import { Hono } from "hono";
10
+ import type { HonoEnv } from "../src/api/types";
11
+ import { errorHandler } from "../src/api/errors";
12
+ import { createAuthRoutes, AuthModuleConfig } from "../src/auth/routes";
13
+ import type { AuthRepository } from "../src/auth/interfaces";
14
+ import { configureJwt, hashRefreshToken } from "../src/auth/jwt";
15
+ import type { AuthHooks } from "../src/auth/auth-hooks";
16
+ import type { AuthResponsePayload, TransformAuthResponseContext } from "@rebasepro/types";
17
+
18
+ // ── Mocks ───────────────────────────────────────────────────────────────────
19
+
20
+ jest.mock("../src/auth/password");
21
+
22
+ jest.mock("../src/utils/logger", () => {
23
+ return {
24
+ logger: {
25
+ info: jest.fn(),
26
+ warn: jest.fn(),
27
+ error: jest.fn(),
28
+ debug: jest.fn(),
29
+ child: jest.fn().mockReturnThis()
30
+ }
31
+ };
32
+ });
33
+
34
+ // Bypass rate limiters — they share state across tests and cause 429s
35
+ jest.mock("../src/auth/rate-limiter", () => {
36
+ const passthrough = async (_c: unknown, next: () => Promise<void>) => next();
37
+ return {
38
+ createRateLimiter: () => passthrough,
39
+ defaultAuthLimiter: passthrough,
40
+ strictAuthLimiter: passthrough
41
+ };
42
+ });
43
+
44
+ import { validatePasswordStrength, hashPassword, verifyPassword } from "../src/auth/password";
45
+ import { logger } from "../src/utils/logger";
46
+
47
+ // ── Helpers ─────────────────────────────────────────────────────────────────
48
+
49
+ const TEST_SECRET = "integration-test-secret-key-that-is-definitely-32-chars-long!!";
50
+
51
+ function mockUser(overrides: Partial<{ id: string; email: string; passwordHash: string | null; displayName: string | null; photoUrl: string | null; provider: string; emailVerified: boolean; emailVerificationToken: string | null }> = {}) {
52
+ return {
53
+ id: overrides.id ?? "user-1",
54
+ email: overrides.email ?? "test@example.com",
55
+ passwordHash: "passwordHash" in overrides ? overrides.passwordHash : "salt:hash",
56
+ displayName: overrides.displayName ?? "Test User",
57
+ photoUrl: overrides.photoUrl ?? null,
58
+ emailVerified: overrides.emailVerified ?? false,
59
+ emailVerificationToken: overrides.emailVerificationToken ?? null,
60
+ emailVerificationSentAt: null,
61
+ createdAt: new Date(),
62
+ updatedAt: new Date()
63
+ };
64
+ }
65
+
66
+ function mockRole(id: string, isAdmin = false) {
67
+ return { id,
68
+ name: id.charAt(0).toUpperCase() + id.slice(1),
69
+ isAdmin,
70
+ defaultPermissions: null,
71
+ collectionPermissions: null };
72
+ }
73
+
74
+ let mockAuthRepo: jest.Mocked<AuthRepository>;
75
+
76
+ function createApp(opts: { authHooks?: AuthHooks } = {}) {
77
+ mockAuthRepo = {
78
+ getUserByEmail: jest.fn().mockResolvedValue(null),
79
+ getUserByIdentity: jest.fn().mockResolvedValue(null),
80
+ linkUserIdentity: jest.fn().mockResolvedValue(undefined),
81
+ getUserIdentities: jest.fn().mockResolvedValue([]),
82
+ getUserById: jest.fn().mockResolvedValue(null),
83
+ createUser: jest.fn().mockImplementation((data) =>
84
+ Promise.resolve(mockUser({ email: data.email,
85
+ displayName: data.displayName,
86
+ passwordHash: data.passwordHash }))
87
+ ),
88
+ listUsers: jest.fn().mockResolvedValue([]),
89
+ getUserRoles: jest.fn().mockResolvedValue([mockRole("editor")]),
90
+ getUserRoleIds: jest.fn().mockResolvedValue(["editor"]),
91
+ assignDefaultRole: jest.fn().mockResolvedValue(undefined),
92
+ setUserRoles: jest.fn().mockResolvedValue(undefined),
93
+ updateUser: jest.fn().mockImplementation((id, data) =>
94
+ Promise.resolve(mockUser({ id,
95
+ ...data }))
96
+ ),
97
+ deleteUser: jest.fn().mockResolvedValue(undefined),
98
+ updatePassword: jest.fn().mockResolvedValue(undefined),
99
+ setEmailVerified: jest.fn().mockResolvedValue(undefined),
100
+ setVerificationToken: jest.fn().mockResolvedValue(undefined),
101
+ getUserByVerificationToken: jest.fn().mockResolvedValue(null),
102
+ getUserWithRoles: jest.fn().mockImplementation(async (userId) => {
103
+ const user = mockUser({ id: userId });
104
+ return { user,
105
+ roles: [mockRole("editor")] };
106
+ }),
107
+ createRefreshToken: jest.fn().mockResolvedValue(undefined),
108
+ findRefreshTokenByHash: jest.fn().mockResolvedValue(null),
109
+ deleteRefreshToken: jest.fn().mockResolvedValue(undefined),
110
+ deleteAllRefreshTokensForUser: jest.fn().mockResolvedValue(undefined),
111
+ listRefreshTokensForUser: jest.fn().mockResolvedValue([]),
112
+ deleteRefreshTokenById: jest.fn().mockResolvedValue(undefined),
113
+ createPasswordResetToken: jest.fn().mockResolvedValue(undefined),
114
+ findValidPasswordResetToken: jest.fn().mockResolvedValue(null),
115
+ markPasswordResetTokenUsed: jest.fn().mockResolvedValue(undefined),
116
+ deleteExpiredPasswordResetTokens: jest.fn().mockResolvedValue(undefined)
117
+ } as unknown as jest.Mocked<AuthRepository>;
118
+
119
+ // Password mocks
120
+ (validatePasswordStrength as jest.Mock).mockReturnValue({ valid: true,
121
+ errors: [] });
122
+ (hashPassword as jest.Mock).mockResolvedValue("hashed-pw");
123
+ (verifyPassword as jest.Mock).mockResolvedValue(true);
124
+
125
+ const config: AuthModuleConfig = {
126
+ authRepo: mockAuthRepo,
127
+ allowRegistration: true,
128
+ authHooks: opts.authHooks
129
+ };
130
+
131
+ const app = new Hono<HonoEnv>();
132
+ app.onError(errorHandler);
133
+ app.route("/auth", createAuthRoutes(config));
134
+ return app;
135
+ }
136
+
137
+ function json(body: Record<string, unknown>) {
138
+ return {
139
+ method: "POST" as const,
140
+ headers: { "Content-Type": "application/json" },
141
+ body: JSON.stringify(body)
142
+ };
143
+ }
144
+
145
+ // ═══════════════════════════════════════════════════════════════════════════
146
+ // TESTS
147
+ // ═══════════════════════════════════════════════════════════════════════════
148
+
149
+ describe("transformAuthResponse hook", () => {
150
+ beforeAll(() => {
151
+ configureJwt({ secret: TEST_SECRET,
152
+ accessExpiresIn: "1h" });
153
+ });
154
+
155
+ beforeEach(() => {
156
+ jest.clearAllMocks();
157
+ });
158
+
159
+ // ── Registration ────────────────────────────────────────────────────
160
+
161
+ describe("POST /auth/register", () => {
162
+ it("calls transformAuthResponse with the response and 'register' method", async () => {
163
+ const transformHook = jest.fn(async (response: AuthResponsePayload) => ({
164
+ ...response,
165
+ tokens: { ...response.tokens, firebaseToken: "custom-firebase-token" }
166
+ }));
167
+ const app = createApp({ authHooks: { transformAuthResponse: transformHook } });
168
+
169
+ const res = await app.request("/auth/register", json({ email: "new@test.com", password: "StrongPass1" }));
170
+ expect(res.status).toBe(201);
171
+ const body = await res.json() as Record<string, unknown>;
172
+ const tokens = body.tokens as Record<string, unknown>;
173
+
174
+ // Verify the hook was called
175
+ expect(transformHook).toHaveBeenCalledTimes(1);
176
+
177
+ // Verify context
178
+ const context = transformHook.mock.calls[0][1] as TransformAuthResponseContext;
179
+ expect(context.method).toBe("register");
180
+ expect(context.userId).toBeTruthy();
181
+ expect(context.request).toBeInstanceOf(Request);
182
+
183
+ // Verify the custom field was injected
184
+ expect(tokens.firebaseToken).toBe("custom-firebase-token");
185
+ // Original fields still present
186
+ expect(tokens.accessToken).toBeTruthy();
187
+ expect(tokens.refreshToken).toBeTruthy();
188
+ });
189
+ });
190
+
191
+ // ── Login ────────────────────────────────────────────────────────────
192
+
193
+ describe("POST /auth/login", () => {
194
+ it("calls transformAuthResponse with 'login' method", async () => {
195
+ const transformHook = jest.fn(async (response: AuthResponsePayload) => ({
196
+ ...response,
197
+ tokens: { ...response.tokens, externalToken: "ext-123" }
198
+ }));
199
+ const app = createApp({ authHooks: { transformAuthResponse: transformHook } });
200
+
201
+ // Setup: user exists
202
+ mockAuthRepo.getUserByEmail.mockResolvedValueOnce(mockUser());
203
+
204
+ const res = await app.request("/auth/login", json({ email: "test@example.com", password: "StrongPass1" }));
205
+ expect(res.status).toBe(200);
206
+ const body = await res.json() as Record<string, unknown>;
207
+ const tokens = body.tokens as Record<string, unknown>;
208
+
209
+ expect(transformHook).toHaveBeenCalledTimes(1);
210
+ const context = transformHook.mock.calls[0][1] as TransformAuthResponseContext;
211
+ expect(context.method).toBe("login");
212
+ expect(tokens.externalToken).toBe("ext-123");
213
+ });
214
+ });
215
+
216
+ // ── Refresh ──────────────────────────────────────────────────────────
217
+
218
+ describe("POST /auth/refresh", () => {
219
+ it("calls transformAuthResponse with 'refresh' method (tokens-only response)", async () => {
220
+ const transformHook = jest.fn(async (response: AuthResponsePayload) => ({
221
+ ...response,
222
+ tokens: { ...response.tokens, refreshedExternalToken: "refreshed-ext" }
223
+ }));
224
+ const app = createApp({ authHooks: { transformAuthResponse: transformHook } });
225
+
226
+ // Setup: valid refresh token exists
227
+ const rawRefreshToken = "a".repeat(80);
228
+ const hashedToken = hashRefreshToken(rawRefreshToken);
229
+ mockAuthRepo.findRefreshTokenByHash.mockResolvedValueOnce({
230
+ id: "rt-1",
231
+ userId: "user-1",
232
+ tokenHash: hashedToken,
233
+ expiresAt: new Date(Date.now() + 86400000), // 1 day from now
234
+ createdAt: new Date(),
235
+ userAgent: "test",
236
+ ipAddress: "127.0.0.1"
237
+ });
238
+ mockAuthRepo.getUserRoleIds.mockResolvedValueOnce(["editor"]);
239
+
240
+ const res = await app.request("/auth/refresh", json({ refreshToken: rawRefreshToken }));
241
+ expect(res.status).toBe(200);
242
+ const body = await res.json() as Record<string, unknown>;
243
+ const tokens = body.tokens as Record<string, unknown>;
244
+
245
+ expect(transformHook).toHaveBeenCalledTimes(1);
246
+
247
+ // Verify refresh context
248
+ const hookResponse = transformHook.mock.calls[0][0] as AuthResponsePayload;
249
+ const context = transformHook.mock.calls[0][1] as TransformAuthResponseContext;
250
+ expect(context.method).toBe("refresh");
251
+ expect(hookResponse.user).toBeUndefined(); // refresh has no user
252
+ expect(tokens.refreshedExternalToken).toBe("refreshed-ext");
253
+ });
254
+ });
255
+
256
+ // ── Error handling ───────────────────────────────────────────────────
257
+
258
+ describe("error handling", () => {
259
+ it("returns untransformed response when hook throws", async () => {
260
+ const transformHook = jest.fn(async () => {
261
+ throw new Error("External service down");
262
+ });
263
+ const app = createApp({ authHooks: { transformAuthResponse: transformHook } });
264
+
265
+ const res = await app.request("/auth/register", json({ email: "new@test.com", password: "StrongPass1" }));
266
+ expect(res.status).toBe(201);
267
+ const body = await res.json() as Record<string, unknown>;
268
+ const tokens = body.tokens as Record<string, unknown>;
269
+ const user = body.user as Record<string, unknown>;
270
+
271
+ // Response is still valid (untransformed fallback)
272
+ expect(tokens.accessToken).toBeTruthy();
273
+ expect(tokens.refreshToken).toBeTruthy();
274
+ expect(user.email).toBe("new@test.com");
275
+
276
+ // Error was logged
277
+ expect(logger.error).toHaveBeenCalledWith(
278
+ "[AuthHooks] transformAuthResponse error",
279
+ expect.objectContaining({ error: "External service down" })
280
+ );
281
+ });
282
+ });
283
+
284
+ // ── Opt-in behavior ──────────────────────────────────────────────────
285
+
286
+ describe("opt-in behavior", () => {
287
+ it("does not alter response when hook is not provided", async () => {
288
+ const app = createApp(); // no hooks
289
+
290
+ const res = await app.request("/auth/register", json({ email: "new@test.com", password: "StrongPass1" }));
291
+ expect(res.status).toBe(201);
292
+ const body = await res.json() as Record<string, unknown>;
293
+ const tokens = body.tokens as Record<string, unknown>;
294
+ const user = body.user as Record<string, unknown>;
295
+
296
+ // Standard response, no extra fields
297
+ expect(tokens.accessToken).toBeTruthy();
298
+ expect(tokens.refreshToken).toBeTruthy();
299
+ expect(user.email).toBe("new@test.com");
300
+ expect(Object.keys(tokens as object)).toEqual(
301
+ expect.arrayContaining(["accessToken", "refreshToken", "accessTokenExpiresAt"])
302
+ );
303
+ });
304
+ });
305
+ });