@rebasepro/server-core 0.2.3 → 0.3.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/dist/common/src/collections/default-collections.d.ts +12 -0
- package/dist/common/src/collections/index.d.ts +1 -0
- package/dist/common/src/util/permissions.d.ts +1 -0
- package/dist/{index-BZoAtuqi.js → index-Cr1D21av.js} +11 -3
- package/dist/index-Cr1D21av.js.map +1 -0
- package/dist/index.es.js +2166 -208
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +2155 -193
- package/dist/index.umd.js.map +1 -1
- package/dist/server-core/src/api/logs-routes.d.ts +37 -0
- package/dist/server-core/src/api/rest/api-generator.d.ts +6 -0
- package/dist/server-core/src/api/types.d.ts +6 -1
- package/dist/server-core/src/auth/adapter-middleware.d.ts +7 -3
- package/dist/server-core/src/auth/admin-routes.d.ts +3 -3
- package/dist/server-core/src/auth/api-keys/api-key-middleware.d.ts +39 -0
- package/dist/server-core/src/auth/api-keys/api-key-permission-guard.d.ts +32 -0
- package/dist/server-core/src/auth/api-keys/api-key-routes.d.ts +20 -0
- package/dist/server-core/src/auth/api-keys/api-key-store.d.ts +35 -0
- package/dist/server-core/src/auth/api-keys/api-key-types.d.ts +88 -0
- package/dist/server-core/src/auth/api-keys/index.d.ts +17 -0
- package/dist/server-core/src/auth/{auth-overrides.d.ts → auth-hooks.d.ts} +69 -11
- package/dist/server-core/src/auth/builtin-auth-adapter.d.ts +3 -3
- package/dist/server-core/src/auth/index.d.ts +5 -3
- package/dist/server-core/src/auth/interfaces.d.ts +93 -3
- package/dist/server-core/src/auth/jwt.d.ts +3 -1
- package/dist/server-core/src/auth/mfa.d.ts +49 -0
- package/dist/server-core/src/auth/middleware.d.ts +7 -0
- package/dist/server-core/src/auth/rate-limiter.d.ts +19 -0
- package/dist/server-core/src/auth/routes.d.ts +3 -3
- package/dist/server-core/src/env.d.ts +6 -0
- package/dist/server-core/src/index.d.ts +1 -0
- package/dist/server-core/src/init.d.ts +3 -3
- package/dist/server-core/src/services/webhook-service.d.ts +29 -0
- package/dist/server-core/src/storage/image-transform.d.ts +48 -0
- package/dist/server-core/src/storage/index.d.ts +3 -0
- package/dist/server-core/src/storage/tus-handler.d.ts +51 -0
- package/dist/types/src/controllers/auth.d.ts +2 -24
- package/dist/types/src/controllers/client.d.ts +0 -3
- package/dist/types/src/controllers/collection_registry.d.ts +1 -1
- package/dist/types/src/controllers/data_driver.d.ts +18 -0
- package/dist/types/src/controllers/registry.d.ts +5 -4
- package/dist/types/src/rebase_context.d.ts +1 -1
- package/dist/types/src/types/auth_adapter.d.ts +2 -4
- package/dist/types/src/types/collections.d.ts +0 -4
- package/dist/types/src/types/component_ref.d.ts +1 -1
- package/dist/types/src/types/cron.d.ts +1 -1
- package/dist/types/src/types/entity_views.d.ts +1 -0
- package/dist/types/src/types/export_import.d.ts +1 -1
- package/dist/types/src/types/formex.d.ts +2 -2
- package/dist/types/src/types/properties.d.ts +2 -2
- package/dist/types/src/types/translations.d.ts +28 -12
- package/dist/types/src/types/user_management_delegate.d.ts +6 -4
- package/dist/types/src/users/roles.d.ts +0 -8
- package/package.json +8 -6
- package/src/api/ast-schema-editor.ts +4 -4
- package/src/api/errors.ts +16 -7
- package/src/api/logs-routes.ts +129 -0
- package/src/api/rest/api-generator.ts +42 -2
- package/src/api/rest/query-parser.ts +37 -1
- package/src/api/types.ts +6 -1
- package/src/auth/adapter-middleware.ts +20 -4
- package/src/auth/admin-routes.ts +39 -14
- package/src/auth/api-keys/api-key-middleware.ts +126 -0
- package/src/auth/api-keys/api-key-permission-guard.ts +64 -0
- package/src/auth/api-keys/api-key-routes.ts +183 -0
- package/src/auth/api-keys/api-key-store.ts +317 -0
- package/src/auth/api-keys/api-key-types.ts +94 -0
- package/src/auth/api-keys/index.ts +37 -0
- package/src/auth/{auth-overrides.ts → auth-hooks.ts} +81 -14
- package/src/auth/builtin-auth-adapter.ts +31 -19
- package/src/auth/index.ts +7 -3
- package/src/auth/interfaces.ts +111 -3
- package/src/auth/jwt.ts +19 -5
- package/src/auth/mfa.ts +160 -0
- package/src/auth/middleware.ts +20 -1
- package/src/auth/rate-limiter.ts +92 -0
- package/src/auth/routes.ts +455 -24
- package/src/cron/cron-loader.ts +5 -10
- package/src/cron/cron-scheduler.ts +11 -12
- package/src/cron/cron-store.ts +8 -7
- package/src/env.ts +2 -0
- package/src/functions/function-loader.ts +6 -9
- package/src/index.ts +1 -2
- package/src/init.ts +37 -7
- package/src/serve-spa.ts +5 -4
- package/src/services/webhook-service.ts +155 -0
- package/src/storage/image-transform.ts +202 -0
- package/src/storage/index.ts +3 -0
- package/src/storage/routes.ts +56 -3
- package/src/storage/tus-handler.ts +315 -0
- package/src/utils/dev-port.ts +14 -0
- package/src/utils/logging.ts +9 -7
- package/test/admin-routes.test.ts +74 -7
- package/test/api-generator.test.ts +0 -1
- package/test/api-key-permission-guard.test.ts +132 -0
- package/test/ast-schema-editor.test.ts +26 -0
- package/test/auth-routes.test.ts +1 -2
- package/test/backend-hooks-admin.test.ts +3 -4
- package/test/email-templates.test.ts +169 -0
- package/test/function-loader.test.ts +124 -0
- package/test/jwt.test.ts +4 -2
- package/test/mfa.test.ts +197 -0
- package/test/middleware.test.ts +10 -5
- package/test/webhook-service.test.ts +249 -0
- package/vite.config.ts +3 -2
- package/dist/index-BZoAtuqi.js.map +0 -1
- package/dist/server-core/src/bootstrappers/index.d.ts +0 -0
- package/src/bootstrappers/index.ts +0 -1
- package/src/singleton.test.ts +0 -28
package/test/mfa.test.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import {
|
|
2
|
+
base32Encode,
|
|
3
|
+
base32Decode,
|
|
4
|
+
generateTotp,
|
|
5
|
+
verifyTotp,
|
|
6
|
+
generateTotpSecret,
|
|
7
|
+
generateRecoveryCodes,
|
|
8
|
+
hashRecoveryCode
|
|
9
|
+
} from "../src/auth/mfa";
|
|
10
|
+
|
|
11
|
+
describe("base32Encode / base32Decode", () => {
|
|
12
|
+
it("round-trips a known buffer", () => {
|
|
13
|
+
const original = Buffer.from("Hello, World!");
|
|
14
|
+
const encoded = base32Encode(original);
|
|
15
|
+
const decoded = base32Decode(encoded);
|
|
16
|
+
expect(decoded.toString()).toBe("Hello, World!");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("encodes known test vectors (RFC 4648)", () => {
|
|
20
|
+
// RFC 4648 test vectors
|
|
21
|
+
expect(base32Encode(Buffer.from(""))).toBe("");
|
|
22
|
+
expect(base32Encode(Buffer.from("f"))).toBe("MY");
|
|
23
|
+
expect(base32Encode(Buffer.from("fo"))).toBe("MZXQ");
|
|
24
|
+
expect(base32Encode(Buffer.from("foo"))).toBe("MZXW6");
|
|
25
|
+
expect(base32Encode(Buffer.from("foob"))).toBe("MZXW6YQ");
|
|
26
|
+
expect(base32Encode(Buffer.from("fooba"))).toBe("MZXW6YTB");
|
|
27
|
+
expect(base32Encode(Buffer.from("foobar"))).toBe("MZXW6YTBOI");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("decodes base32 with trailing padding characters", () => {
|
|
31
|
+
const decoded = base32Decode("MZXW6===");
|
|
32
|
+
expect(decoded.toString()).toBe("foo");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("is case-insensitive when decoding", () => {
|
|
36
|
+
const upper = base32Decode("MZXW6YTBOI");
|
|
37
|
+
const lower = base32Decode("mzxw6ytboi");
|
|
38
|
+
expect(upper.equals(lower)).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("handles single byte", () => {
|
|
42
|
+
const buf = Buffer.from([0xff]);
|
|
43
|
+
const encoded = base32Encode(buf);
|
|
44
|
+
const decoded = base32Decode(encoded);
|
|
45
|
+
expect(decoded[0]).toBe(0xff);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("round-trips random 20-byte secret", () => {
|
|
49
|
+
const original = Buffer.from("abcdefghij1234567890");
|
|
50
|
+
const encoded = base32Encode(original);
|
|
51
|
+
const decoded = base32Decode(encoded);
|
|
52
|
+
expect(decoded.toString()).toBe(original.toString());
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("generateTotp", () => {
|
|
57
|
+
it("returns a 6-digit string", () => {
|
|
58
|
+
const secret = Buffer.from("12345678901234567890");
|
|
59
|
+
const code = generateTotp(secret);
|
|
60
|
+
expect(code).toMatch(/^\d{6}$/);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("returns consistent results for the same time step", () => {
|
|
64
|
+
const secret = Buffer.from("test-secret-1234567");
|
|
65
|
+
const code1 = generateTotp(secret);
|
|
66
|
+
const code2 = generateTotp(secret);
|
|
67
|
+
expect(code1).toBe(code2);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("pads short codes with leading zeros", () => {
|
|
71
|
+
// Generate many codes to increase chance of seeing leading zeros behavior
|
|
72
|
+
const secret = Buffer.from("12345678901234567890");
|
|
73
|
+
const code = generateTotp(secret);
|
|
74
|
+
expect(code.length).toBe(6);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("verifyTotp", () => {
|
|
79
|
+
it("verifies a freshly generated token", () => {
|
|
80
|
+
const secret = Buffer.from("12345678901234567890");
|
|
81
|
+
const token = generateTotp(secret);
|
|
82
|
+
expect(verifyTotp(secret, token)).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("rejects an incorrect token", () => {
|
|
86
|
+
const secret = Buffer.from("12345678901234567890");
|
|
87
|
+
expect(verifyTotp(secret, "000000")).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("rejects token from different secret", () => {
|
|
91
|
+
const secret1 = Buffer.from("secret-one-12345678");
|
|
92
|
+
const secret2 = Buffer.from("secret-two-12345678");
|
|
93
|
+
const token = generateTotp(secret1);
|
|
94
|
+
expect(verifyTotp(secret2, token)).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("uses window parameter to check adjacent time steps", () => {
|
|
98
|
+
const secret = Buffer.from("12345678901234567890");
|
|
99
|
+
const token = generateTotp(secret);
|
|
100
|
+
// Window of 0 should still pass for current step
|
|
101
|
+
expect(verifyTotp(secret, token, 0)).toBe(true);
|
|
102
|
+
// Window of 2 should also pass for current step
|
|
103
|
+
expect(verifyTotp(secret, token, 2)).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("generateTotpSecret", () => {
|
|
108
|
+
it("returns a base32 secret and otpauth URI", () => {
|
|
109
|
+
const result = generateTotpSecret("MyApp", "user@example.com");
|
|
110
|
+
expect(result.secret).toBeTruthy();
|
|
111
|
+
expect(result.uri).toBeTruthy();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("generates a valid otpauth URI format", () => {
|
|
115
|
+
const result = generateTotpSecret("MyApp", "user@example.com");
|
|
116
|
+
expect(result.uri).toMatch(/^otpauth:\/\/totp\//);
|
|
117
|
+
expect(result.uri).toContain("secret=");
|
|
118
|
+
expect(result.uri).toContain("issuer=MyApp");
|
|
119
|
+
expect(result.uri).toContain("algorithm=SHA1");
|
|
120
|
+
expect(result.uri).toContain("digits=6");
|
|
121
|
+
expect(result.uri).toContain("period=30");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("encodes special characters in issuer and account", () => {
|
|
125
|
+
const result = generateTotpSecret("My App & Co.", "user+test@example.com");
|
|
126
|
+
expect(result.uri).toContain(encodeURIComponent("My App & Co."));
|
|
127
|
+
expect(result.uri).toContain(encodeURIComponent("user+test@example.com"));
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("generates unique secrets on each call", () => {
|
|
131
|
+
const result1 = generateTotpSecret("App", "user@test.com");
|
|
132
|
+
const result2 = generateTotpSecret("App", "user@test.com");
|
|
133
|
+
expect(result1.secret).not.toBe(result2.secret);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("secret can be decoded back to a 20-byte buffer", () => {
|
|
137
|
+
const result = generateTotpSecret("App", "user@test.com");
|
|
138
|
+
const decoded = base32Decode(result.secret);
|
|
139
|
+
expect(decoded.length).toBe(20);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("generateRecoveryCodes", () => {
|
|
144
|
+
it("generates 10 codes by default", () => {
|
|
145
|
+
const codes = generateRecoveryCodes();
|
|
146
|
+
expect(codes).toHaveLength(10);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("generates the specified number of codes", () => {
|
|
150
|
+
const codes = generateRecoveryCodes(5);
|
|
151
|
+
expect(codes).toHaveLength(5);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("generates codes in XXXXX-XXXXX format", () => {
|
|
155
|
+
const codes = generateRecoveryCodes();
|
|
156
|
+
for (const code of codes) {
|
|
157
|
+
expect(code).toMatch(/^[A-F0-9]{5}-[A-F0-9]{5}$/);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("generates unique codes", () => {
|
|
162
|
+
const codes = generateRecoveryCodes(20);
|
|
163
|
+
const unique = new Set(codes);
|
|
164
|
+
expect(unique.size).toBe(codes.length);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("hashRecoveryCode", () => {
|
|
169
|
+
it("returns a hex string", () => {
|
|
170
|
+
const hash = hashRecoveryCode("ABC12-DEF34");
|
|
171
|
+
expect(hash).toMatch(/^[a-f0-9]{64}$/);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("is deterministic", () => {
|
|
175
|
+
const hash1 = hashRecoveryCode("ABC12-DEF34");
|
|
176
|
+
const hash2 = hashRecoveryCode("ABC12-DEF34");
|
|
177
|
+
expect(hash1).toBe(hash2);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("strips dashes before hashing", () => {
|
|
181
|
+
const withDashes = hashRecoveryCode("ABC12-DEF34");
|
|
182
|
+
const withoutDashes = hashRecoveryCode("ABC12DEF34");
|
|
183
|
+
expect(withDashes).toBe(withoutDashes);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("normalizes to uppercase before hashing", () => {
|
|
187
|
+
const upper = hashRecoveryCode("ABC12-DEF34");
|
|
188
|
+
const lower = hashRecoveryCode("abc12-def34");
|
|
189
|
+
expect(upper).toBe(lower);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("different codes produce different hashes", () => {
|
|
193
|
+
const hash1 = hashRecoveryCode("ABC12-DEF34");
|
|
194
|
+
const hash2 = hashRecoveryCode("XYZ98-UVW76");
|
|
195
|
+
expect(hash1).not.toBe(hash2);
|
|
196
|
+
});
|
|
197
|
+
});
|
package/test/middleware.test.ts
CHANGED
|
@@ -67,7 +67,8 @@ describe("Auth Middleware", () => {
|
|
|
67
67
|
expect(nextFn).toHaveBeenCalled();
|
|
68
68
|
expect(getUser()).toEqual({
|
|
69
69
|
userId: "user-123",
|
|
70
|
-
roles: ["admin", "editor"]
|
|
70
|
+
roles: ["admin", "editor"],
|
|
71
|
+
aal: "aal1"
|
|
71
72
|
});
|
|
72
73
|
});
|
|
73
74
|
|
|
@@ -144,7 +145,8 @@ describe("Auth Middleware", () => {
|
|
|
144
145
|
expect(nextFn).toHaveBeenCalled();
|
|
145
146
|
expect(getUser()).toEqual({
|
|
146
147
|
userId: "user-123",
|
|
147
|
-
roles: ["viewer"]
|
|
148
|
+
roles: ["viewer"],
|
|
149
|
+
aal: "aal1"
|
|
148
150
|
});
|
|
149
151
|
});
|
|
150
152
|
});
|
|
@@ -159,7 +161,8 @@ describe("Auth Middleware", () => {
|
|
|
159
161
|
expect(nextFn).toHaveBeenCalled();
|
|
160
162
|
expect(getUser()).toEqual({
|
|
161
163
|
userId: "user-456",
|
|
162
|
-
roles: ["viewer"]
|
|
164
|
+
roles: ["viewer"],
|
|
165
|
+
aal: "aal1"
|
|
163
166
|
});
|
|
164
167
|
});
|
|
165
168
|
|
|
@@ -273,7 +276,8 @@ roles: ["schema-adminstration", "admins", "admin "] } });
|
|
|
273
276
|
|
|
274
277
|
expect(payload).toEqual({
|
|
275
278
|
userId: "ws-user-123",
|
|
276
|
-
roles: ["admin"]
|
|
279
|
+
roles: ["admin"],
|
|
280
|
+
aal: "aal1"
|
|
277
281
|
});
|
|
278
282
|
});
|
|
279
283
|
|
|
@@ -293,7 +297,8 @@ roles: ["schema-adminstration", "admins", "admin "] } });
|
|
|
293
297
|
|
|
294
298
|
expect(payload).toEqual({
|
|
295
299
|
userId: "user-no-roles",
|
|
296
|
-
roles: []
|
|
300
|
+
roles: [],
|
|
301
|
+
aal: "aal1"
|
|
297
302
|
});
|
|
298
303
|
});
|
|
299
304
|
});
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { WebhookDispatcher, WebhookConfig } from "../src/services/webhook-service";
|
|
2
|
+
import { createHmac } from "crypto";
|
|
3
|
+
|
|
4
|
+
// Mock global fetch
|
|
5
|
+
const mockFetch = jest.fn();
|
|
6
|
+
global.fetch = mockFetch as unknown as typeof fetch;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
mockFetch.mockReset();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
function makeWebhook(overrides: Partial<WebhookConfig> = {}): WebhookConfig {
|
|
13
|
+
return {
|
|
14
|
+
id: "wh_1",
|
|
15
|
+
url: "https://example.com/webhook",
|
|
16
|
+
events: ["INSERT", "UPDATE", "DELETE"],
|
|
17
|
+
table: "users",
|
|
18
|
+
enabled: true,
|
|
19
|
+
...overrides
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("WebhookDispatcher", () => {
|
|
24
|
+
describe("setWebhooks", () => {
|
|
25
|
+
it("filters out disabled webhooks", () => {
|
|
26
|
+
const dispatcher = new WebhookDispatcher();
|
|
27
|
+
dispatcher.setWebhooks([
|
|
28
|
+
makeWebhook({ id: "wh_1", enabled: true }),
|
|
29
|
+
makeWebhook({ id: "wh_2", enabled: false }),
|
|
30
|
+
makeWebhook({ id: "wh_3", enabled: true })
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
// Disabled webhook should not trigger
|
|
34
|
+
mockFetch.mockResolvedValue({
|
|
35
|
+
status: 200,
|
|
36
|
+
text: () => Promise.resolve("OK")
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// This should only match enabled webhooks
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("onEntityChange", () => {
|
|
44
|
+
it("returns empty array when no webhooks match", async () => {
|
|
45
|
+
const dispatcher = new WebhookDispatcher();
|
|
46
|
+
dispatcher.setWebhooks([makeWebhook({ table: "posts" })]);
|
|
47
|
+
|
|
48
|
+
const results = await dispatcher.onEntityChange(
|
|
49
|
+
"users", "INSERT", "id_1", { id: "id_1", name: "Test" }
|
|
50
|
+
);
|
|
51
|
+
expect(results).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("matches webhooks by table AND event", async () => {
|
|
55
|
+
const dispatcher = new WebhookDispatcher();
|
|
56
|
+
dispatcher.setWebhooks([makeWebhook({ table: "users", events: ["INSERT"] })]);
|
|
57
|
+
|
|
58
|
+
mockFetch.mockResolvedValue({
|
|
59
|
+
status: 200,
|
|
60
|
+
text: () => Promise.resolve("OK")
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const results = await dispatcher.onEntityChange(
|
|
64
|
+
"users", "INSERT", "id_1", { id: "id_1" }
|
|
65
|
+
);
|
|
66
|
+
expect(results).toHaveLength(1);
|
|
67
|
+
expect(results[0].success).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("does not match different table", async () => {
|
|
71
|
+
const dispatcher = new WebhookDispatcher();
|
|
72
|
+
dispatcher.setWebhooks([makeWebhook({ table: "users" })]);
|
|
73
|
+
|
|
74
|
+
const results = await dispatcher.onEntityChange(
|
|
75
|
+
"posts", "INSERT", "id_1", { id: "id_1" }
|
|
76
|
+
);
|
|
77
|
+
expect(results).toEqual([]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("does not match different event", async () => {
|
|
81
|
+
const dispatcher = new WebhookDispatcher();
|
|
82
|
+
dispatcher.setWebhooks([makeWebhook({ events: ["INSERT"] })]);
|
|
83
|
+
|
|
84
|
+
const results = await dispatcher.onEntityChange(
|
|
85
|
+
"users", "DELETE", "id_1", { id: "id_1" }
|
|
86
|
+
);
|
|
87
|
+
expect(results).toEqual([]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("includes HMAC signature when webhook has secret", async () => {
|
|
91
|
+
const secret = "my-webhook-secret";
|
|
92
|
+
const dispatcher = new WebhookDispatcher();
|
|
93
|
+
dispatcher.setWebhooks([makeWebhook({ secret })]);
|
|
94
|
+
|
|
95
|
+
mockFetch.mockResolvedValue({
|
|
96
|
+
status: 200,
|
|
97
|
+
text: () => Promise.resolve("OK")
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
await dispatcher.onEntityChange(
|
|
101
|
+
"users", "INSERT", "id_1", { id: "id_1" }
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
105
|
+
const [, options] = mockFetch.mock.calls[0];
|
|
106
|
+
const headers = options.headers as Record<string, string>;
|
|
107
|
+
expect(headers["X-Webhook-Signature"]).toBeDefined();
|
|
108
|
+
expect(headers["X-Webhook-Signature"]).toMatch(/^sha256=/);
|
|
109
|
+
|
|
110
|
+
// Verify the signature is correct
|
|
111
|
+
const body = options.body as string;
|
|
112
|
+
const expectedSig = createHmac("sha256", secret).update(body).digest("hex");
|
|
113
|
+
expect(headers["X-Webhook-Signature"]).toBe(`sha256=${expectedSig}`);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("does not include signature header when no secret", async () => {
|
|
117
|
+
const dispatcher = new WebhookDispatcher();
|
|
118
|
+
dispatcher.setWebhooks([makeWebhook({ secret: undefined })]);
|
|
119
|
+
|
|
120
|
+
mockFetch.mockResolvedValue({
|
|
121
|
+
status: 200,
|
|
122
|
+
text: () => Promise.resolve("OK")
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await dispatcher.onEntityChange(
|
|
126
|
+
"users", "INSERT", "id_1", { id: "id_1" }
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const [, options] = mockFetch.mock.calls[0];
|
|
130
|
+
const headers = options.headers as Record<string, string>;
|
|
131
|
+
expect(headers["X-Webhook-Signature"]).toBeUndefined();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("payload includes type, table, record, and timestamp", async () => {
|
|
135
|
+
const dispatcher = new WebhookDispatcher();
|
|
136
|
+
dispatcher.setWebhooks([makeWebhook()]);
|
|
137
|
+
|
|
138
|
+
mockFetch.mockResolvedValue({
|
|
139
|
+
status: 200,
|
|
140
|
+
text: () => Promise.resolve("OK")
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const entity = { id: "id_1", name: "Test User" };
|
|
144
|
+
await dispatcher.onEntityChange("users", "INSERT", "id_1", entity);
|
|
145
|
+
|
|
146
|
+
const [, options] = mockFetch.mock.calls[0];
|
|
147
|
+
const payload = JSON.parse(options.body as string);
|
|
148
|
+
expect(payload.type).toBe("INSERT");
|
|
149
|
+
expect(payload.table).toBe("users");
|
|
150
|
+
expect(payload.record).toEqual(entity);
|
|
151
|
+
expect(payload.timestamp).toBeTruthy();
|
|
152
|
+
expect(payload.schema).toBe("public");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("UPDATE payload includes old_record", async () => {
|
|
156
|
+
const dispatcher = new WebhookDispatcher();
|
|
157
|
+
dispatcher.setWebhooks([makeWebhook()]);
|
|
158
|
+
|
|
159
|
+
mockFetch.mockResolvedValue({
|
|
160
|
+
status: 200,
|
|
161
|
+
text: () => Promise.resolve("OK")
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const entity = { id: "id_1", name: "Updated" };
|
|
165
|
+
const previous = { id: "id_1", name: "Original" };
|
|
166
|
+
await dispatcher.onEntityChange("users", "UPDATE", "id_1", entity, previous);
|
|
167
|
+
|
|
168
|
+
const [, options] = mockFetch.mock.calls[0];
|
|
169
|
+
const payload = JSON.parse(options.body as string);
|
|
170
|
+
expect(payload.old_record).toEqual(previous);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("returns success: false on fetch failure", async () => {
|
|
174
|
+
const dispatcher = new WebhookDispatcher();
|
|
175
|
+
dispatcher.setWebhooks([makeWebhook()]);
|
|
176
|
+
|
|
177
|
+
mockFetch.mockRejectedValue(new Error("Network error"));
|
|
178
|
+
|
|
179
|
+
const results = await dispatcher.onEntityChange(
|
|
180
|
+
"users", "INSERT", "id_1", { id: "id_1" }
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
// After retries, last result should be failure
|
|
184
|
+
expect(results).toHaveLength(1);
|
|
185
|
+
expect(results[0].success).toBe(false);
|
|
186
|
+
expect(results[0].responseBody).toContain("Network error");
|
|
187
|
+
}, 30000);
|
|
188
|
+
|
|
189
|
+
it("returns success: false for non-2xx status codes", async () => {
|
|
190
|
+
const dispatcher = new WebhookDispatcher();
|
|
191
|
+
dispatcher.setWebhooks([makeWebhook()]);
|
|
192
|
+
|
|
193
|
+
mockFetch.mockResolvedValue({
|
|
194
|
+
status: 500,
|
|
195
|
+
text: () => Promise.resolve("Internal Server Error")
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const results = await dispatcher.onEntityChange(
|
|
199
|
+
"users", "INSERT", "id_1", { id: "id_1" }
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
expect(results).toHaveLength(1);
|
|
203
|
+
expect(results[0].success).toBe(false);
|
|
204
|
+
expect(results[0].statusCode).toBe(500);
|
|
205
|
+
}, 30000);
|
|
206
|
+
|
|
207
|
+
it("includes custom headers from webhook config", async () => {
|
|
208
|
+
const dispatcher = new WebhookDispatcher();
|
|
209
|
+
dispatcher.setWebhooks([
|
|
210
|
+
makeWebhook({
|
|
211
|
+
headers: { "X-Custom": "my-value", "Authorization": "Bearer token" }
|
|
212
|
+
})
|
|
213
|
+
]);
|
|
214
|
+
|
|
215
|
+
mockFetch.mockResolvedValue({
|
|
216
|
+
status: 200,
|
|
217
|
+
text: () => Promise.resolve("OK")
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
await dispatcher.onEntityChange("users", "INSERT", "id_1", { id: "id_1" });
|
|
221
|
+
|
|
222
|
+
const [, options] = mockFetch.mock.calls[0];
|
|
223
|
+
const headers = options.headers as Record<string, string>;
|
|
224
|
+
expect(headers["X-Custom"]).toBe("my-value");
|
|
225
|
+
expect(headers["Authorization"]).toBe("Bearer token");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("dispatches to multiple matching webhooks", async () => {
|
|
229
|
+
const dispatcher = new WebhookDispatcher();
|
|
230
|
+
dispatcher.setWebhooks([
|
|
231
|
+
makeWebhook({ id: "wh_1", url: "https://a.com/hook" }),
|
|
232
|
+
makeWebhook({ id: "wh_2", url: "https://b.com/hook" })
|
|
233
|
+
]);
|
|
234
|
+
|
|
235
|
+
mockFetch.mockResolvedValue({
|
|
236
|
+
status: 200,
|
|
237
|
+
text: () => Promise.resolve("OK")
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const results = await dispatcher.onEntityChange(
|
|
241
|
+
"users", "INSERT", "id_1", { id: "id_1" }
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
expect(results).toHaveLength(2);
|
|
245
|
+
expect(results[0].webhookId).toBe("wh_1");
|
|
246
|
+
expect(results[1].webhookId).toBe("wh_2");
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
});
|
package/vite.config.ts
CHANGED
|
@@ -24,7 +24,8 @@ const CONSUMER_EXTERNALS = [
|
|
|
24
24
|
"chokidar",
|
|
25
25
|
"fsevents",
|
|
26
26
|
"ws",
|
|
27
|
-
"ts-morph"
|
|
27
|
+
"ts-morph",
|
|
28
|
+
"sharp"
|
|
28
29
|
];
|
|
29
30
|
const isExternal = (id: string) => {
|
|
30
31
|
if (id.startsWith(".") || path.isAbsolute(id)) return false;
|
|
@@ -33,7 +34,7 @@ const isExternal = (id: string) => {
|
|
|
33
34
|
// Externalize only deps the consumer app explicitly installs
|
|
34
35
|
if (CONSUMER_EXTERNALS.some(ext => id === ext || id.startsWith(ext + "/"))) return true;
|
|
35
36
|
// Externalize Node built-ins
|
|
36
|
-
if (["fs", "path", "url", "util", "crypto", "http", "https", "net", "tls", "stream", "events", "os", "child_process", "buffer", "assert", "node:"].some(b => id === b || id.startsWith("node:") || id.startsWith(b + "/"))) return true;
|
|
37
|
+
if (["fs", "path", "url", "util", "crypto", "http", "https", "net", "tls", "stream", "events", "os", "child_process", "buffer", "assert", "dns", "zlib", "querystring", "node:"].some(b => id === b || id.startsWith("node:") || id.startsWith(b + "/"))) return true;
|
|
37
38
|
// Inline everything else (jsonwebtoken, ws, zod, etc.)
|
|
38
39
|
return false;
|
|
39
40
|
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index-BZoAtuqi.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}
|
|
File without changes
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
// PostgresBootstrapper was moved to @rebasepro/server-postgresql
|
package/src/singleton.test.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { rebase, _initRebase } from "./singleton";
|
|
2
|
-
import type { RebaseClient } from "@rebasepro/types";
|
|
3
|
-
|
|
4
|
-
describe("rebase singleton", () => {
|
|
5
|
-
beforeEach(() => {
|
|
6
|
-
// Reset the singleton before each test
|
|
7
|
-
// By calling _initRebase with null (cast to bypass type checking for the reset)
|
|
8
|
-
_initRebase(null as unknown as RebaseClient);
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it("should throw an error if accessed before initialization", () => {
|
|
12
|
-
expect(() => rebase.data).toThrow(
|
|
13
|
-
"rebase.data: server not initialized yet. The singleton is available after Rebase starts — don't call it at import time."
|
|
14
|
-
);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("should return the correctly initialized instance properties", () => {
|
|
18
|
-
const mockClient = {
|
|
19
|
-
data: { test: "mockData" },
|
|
20
|
-
auth: { test: "mockAuth" }
|
|
21
|
-
} as unknown as RebaseClient;
|
|
22
|
-
|
|
23
|
-
_initRebase(mockClient);
|
|
24
|
-
|
|
25
|
-
expect(rebase.data).toEqual({ test: "mockData" });
|
|
26
|
-
expect(rebase.auth).toEqual({ test: "mockAuth" });
|
|
27
|
-
});
|
|
28
|
-
});
|