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.
@@ -11,7 +11,7 @@ import {
11
11
  withConfigTable,
12
12
  type ConfigSource,
13
13
  } from "./config-source.ts";
14
- import { appendVary } from "./headers.ts";
14
+ import { appendVary } from "./response-headers.ts";
15
15
 
16
16
  /** Options for {@link compression}. */
17
17
  export interface CompressionOptions {
@@ -19,7 +19,7 @@ import { createPluginRegistry } from "../kernel/registry.ts";
19
19
  import { http } from "../kernel/triggers.ts";
20
20
  import { configSource, resolvePluginOptions } from "./config-source.ts";
21
21
  import { maintenanceMode } from "./maintenance-mode.ts";
22
- import { securityHeaders } from "./security-headers.ts";
22
+ import { headers } from "./headers.ts";
23
23
 
24
24
  beforeEach(() => {
25
25
  resetBindings();
@@ -74,7 +74,7 @@ describe("configSource — box semantics", () => {
74
74
  };
75
75
  const fx = createFxContext({ flow: "x.config-sync", effects, storeRuntime: runtime }).fx;
76
76
 
77
- const table = defineTable("security_headers_config", { key: true, value: true });
77
+ const table = defineTable("headers_config", { key: true, value: true });
78
78
  const sql = fx.store(db) as SqlStoreHandle;
79
79
  await sql.ensureTable(table);
80
80
  await sql
@@ -83,7 +83,7 @@ describe("configSource — box semantics", () => {
83
83
  .execute();
84
84
 
85
85
  const source = configSource({
86
- plugin: "security-headers",
86
+ plugin: "headers",
87
87
  code: { hsts: false },
88
88
  db: { store: db },
89
89
  kv: cfg,
@@ -110,11 +110,11 @@ describe("configSource — box semantics", () => {
110
110
 
111
111
  const effects = { reads: ["sql:cfgdb" as const], writes: ["sql:cfgdb" as const] };
112
112
  const fx = createFxContext({ flow: "x.config-sync", effects, storeRuntime: runtime }).fx;
113
- const table = defineTable("security_headers_config", { key: true, value: true });
113
+ const table = defineTable("headers_config", { key: true, value: true });
114
114
  await (fx.store(db) as SqlStoreHandle).ensureTable(table);
115
115
 
116
116
  const source = configSource({
117
- plugin: "security-headers",
117
+ plugin: "headers",
118
118
  code: { frameOptions: "DENY" as const },
119
119
  db: { store: db },
120
120
  });
@@ -132,13 +132,13 @@ describe("configSource — box semantics", () => {
132
132
 
133
133
  const effects = { reads: ["sql:cfgdb" as const], writes: ["sql:cfgdb" as const] };
134
134
  const fx = createFxContext({ flow: "x.config-sync", effects, storeRuntime: runtime }).fx;
135
- const table = defineTable("security_headers_config", { key: true, value: true });
135
+ const table = defineTable("headers_config", { key: true, value: true });
136
136
  const sql = fx.store(db) as SqlStoreHandle;
137
137
  await sql.ensureTable(table);
138
138
  await sql.insert(table).values({ key: "config", value: "{not json" }).execute();
139
139
 
140
140
  const source = configSource({
141
- plugin: "security-headers",
141
+ plugin: "headers",
142
142
  code: {},
143
143
  db: { store: db },
144
144
  });
@@ -171,17 +171,17 @@ describe("configSource — plugin wiring", () => {
171
171
  test("DB-backed sources contribute their config table; static options do not", () => {
172
172
  const db = store.sql("cfgdb");
173
173
  const source = configSource({
174
- plugin: "security-headers",
174
+ plugin: "headers",
175
175
  code: {},
176
176
  db: { store: db },
177
177
  });
178
178
 
179
179
  const registry = createPluginRegistry();
180
- const registration = registry.plug(securityHeaders(source), { kind: "app" });
181
- expect(registration?.tables.map((t) => t.name)).toEqual(["security_headers_config"]);
180
+ const registration = registry.plug(headers(source), { kind: "app" });
181
+ expect(registration?.tables.map((t) => t.name)).toEqual(["headers_config"]);
182
182
 
183
183
  const staticRegistry = createPluginRegistry();
184
- const staticRegistration = staticRegistry.plug(securityHeaders({}), { kind: "app" });
184
+ const staticRegistration = staticRegistry.plug(headers({}), { kind: "app" });
185
185
  expect(staticRegistration?.tables).toEqual([]);
186
186
  });
187
187
 
@@ -35,8 +35,8 @@ export interface ConfigSourceDb {
35
35
  /** Options for {@link configSource}. */
36
36
  export interface ConfigSourceOptions<T extends object> {
37
37
  /**
38
- * Plugin id this config belongs to (e.g. `"security-headers"`) — derives
39
- * the DB row key, the KV key, and the default table name.
38
+ * Plugin id this config belongs to (e.g. `"headers"`) — derives the DB
39
+ * row key, the KV key, and the default table name.
40
40
  */
41
41
  readonly plugin: string;
42
42
  /**
@@ -13,7 +13,7 @@ import {
13
13
  isConfigSource,
14
14
  type ConfigSource,
15
15
  } from "./config-source.ts";
16
- import { appendVary, withHeaders } from "./headers.ts";
16
+ import { appendVary, withHeaders } from "./response-headers.ts";
17
17
 
18
18
  /** Options for {@link cors}. */
19
19
  export interface CorsOptions {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `security-headers` plugin — defaults on success, presence on failure,
2
+ * `headers` plugin — defaults on success, presence on failure,
3
3
  * and app-wins override semantics through the real pipeline.
4
4
  */
5
5
 
@@ -9,17 +9,17 @@ import { flow, resetFlowSeq } from "../kernel/flow.ts";
9
9
  import { fail } from "../kernel/index.ts";
10
10
  import { on, resetBindings } from "../kernel/on.ts";
11
11
  import { http } from "../kernel/triggers.ts";
12
- import { defaultCspDirectives, securityHeaders } from "./security-headers.ts";
12
+ import { defaultCspDirectives, headers } from "./headers.ts";
13
13
 
14
14
  beforeEach(() => {
15
15
  resetBindings();
16
16
  resetFlowSeq();
17
17
  });
18
18
 
19
- describe("securityHeaders plugin", () => {
19
+ describe("headers plugin", () => {
20
20
  test("sets the default trio on a successful response", async () => {
21
21
  on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
22
- const app = oke({ autoBoot: false, name: "sec" }).plug(securityHeaders());
22
+ const app = oke({ autoBoot: false, name: "sec" }).plug(headers());
23
23
 
24
24
  const res = await app.fetch(new Request("http://localhost/x"));
25
25
 
@@ -32,7 +32,7 @@ describe("securityHeaders plugin", () => {
32
32
 
33
33
  test("helmet-parity defaults: agent cluster, dns prefetch, download, cross-domain, xss filter", async () => {
34
34
  on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
35
- const app = oke({ autoBoot: false, name: "sec-parity" }).plug(securityHeaders());
35
+ const app = oke({ autoBoot: false, name: "sec-parity" }).plug(headers());
36
36
 
37
37
  const res = await app.fetch(new Request("http://localhost/x"));
38
38
 
@@ -46,7 +46,7 @@ describe("securityHeaders plugin", () => {
46
46
  test("each parity default can be switched off", async () => {
47
47
  on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
48
48
  const app = oke({ autoBoot: false, name: "sec-off" }).plug(
49
- securityHeaders({
49
+ headers({
50
50
  originAgentCluster: false,
51
51
  dnsPrefetchControl: false,
52
52
  downloadOptions: false,
@@ -66,7 +66,7 @@ describe("securityHeaders plugin", () => {
66
66
  test("dnsPrefetchControl allow, custom cross-domain policy, COEP stamps when configured", async () => {
67
67
  on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
68
68
  const app = oke({ autoBoot: false, name: "sec-tuned" }).plug(
69
- securityHeaders({
69
+ headers({
70
70
  dnsPrefetchControl: { allow: true },
71
71
  permittedCrossDomainPolicies: "by-content-type",
72
72
  crossOriginEmbedderPolicy: "require-corp",
@@ -81,7 +81,7 @@ describe("securityHeaders plugin", () => {
81
81
  });
82
82
 
83
83
  test("x-powered-by is removed by default; a string sets a decoy; false keeps the app's", async () => {
84
- const poweredByApp = (options: Parameters<typeof securityHeaders>[0], name: string) => {
84
+ const poweredByApp = (options: Parameters<typeof headers>[0], name: string) => {
85
85
  on(
86
86
  http.get("/x"),
87
87
  flow({
@@ -92,7 +92,7 @@ describe("securityHeaders plugin", () => {
92
92
  }),
93
93
  }),
94
94
  );
95
- return oke({ autoBoot: false, name }).plug(securityHeaders(options));
95
+ return oke({ autoBoot: false, name }).plug(headers(options));
96
96
  };
97
97
 
98
98
  const removed = await poweredByApp({}, "sec-pb-off").fetch(new Request("http://localhost/x"));
@@ -113,7 +113,7 @@ describe("securityHeaders plugin", () => {
113
113
  test("structured CSP merges over helmet's defaults, camelCase keys, report-only mode", async () => {
114
114
  on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
115
115
  const app = oke({ autoBoot: false, name: "sec-csp-builder" }).plug(
116
- securityHeaders({
116
+ headers({
117
117
  contentSecurityPolicy: {
118
118
  directives: { scriptSrc: ["'self'", "https://cdn.example.com"] },
119
119
  reportOnly: true,
@@ -133,7 +133,7 @@ describe("securityHeaders plugin", () => {
133
133
  test("structured CSP with useDefaults: false emits exactly the given directives", async () => {
134
134
  on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
135
135
  const app = oke({ autoBoot: false, name: "sec-csp-bare" }).plug(
136
- securityHeaders({
136
+ headers({
137
137
  contentSecurityPolicy: { useDefaults: false, directives: { "default-src": ["'none'"] } },
138
138
  }),
139
139
  );
@@ -155,7 +155,7 @@ describe("securityHeaders plugin", () => {
155
155
  fail("Forbidden", {}),
156
156
  ),
157
157
  );
158
- const app = oke({ autoBoot: false, name: "sec-fail" }).plug(securityHeaders());
158
+ const app = oke({ autoBoot: false, name: "sec-fail" }).plug(headers());
159
159
 
160
160
  const res = await app.fetch(new Request("http://localhost/deny"));
161
161
 
@@ -166,7 +166,7 @@ describe("securityHeaders plugin", () => {
166
166
  test("adds CSP only when configured", async () => {
167
167
  on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
168
168
  const app = oke({ autoBoot: false, name: "sec-csp" }).plug(
169
- securityHeaders({ contentSecurityPolicy: "default-src 'self'" }),
169
+ headers({ contentSecurityPolicy: "default-src 'self'" }),
170
170
  );
171
171
 
172
172
  const res = await app.fetch(new Request("http://localhost/x"));
@@ -176,7 +176,7 @@ describe("securityHeaders plugin", () => {
176
176
 
177
177
  test("HSTS is off by default", async () => {
178
178
  on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
179
- const app = oke({ autoBoot: false, name: "sec-hsts-off" }).plug(securityHeaders());
179
+ const app = oke({ autoBoot: false, name: "sec-hsts-off" }).plug(headers());
180
180
 
181
181
  const res = await app.fetch(new Request("http://localhost/x"));
182
182
 
@@ -185,7 +185,7 @@ describe("securityHeaders plugin", () => {
185
185
 
186
186
  test("hsts: true stamps a one-year max-age", async () => {
187
187
  on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
188
- const app = oke({ autoBoot: false, name: "sec-hsts-on" }).plug(securityHeaders({ hsts: true }));
188
+ const app = oke({ autoBoot: false, name: "sec-hsts-on" }).plug(headers({ hsts: true }));
189
189
 
190
190
  const res = await app.fetch(new Request("http://localhost/x"));
191
191
 
@@ -195,7 +195,7 @@ describe("securityHeaders plugin", () => {
195
195
  test("hsts object tunes max-age, subdomains, preload", async () => {
196
196
  on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
197
197
  const app = oke({ autoBoot: false, name: "sec-hsts-tuned" }).plug(
198
- securityHeaders({ hsts: { maxAge: 63072000, includeSubDomains: true, preload: true } }),
198
+ headers({ hsts: { maxAge: 63072000, includeSubDomains: true, preload: true } }),
199
199
  );
200
200
 
201
201
  const res = await app.fetch(new Request("http://localhost/x"));
@@ -208,7 +208,7 @@ describe("securityHeaders plugin", () => {
208
208
  test("permissions-policy and cross-origin policies stamp when configured", async () => {
209
209
  on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
210
210
  const app = oke({ autoBoot: false, name: "sec-extra" }).plug(
211
- securityHeaders({
211
+ headers({
212
212
  permissionsPolicy: "camera=(), microphone=()",
213
213
  crossOriginOpenerPolicy: "same-origin",
214
214
  crossOriginResourcePolicy: "same-site",
@@ -234,7 +234,7 @@ describe("securityHeaders plugin", () => {
234
234
  headers,
235
235
  });
236
236
  });
237
- app.plug(securityHeaders());
237
+ app.plug(headers());
238
238
 
239
239
  const res = await app.fetch(new Request("http://localhost/x"));
240
240
 
@@ -1,54 +1,253 @@
1
1
  /**
2
- * Shared response-header mutation for HTTP middleware plugins.
3
- * `Headers` are immutable-in-place on a built `Response` rebuild instead.
2
+ * Official `headers` plugin — the complete secure-headers set on every HTTP
3
+ * response (helmet parity, API-first defaults). Uses only the public plugin
4
+ * API (unified-theory §14).
4
5
  */
5
6
 
6
- /**
7
- * Rebuild a response with headers mutated by `fn`.
8
- *
9
- * @param response - Original response
10
- * @param fn - Header mutation
11
- */
12
- export function withHeaders(response: Response, fn: (headers: Headers) => void): Response {
13
- const headers = new Headers(response.headers);
14
- fn(headers);
15
- return new Response(response.body, {
16
- status: response.status,
17
- statusText: response.statusText,
18
- headers,
19
- });
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 "./response-headers.ts";
16
+
17
+ /** HSTS value options (see {@link HeadersOptions.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 HeadersOptions.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;
20
39
  }
21
40
 
22
41
  /**
23
- * Set a header only when absent (or when `override` is on).
24
- *
25
- * @param headers - Mutable headers
26
- * @param name - Header name
27
- * @param value - Header value
28
- * @param override - Replace an app-set value
42
+ * Helmet's default CSP the baseline `directives` merge over unless
43
+ * `useDefaults: false`.
29
44
  */
30
- export function setUnlessPresent(
31
- headers: Headers,
32
- name: string,
33
- value: string,
34
- override: boolean,
35
- ): void {
36
- if (!override && headers.has(name)) return;
37
- headers.set(name, value);
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 headers}. */
60
+ export interface HeadersOptions {
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
+ };
38
155
  }
39
156
 
40
157
  /**
41
- * Append a token to `Vary` without duplicating it.
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).
42
165
  *
43
- * @param headers - Mutable headers
44
- * @param token - Vary token (e.g. `"origin"`)
166
+ * @param options - Header values, or a config source
45
167
  */
46
- export function appendVary(headers: Headers, token: string): void {
47
- const vary = headers.get("vary");
48
- if (vary === null) {
49
- headers.set("vary", token);
50
- return;
51
- }
52
- const pattern = new RegExp(`\\b${token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i");
53
- if (!pattern.test(vary)) headers.set("vary", `${vary}, ${token}`);
168
+ export function headers(options: HeadersOptions | ConfigSource<HeadersOptions> = {}): PluginDef {
169
+ const def = plugin("headers", {
170
+ version: "0.1.0",
171
+ config: pluginConfigSnapshot(options),
172
+ }).hook("onResponse", (ctx) => {
173
+ if (!ctx.response) return;
174
+ const resolved = resolvePluginOptions(options);
175
+ const override = resolved.override ?? false;
176
+
177
+ ctx.response = withHeaders(ctx.response, (headers) => {
178
+ setUnlessPresent(headers, "x-content-type-options", "nosniff", override);
179
+ setUnlessPresent(headers, "x-frame-options", resolved.frameOptions ?? "DENY", override);
180
+ setUnlessPresent(
181
+ headers,
182
+ "referrer-policy",
183
+ resolved.referrerPolicy ?? "no-referrer",
184
+ override,
185
+ );
186
+
187
+ if (resolved.originAgentCluster ?? true) {
188
+ setUnlessPresent(headers, "origin-agent-cluster", "?1", override);
189
+ }
190
+ const dns = resolved.dnsPrefetchControl ?? true;
191
+ if (dns !== false) {
192
+ const allow = typeof dns === "object" ? dns.allow : false;
193
+ setUnlessPresent(headers, "x-dns-prefetch-control", allow ? "on" : "off", override);
194
+ }
195
+ if (resolved.downloadOptions ?? true) {
196
+ setUnlessPresent(headers, "x-download-options", "noopen", override);
197
+ }
198
+ setUnlessPresent(
199
+ headers,
200
+ "x-permitted-cross-domain-policies",
201
+ resolved.permittedCrossDomainPolicies ?? "none",
202
+ override,
203
+ );
204
+ if (resolved.xssProtection ?? true) {
205
+ setUnlessPresent(headers, "x-xss-protection", "0", override);
206
+ }
207
+
208
+ const poweredBy = resolved.poweredBy ?? true;
209
+ if (poweredBy === true) {
210
+ headers.delete("x-powered-by");
211
+ } else if (typeof poweredBy === "string") {
212
+ setUnlessPresent(headers, "x-powered-by", poweredBy, override);
213
+ }
214
+
215
+ if (resolved.contentSecurityPolicy !== undefined) {
216
+ const csp = cspEntry(resolved.contentSecurityPolicy);
217
+ setUnlessPresent(headers, csp.name, csp.value, override);
218
+ }
219
+ if (resolved.hsts !== undefined && resolved.hsts !== false) {
220
+ setUnlessPresent(headers, "strict-transport-security", hstsValue(resolved.hsts), override);
221
+ }
222
+ if (resolved.permissionsPolicy !== undefined) {
223
+ setUnlessPresent(headers, "permissions-policy", resolved.permissionsPolicy, override);
224
+ }
225
+ if (resolved.crossOriginOpenerPolicy !== undefined) {
226
+ setUnlessPresent(
227
+ headers,
228
+ "cross-origin-opener-policy",
229
+ resolved.crossOriginOpenerPolicy,
230
+ override,
231
+ );
232
+ }
233
+ if (resolved.crossOriginResourcePolicy !== undefined) {
234
+ setUnlessPresent(
235
+ headers,
236
+ "cross-origin-resource-policy",
237
+ resolved.crossOriginResourcePolicy,
238
+ override,
239
+ );
240
+ }
241
+ if (resolved.crossOriginEmbedderPolicy !== undefined) {
242
+ setUnlessPresent(
243
+ headers,
244
+ "cross-origin-embedder-policy",
245
+ resolved.crossOriginEmbedderPolicy,
246
+ override,
247
+ );
248
+ }
249
+ });
250
+ });
251
+
252
+ return isConfigSource(options) ? withConfigTable(def, options) : def;
54
253
  }
@@ -27,6 +27,14 @@ export {
27
27
  type PasskeyOptions,
28
28
  type PasskeyStore,
29
29
  } from "./passkey.ts";
30
+ export {
31
+ b64urlDecode,
32
+ b64urlEncode,
33
+ buildAuthenticatorData,
34
+ signWebAuthnAssertion,
35
+ verifyWebAuthnCeremony,
36
+ type WebAuthnVerifyOptions,
37
+ } from "./passkey-webauthn.ts";
30
38
  export {
31
39
  createPhoneStore,
32
40
  phoneNumber,
@@ -35,11 +43,11 @@ export {
35
43
  } from "./phone-number.ts";
36
44
  export {
37
45
  defaultCspDirectives,
38
- securityHeaders,
46
+ headers,
39
47
  type CspOptions,
48
+ type HeadersOptions,
40
49
  type HstsOptions,
41
- type SecurityHeadersOptions,
42
- } from "./security-headers.ts";
50
+ } from "./headers.ts";
43
51
  export {
44
52
  createTwoFactorStore,
45
53
  twoFactor,