@rebasepro/server-core 0.6.1 → 0.7.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.
- package/package.json +14 -14
- package/src/api/errors.ts +16 -2
- package/src/api/graphql/graphql-schema-generator.ts +45 -10
- package/src/api/server.ts +10 -2
- package/src/api/types.ts +6 -0
- package/src/auth/adapter-middleware.ts +10 -2
- package/src/auth/admin-roles-route.ts +36 -0
- package/src/auth/admin-users-route.ts +302 -0
- package/src/auth/api-keys/api-key-middleware.ts +4 -3
- package/src/auth/api-keys/api-key-routes.ts +12 -2
- package/src/auth/api-keys/api-key-store.ts +83 -66
- package/src/auth/api-keys/api-key-types.ts +8 -0
- package/src/auth/apple-oauth.ts +2 -1
- package/src/auth/auth-hooks.ts +21 -0
- package/src/auth/bitbucket-oauth.ts +2 -1
- package/src/auth/builtin-auth-adapter.ts +28 -4
- package/src/auth/custom-auth-adapter.ts +2 -0
- package/src/auth/discord-oauth.ts +2 -1
- package/src/auth/facebook-oauth.ts +2 -1
- package/src/auth/github-oauth.ts +2 -1
- package/src/auth/gitlab-oauth.ts +2 -1
- package/src/auth/google-oauth.ts +8 -4
- package/src/auth/index.ts +2 -0
- package/src/auth/interfaces.ts +27 -0
- package/src/auth/linkedin-oauth.ts +2 -1
- package/src/auth/magic-link-routes.ts +167 -0
- package/src/auth/mfa-crypto.ts +91 -0
- package/src/auth/mfa-routes.ts +34 -10
- package/src/auth/microsoft-oauth.ts +2 -1
- package/src/auth/middleware.ts +10 -1
- package/src/auth/reset-password-admin.ts +17 -1
- package/src/auth/routes.ts +77 -8
- package/src/auth/session-routes.ts +15 -3
- package/src/auth/slack-oauth.ts +2 -1
- package/src/auth/spotify-oauth.ts +2 -1
- package/src/auth/twitter-oauth.ts +8 -1
- package/src/cron/cron-store.ts +25 -23
- package/src/email/index.ts +3 -2
- package/src/email/templates.ts +82 -0
- package/src/email/types.ts +16 -0
- package/src/init.ts +137 -5
- package/src/services/routed-realtime-service.ts +113 -0
- package/src/utils/dev-port.ts +13 -7
- package/test/auth-routes.test.ts +54 -4
- package/test/custom-auth-adapter.test.ts +20 -1
- package/test/env.test.ts +9 -19
- package/test/multi-datasource-routing.test.ts +113 -0
- package/test/routed-realtime-service.test.ts +86 -0
- package/test/transform-auth-response.test.ts +305 -0
- package/build-errors.txt +0 -52
- package/dist/api/ast-schema-editor.d.ts +0 -21
- package/dist/api/collections_for_test/callbacks_test_collection.d.ts +0 -2
- package/dist/api/errors.d.ts +0 -53
- package/dist/api/graphql/graphql-schema-generator.d.ts +0 -35
- package/dist/api/graphql/index.d.ts +0 -1
- package/dist/api/index.d.ts +0 -9
- package/dist/api/logs-routes.d.ts +0 -37
- package/dist/api/openapi-generator.d.ts +0 -16
- package/dist/api/rest/api-generator.d.ts +0 -88
- package/dist/api/rest/index.d.ts +0 -1
- package/dist/api/rest/query-parser.d.ts +0 -11
- package/dist/api/schema-editor-routes.d.ts +0 -3
- package/dist/api/server.d.ts +0 -40
- package/dist/api/types.d.ts +0 -98
- package/dist/auth/adapter-middleware.d.ts +0 -37
- package/dist/auth/admin-user-ops.d.ts +0 -79
- package/dist/auth/api-keys/api-key-middleware.d.ts +0 -39
- package/dist/auth/api-keys/api-key-permission-guard.d.ts +0 -32
- package/dist/auth/api-keys/api-key-routes.d.ts +0 -20
- package/dist/auth/api-keys/api-key-store.d.ts +0 -35
- package/dist/auth/api-keys/api-key-types.d.ts +0 -88
- package/dist/auth/api-keys/index.d.ts +0 -17
- package/dist/auth/apple-oauth.d.ts +0 -30
- package/dist/auth/auth-hooks.d.ts +0 -225
- package/dist/auth/bitbucket-oauth.d.ts +0 -11
- package/dist/auth/builtin-auth-adapter.d.ts +0 -49
- package/dist/auth/crypto-utils.d.ts +0 -16
- package/dist/auth/custom-auth-adapter.d.ts +0 -39
- package/dist/auth/discord-oauth.d.ts +0 -14
- package/dist/auth/facebook-oauth.d.ts +0 -14
- package/dist/auth/github-oauth.d.ts +0 -15
- package/dist/auth/gitlab-oauth.d.ts +0 -13
- package/dist/auth/google-oauth.d.ts +0 -47
- package/dist/auth/index.d.ts +0 -36
- package/dist/auth/interfaces.d.ts +0 -401
- package/dist/auth/jwt.d.ts +0 -55
- package/dist/auth/linkedin-oauth.d.ts +0 -18
- package/dist/auth/mfa-routes.d.ts +0 -6
- package/dist/auth/mfa.d.ts +0 -49
- package/dist/auth/microsoft-oauth.d.ts +0 -16
- package/dist/auth/middleware.d.ts +0 -106
- package/dist/auth/password.d.ts +0 -22
- package/dist/auth/rate-limiter.d.ts +0 -50
- package/dist/auth/reset-password-admin.d.ts +0 -29
- package/dist/auth/rls-scope.d.ts +0 -31
- package/dist/auth/routes.d.ts +0 -33
- package/dist/auth/session-routes.d.ts +0 -25
- package/dist/auth/slack-oauth.d.ts +0 -12
- package/dist/auth/spotify-oauth.d.ts +0 -12
- package/dist/auth/twitter-oauth.d.ts +0 -18
- package/dist/backend-CIxN4FVm.js +0 -15
- package/dist/backend-CIxN4FVm.js.map +0 -1
- package/dist/chunk-Dze3rakg.js +0 -42
- package/dist/collections/BackendCollectionRegistry.d.ts +0 -13
- package/dist/collections/loader.d.ts +0 -5
- package/dist/cron/cron-loader.d.ts +0 -17
- package/dist/cron/cron-routes.d.ts +0 -14
- package/dist/cron/cron-scheduler.d.ts +0 -106
- package/dist/cron/cron-store.d.ts +0 -32
- package/dist/cron/index.d.ts +0 -6
- package/dist/db/interfaces.d.ts +0 -18
- package/dist/dist-CZKP-Xz4.js +0 -832
- package/dist/dist-CZKP-Xz4.js.map +0 -1
- package/dist/email/index.d.ts +0 -6
- package/dist/email/smtp-email-service.d.ts +0 -25
- package/dist/email/templates.d.ts +0 -42
- package/dist/email/types.d.ts +0 -108
- package/dist/env.d.ts +0 -102
- package/dist/from-VbwD7xRf.js +0 -3849
- package/dist/from-VbwD7xRf.js.map +0 -1
- package/dist/functions/function-loader.d.ts +0 -17
- package/dist/functions/function-routes.d.ts +0 -10
- package/dist/functions/index.d.ts +0 -3
- package/dist/history/history-routes.d.ts +0 -23
- package/dist/history/index.d.ts +0 -1
- package/dist/index.d.ts +0 -33
- package/dist/index.es.js +0 -61731
- package/dist/index.es.js.map +0 -1
- package/dist/index.umd.js +0 -73221
- package/dist/index.umd.js.map +0 -1
- package/dist/init/docs.d.ts +0 -4
- package/dist/init/health.d.ts +0 -2
- package/dist/init/middlewares.d.ts +0 -10
- package/dist/init/shutdown.d.ts +0 -11
- package/dist/init/storage.d.ts +0 -5
- package/dist/init.d.ts +0 -258
- package/dist/jwt-DHcQRGC3.js +0 -4168
- package/dist/jwt-DHcQRGC3.js.map +0 -1
- package/dist/logger-BYU66ENZ.js +0 -94
- package/dist/logger-BYU66ENZ.js.map +0 -1
- package/dist/ms-BeBSuOXN.js +0 -125
- package/dist/ms-BeBSuOXN.js.map +0 -1
- package/dist/multipart-parser-CedBDOeC.js +0 -299
- package/dist/multipart-parser-CedBDOeC.js.map +0 -1
- package/dist/serve-spa.d.ts +0 -30
- package/dist/services/driver-registry.d.ts +0 -78
- package/dist/services/webhook-service.d.ts +0 -29
- package/dist/singleton.d.ts +0 -35
- package/dist/src-COaj0G3P.js +0 -1182
- package/dist/src-COaj0G3P.js.map +0 -1
- package/dist/storage/LocalStorageController.d.ts +0 -46
- package/dist/storage/S3StorageController.d.ts +0 -36
- package/dist/storage/image-transform.d.ts +0 -50
- package/dist/storage/index.d.ts +0 -28
- package/dist/storage/routes.d.ts +0 -38
- package/dist/storage/storage-registry.d.ts +0 -78
- package/dist/storage/tus-handler.d.ts +0 -51
- package/dist/storage/types.d.ts +0 -103
- package/dist/types/index.d.ts +0 -11
- package/dist/utils/dev-port.d.ts +0 -35
- package/dist/utils/logger.d.ts +0 -31
- package/dist/utils/logging.d.ts +0 -9
- package/dist/utils/request-id.d.ts +0 -4
- package/dist/utils/request-logger.d.ts +0 -19
- package/dist/utils/sql.d.ts +0 -27
package/test/auth-routes.test.ts
CHANGED
|
@@ -17,6 +17,18 @@ import { configureJwt, generateAccessToken, hashRefreshToken } from "../src/auth
|
|
|
17
17
|
|
|
18
18
|
jest.mock("../src/auth/password");
|
|
19
19
|
|
|
20
|
+
jest.mock("../src/utils/logger", () => {
|
|
21
|
+
return {
|
|
22
|
+
logger: {
|
|
23
|
+
info: jest.fn(),
|
|
24
|
+
warn: jest.fn(),
|
|
25
|
+
error: jest.fn(),
|
|
26
|
+
debug: jest.fn(),
|
|
27
|
+
child: jest.fn().mockReturnThis()
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
|
|
20
32
|
// Bypass rate limiters — they share state across tests and cause 429s
|
|
21
33
|
jest.mock("../src/auth/rate-limiter", () => {
|
|
22
34
|
const passthrough = async (_c: unknown, next: () => Promise<void>) => next();
|
|
@@ -28,6 +40,7 @@ jest.mock("../src/auth/rate-limiter", () => {
|
|
|
28
40
|
});
|
|
29
41
|
|
|
30
42
|
import { hashPassword, verifyPassword, validatePasswordStrength } from "../src/auth/password";
|
|
43
|
+
import { logger } from "../src/utils/logger";
|
|
31
44
|
import { z } from "zod";
|
|
32
45
|
|
|
33
46
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
@@ -139,15 +152,18 @@ verifyEmailUrl: "https://app.test" } : undefined,
|
|
|
139
152
|
if (idToken === "link-token") return { providerId: "g-456",
|
|
140
153
|
email: "existing@test.com",
|
|
141
154
|
displayName: "Existing",
|
|
142
|
-
photoUrl: null
|
|
155
|
+
photoUrl: null,
|
|
156
|
+
emailVerified: true };
|
|
143
157
|
if (idToken === "returning-token") return { providerId: "g-789",
|
|
144
158
|
email: "returning@test.com",
|
|
145
159
|
displayName: "Updated Name",
|
|
146
|
-
photoUrl: "https://new-photo.url"
|
|
160
|
+
photoUrl: "https://new-photo.url",
|
|
161
|
+
emailVerified: true };
|
|
147
162
|
if (idToken === "valid-token") return { providerId: "g-123",
|
|
148
163
|
email: "google@test.com",
|
|
149
164
|
displayName: "Google User",
|
|
150
|
-
photoUrl: "https://photo.url"
|
|
165
|
+
photoUrl: "https://photo.url",
|
|
166
|
+
emailVerified: true };
|
|
151
167
|
return null;
|
|
152
168
|
}
|
|
153
169
|
}
|
|
@@ -361,9 +377,43 @@ password: "Any1" }));
|
|
|
361
377
|
mockAuthRepo.getUserByEmail.mockResolvedValueOnce(mockUser());
|
|
362
378
|
|
|
363
379
|
await app.request("/auth/login", json({ email: "test@example.com",
|
|
364
|
-
password: "ValidPass1" }));
|
|
380
|
+
password: "ValidPass1" }));
|
|
365
381
|
expect(mockAuthRepo.createRefreshToken).toHaveBeenCalledTimes(1);
|
|
366
382
|
});
|
|
383
|
+
|
|
384
|
+
it("emits a structured security audit log on successful login", async () => {
|
|
385
|
+
const app = createApp();
|
|
386
|
+
const user = mockUser({ passwordHash: "salt:hash" });
|
|
387
|
+
mockAuthRepo.getUserByEmail.mockResolvedValueOnce(user);
|
|
388
|
+
|
|
389
|
+
await app.request("/auth/login", json({ email: "test@example.com", password: "ValidPass1" }));
|
|
390
|
+
|
|
391
|
+
expect(logger.info).toHaveBeenCalledWith(
|
|
392
|
+
expect.stringContaining("[Security Audit] Auth login success"),
|
|
393
|
+
expect.objectContaining({
|
|
394
|
+
userId: "user-1",
|
|
395
|
+
email: "test@example.com",
|
|
396
|
+
eventType: "auth.login.success"
|
|
397
|
+
})
|
|
398
|
+
);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it("emits a structured security audit log on failed login (wrong password)", async () => {
|
|
402
|
+
const app = createApp();
|
|
403
|
+
mockAuthRepo.getUserByEmail.mockResolvedValueOnce(mockUser());
|
|
404
|
+
(verifyPassword as jest.Mock).mockResolvedValueOnce(false);
|
|
405
|
+
|
|
406
|
+
await app.request("/auth/login", json({ email: "test@example.com", password: "WrongPassword" }));
|
|
407
|
+
|
|
408
|
+
expect(logger.warn).toHaveBeenCalledWith(
|
|
409
|
+
expect.stringContaining("[Security Audit] Auth login failure"),
|
|
410
|
+
expect.objectContaining({
|
|
411
|
+
email: "test@example.com",
|
|
412
|
+
userId: "user-1",
|
|
413
|
+
eventType: "auth.login.failure"
|
|
414
|
+
})
|
|
415
|
+
);
|
|
416
|
+
});
|
|
367
417
|
});
|
|
368
418
|
|
|
369
419
|
// ── Google OAuth ────────────────────────────────────────────────────
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createCustomAuthAdapter } from "../src/auth/custom-auth-adapter";
|
|
2
|
-
import type { AuthenticatedUser, CustomAuthAdapterOptions } from "@rebasepro/types";
|
|
2
|
+
import type { AuthenticatedUser, AuthResponsePayload, CustomAuthAdapterOptions } from "@rebasepro/types";
|
|
3
3
|
|
|
4
4
|
const TEST_USER: AuthenticatedUser = {
|
|
5
5
|
uid: "user-123",
|
|
@@ -118,6 +118,7 @@ describe("createCustomAuthAdapter", () => {
|
|
|
118
118
|
sessionManagement: false,
|
|
119
119
|
profileUpdate: false,
|
|
120
120
|
emailVerification: false,
|
|
121
|
+
magicLink: false,
|
|
121
122
|
enabledProviders: []
|
|
122
123
|
});
|
|
123
124
|
});
|
|
@@ -167,4 +168,22 @@ registration: true }
|
|
|
167
168
|
});
|
|
168
169
|
expect(adapter.userManagement).toBeUndefined();
|
|
169
170
|
});
|
|
171
|
+
|
|
172
|
+
// ── transformAuthResponse ────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
it("passes through transformAuthResponse when provided", () => {
|
|
175
|
+
const transformFn = jest.fn(async (response: AuthResponsePayload) => response);
|
|
176
|
+
const adapter = createCustomAuthAdapter({
|
|
177
|
+
verifyRequest: async () => null,
|
|
178
|
+
transformAuthResponse: transformFn
|
|
179
|
+
});
|
|
180
|
+
expect(adapter.transformAuthResponse).toBe(transformFn);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("omits transformAuthResponse when not provided", () => {
|
|
184
|
+
const adapter = createCustomAuthAdapter({
|
|
185
|
+
verifyRequest: async () => null
|
|
186
|
+
});
|
|
187
|
+
expect(adapter.transformAuthResponse).toBeUndefined();
|
|
188
|
+
});
|
|
170
189
|
});
|
package/test/env.test.ts
CHANGED
|
@@ -1,32 +1,22 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
1
|
import { loadEnv } from "../src/env";
|
|
2
|
+
import { z } from "zod";
|
|
3
3
|
|
|
4
4
|
describe("env configuration and localhost validation", () => {
|
|
5
|
-
|
|
5
|
+
const originalEnv = { ...process.env };
|
|
6
6
|
|
|
7
7
|
beforeEach(() => {
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
delete process.env.ADMIN_CONNECTION_STRING;
|
|
14
|
-
delete process.env.JWT_SECRET;
|
|
15
|
-
delete process.env.FRONTEND_URL;
|
|
16
|
-
delete process.env.CORS_ORIGINS;
|
|
17
|
-
delete process.env.ALLOW_LOCALHOST_IN_PRODUCTION;
|
|
8
|
+
// Clear process.env to ensure fresh test runs
|
|
9
|
+
process.env = {};
|
|
10
|
+
// Add basic fallback requirements to prevent bootstrap crashes
|
|
11
|
+
process.env.DATABASE_URL = "postgresql://localhost:5432/rebase";
|
|
12
|
+
process.env.JWT_SECRET = "super-secret-jwt-key-must-be-long-long-long";
|
|
18
13
|
});
|
|
19
14
|
|
|
20
15
|
afterEach(() => {
|
|
21
|
-
|
|
22
|
-
process.env = originalEnv;
|
|
16
|
+
process.env = { ...originalEnv };
|
|
23
17
|
});
|
|
24
18
|
|
|
25
|
-
it("should
|
|
26
|
-
process.env.NODE_ENV = "development";
|
|
27
|
-
process.env.DATABASE_URL = "postgresql://localhost:5432/rebase";
|
|
28
|
-
process.env.JWT_SECRET = "12345678901234567890123456789012";
|
|
29
|
-
|
|
19
|
+
it("should parse default env variables in development mode", () => {
|
|
30
20
|
expect(() => loadEnv()).not.toThrow();
|
|
31
21
|
const env = loadEnv();
|
|
32
22
|
expect(env.DATABASE_URL).toBe("postgresql://localhost:5432/rebase");
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from "@jest/globals";
|
|
2
|
+
import { Hono, Context } from "hono";
|
|
3
|
+
import { configureJwt, generateAccessToken } from "../src/auth/jwt";
|
|
4
|
+
import { createAuthMiddleware } from "../src/auth/middleware";
|
|
5
|
+
import { createAdapterAuthMiddleware } from "../src/auth/adapter-middleware";
|
|
6
|
+
import type { HonoEnv } from "../src/api/types";
|
|
7
|
+
import type { DataDriver, AuthAdapter } from "@rebasepro/types";
|
|
8
|
+
|
|
9
|
+
const TEST_SECRET = "test-secret-key-for-multi-datasource-routing-1234567890";
|
|
10
|
+
|
|
11
|
+
// A mock driver that tags itself so we can assert which one was scoped into
|
|
12
|
+
// the request context. The Postgres-like one implements withAuth (RLS), the
|
|
13
|
+
// Mongo-like one does not (scopeDataDriver must no-op for it).
|
|
14
|
+
function mockDriver(key: string, withRls: boolean): DataDriver {
|
|
15
|
+
const base: Record<string, unknown> = {
|
|
16
|
+
key,
|
|
17
|
+
fetchCollection: async () => [],
|
|
18
|
+
fetchEntity: async () => undefined,
|
|
19
|
+
saveEntity: async () => ({ id: "x", path: key, values: {} }),
|
|
20
|
+
deleteEntity: async () => undefined
|
|
21
|
+
};
|
|
22
|
+
if (withRls) {
|
|
23
|
+
// Returns a scoped clone that keeps the same key tag.
|
|
24
|
+
base.withAuth = async () => ({ ...base, scoped: true });
|
|
25
|
+
}
|
|
26
|
+
return base as unknown as DataDriver;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("Multi-data-source request routing (middleware)", () => {
|
|
30
|
+
|
|
31
|
+
beforeAll(() => {
|
|
32
|
+
configureJwt({ secret: TEST_SECRET, accessExpiresIn: "1h" });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const pg = mockDriver("postgres", true);
|
|
36
|
+
const mongo = mockDriver("mongodb", false);
|
|
37
|
+
|
|
38
|
+
// Route by first path segment: /products → pg, /events → mongo.
|
|
39
|
+
const resolveDriver = (c: Context<HonoEnv>): DataDriver => {
|
|
40
|
+
const slug = c.req.path.replace(/^\/+/, "").split("/")[0];
|
|
41
|
+
return slug === "events" ? mongo : pg;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function createApp() {
|
|
45
|
+
const app = new Hono<HonoEnv>();
|
|
46
|
+
app.use("/*", createAuthMiddleware({ driver: pg, resolveDriver, requireAuth: false }));
|
|
47
|
+
app.get("/:slug", (c: Context<HonoEnv>) => {
|
|
48
|
+
const driver = c.get("driver") as DataDriver | undefined;
|
|
49
|
+
return c.json({ key: driver?.key });
|
|
50
|
+
});
|
|
51
|
+
return app;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
it("scopes the Postgres delegate for a Postgres collection path", async () => {
|
|
55
|
+
const token = generateAccessToken("u1", ["admin"]);
|
|
56
|
+
const res = await createApp().request("/products", { headers: { Authorization: `Bearer ${token}` } });
|
|
57
|
+
const body = await res.json() as { key: string };
|
|
58
|
+
expect(body.key).toBe("postgres");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("scopes the Mongo delegate for a Mongo collection path", async () => {
|
|
62
|
+
const token = generateAccessToken("u1", ["admin"]);
|
|
63
|
+
const res = await createApp().request("/events", { headers: { Authorization: `Bearer ${token}` } });
|
|
64
|
+
const body = await res.json() as { key: string };
|
|
65
|
+
expect(body.key).toBe("mongodb");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("routes per request based on the resolver, not a fixed driver", async () => {
|
|
69
|
+
const token = generateAccessToken("u1", ["admin"]);
|
|
70
|
+
const app = createApp();
|
|
71
|
+
const a = await (await app.request("/events", { headers: { Authorization: `Bearer ${token}` } })).json() as { key: string };
|
|
72
|
+
const b = await (await app.request("/products", { headers: { Authorization: `Bearer ${token}` } })).json() as { key: string };
|
|
73
|
+
expect(a.key).toBe("mongodb");
|
|
74
|
+
expect(b.key).toBe("postgres");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("falls back to the base driver when no resolver is provided", async () => {
|
|
78
|
+
const app = new Hono<HonoEnv>();
|
|
79
|
+
app.use("/*", createAuthMiddleware({ driver: mongo, requireAuth: false }));
|
|
80
|
+
app.get("/:slug", (c: Context<HonoEnv>) => c.json({ key: (c.get("driver") as DataDriver | undefined)?.key }));
|
|
81
|
+
const res = await app.request("/anything");
|
|
82
|
+
const body = await res.json() as { key: string };
|
|
83
|
+
expect(body.key).toBe("mongodb");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("routes a nested/subcollection path by its top-level segment", async () => {
|
|
87
|
+
const token = generateAccessToken("u1", ["admin"]);
|
|
88
|
+
const app = new Hono<HonoEnv>();
|
|
89
|
+
app.use("/*", createAuthMiddleware({ driver: pg, resolveDriver, requireAuth: false }));
|
|
90
|
+
app.get("/events/:id/attendees", (c: Context<HonoEnv>) =>
|
|
91
|
+
c.json({ key: (c.get("driver") as DataDriver | undefined)?.key }));
|
|
92
|
+
const res = await app.request("/events/e1/attendees", { headers: { Authorization: `Bearer ${token}` } });
|
|
93
|
+
const body = await res.json() as { key: string };
|
|
94
|
+
expect(body.key).toBe("mongodb");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("routes per request through the adapter auth middleware too", async () => {
|
|
98
|
+
const adapter: AuthAdapter = {
|
|
99
|
+
id: "test-adapter",
|
|
100
|
+
serviceKey: undefined,
|
|
101
|
+
verifyRequest: async () => ({ uid: "u1", roles: ["admin"] })
|
|
102
|
+
} as unknown as AuthAdapter;
|
|
103
|
+
|
|
104
|
+
const app = new Hono<HonoEnv>();
|
|
105
|
+
app.use("/*", createAdapterAuthMiddleware({ adapter, driver: pg, resolveDriver, requireAuth: false }));
|
|
106
|
+
app.get("/:slug", (c: Context<HonoEnv>) => c.json({ key: (c.get("driver") as DataDriver | undefined)?.key }));
|
|
107
|
+
|
|
108
|
+
const a = await (await app.request("/events")).json() as { key: string };
|
|
109
|
+
const b = await (await app.request("/products")).json() as { key: string };
|
|
110
|
+
expect(a.key).toBe("mongodb");
|
|
111
|
+
expect(b.key).toBe("postgres");
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, it, expect, jest, beforeEach } from "@jest/globals";
|
|
2
|
+
import { createRoutedRealtimeService } from "../src/services/routed-realtime-service";
|
|
3
|
+
import type { RealtimeProvider } from "@rebasepro/types";
|
|
4
|
+
|
|
5
|
+
function mockProvider() {
|
|
6
|
+
return {
|
|
7
|
+
addClient: jest.fn(),
|
|
8
|
+
handleClientMessage: jest.fn(async () => {}),
|
|
9
|
+
subscribeToCollection: jest.fn(),
|
|
10
|
+
subscribeToEntity: jest.fn(),
|
|
11
|
+
unsubscribe: jest.fn(),
|
|
12
|
+
notifyEntityUpdate: jest.fn(async () => {}),
|
|
13
|
+
onServerReady: jest.fn(),
|
|
14
|
+
destroy: jest.fn(async () => {}),
|
|
15
|
+
stopListening: jest.fn(async () => {})
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("createRoutedRealtimeService", () => {
|
|
20
|
+
let pg: ReturnType<typeof mockProvider>;
|
|
21
|
+
let mongo: ReturnType<typeof mockProvider>;
|
|
22
|
+
let routed: ReturnType<typeof createRoutedRealtimeService>;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
pg = mockProvider();
|
|
26
|
+
mongo = mockProvider();
|
|
27
|
+
routed = createRoutedRealtimeService({
|
|
28
|
+
providers: { "(default)": pg as unknown as RealtimeProvider, mongo: mongo as unknown as RealtimeProvider },
|
|
29
|
+
defaultKey: "(default)",
|
|
30
|
+
resolveKey: (path) => (path.split("/")[0] === "events" ? "mongo" : "(default)")
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("routes subscribe_collection to the provider owning the collection", async () => {
|
|
35
|
+
await routed.handleClientMessage("c1", { type: "subscribe_collection", payload: { path: "events", subscriptionId: "s1" } });
|
|
36
|
+
expect(mongo.handleClientMessage).toHaveBeenCalledTimes(1);
|
|
37
|
+
expect(pg.handleClientMessage).not.toHaveBeenCalled();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("routes subscribe_entity by path", async () => {
|
|
41
|
+
await routed.handleClientMessage("c1", { type: "subscribe_entity", payload: { path: "products", subscriptionId: "s2" } });
|
|
42
|
+
expect(pg.handleClientMessage).toHaveBeenCalledTimes(1);
|
|
43
|
+
expect(mongo.handleClientMessage).not.toHaveBeenCalled();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("forwards unsubscribe to all providers (owner acts, others no-op)", async () => {
|
|
47
|
+
await routed.handleClientMessage("c1", { type: "unsubscribe", subscriptionId: "s1" });
|
|
48
|
+
expect(pg.handleClientMessage).toHaveBeenCalledTimes(1);
|
|
49
|
+
expect(mongo.handleClientMessage).toHaveBeenCalledTimes(1);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("sends channel/presence/broadcast to the default provider only", async () => {
|
|
53
|
+
await routed.handleClientMessage("c1", { type: "broadcast", payload: { channel: "room", event: "x" } });
|
|
54
|
+
await routed.handleClientMessage("c1", { type: "join_channel", payload: { channel: "room" } });
|
|
55
|
+
await routed.handleClientMessage("c1", { type: "presence_track", payload: { channel: "room" } });
|
|
56
|
+
expect(pg.handleClientMessage).toHaveBeenCalledTimes(3);
|
|
57
|
+
expect(mongo.handleClientMessage).not.toHaveBeenCalled();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("addClient is forwarded to every provider", () => {
|
|
61
|
+
const ws = {};
|
|
62
|
+
routed.addClient("c1", ws);
|
|
63
|
+
expect(pg.addClient).toHaveBeenCalledWith("c1", ws);
|
|
64
|
+
expect(mongo.addClient).toHaveBeenCalledWith("c1", ws);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("notifyEntityUpdate routes by path", async () => {
|
|
68
|
+
await routed.notifyEntityUpdate("events", "e1", null);
|
|
69
|
+
expect(mongo.notifyEntityUpdate).toHaveBeenCalledTimes(1);
|
|
70
|
+
expect(pg.notifyEntityUpdate).not.toHaveBeenCalled();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("unknown path falls back to the default provider", async () => {
|
|
74
|
+
await routed.handleClientMessage("c1", { type: "subscribe_collection", payload: { path: "mystery", subscriptionId: "s9" } });
|
|
75
|
+
expect(pg.handleClientMessage).toHaveBeenCalledTimes(1);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("lifecycle methods fan out to all providers", async () => {
|
|
79
|
+
routed.onServerReady?.({ port: 3000 });
|
|
80
|
+
await routed.destroy?.();
|
|
81
|
+
expect(pg.onServerReady).toHaveBeenCalledTimes(1);
|
|
82
|
+
expect(mongo.onServerReady).toHaveBeenCalledTimes(1);
|
|
83
|
+
expect(pg.destroy).toHaveBeenCalledTimes(1);
|
|
84
|
+
expect(mongo.destroy).toHaveBeenCalledTimes(1);
|
|
85
|
+
});
|
|
86
|
+
});
|