@teamscala/service-boot 0.23.399 → 0.23.401
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/package.json
CHANGED
|
@@ -190,7 +190,7 @@ async function sendAuthorityReportEmail(opts: {
|
|
|
190
190
|
const b = opts.stageBand;
|
|
191
191
|
const sectionHtml = (heading: string, inner: string) =>
|
|
192
192
|
inner
|
|
193
|
-
? `<h3 style="margin:24px 0 8px;font-size:
|
|
193
|
+
? `<h3 style="margin:24px 0 8px;font-size:16px;font-weight:700;color:${primary}">${heading}</h3>${inner}`
|
|
194
194
|
: "";
|
|
195
195
|
const paraHtml = (text?: string) =>
|
|
196
196
|
text ? `<p style="margin:0 0 12px;color:${muted}">${text}</p>` : "";
|
|
@@ -205,14 +205,14 @@ async function sendAuthorityReportEmail(opts: {
|
|
|
205
205
|
? b.priorityActions
|
|
206
206
|
.map(
|
|
207
207
|
(a) =>
|
|
208
|
-
`<p style="margin:0 0 10px"><strong style="color:${primary}">${a.title}</strong><br><span style="color:${muted}">${a.description ?? ""}</span></p>`,
|
|
208
|
+
`<p style="margin:0 0 10px"><strong style="font-size:14px;color:${primary}">${a.title}</strong><br><span style="color:${muted}">${a.description ?? ""}</span></p>`,
|
|
209
209
|
)
|
|
210
210
|
.join("")
|
|
211
211
|
: "";
|
|
212
212
|
const roadmapHtml = orderRoadmap(b?.roadmap)
|
|
213
213
|
.map(
|
|
214
214
|
(phase) =>
|
|
215
|
-
`<p style="margin:0 0 10px"><strong style="color:${primary}">${phase.focus ?? ""}</strong><br><span style="color:${muted}">${(phase.actions ?? []).join(" · ")}</span></p>`,
|
|
215
|
+
`<p style="margin:0 0 10px"><strong style="font-size:14px;color:${primary}">${phase.focus ?? ""}</strong><br><span style="color:${muted}">${(phase.actions ?? []).join(" · ")}</span></p>`,
|
|
216
216
|
)
|
|
217
217
|
.join("");
|
|
218
218
|
const notes = b?.pillarNotes ?? {};
|
|
@@ -220,7 +220,7 @@ async function sendAuthorityReportEmail(opts: {
|
|
|
220
220
|
? Object.entries(notes)
|
|
221
221
|
.map(
|
|
222
222
|
([k, v]) =>
|
|
223
|
-
`<p style="margin:0 0 8px;color:${muted}"><strong style="color:${primary};text-transform:capitalize">${k}</strong> — ${v}</p>`,
|
|
223
|
+
`<p style="margin:0 0 8px;color:${muted}"><strong style="font-size:14px;color:${primary};text-transform:capitalize">${k}</strong> — ${v}</p>`,
|
|
224
224
|
)
|
|
225
225
|
.join("")
|
|
226
226
|
: "";
|
|
@@ -80,3 +80,42 @@ describe("injectSecurityHeaders (fallback mode, no registry)", () => {
|
|
|
80
80
|
expect((res.headers.get("Content-Security-Policy") ?? "")).not.toContain("{{nonce}}");
|
|
81
81
|
});
|
|
82
82
|
});
|
|
83
|
+
|
|
84
|
+
describe("Cross-Origin-Resource-Policy — public assets stay embeddable", () => {
|
|
85
|
+
function appWithAssetRoute(): Hono {
|
|
86
|
+
const app = new Hono();
|
|
87
|
+
app.use("*", injectSecurityHeaders);
|
|
88
|
+
app.get("/api/assets/*", (c) => c.body("png-bytes", 200, { "Content-Type": "image/png" }));
|
|
89
|
+
app.get("/", (c) => c.text("ok"));
|
|
90
|
+
return app;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
it("serves cross-origin CORP for /api/assets/* so an email can render the logo", async () => {
|
|
94
|
+
clearRegistryHeaders();
|
|
95
|
+
const res = await appWithAssetRoute().request("/api/assets/org/logos/launcha.png");
|
|
96
|
+
// same-origin here means every mail client and every other origin gets the
|
|
97
|
+
// image blocked at fetch while curl still sees 200 — the exact failure this
|
|
98
|
+
// guards. cross-origin is the only value that lets the embed through.
|
|
99
|
+
expect(res.headers.get("Cross-Origin-Resource-Policy")).toBe("cross-origin");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("keeps same-origin CORP on page responses", async () => {
|
|
103
|
+
clearRegistryHeaders();
|
|
104
|
+
const res = await appWithAssetRoute().request("/");
|
|
105
|
+
expect(res.headers.get("Cross-Origin-Resource-Policy")).toBe("same-origin");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("still serves cross-origin CORP for assets when the registry row is applied", async () => {
|
|
109
|
+
seedRegistryHeaders({ "Referrer-Policy": "strict-origin-when-cross-origin" });
|
|
110
|
+
const res = await appWithAssetRoute().request("/api/assets/org/logos/launcha.png");
|
|
111
|
+
expect(res.headers.get("Cross-Origin-Resource-Policy")).toBe("cross-origin");
|
|
112
|
+
clearRegistryHeaders();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("lets the registry row override CORP when it names the header", async () => {
|
|
116
|
+
seedRegistryHeaders({ "Cross-Origin-Resource-Policy": "same-site" });
|
|
117
|
+
const res = await appWithAssetRoute().request("/api/assets/org/logos/launcha.png");
|
|
118
|
+
expect(res.headers.get("Cross-Origin-Resource-Policy")).toBe("same-site");
|
|
119
|
+
clearRegistryHeaders();
|
|
120
|
+
});
|
|
121
|
+
});
|
package/src/security-headers.ts
CHANGED
|
@@ -35,7 +35,18 @@ const FALLBACK_CSP = [
|
|
|
35
35
|
// present some browsers enforce it over CSP. Frame control is owned solely by
|
|
36
36
|
// CSP frame-ancestors (registry security-headers config), which allows
|
|
37
37
|
// platform self-framing — e.g. the GPT panel framing a forms page.
|
|
38
|
-
const baseSecureHeaders = secureHeaders({ xFrameOptions: false });
|
|
38
|
+
const baseSecureHeaders = secureHeaders({ xFrameOptions: false, crossOriginResourcePolicy: false });
|
|
39
|
+
|
|
40
|
+
// Public brand assets are embedded BY DESIGN from other origins — most
|
|
41
|
+
// importantly report emails, which render the org logo from the serving
|
|
42
|
+
// origin. Hono's default CORP (same-origin) blocks every such embed at fetch
|
|
43
|
+
// time, so the logo silently fails to load while the URL still answers 200 to
|
|
44
|
+
// a non-browser client like curl. CORP cannot be corrected in the asset
|
|
45
|
+
// handler: Hono's `c.res` setter copies already-set middleware headers over
|
|
46
|
+
// the handler's response, so the middleware value always wins. The decision
|
|
47
|
+
// therefore lives here — cross-origin for the public asset surface,
|
|
48
|
+
// same-origin for every page and API response.
|
|
49
|
+
const CROSS_ORIGIN_EMBEDDABLE_PREFIX = "/api/assets/";
|
|
39
50
|
|
|
40
51
|
export async function injectSecurityHeaders(c: Context, next: Next) {
|
|
41
52
|
await baseSecureHeaders(c, async () => {});
|
|
@@ -55,5 +66,12 @@ export async function injectSecurityHeaders(c: Context, next: Next) {
|
|
|
55
66
|
}
|
|
56
67
|
}
|
|
57
68
|
|
|
69
|
+
if (!config?.["Cross-Origin-Resource-Policy"]) {
|
|
70
|
+
c.header(
|
|
71
|
+
"Cross-Origin-Resource-Policy",
|
|
72
|
+
new URL(c.req.url).pathname.startsWith(CROSS_ORIGIN_EMBEDDABLE_PREFIX) ? "cross-origin" : "same-origin",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
58
76
|
await next();
|
|
59
77
|
}
|