create-shibumi 0.2.9 → 0.3.2

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.
@@ -1,145 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { app, createApp } from "../src/app";
3
-
4
- // Independent copy of the required header contract. If src/app.ts weakens a
5
- // header, this literal makes the test fail instead of silently adapting.
6
- const REQUIRED_HEADERS: Record<string, string> = {
7
- "Content-Security-Policy":
8
- "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'",
9
- "X-Content-Type-Options": "nosniff",
10
- "X-Frame-Options": "DENY",
11
- "Referrer-Policy": "strict-origin-when-cross-origin",
12
- "Permissions-Policy": "camera=(), geolocation=(), microphone=()",
13
- "Cross-Origin-Opener-Policy": "same-origin",
14
- "Cross-Origin-Resource-Policy": "same-origin",
15
- };
16
-
17
- async function req(path: string, init?: RequestInit): Promise<Response> {
18
- return app.fetch(new Request(`http://localhost${path}`, init));
19
- }
20
-
21
- function expectSecurityHeaders(res: Response): void {
22
- for (const [name, value] of Object.entries(REQUIRED_HEADERS)) {
23
- expect(res.headers.get(name)).toBe(value);
24
- }
25
- }
26
-
27
- describe("routes", () => {
28
- it("serves the home page", async () => {
29
- const res = await req("/");
30
- expect(res.status).toBe(200);
31
- const html = await res.text();
32
- expect(html).toContain("Web app");
33
- // app.js must come before Alpine so the alpine:init listener registers
34
- // before Alpine starts.
35
- expect(html.indexOf("/public/app.js")).toBeLessThan(
36
- html.indexOf("/public/vendor/alpine-csp-3.16.2.min.js")
37
- );
38
- expectSecurityHeaders(res);
39
- });
40
-
41
- it("answers the health check", async () => {
42
- const res = await req("/healthz");
43
- expect(res.status).toBe(200);
44
- expect(await res.json()).toEqual({ ok: true });
45
- expectSecurityHeaders(res);
46
- });
47
-
48
- it("validates API query input", async () => {
49
- const ok = await req("/api/hello?name=you");
50
- expect(ok.status).toBe(200);
51
- expect(await ok.json()).toEqual({ hello: "you" });
52
-
53
- const bad = await req(`/api/hello?name=${"x".repeat(101)}`);
54
- expect(bad.status).toBe(400);
55
- expectSecurityHeaders(bad);
56
- });
57
-
58
- it("returns 404 with security headers", async () => {
59
- const res = await req("/nope");
60
- expect(res.status).toBe(404);
61
- expectSecurityHeaders(res);
62
- });
63
-
64
- it("serves static assets with security headers", async () => {
65
- const res = await req("/public/style.css");
66
- expect(res.status).toBe(200);
67
- expectSecurityHeaders(res);
68
- });
69
-
70
- it("keeps security headers on thrown-error responses", async () => {
71
- // Fresh instance: the shared app's router freezes on first dispatch.
72
- const probe = createApp();
73
- probe.get("/__boom", () => {
74
- throw new Error("test explosion");
75
- });
76
- // onError logs the exception before answering; silence it so the
77
- // deliberate explosion does not read as a failure in test output.
78
- const errorLog = console.error;
79
- console.error = () => {};
80
- try {
81
- const res = await probe.fetch(new Request("http://localhost/__boom"));
82
- expect(res.status).toBe(500);
83
- expectSecurityHeaders(res);
84
- } finally {
85
- console.error = errorLog;
86
- }
87
- });
88
- });
89
-
90
- describe("security", () => {
91
- it("registers exactly the expected routes", () => {
92
- // Exact allowlist: any new route (including app.all handlers that could
93
- // hide a mutation endpoint) must be added here deliberately.
94
- const routes = app.routes.map((r) => `${r.method} ${r.path}`);
95
- expect(routes.sort()).toEqual(
96
- [
97
- "ALL /*",
98
- "GET /public/*",
99
- "GET /",
100
- "GET /api/hello",
101
- "GET /healthz",
102
- ].sort()
103
- );
104
- });
105
-
106
- it("rejects every mutation verb on every route", async () => {
107
- for (const path of ["/", "/api/hello", "/healthz", "/public/style.css"]) {
108
- for (const method of ["POST", "PUT", "PATCH", "DELETE"]) {
109
- const res = await req(path, { method });
110
- expect([404, 405]).toContain(res.status);
111
- }
112
- }
113
- });
114
-
115
- it("does not expose files outside public/ via traversal", async () => {
116
- for (const path of [
117
- "/public/../package.json",
118
- "/public/%2e%2e/package.json",
119
- "/public/..%2fpackage.json",
120
- "/public/%2e%2e%2fsrc/app.ts",
121
- "/public/..\\package.json",
122
- "/public/../.env",
123
- ]) {
124
- const res = await req(path);
125
- if (res.status === 200) {
126
- const body = await res.text();
127
- expect(body).not.toContain('"scripts"');
128
- expect(body).not.toContain("Bun.serve");
129
- } else {
130
- expect([400, 404]).toContain(res.status);
131
- }
132
- }
133
- });
134
-
135
- it("pins a CSP without unsafe-inline or unsafe-eval", () => {
136
- const csp = REQUIRED_HEADERS["Content-Security-Policy"]!;
137
- expect(csp).not.toContain("unsafe-inline");
138
- expect(csp).not.toContain("unsafe-eval");
139
- });
140
-
141
- it("ships the pinned Alpine build the page references", async () => {
142
- const file = Bun.file(new URL("../public/vendor/alpine-csp-3.16.2.min.js", import.meta.url));
143
- expect(await file.exists()).toBe(true);
144
- });
145
- });
@@ -1,13 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ESNext",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "resolveJsonModule": true,
7
- "noEmit": true,
8
- "strict": true,
9
- "noUncheckedIndexedAccess": true,
10
- "types": ["bun-types"]
11
- },
12
- "include": ["src", "test"]
13
- }