@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
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "name": "@teamscala/service-boot",
8
8
  "type": "module",
9
- "version": "0.23.399",
9
+ "version": "0.23.401",
10
10
  "sideEffects": false,
11
11
  "imports": {
12
12
  "#service-boot/*": "./src/*",
@@ -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:15px;font-weight:700;color:${primary}">${heading}</h3>${inner}`
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
+ });
@@ -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
  }