@rebasepro/server-core 0.1.2 → 0.2.1
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/LICENSE +22 -6
- package/dist/common/src/util/entities.d.ts +2 -2
- package/dist/common/src/util/relations.d.ts +1 -1
- package/dist/{index-DXVBFp5V.js → index-BZoAtuqi.js} +6 -2
- package/dist/index-BZoAtuqi.js.map +1 -0
- package/dist/index.es.js +15851 -15065
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +15825 -15035
- package/dist/index.umd.js.map +1 -1
- package/dist/server-core/src/auth/adapter-middleware.d.ts +33 -0
- package/dist/server-core/src/auth/admin-routes.d.ts +6 -0
- package/dist/server-core/src/auth/auth-overrides.d.ts +139 -0
- package/dist/server-core/src/auth/builtin-auth-adapter.d.ts +49 -0
- package/dist/server-core/src/auth/crypto-utils.d.ts +16 -0
- package/dist/server-core/src/auth/custom-auth-adapter.d.ts +39 -0
- package/dist/server-core/src/auth/index.d.ts +7 -0
- package/dist/server-core/src/auth/interfaces.d.ts +2 -0
- package/dist/server-core/src/auth/middleware.d.ts +18 -0
- package/dist/server-core/src/auth/rls-scope.d.ts +31 -0
- package/dist/server-core/src/auth/routes.d.ts +7 -1
- package/dist/server-core/src/env.d.ts +131 -0
- package/dist/server-core/src/index.d.ts +2 -0
- package/dist/server-core/src/init.d.ts +62 -3
- package/dist/types/src/controllers/auth.d.ts +9 -8
- package/dist/types/src/controllers/client.d.ts +3 -0
- package/dist/types/src/types/auth_adapter.d.ts +356 -0
- package/dist/types/src/types/collections.d.ts +67 -2
- package/dist/types/src/types/database_adapter.d.ts +94 -0
- package/dist/types/src/types/entity_actions.d.ts +7 -1
- package/dist/types/src/types/entity_callbacks.d.ts +1 -1
- package/dist/types/src/types/entity_views.d.ts +36 -1
- package/dist/types/src/types/index.d.ts +2 -0
- package/dist/types/src/types/plugins.d.ts +1 -1
- package/dist/types/src/types/properties.d.ts +24 -5
- package/dist/types/src/types/property_config.d.ts +6 -2
- package/dist/types/src/types/relations.d.ts +1 -1
- package/dist/types/src/types/translations.d.ts +8 -0
- package/dist/types/src/users/user.d.ts +5 -0
- package/package.json +26 -26
- package/src/api/errors.ts +1 -1
- package/src/api/graphql/graphql-schema-generator.ts +7 -0
- package/src/api/openapi-generator.ts +13 -1
- package/src/api/rest/api-generator-count.test.ts +14 -12
- package/src/api/rest/query-parser.ts +2 -20
- package/src/auth/adapter-middleware.ts +83 -0
- package/src/auth/admin-routes.ts +36 -43
- package/src/auth/auth-overrides.ts +172 -0
- package/src/auth/builtin-auth-adapter.ts +384 -0
- package/src/auth/crypto-utils.ts +31 -0
- package/src/auth/custom-auth-adapter.ts +85 -0
- package/src/auth/index.ts +10 -0
- package/src/auth/interfaces.ts +2 -0
- package/src/auth/jwt.ts +3 -1
- package/src/auth/middleware.ts +2 -46
- package/src/auth/rls-scope.ts +58 -0
- package/src/auth/routes.ts +74 -32
- package/src/cron/cron-scheduler.test.ts +9 -9
- package/src/cron/cron-scheduler.ts +1 -1
- package/src/env.ts +224 -0
- package/src/index.ts +4 -0
- package/src/init.ts +355 -135
- package/src/storage/routes.ts +1 -19
- package/src/utils/logging.ts +3 -3
- package/test/admin-routes.test.ts +10 -4
- package/test/auth-routes.test.ts +2 -2
- package/test/backend-hooks-admin.test.ts +32 -12
- package/test/custom-auth-adapter.test.ts +177 -0
- package/test/env.test.ts +138 -0
- package/test/query-parser.test.ts +0 -29
- package/tsconfig.json +3 -0
- package/dist/index-DXVBFp5V.js.map +0 -1
package/src/storage/routes.ts
CHANGED
|
@@ -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
|
-
|
|
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> = {};
|
package/src/utils/logging.ts
CHANGED
|
@@ -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
|
|
25
|
-
(global as
|
|
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
|
|
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.
|
|
230
|
-
|
|
230
|
+
mockAuthRepo.listUsersPaginated.mockResolvedValueOnce({
|
|
231
|
+
users: [
|
|
232
|
+
mockUser({ id: "u1",
|
|
231
233
|
email: "a@test.com" }),
|
|
232
|
-
|
|
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"]);
|
package/test/auth-routes.test.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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.
|
|
190
|
-
|
|
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.
|
|
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.
|
|
383
|
-
|
|
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
|
+
});
|
package/test/env.test.ts
ADDED
|
@@ -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
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index-DXVBFp5V.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}
|