@sigil-dev/grimoire 0.3.0 → 0.5.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/README.md +174 -1
- package/index.ts +29 -22
- package/package.json +1 -1
- package/public/__grimoire__/client.js +5 -36
- package/public/__grimoire__/hydrate.js +7 -45
- package/src/build.ts +88 -0
- package/src/client-router.ts +1 -1
- package/src/cookie-utils.ts +66 -66
- package/src/enhance.ts +1 -1
- package/src/hydrate.ts +1 -1
- package/src/plugins.ts +19 -5
- package/src/renderer.ts +38 -6
- package/src/router.ts +2 -2
- package/src/scanner.ts +20 -0
- package/src/server.ts +83 -83
- package/src/ssrPlugin.ts +7 -2
- package/src/transform-routes.ts +6 -1
- package/src/typegen.ts +77 -3
- package/src/types.ts +45 -1
- package/test/exports.test.ts +38 -0
- package/test/fail.test.ts +46 -46
- package/test/headers.test.ts +96 -96
- package/test/middleware.test.ts +217 -217
- package/test/redirect-error.test.ts +112 -112
- package/test/rendering.test.ts +310 -172
- package/test/scanning.test.ts +87 -2
- package/test/server.test.ts +178 -8
- package/test/streaming.test.ts +132 -132
- package/test/typegen.test.ts +6 -6
- package/.grimoire/_routes.dom.js +0 -4
- package/.grimoire/_routes.hydrate.js +0 -4
- package/.grimoire/tsconfig.generated.json +0 -11
- package/.grimoire/types/ambient.d.ts +0 -6
- package/.grimoire/types/api/hello/$types.d.ts +0 -29
package/test/fail.test.ts
CHANGED
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { fail, isFailResult } from "../src/fail";
|
|
3
|
-
|
|
4
|
-
describe("fail()", () => {
|
|
5
|
-
test("returns a FailResult with status and data", () => {
|
|
6
|
-
const result = fail(400, { name: "required" });
|
|
7
|
-
expect(result).toEqual({
|
|
8
|
-
__fail: true,
|
|
9
|
-
status: 400,
|
|
10
|
-
data: { name: "required" },
|
|
11
|
-
});
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
test("isFailResult returns true for fail()", () => {
|
|
15
|
-
const result = fail(422, { errors: { email: "invalid" } });
|
|
16
|
-
expect(isFailResult(result)).toBe(true);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test("isFailResult returns false for plain objects", () => {
|
|
20
|
-
expect(isFailResult({ redirect: "/foo" })).toBe(false);
|
|
21
|
-
expect(isFailResult(null)).toBe(false);
|
|
22
|
-
expect(isFailResult(undefined)).toBe(false);
|
|
23
|
-
expect(isFailResult("string")).toBe(false);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test("preserves complex data structures", () => {
|
|
27
|
-
const data = {
|
|
28
|
-
formData: { email: "test@example.com", name: "" },
|
|
29
|
-
errors: {
|
|
30
|
-
name: "Name is required",
|
|
31
|
-
email: null,
|
|
32
|
-
},
|
|
33
|
-
timestamp: Date.now(),
|
|
34
|
-
};
|
|
35
|
-
const result = fail(400, data);
|
|
36
|
-
expect(result.data).toEqual(data);
|
|
37
|
-
expect(result.status).toBe(400);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test("works with different status codes", () => {
|
|
41
|
-
expect(fail(400, {}).status).toBe(400);
|
|
42
|
-
expect(fail(403, {}).status).toBe(403);
|
|
43
|
-
expect(fail(422, {}).status).toBe(422);
|
|
44
|
-
expect(fail(500, {}).status).toBe(500);
|
|
45
|
-
});
|
|
46
|
-
});
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { fail, isFailResult } from "../src/fail";
|
|
3
|
+
|
|
4
|
+
describe("fail()", () => {
|
|
5
|
+
test("returns a FailResult with status and data", () => {
|
|
6
|
+
const result = fail(400, { name: "required" });
|
|
7
|
+
expect(result).toEqual({
|
|
8
|
+
__fail: true,
|
|
9
|
+
status: 400,
|
|
10
|
+
data: { name: "required" },
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("isFailResult returns true for fail()", () => {
|
|
15
|
+
const result = fail(422, { errors: { email: "invalid" } });
|
|
16
|
+
expect(isFailResult(result)).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("isFailResult returns false for plain objects", () => {
|
|
20
|
+
expect(isFailResult({ redirect: "/foo" })).toBe(false);
|
|
21
|
+
expect(isFailResult(null)).toBe(false);
|
|
22
|
+
expect(isFailResult(undefined)).toBe(false);
|
|
23
|
+
expect(isFailResult("string")).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("preserves complex data structures", () => {
|
|
27
|
+
const data = {
|
|
28
|
+
formData: { email: "test@example.com", name: "" },
|
|
29
|
+
errors: {
|
|
30
|
+
name: "Name is required",
|
|
31
|
+
email: null,
|
|
32
|
+
},
|
|
33
|
+
timestamp: Date.now(),
|
|
34
|
+
};
|
|
35
|
+
const result = fail(400, data);
|
|
36
|
+
expect(result.data).toEqual(data);
|
|
37
|
+
expect(result.status).toBe(400);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("works with different status codes", () => {
|
|
41
|
+
expect(fail(400, {}).status).toBe(400);
|
|
42
|
+
expect(fail(403, {}).status).toBe(403);
|
|
43
|
+
expect(fail(422, {}).status).toBe(422);
|
|
44
|
+
expect(fail(500, {}).status).toBe(500);
|
|
45
|
+
});
|
|
46
|
+
});
|
package/test/headers.test.ts
CHANGED
|
@@ -1,96 +1,96 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { securityHeaders } from "../src/headers";
|
|
3
|
-
|
|
4
|
-
function fakeRequest(path = "/") {
|
|
5
|
-
return new Request(`http://localhost${path}`);
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
async function runPlugin(
|
|
9
|
-
config: Parameters<typeof securityHeaders>[0],
|
|
10
|
-
req?: Request,
|
|
11
|
-
): Promise<Headers> {
|
|
12
|
-
const plugin = securityHeaders(config);
|
|
13
|
-
const res = new Response("<html></html>", {
|
|
14
|
-
headers: { "Content-Type": "text/html" },
|
|
15
|
-
});
|
|
16
|
-
const result = await plugin.onRequest!(req ?? fakeRequest(), async () => res);
|
|
17
|
-
return result.headers;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
describe("securityHeaders()", () => {
|
|
21
|
-
test("applies default headers", async () => {
|
|
22
|
-
const headers = await runPlugin({});
|
|
23
|
-
expect(headers.get("X-Content-Type-Options")).toBe("nosniff");
|
|
24
|
-
expect(headers.get("X-Frame-Options")).toBe("DENY");
|
|
25
|
-
expect(headers.get("Referrer-Policy")).toBe("strict-origin-when-cross-origin");
|
|
26
|
-
expect(headers.get("Permissions-Policy")).toBe(
|
|
27
|
-
"camera=(), microphone=(), geolocation=()",
|
|
28
|
-
);
|
|
29
|
-
expect(headers.get("X-XSS-Protection")).toBe("0");
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test("applies CSP by default", async () => {
|
|
33
|
-
const headers = await runPlugin({});
|
|
34
|
-
expect(headers.get("Content-Security-Policy")).toContain("default-src 'self'");
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test("allows overriding individual headers", async () => {
|
|
38
|
-
const headers = await runPlugin({
|
|
39
|
-
frameOptions: "SAMEORIGIN",
|
|
40
|
-
contentTypeOptions: false,
|
|
41
|
-
});
|
|
42
|
-
expect(headers.get("X-Frame-Options")).toBe("SAMEORIGIN");
|
|
43
|
-
expect(headers.has("X-Content-Type-Options")).toBe(false);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("disables headers when set to false", async () => {
|
|
47
|
-
const headers = await runPlugin({
|
|
48
|
-
contentSecurityPolicy: false,
|
|
49
|
-
strictTransportSecurity: false,
|
|
50
|
-
permissionsPolicy: false,
|
|
51
|
-
});
|
|
52
|
-
expect(headers.has("Content-Security-Policy")).toBe(false);
|
|
53
|
-
expect(headers.has("Strict-Transport-Security")).toBe(false);
|
|
54
|
-
expect(headers.has("Permissions-Policy")).toBe(false);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("applies route overrides", async () => {
|
|
58
|
-
const headers = await runPlugin(
|
|
59
|
-
{
|
|
60
|
-
frameOptions: "DENY",
|
|
61
|
-
routes: {
|
|
62
|
-
"/admin": { frameOptions: "SAMEORIGIN" },
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
fakeRequest("/admin/settings"),
|
|
66
|
-
);
|
|
67
|
-
expect(headers.get("X-Frame-Options")).toBe("SAMEORIGIN");
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test("route override only applies to matching prefix", async () => {
|
|
71
|
-
const headers = await runPlugin(
|
|
72
|
-
{
|
|
73
|
-
frameOptions: "DENY",
|
|
74
|
-
routes: {
|
|
75
|
-
"/admin": { frameOptions: "SAMEORIGIN" },
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
fakeRequest("/dashboard"),
|
|
79
|
-
);
|
|
80
|
-
expect(headers.get("X-Frame-Options")).toBe("DENY");
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test("preserves existing response headers", async () => {
|
|
84
|
-
const plugin = securityHeaders({});
|
|
85
|
-
const res = new Response("<html></html>", {
|
|
86
|
-
headers: {
|
|
87
|
-
"Content-Type": "text/html",
|
|
88
|
-
"Set-Cookie": "session=abc",
|
|
89
|
-
},
|
|
90
|
-
});
|
|
91
|
-
const result = await plugin.onRequest!(fakeRequest(), async () => res);
|
|
92
|
-
expect(result.headers.get("Content-Type")).toBe("text/html");
|
|
93
|
-
expect(result.headers.get("Set-Cookie")).toBe("session=abc");
|
|
94
|
-
expect(result.headers.get("X-Frame-Options")).toBe("DENY");
|
|
95
|
-
});
|
|
96
|
-
});
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { securityHeaders } from "../src/headers";
|
|
3
|
+
|
|
4
|
+
function fakeRequest(path = "/") {
|
|
5
|
+
return new Request(`http://localhost${path}`);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async function runPlugin(
|
|
9
|
+
config: Parameters<typeof securityHeaders>[0],
|
|
10
|
+
req?: Request,
|
|
11
|
+
): Promise<Headers> {
|
|
12
|
+
const plugin = securityHeaders(config);
|
|
13
|
+
const res = new Response("<html></html>", {
|
|
14
|
+
headers: { "Content-Type": "text/html" },
|
|
15
|
+
});
|
|
16
|
+
const result = await plugin.onRequest!(req ?? fakeRequest(), async () => res);
|
|
17
|
+
return result.headers;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("securityHeaders()", () => {
|
|
21
|
+
test("applies default headers", async () => {
|
|
22
|
+
const headers = await runPlugin({});
|
|
23
|
+
expect(headers.get("X-Content-Type-Options")).toBe("nosniff");
|
|
24
|
+
expect(headers.get("X-Frame-Options")).toBe("DENY");
|
|
25
|
+
expect(headers.get("Referrer-Policy")).toBe("strict-origin-when-cross-origin");
|
|
26
|
+
expect(headers.get("Permissions-Policy")).toBe(
|
|
27
|
+
"camera=(), microphone=(), geolocation=()",
|
|
28
|
+
);
|
|
29
|
+
expect(headers.get("X-XSS-Protection")).toBe("0");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("applies CSP by default", async () => {
|
|
33
|
+
const headers = await runPlugin({});
|
|
34
|
+
expect(headers.get("Content-Security-Policy")).toContain("default-src 'self'");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("allows overriding individual headers", async () => {
|
|
38
|
+
const headers = await runPlugin({
|
|
39
|
+
frameOptions: "SAMEORIGIN",
|
|
40
|
+
contentTypeOptions: false,
|
|
41
|
+
});
|
|
42
|
+
expect(headers.get("X-Frame-Options")).toBe("SAMEORIGIN");
|
|
43
|
+
expect(headers.has("X-Content-Type-Options")).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("disables headers when set to false", async () => {
|
|
47
|
+
const headers = await runPlugin({
|
|
48
|
+
contentSecurityPolicy: false,
|
|
49
|
+
strictTransportSecurity: false,
|
|
50
|
+
permissionsPolicy: false,
|
|
51
|
+
});
|
|
52
|
+
expect(headers.has("Content-Security-Policy")).toBe(false);
|
|
53
|
+
expect(headers.has("Strict-Transport-Security")).toBe(false);
|
|
54
|
+
expect(headers.has("Permissions-Policy")).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("applies route overrides", async () => {
|
|
58
|
+
const headers = await runPlugin(
|
|
59
|
+
{
|
|
60
|
+
frameOptions: "DENY",
|
|
61
|
+
routes: {
|
|
62
|
+
"/admin": { frameOptions: "SAMEORIGIN" },
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
fakeRequest("/admin/settings"),
|
|
66
|
+
);
|
|
67
|
+
expect(headers.get("X-Frame-Options")).toBe("SAMEORIGIN");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("route override only applies to matching prefix", async () => {
|
|
71
|
+
const headers = await runPlugin(
|
|
72
|
+
{
|
|
73
|
+
frameOptions: "DENY",
|
|
74
|
+
routes: {
|
|
75
|
+
"/admin": { frameOptions: "SAMEORIGIN" },
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
fakeRequest("/dashboard"),
|
|
79
|
+
);
|
|
80
|
+
expect(headers.get("X-Frame-Options")).toBe("DENY");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("preserves existing response headers", async () => {
|
|
84
|
+
const plugin = securityHeaders({});
|
|
85
|
+
const res = new Response("<html></html>", {
|
|
86
|
+
headers: {
|
|
87
|
+
"Content-Type": "text/html",
|
|
88
|
+
"Set-Cookie": "session=abc",
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
const result = await plugin.onRequest!(fakeRequest(), async () => res);
|
|
92
|
+
expect(result.headers.get("Content-Type")).toBe("text/html");
|
|
93
|
+
expect(result.headers.get("Set-Cookie")).toBe("session=abc");
|
|
94
|
+
expect(result.headers.get("X-Frame-Options")).toBe("DENY");
|
|
95
|
+
});
|
|
96
|
+
});
|