okengine 0.5.0 → 0.5.1

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,255 +0,0 @@
1
- /**
2
- * Official `security-headers` plugin — the complete secure-headers set on
3
- * every HTTP response (helmet parity, API-first defaults). Uses only the
4
- * public plugin API (unified-theory §14).
5
- */
6
-
7
- import { plugin, type PluginDef } from "../kernel/plugin.ts";
8
- import {
9
- isConfigSource,
10
- pluginConfigSnapshot,
11
- resolvePluginOptions,
12
- withConfigTable,
13
- type ConfigSource,
14
- } from "./config-source.ts";
15
- import { setUnlessPresent, withHeaders } from "./headers.ts";
16
-
17
- /** HSTS value options (see {@link SecurityHeadersOptions.hsts}). */
18
- export interface HstsOptions {
19
- /** `max-age` in seconds. Default `31536000` (one year). */
20
- readonly maxAge?: number;
21
- /** Append `includeSubDomains`. Default `false`. */
22
- readonly includeSubDomains?: boolean;
23
- /** Append `preload`. Only meaningful with subdomains + a ≥1-year max-age. */
24
- readonly preload?: boolean;
25
- }
26
-
27
- /** Structured CSP (see {@link SecurityHeadersOptions.contentSecurityPolicy}). */
28
- export interface CspOptions {
29
- /**
30
- * Directive map — kebab-case (`script-src`) or camelCase (`scriptSrc`)
31
- * keys, values as string arrays. An empty array emits a bare directive
32
- * (e.g. `upgrade-insecure-requests`).
33
- */
34
- readonly directives: Readonly<Record<string, readonly string[]>>;
35
- /** Merge over {@link defaultCspDirectives}. Default `true`. */
36
- readonly useDefaults?: boolean;
37
- /** Emit as `Content-Security-Policy-Report-Only`. Default `false`. */
38
- readonly reportOnly?: boolean;
39
- }
40
-
41
- /**
42
- * Helmet's default CSP — the baseline `directives` merge over unless
43
- * `useDefaults: false`.
44
- */
45
- export const defaultCspDirectives: Readonly<Record<string, readonly string[]>> = {
46
- "default-src": ["'self'"],
47
- "base-uri": ["'self'"],
48
- "font-src": ["'self'", "https:", "data:"],
49
- "form-action": ["'self'"],
50
- "frame-ancestors": ["'self'"],
51
- "img-src": ["'self'", "data:"],
52
- "object-src": ["'none'"],
53
- "script-src": ["'self'"],
54
- "script-src-attr": ["'none'"],
55
- "style-src": ["'self'", "https:", "'unsafe-inline'"],
56
- "upgrade-insecure-requests": [],
57
- };
58
-
59
- /** Options for {@link securityHeaders}. */
60
- export interface SecurityHeadersOptions {
61
- /**
62
- * Content-Security-Policy — a raw header string, or a structured
63
- * {@link CspOptions} (directive builder over helmet's defaults, optional
64
- * report-only mode). Omitted unless provided.
65
- */
66
- readonly contentSecurityPolicy?: string | CspOptions;
67
- /** X-Frame-Options value. Default `"DENY"`. */
68
- readonly frameOptions?: "DENY" | "SAMEORIGIN";
69
- /** Referrer-Policy value. Default `"no-referrer"`. */
70
- readonly referrerPolicy?: string;
71
- /**
72
- * Strict-Transport-Security. `true` → one-year `max-age`; pass an object
73
- * to tune. Default off — HSTS is sticky, and local dev is plain HTTP, so
74
- * enable it deliberately for HTTPS deployments.
75
- */
76
- readonly hsts?: boolean | HstsOptions;
77
- /** Permissions-Policy value (e.g. `"camera=(), microphone=()"`). Omitted unless provided. */
78
- readonly permissionsPolicy?: string;
79
- /**
80
- * Cross-Origin-Opener-Policy value. Omitted unless provided — opt-in
81
- * because OKE serves APIs, where cross-origin clients are legitimate
82
- * (helmet targets web pages and defaults it on).
83
- */
84
- readonly crossOriginOpenerPolicy?: "same-origin" | "same-origin-allow-popups" | "unsafe-none";
85
- /** Cross-Origin-Resource-Policy value. Omitted unless provided (same API rationale). */
86
- readonly crossOriginResourcePolicy?: "same-origin" | "same-site" | "cross-origin";
87
- /** Cross-Origin-Embedder-Policy value. Omitted unless provided (helmet also defaults it off). */
88
- readonly crossOriginEmbedderPolicy?: "require-corp" | "credentialless";
89
- /** Origin-Agent-Cluster header. Default `true` → `?1`. */
90
- readonly originAgentCluster?: boolean;
91
- /**
92
- * X-DNS-Prefetch-Control. Default `true` → `off` (privacy-preserving);
93
- * `{ allow: true }` → `on`; `false` omits the header.
94
- */
95
- readonly dnsPrefetchControl?: boolean | { readonly allow: boolean };
96
- /** X-Download-Options: noopen (legacy IE8 mitigation, helmet parity). Default `true`. */
97
- readonly downloadOptions?: boolean;
98
- /** X-Permitted-Cross-Domain-Policies value. Default `"none"`. */
99
- readonly permittedCrossDomainPolicies?: "none" | "master-only" | "by-content-type" | "all";
100
- /**
101
- * X-Powered-By handling. Default `true` → remove the header (it leaks
102
- * framework fingerprints). A string sets a custom value; `false` keeps it.
103
- */
104
- readonly poweredBy?: boolean | string;
105
- /**
106
- * X-XSS-Protection: 0 — disables the legacy, buggy browser XSS auditor
107
- * (helmet parity; modern defense is CSP). Default `true`.
108
- */
109
- readonly xssProtection?: boolean;
110
- /**
111
- * Replace headers the app already set. Default `false` — an explicit
112
- * app-level value always wins.
113
- */
114
- readonly override?: boolean;
115
- }
116
-
117
- /** Render the Strict-Transport-Security header value. */
118
- function hstsValue(hsts: boolean | HstsOptions): string {
119
- const opts: HstsOptions = typeof hsts === "object" ? hsts : {};
120
- let value = `max-age=${opts.maxAge ?? 31536000}`;
121
- if (opts.includeSubDomains) value += "; includeSubDomains";
122
- if (opts.preload) value += "; preload";
123
- return value;
124
- }
125
-
126
- /** camelCase directive keys → kebab-case (`scriptSrc` → `script-src`). */
127
- function directiveName(name: string): string {
128
- return name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
129
- }
130
-
131
- /** Build a CSP header value from a directive map. */
132
- function buildCsp(directives: Readonly<Record<string, readonly string[]>>): string {
133
- return Object.entries(directives)
134
- .map(([name, values]) => {
135
- const kebab = directiveName(name);
136
- return values.length === 0 ? kebab : `${kebab} ${values.join(" ")}`;
137
- })
138
- .join("; ");
139
- }
140
-
141
- /** Resolve the CSP option into header name + value. */
142
- function cspEntry(option: string | CspOptions): { readonly name: string; readonly value: string } {
143
- if (typeof option === "string") return { name: "content-security-policy", value: option };
144
- const useDefaults = option.useDefaults ?? true;
145
- const merged: Record<string, readonly string[]> = useDefaults
146
- ? { ...defaultCspDirectives, ...option.directives }
147
- : { ...option.directives };
148
- return {
149
- name:
150
- (option.reportOnly ?? false)
151
- ? "content-security-policy-report-only"
152
- : "content-security-policy",
153
- value: buildCsp(merged),
154
- };
155
- }
156
-
157
- /**
158
- * Apply the complete secure-headers set to every HTTP response, including
159
- * failures (runs at `onResponse`, which fires after `onError` too). Covers
160
- * every helmet.js middleware; API-first deviations (opt-in HSTS, COOP,
161
- * CORP) are documented per option.
162
- *
163
- * Accepts static options or a {@link ConfigSource} for DB-driven config
164
- * (e.g. flip `hsts` from the database without a redeploy).
165
- *
166
- * @param options - Header values, or a config source
167
- */
168
- export function securityHeaders(
169
- options: SecurityHeadersOptions | ConfigSource<SecurityHeadersOptions> = {},
170
- ): PluginDef {
171
- const def = plugin("security-headers", {
172
- version: "0.1.0",
173
- config: pluginConfigSnapshot(options),
174
- }).hook("onResponse", (ctx) => {
175
- if (!ctx.response) return;
176
- const resolved = resolvePluginOptions(options);
177
- const override = resolved.override ?? false;
178
-
179
- ctx.response = withHeaders(ctx.response, (headers) => {
180
- setUnlessPresent(headers, "x-content-type-options", "nosniff", override);
181
- setUnlessPresent(headers, "x-frame-options", resolved.frameOptions ?? "DENY", override);
182
- setUnlessPresent(
183
- headers,
184
- "referrer-policy",
185
- resolved.referrerPolicy ?? "no-referrer",
186
- override,
187
- );
188
-
189
- if (resolved.originAgentCluster ?? true) {
190
- setUnlessPresent(headers, "origin-agent-cluster", "?1", override);
191
- }
192
- const dns = resolved.dnsPrefetchControl ?? true;
193
- if (dns !== false) {
194
- const allow = typeof dns === "object" ? dns.allow : false;
195
- setUnlessPresent(headers, "x-dns-prefetch-control", allow ? "on" : "off", override);
196
- }
197
- if (resolved.downloadOptions ?? true) {
198
- setUnlessPresent(headers, "x-download-options", "noopen", override);
199
- }
200
- setUnlessPresent(
201
- headers,
202
- "x-permitted-cross-domain-policies",
203
- resolved.permittedCrossDomainPolicies ?? "none",
204
- override,
205
- );
206
- if (resolved.xssProtection ?? true) {
207
- setUnlessPresent(headers, "x-xss-protection", "0", override);
208
- }
209
-
210
- const poweredBy = resolved.poweredBy ?? true;
211
- if (poweredBy === true) {
212
- headers.delete("x-powered-by");
213
- } else if (typeof poweredBy === "string") {
214
- setUnlessPresent(headers, "x-powered-by", poweredBy, override);
215
- }
216
-
217
- if (resolved.contentSecurityPolicy !== undefined) {
218
- const csp = cspEntry(resolved.contentSecurityPolicy);
219
- setUnlessPresent(headers, csp.name, csp.value, override);
220
- }
221
- if (resolved.hsts !== undefined && resolved.hsts !== false) {
222
- setUnlessPresent(headers, "strict-transport-security", hstsValue(resolved.hsts), override);
223
- }
224
- if (resolved.permissionsPolicy !== undefined) {
225
- setUnlessPresent(headers, "permissions-policy", resolved.permissionsPolicy, override);
226
- }
227
- if (resolved.crossOriginOpenerPolicy !== undefined) {
228
- setUnlessPresent(
229
- headers,
230
- "cross-origin-opener-policy",
231
- resolved.crossOriginOpenerPolicy,
232
- override,
233
- );
234
- }
235
- if (resolved.crossOriginResourcePolicy !== undefined) {
236
- setUnlessPresent(
237
- headers,
238
- "cross-origin-resource-policy",
239
- resolved.crossOriginResourcePolicy,
240
- override,
241
- );
242
- }
243
- if (resolved.crossOriginEmbedderPolicy !== undefined) {
244
- setUnlessPresent(
245
- headers,
246
- "cross-origin-embedder-policy",
247
- resolved.crossOriginEmbedderPolicy,
248
- override,
249
- );
250
- }
251
- });
252
- });
253
-
254
- return isConfigSource(options) ? withConfigTable(def, options) : def;
255
- }