@rebasepro/server-core 0.1.2 → 0.2.3

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 (75) hide show
  1. package/LICENSE +22 -6
  2. package/dist/common/src/data/query_builder.d.ts +51 -0
  3. package/dist/common/src/index.d.ts +1 -0
  4. package/dist/common/src/util/entities.d.ts +2 -2
  5. package/dist/common/src/util/relations.d.ts +1 -1
  6. package/dist/{index-DXVBFp5V.js → index-BZoAtuqi.js} +6 -2
  7. package/dist/index-BZoAtuqi.js.map +1 -0
  8. package/dist/index.es.js +16038 -15240
  9. package/dist/index.es.js.map +1 -1
  10. package/dist/index.umd.js +15980 -15178
  11. package/dist/index.umd.js.map +1 -1
  12. package/dist/server-core/src/auth/adapter-middleware.d.ts +33 -0
  13. package/dist/server-core/src/auth/admin-routes.d.ts +6 -0
  14. package/dist/server-core/src/auth/auth-overrides.d.ts +139 -0
  15. package/dist/server-core/src/auth/builtin-auth-adapter.d.ts +49 -0
  16. package/dist/server-core/src/auth/crypto-utils.d.ts +16 -0
  17. package/dist/server-core/src/auth/custom-auth-adapter.d.ts +39 -0
  18. package/dist/server-core/src/auth/index.d.ts +7 -0
  19. package/dist/server-core/src/auth/interfaces.d.ts +2 -0
  20. package/dist/server-core/src/auth/middleware.d.ts +18 -0
  21. package/dist/server-core/src/auth/rls-scope.d.ts +31 -0
  22. package/dist/server-core/src/auth/routes.d.ts +7 -1
  23. package/dist/server-core/src/env.d.ts +131 -0
  24. package/dist/server-core/src/index.d.ts +2 -0
  25. package/dist/server-core/src/init.d.ts +62 -3
  26. package/dist/types/src/controllers/auth.d.ts +9 -8
  27. package/dist/types/src/controllers/client.d.ts +3 -0
  28. package/dist/types/src/controllers/data.d.ts +21 -0
  29. package/dist/types/src/types/auth_adapter.d.ts +356 -0
  30. package/dist/types/src/types/collections.d.ts +67 -2
  31. package/dist/types/src/types/database_adapter.d.ts +94 -0
  32. package/dist/types/src/types/entity_actions.d.ts +7 -1
  33. package/dist/types/src/types/entity_callbacks.d.ts +1 -1
  34. package/dist/types/src/types/entity_views.d.ts +36 -1
  35. package/dist/types/src/types/index.d.ts +2 -0
  36. package/dist/types/src/types/plugins.d.ts +1 -1
  37. package/dist/types/src/types/properties.d.ts +24 -5
  38. package/dist/types/src/types/property_config.d.ts +6 -2
  39. package/dist/types/src/types/relations.d.ts +1 -1
  40. package/dist/types/src/types/translations.d.ts +8 -0
  41. package/dist/types/src/users/user.d.ts +5 -0
  42. package/jest.config.cjs +4 -1
  43. package/package.json +27 -27
  44. package/src/api/errors.ts +1 -1
  45. package/src/api/graphql/graphql-schema-generator.ts +7 -0
  46. package/src/api/openapi-generator.ts +13 -1
  47. package/src/api/rest/api-generator-count.test.ts +14 -12
  48. package/src/api/rest/query-parser.ts +2 -20
  49. package/src/auth/adapter-middleware.ts +83 -0
  50. package/src/auth/admin-routes.ts +36 -43
  51. package/src/auth/auth-overrides.ts +172 -0
  52. package/src/auth/builtin-auth-adapter.ts +384 -0
  53. package/src/auth/crypto-utils.ts +31 -0
  54. package/src/auth/custom-auth-adapter.ts +85 -0
  55. package/src/auth/index.ts +10 -0
  56. package/src/auth/interfaces.ts +2 -0
  57. package/src/auth/jwt.ts +3 -1
  58. package/src/auth/middleware.ts +2 -46
  59. package/src/auth/rls-scope.ts +58 -0
  60. package/src/auth/routes.ts +74 -32
  61. package/src/cron/cron-scheduler.test.ts +9 -9
  62. package/src/cron/cron-scheduler.ts +1 -1
  63. package/src/env.ts +224 -0
  64. package/src/index.ts +4 -0
  65. package/src/init.ts +355 -135
  66. package/src/storage/routes.ts +1 -19
  67. package/src/utils/logging.ts +3 -3
  68. package/test/admin-routes.test.ts +10 -4
  69. package/test/auth-routes.test.ts +2 -2
  70. package/test/backend-hooks-admin.test.ts +32 -12
  71. package/test/custom-auth-adapter.test.ts +177 -0
  72. package/test/env.test.ts +138 -0
  73. package/test/query-parser.test.ts +0 -29
  74. package/tsconfig.json +3 -0
  75. package/dist/index-DXVBFp5V.js.map +0 -1
@@ -94,25 +94,7 @@ export function createStorageRoutes(config: StorageRoutesConfig): Hono<HonoEnv>
94
94
  const key = typeof body["key"] === "string" ? body["key"] : "";
95
95
  const bucket = typeof body["bucket"] === "string" ? body["bucket"] : undefined;
96
96
 
97
- // Backward compatibility support for older clients sending path and fileName
98
- const legacyPath = typeof body["path"] === "string" ? body["path"] : "";
99
- const legacyFileName = typeof body["fileName"] === "string" ? body["fileName"] : undefined;
100
-
101
- let finalKey = key;
102
- if (!finalKey) {
103
- if (legacyPath || legacyFileName) {
104
- const parts = [];
105
- if (legacyPath) parts.push(legacyPath);
106
- if (legacyFileName) {
107
- parts.push(legacyFileName);
108
- } else {
109
- parts.push(uploadedFile.name || "unnamed");
110
- }
111
- finalKey = parts.join("/");
112
- } else {
113
- finalKey = uploadedFile.name || "unnamed";
114
- }
115
- }
97
+ const finalKey = key || uploadedFile.name || "unnamed";
116
98
 
117
99
  // Extract custom metadata from request body
118
100
  const metadata: Record<string, unknown> = {};
@@ -21,8 +21,8 @@ debug: 3 };
21
21
  */
22
22
  export function resetConsole() {
23
23
  // Store original methods if not already stored
24
- if (!(global as unknown as Record<string, unknown>).__originalConsole) {
25
- (global as unknown as Record<string, unknown>).__originalConsole = {
24
+ if (!(global as Record<string, unknown>).__originalConsole) {
25
+ (global as Record<string, unknown>).__originalConsole = {
26
26
  log: console.log,
27
27
  warn: console.warn,
28
28
  error: console.error,
@@ -30,7 +30,7 @@ export function resetConsole() {
30
30
  };
31
31
  }
32
32
 
33
- const original = (global as unknown as Record<string, unknown>).__originalConsole as Console;
33
+ const original = (global as Record<string, unknown>).__originalConsole as Console;
34
34
  console.log = original.log;
35
35
  console.warn = original.warn;
36
36
  console.error = original.error;
@@ -63,6 +63,7 @@ displayName: data.displayName,
63
63
  passwordHash: data.passwordHash }))
64
64
  ),
65
65
  listUsers: jest.fn().mockResolvedValue([]),
66
+ listUsersPaginated: jest.fn().mockResolvedValue({ users: [], total: 0, limit: 25, offset: 0 }),
66
67
  getUserRoles: jest.fn().mockResolvedValue([mockRole("editor")]),
67
68
  getUserRoleIds: jest.fn().mockResolvedValue(["editor"]),
68
69
  assignDefaultRole: jest.fn().mockResolvedValue(undefined),
@@ -226,12 +227,17 @@ accessExpiresIn: "1h" });
226
227
  describe("GET /admin/users", () => {
227
228
  it("returns list of users with roles", async () => {
228
229
  const app = createApp();
229
- mockAuthRepo.listUsers.mockResolvedValueOnce([
230
- mockUser({ id: "u1",
230
+ mockAuthRepo.listUsersPaginated.mockResolvedValueOnce({
231
+ users: [
232
+ mockUser({ id: "u1",
231
233
  email: "a@test.com" }),
232
- mockUser({ id: "u2",
234
+ mockUser({ id: "u2",
233
235
  email: "b@test.com" })
234
- ]);
236
+ ],
237
+ total: 2,
238
+ limit: 25,
239
+ offset: 0
240
+ });
235
241
  mockAuthRepo.getUserRoleIds
236
242
  .mockResolvedValueOnce(["admin"])
237
243
  .mockResolvedValueOnce(["editor"]);
@@ -916,7 +916,7 @@ withEmail: false });
916
916
  const body = await res.json() as any;
917
917
  expect(body.needsSetup).toBe(false);
918
918
  expect(body.registrationEnabled).toBe(false);
919
- expect(body.googleEnabled).toBe(false);
919
+ expect(body.enabledProviders).toEqual([]);
920
920
  });
921
921
 
922
922
  it("reports Google enabled when configured", async () => {
@@ -925,7 +925,7 @@ withEmail: false });
925
925
 
926
926
  const res = await app.request("/auth/config");
927
927
  const body = await res.json() as any;
928
- expect(body.googleEnabled).toBe(true);
928
+ expect(body.enabledProviders).toContain("google");
929
929
  });
930
930
  });
931
931
 
@@ -159,11 +159,16 @@ describe("BackendHooks — Admin Routes", () => {
159
159
  }
160
160
  };
161
161
  const app = createApp(hooks);
162
- mockAuthRepo.listUsers.mockResolvedValueOnce([
163
- mockUser({ id: "u1", email: "alice@test.com" }),
164
- mockUser({ id: "u2", email: "bot@system.internal" }),
165
- mockUser({ id: "u3", email: "bob@test.com" })
166
- ]);
162
+ mockAuthRepo.listUsersPaginated.mockResolvedValueOnce({
163
+ users: [
164
+ mockUser({ id: "u1", email: "alice@test.com" }),
165
+ mockUser({ id: "u2", email: "bot@system.internal" }),
166
+ mockUser({ id: "u3", email: "bob@test.com" })
167
+ ],
168
+ total: 3,
169
+ limit: 25,
170
+ offset: 0
171
+ });
167
172
  mockAuthRepo.getUserRoleIds
168
173
  .mockResolvedValueOnce(["editor"])
169
174
  .mockResolvedValueOnce(["editor"])
@@ -186,9 +191,14 @@ describe("BackendHooks — Admin Routes", () => {
186
191
  }
187
192
  };
188
193
  const app = createApp(hooks);
189
- mockAuthRepo.listUsers.mockResolvedValueOnce([
190
- mockUser({ id: "u1", email: "alice@secret.com" })
191
- ]);
194
+ mockAuthRepo.listUsersPaginated.mockResolvedValueOnce({
195
+ users: [
196
+ mockUser({ id: "u1", email: "alice@secret.com" })
197
+ ],
198
+ total: 1,
199
+ limit: 25,
200
+ offset: 0
201
+ });
192
202
  mockAuthRepo.getUserRoleIds.mockResolvedValueOnce(["editor"]);
193
203
 
194
204
  const res = await app.request("/admin/users", { headers: { ...adminAuth() } });
@@ -219,7 +229,12 @@ describe("BackendHooks — Admin Routes", () => {
219
229
  const afterReadSpy = jest.fn((user, ctx) => user);
220
230
  const hooks: BackendHooks = { users: { afterRead: afterReadSpy } };
221
231
  const app = createApp(hooks);
222
- mockAuthRepo.listUsers.mockResolvedValueOnce([mockUser({ id: "u1" })]);
232
+ mockAuthRepo.listUsersPaginated.mockResolvedValueOnce({
233
+ users: [mockUser({ id: "u1" })],
234
+ total: 1,
235
+ limit: 25,
236
+ offset: 0
237
+ });
223
238
  mockAuthRepo.getUserRoleIds.mockResolvedValueOnce(["editor"]);
224
239
 
225
240
  await app.request("/admin/users", { headers: { ...adminAuth("admin-42") } });
@@ -379,9 +394,14 @@ describe("BackendHooks — Admin Routes", () => {
379
394
  describe("no hooks configured", () => {
380
395
  it("returns data unchanged when no hooks are provided", async () => {
381
396
  const app = createApp(); // no hooks
382
- mockAuthRepo.listUsers.mockResolvedValueOnce([
383
- mockUser({ id: "u1", email: "alice@test.com" })
384
- ]);
397
+ mockAuthRepo.listUsersPaginated.mockResolvedValueOnce({
398
+ users: [
399
+ mockUser({ id: "u1", email: "alice@test.com" })
400
+ ],
401
+ total: 1,
402
+ limit: 25,
403
+ offset: 0
404
+ });
385
405
  mockAuthRepo.getUserRoleIds.mockResolvedValueOnce(["editor"]);
386
406
 
387
407
  const res = await app.request("/admin/users", { headers: { ...adminAuth() } });
@@ -0,0 +1,177 @@
1
+ import { createCustomAuthAdapter } from "../src/auth/custom-auth-adapter";
2
+ import type { AuthenticatedUser, CustomAuthAdapterOptions } from "@rebasepro/types";
3
+
4
+ const TEST_USER: AuthenticatedUser = {
5
+ uid: "user-123",
6
+ email: "test@example.com",
7
+ displayName: "Test User",
8
+ roles: ["editor"],
9
+ isAdmin: false,
10
+ rawToken: "tok_abc",
11
+ };
12
+
13
+ const ADMIN_USER: AuthenticatedUser = {
14
+ uid: "admin-1",
15
+ email: "admin@example.com",
16
+ roles: ["admin"],
17
+ isAdmin: true,
18
+ };
19
+
20
+ describe("createCustomAuthAdapter", () => {
21
+ it("sets the adapter id to 'custom'", () => {
22
+ const adapter = createCustomAuthAdapter({
23
+ verifyRequest: async () => null,
24
+ });
25
+ expect(adapter.id).toBe("custom");
26
+ });
27
+
28
+ it("delegates verifyRequest to the provided function", async () => {
29
+ const verifyRequest = jest.fn(async () => TEST_USER);
30
+ const adapter = createCustomAuthAdapter({ verifyRequest });
31
+ const req = new Request("http://localhost/api", {
32
+ headers: { Authorization: "Bearer test-token" },
33
+ });
34
+ const result = await adapter.verifyRequest(req);
35
+ expect(verifyRequest).toHaveBeenCalledWith(req);
36
+ expect(result).toBe(TEST_USER);
37
+ });
38
+
39
+ it("returns null from verifyRequest when the user function returns null", async () => {
40
+ const adapter = createCustomAuthAdapter({
41
+ verifyRequest: async () => null,
42
+ });
43
+ const result = await adapter.verifyRequest(new Request("http://localhost/"));
44
+ expect(result).toBeNull();
45
+ });
46
+
47
+ // ── verifyToken (fallback via synthetic Request) ──────────────────
48
+
49
+ it("synthesizes a Request when verifyToken is not provided", async () => {
50
+ const verifyRequest = jest.fn(async (request: Request) => {
51
+ const auth = request.headers.get("authorization");
52
+ if (auth === "Bearer my-token") return TEST_USER;
53
+ return null;
54
+ });
55
+ const adapter = createCustomAuthAdapter({ verifyRequest });
56
+
57
+ expect(adapter.verifyToken).toBeDefined();
58
+
59
+ const result = await adapter.verifyToken!("my-token");
60
+ expect(result).toBe(TEST_USER);
61
+
62
+ // Verify the synthetic request was created correctly
63
+ expect(verifyRequest).toHaveBeenCalledTimes(1);
64
+ const calledRequest = verifyRequest.mock.calls[0][0] as Request;
65
+ expect(calledRequest.headers.get("authorization")).toBe("Bearer my-token");
66
+ expect(calledRequest.url).toContain("_ws_auth");
67
+ });
68
+
69
+ it("returns null from fallback verifyToken for invalid token", async () => {
70
+ const adapter = createCustomAuthAdapter({
71
+ verifyRequest: async () => null,
72
+ });
73
+ const result = await adapter.verifyToken!("bad-token");
74
+ expect(result).toBeNull();
75
+ });
76
+
77
+ // ── verifyToken (direct passthrough) ──────────────────────────────
78
+
79
+ it("uses the user-provided verifyToken when given", async () => {
80
+ const customVerifyToken = jest.fn(async (token: string) => {
81
+ if (token === "direct-token") return ADMIN_USER;
82
+ return null;
83
+ });
84
+ const verifyRequest = jest.fn(async () => null);
85
+ const adapter = createCustomAuthAdapter({
86
+ verifyRequest,
87
+ verifyToken: customVerifyToken,
88
+ });
89
+
90
+ const result = await adapter.verifyToken!("direct-token");
91
+ expect(result).toBe(ADMIN_USER);
92
+
93
+ // verifyRequest should NOT be called — verifyToken takes precedence
94
+ expect(verifyRequest).not.toHaveBeenCalled();
95
+ });
96
+
97
+ it("returns null from user-provided verifyToken for unknown token", async () => {
98
+ const adapter = createCustomAuthAdapter({
99
+ verifyRequest: async () => null,
100
+ verifyToken: async () => null,
101
+ });
102
+ const result = await adapter.verifyToken!("unknown");
103
+ expect(result).toBeNull();
104
+ });
105
+
106
+ // ── Capabilities ──────────────────────────────────────────────────
107
+
108
+ it("returns default capabilities when none are overridden", async () => {
109
+ const adapter = createCustomAuthAdapter({
110
+ verifyRequest: async () => null,
111
+ });
112
+ const caps = await adapter.getCapabilities!();
113
+ expect(caps).toEqual({
114
+ hasBuiltInAuthRoutes: false,
115
+ emailPasswordLogin: false,
116
+ registration: false,
117
+ passwordReset: false,
118
+ sessionManagement: false,
119
+ profileUpdate: false,
120
+ emailVerification: false,
121
+ enabledProviders: [],
122
+ });
123
+ });
124
+
125
+ it("merges user-provided capabilities with defaults", async () => {
126
+ const adapter = createCustomAuthAdapter({
127
+ verifyRequest: async () => null,
128
+ capabilities: { emailPasswordLogin: true, registration: true },
129
+ });
130
+ const caps = await adapter.getCapabilities!();
131
+ expect(caps.emailPasswordLogin).toBe(true);
132
+ expect(caps.registration).toBe(true);
133
+ expect(caps.hasBuiltInAuthRoutes).toBe(false);
134
+ });
135
+
136
+ // ── Optional fields ───────────────────────────────────────────────
137
+
138
+ it("passes through serviceKey", () => {
139
+ const adapter = createCustomAuthAdapter({
140
+ verifyRequest: async () => null,
141
+ serviceKey: "sk_live_123",
142
+ });
143
+ expect(adapter.serviceKey).toBe("sk_live_123");
144
+ });
145
+
146
+ it("passes through userManagement and roleManagement when provided", () => {
147
+ const userMgmt = {
148
+ getUser: jest.fn(),
149
+ listUsers: jest.fn(),
150
+ createUser: jest.fn(),
151
+ updateUser: jest.fn(),
152
+ deleteUser: jest.fn(),
153
+ };
154
+ const roleMgmt = {
155
+ listRoles: jest.fn(),
156
+ getUserRoles: jest.fn(),
157
+ setUserRoles: jest.fn(),
158
+ };
159
+
160
+ const adapter = createCustomAuthAdapter({
161
+ verifyRequest: async () => null,
162
+ userManagement: userMgmt as unknown as CustomAuthAdapterOptions["userManagement"],
163
+ roleManagement: roleMgmt as unknown as CustomAuthAdapterOptions["roleManagement"],
164
+ });
165
+
166
+ expect(adapter.userManagement).toBe(userMgmt);
167
+ expect(adapter.roleManagement).toBe(roleMgmt);
168
+ });
169
+
170
+ it("omits userManagement and roleManagement when not provided", () => {
171
+ const adapter = createCustomAuthAdapter({
172
+ verifyRequest: async () => null,
173
+ });
174
+ expect(adapter.userManagement).toBeUndefined();
175
+ expect(adapter.roleManagement).toBeUndefined();
176
+ });
177
+ });
@@ -0,0 +1,138 @@
1
+ import { z } from "zod";
2
+ import { loadEnv } from "../src/env";
3
+
4
+ describe("env configuration and localhost validation", () => {
5
+ let originalEnv: NodeJS.ProcessEnv;
6
+
7
+ beforeEach(() => {
8
+ // Save a backup of the original process.env
9
+ originalEnv = { ...process.env };
10
+ // Clear env vars that might interfere with tests
11
+ delete process.env.NODE_ENV;
12
+ delete process.env.DATABASE_URL;
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;
18
+ });
19
+
20
+ afterEach(() => {
21
+ // Restore process.env after each test
22
+ process.env = originalEnv;
23
+ });
24
+
25
+ it("should allow localhost URLs in development mode", () => {
26
+ process.env.NODE_ENV = "development";
27
+ process.env.DATABASE_URL = "postgresql://localhost:5432/rebase";
28
+ process.env.JWT_SECRET = "12345678901234567890123456789012";
29
+
30
+ expect(() => loadEnv()).not.toThrow();
31
+ const env = loadEnv();
32
+ expect(env.DATABASE_URL).toBe("postgresql://localhost:5432/rebase");
33
+ });
34
+
35
+ it("should fail validation in production if DATABASE_URL contains localhost", () => {
36
+ process.env.NODE_ENV = "production";
37
+ process.env.DATABASE_URL = "postgresql://localhost:5432/rebase";
38
+ process.env.JWT_SECRET = "12345678901234567890123456789012";
39
+ process.env.FRONTEND_URL = "https://my-app.com";
40
+
41
+ expect(() => loadEnv()).toThrowError(/postgresql:\/\/localhost:5432\/rebase/);
42
+ });
43
+
44
+ it("should fail validation in production if DATABASE_URL contains 127.0.0.1", () => {
45
+ process.env.NODE_ENV = "production";
46
+ process.env.DATABASE_URL = "postgresql://127.0.0.1:5432/rebase";
47
+ process.env.JWT_SECRET = "12345678901234567890123456789012";
48
+ process.env.FRONTEND_URL = "https://my-app.com";
49
+
50
+ expect(() => loadEnv()).toThrowError(/postgresql:\/\/127\.0\.0\.1:5432\/rebase/);
51
+ });
52
+
53
+ it("should fail validation in production if DATABASE_URL contains an IPv6 loopback [::1]", () => {
54
+ process.env.NODE_ENV = "production";
55
+ process.env.DATABASE_URL = "postgresql://[::1]:5432/rebase";
56
+ process.env.JWT_SECRET = "12345678901234567890123456789012";
57
+ process.env.FRONTEND_URL = "https://my-app.com";
58
+
59
+ expect(() => loadEnv()).toThrowError(/postgresql:\/\/\[::1\]:5432\/rebase/);
60
+ });
61
+
62
+ it("should fail validation in production if DATABASE_URL contains a loopback in the 127.x.x.x range", () => {
63
+ process.env.NODE_ENV = "production";
64
+ process.env.DATABASE_URL = "postgresql://127.0.0.2:5432/rebase";
65
+ process.env.JWT_SECRET = "12345678901234567890123456789012";
66
+ process.env.FRONTEND_URL = "https://my-app.com";
67
+
68
+ expect(() => loadEnv()).toThrowError(/postgresql:\/\/127\.0\.0\.2:5432\/rebase/);
69
+ });
70
+
71
+ it("should succeed validation in production with a non-localhost DATABASE_URL", () => {
72
+ process.env.NODE_ENV = "production";
73
+ process.env.DATABASE_URL = "postgresql://db.my-app.com:5432/rebase";
74
+ process.env.JWT_SECRET = "12345678901234567890123456789012";
75
+ process.env.FRONTEND_URL = "https://my-app.com";
76
+
77
+ expect(() => loadEnv()).not.toThrow();
78
+ const env = loadEnv();
79
+ expect(env.DATABASE_URL).toBe("postgresql://db.my-app.com:5432/rebase");
80
+ });
81
+
82
+ it("should allow localhost URLs in production if ALLOW_LOCALHOST_IN_PRODUCTION is set to true", () => {
83
+ process.env.NODE_ENV = "production";
84
+ process.env.DATABASE_URL = "postgresql://localhost:5432/rebase";
85
+ process.env.JWT_SECRET = "12345678901234567890123456789012";
86
+ process.env.FRONTEND_URL = "https://my-app.com";
87
+ process.env.ALLOW_LOCALHOST_IN_PRODUCTION = "true";
88
+
89
+ expect(() => loadEnv()).not.toThrow();
90
+ const env = loadEnv();
91
+ expect(env.DATABASE_URL).toBe("postgresql://localhost:5432/rebase");
92
+ });
93
+
94
+ it("should not block localhost URLs in CORS_ORIGINS in production mode", () => {
95
+ process.env.NODE_ENV = "production";
96
+ process.env.DATABASE_URL = "postgresql://db.my-app.com:5432/rebase";
97
+ process.env.JWT_SECRET = "12345678901234567890123456789012";
98
+ process.env.FRONTEND_URL = "https://my-app.com";
99
+ process.env.CORS_ORIGINS = "http://localhost:3000,https://my-app.com";
100
+
101
+ expect(() => loadEnv()).not.toThrow();
102
+ const env = loadEnv();
103
+ expect(env.CORS_ORIGINS).toBe("http://localhost:3000,https://my-app.com");
104
+ });
105
+
106
+ it("should validate and block localhost in extended variables", () => {
107
+ process.env.NODE_ENV = "production";
108
+ process.env.DATABASE_URL = "postgresql://db.my-app.com:5432/rebase";
109
+ process.env.JWT_SECRET = "12345678901234567890123456789012";
110
+ process.env.FRONTEND_URL = "https://my-app.com";
111
+ // Extended URL points to localhost
112
+ process.env.EXTERNAL_SERVICE_URL = "https://localhost:8080/api";
113
+
114
+ const extension = {
115
+ extend: z.object({
116
+ EXTERNAL_SERVICE_URL: z.string().url(),
117
+ }),
118
+ };
119
+
120
+ expect(() => loadEnv(extension)).toThrowError(/https:\/\/localhost:8080\/api/);
121
+ });
122
+
123
+ it("should validate and block plain host string matching localhost", () => {
124
+ process.env.NODE_ENV = "production";
125
+ process.env.DATABASE_URL = "postgresql://db.my-app.com:5432/rebase";
126
+ process.env.JWT_SECRET = "12345678901234567890123456789012";
127
+ process.env.FRONTEND_URL = "https://my-app.com";
128
+ process.env.DB_HOST = "localhost";
129
+
130
+ const extension = {
131
+ extend: z.object({
132
+ DB_HOST: z.string(),
133
+ }),
134
+ };
135
+
136
+ expect(() => loadEnv(extension)).toThrowError(/localhost/);
137
+ });
138
+ });
@@ -156,36 +156,7 @@ describe("parseQueryOptions — PostgREST filters", () => {
156
156
  });
157
157
  });
158
158
 
159
- // ─────────────────────────────────────────────────────────────
160
- // parseQueryOptions — Legacy JSON where
161
- // ─────────────────────────────────────────────────────────────
162
- describe("parseQueryOptions — legacy JSON where", () => {
163
- it("parses JSON where string", () => {
164
- const result = parseQueryOptions({
165
- where: JSON.stringify({ status: ["==", "published"] })
166
- });
167
- expect(result.where?.status).toEqual(["==", "published"]);
168
- });
169
-
170
- it("accepts object where directly", () => {
171
- const result = parseQueryOptions({
172
- where: { status: ["==", "draft"] }
173
- });
174
- expect(result.where?.status).toEqual(["==", "draft"]);
175
- });
176
-
177
- it("throws for malformed JSON where", () => {
178
- expect(() => parseQueryOptions({ where: "not valid json{" })).toThrow("Invalid 'where' filter");
179
- });
180
159
 
181
- it("throws for array where", () => {
182
- expect(() => parseQueryOptions({ where: JSON.stringify([1, 2]) })).toThrow("Filter must be a JSON object");
183
- });
184
-
185
- it("throws for null where", () => {
186
- expect(() => parseQueryOptions({ where: JSON.stringify(null) })).toThrow("Filter must be a JSON object");
187
- });
188
- });
189
160
 
190
161
  // ─────────────────────────────────────────────────────────────
191
162
  // parseQueryOptions — Sorting
package/tsconfig.json CHANGED
@@ -35,6 +35,9 @@
35
35
  ],
36
36
  "@rebasepro/common": [
37
37
  "../common/src"
38
+ ],
39
+ "hono": [
40
+ "node_modules/hono"
38
41
  ]
39
42
  }
40
43
  },
@@ -1 +0,0 @@
1
- {"version":3,"file":"index-DXVBFp5V.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}