@stnd/modules 0.6.46 → 0.8.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/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @stnd/folios
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Auto-bumped @stnd/modules due to modified code.
8
+
9
+ ## 0.7.0
10
+
11
+ ### Minor Changes
12
+
13
+ - Auto-bumped @stnd/modules due to modified code.
14
+
15
+ ### Patch Changes
16
+
17
+ - Updated dependencies
18
+ - @stnd/server@0.8.0
19
+
3
20
  ## 0.6.46
4
21
 
5
22
  ### Patch Changes
@@ -0,0 +1,30 @@
1
+ import { normaliseReports } from "@stnd/server/cspReports";
2
+ import Log from "@stnd/log";
3
+
4
+ export const prerender = false;
5
+
6
+ const log = Log({ scope: "CSP" });
7
+ const noContent = () =>
8
+ new Response(null, { status: 204, headers: { "Cache-Control": "no-store" } });
9
+
10
+ /**
11
+ * POST /api/csp-report — where browsers send content-security-policy violations
12
+ * (`report-uri` in the policy). Public by nature, so it only reduces the report
13
+ * to a few short fields and always answers 204.
14
+ *
15
+ * An app that wants the reports counted sets `locals.cspReports` from its own
16
+ * middleware: `{ record(reports, request): Promise<void> }` (it owns rate
17
+ * limiting and storage). Without one, each report is logged.
18
+ */
19
+ export async function POST({ request, locals }) {
20
+ try {
21
+ const text = (await request.text()).slice(0, 8000);
22
+ const reports = normaliseReports(JSON.parse(text));
23
+ if (!reports.length) return noContent();
24
+ if (locals?.cspReports?.record) await locals.cspReports.record(reports, request);
25
+ else for (const r of reports) log.warn("CSP violation", r);
26
+ } catch {
27
+ /* a malformed report is not worth an error */
28
+ }
29
+ return noContent();
30
+ }
@@ -1,16 +1,29 @@
1
1
  /**
2
2
  * Security Headers core module
3
3
  *
4
- * Exposes the /_headers route via the Standard module system.
4
+ * - Middleware: security headers (HSTS, nosniff, frame options, referrer and
5
+ * permissions policies, a content-security-policy) on every response.
6
+ * Policy lives in `@stnd/server/security`; an app extends it with
7
+ * `standard({ security: { csp: { extend: { "script-src": [...] } } } })`.
8
+ * - `/api/csp-report`: where the policy's violations are sent.
9
+ * - Build: the same headers appended to `_headers` for prerendered files.
10
+ * - `/_headers`: the same basics as a Cloudflare headers file.
5
11
  */
6
12
  export default {
7
13
  id: "stnd-headers",
8
14
  name: "Stnd :: Headers",
9
- description: "Emits security headers (_headers) from site configuration.",
15
+ description: "Security headers on every response, and the CSP violation report endpoint.",
16
+ // Prerendered pages never reach the Worker: they get the same headers from the build's _headers file.
17
+ hooks: { "astro:build:done": [{ action: "./static-headers.js" }] },
18
+ middleware: [{ entrypoint: "./middleware.js", order: -1000 }],
10
19
  routes: [
11
20
  {
12
21
  path: "/_headers",
13
22
  entrypoint: "./route.js",
14
23
  },
24
+ {
25
+ path: "/api/csp-report",
26
+ entrypoint: "./csp-report.js",
27
+ },
15
28
  ],
16
29
  };
@@ -0,0 +1,14 @@
1
+ import config from "virtual:stnd/config";
2
+ import { withSecurityHeaders } from "@stnd/server/security";
3
+
4
+ /**
5
+ * Every response, whichever way it was produced (page, API, redirect, error),
6
+ * leaves with the framework's security headers. Registered with a very low
7
+ * `order` so it wraps the whole middleware chain.
8
+ *
9
+ * Configured with `standard({ security: { csp: {...}, hsts, frameOptions } })`.
10
+ */
11
+ export async function onRequest(context, next) {
12
+ const response = await next();
13
+ return withSecurityHeaders(response, context.url, config.security || {});
14
+ }
@@ -0,0 +1,39 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import Log from "@stnd/log";
5
+
6
+ const log = Log({ scope: "Headers" });
7
+ const MARKER = "# stnd:security-headers";
8
+
9
+ /**
10
+ * astro:build:done — the middleware only sees responses the Worker produces.
11
+ * Prerendered pages are static files served straight from Cloudflare's asset
12
+ * store, never reaching it, so they get the same headers through the build's
13
+ * `_headers` file. Headers the app's own `_headers` already sets are left alone
14
+ * (Cloudflare would otherwise send both values).
15
+ */
16
+ export default async function writeStaticHeaders({ dir }) {
17
+ const options = globalThis.__STANDARD_CONFIG__?.security || {};
18
+ if (options.enabled === false) return;
19
+
20
+ const { securityHeaders } = await import("@stnd/server/security");
21
+ const wanted = securityHeaders(new URL("https://static.invalid/"), "text/html", options);
22
+
23
+ const outDir = fileURLToPath(dir);
24
+ const file = path.join(outDir, "_headers");
25
+ const existing = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
26
+ if (existing.includes(MARKER)) return;
27
+
28
+ const declared = new Set(
29
+ [...existing.matchAll(/^[ \t]+([A-Za-z0-9-]+):/gm)].map((m) => m[1].toLowerCase()),
30
+ );
31
+ const lines = Object.entries(wanted)
32
+ .filter(([name]) => !declared.has(name.toLowerCase()))
33
+ .map(([name, value]) => ` ${name}: ${value}`);
34
+ if (!lines.length) return;
35
+
36
+ const block = `${existing && !existing.endsWith("\n") ? "\n" : ""}\n${MARKER}\n/*\n${lines.join("\n")}\n`;
37
+ fs.writeFileSync(file, existing + block);
38
+ log.info(`Security headers added to ${path.relative(process.cwd(), file) || file} for prerendered files`);
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stnd/modules",
3
- "version": "0.6.46",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "svelte": "./stripe/views/CheckoutView.svelte",
6
6
  "author": "Francis Fontaine",
@@ -19,8 +19,9 @@
19
19
  "@astrojs/sitemap": "^3.7.3",
20
20
  "@stnd/client": "0.7.5",
21
21
  "@stnd/icon": "0.7.0",
22
- "@stnd/layout": "0.7.7",
23
22
  "@stnd/log": "0.5.2",
23
+ "@stnd/server": "0.8.0",
24
+ "@stnd/layout": "0.7.7",
24
25
  "@stnd/styles": "0.13.0",
25
26
  "@stnd/utils": "0.7.3"
26
27
  },