{
  "$schema": "https://hyperjs.ai/schema/registry-item.json",
  "name": "csp",
  "version": "0.1.0",
  "description": "Content-Security-Policy + sibling headers (CSP, CORP, COEP, COOP, Report-To) for Hyper.",
  "readme": "# @hyper/csp\n\nContent-Security-Policy + sibling headers (CSP, CORP, COEP, COOP, Report-To) for Hyper.\n\n## Install\n\nComponents are installed as source into your repo, not pulled from npm:\n\n```bash\nbunx hyper add csp\n```\n\nWires the alias `@hyper/csp` to `src/hyper/csp/` (configurable in `hyper.config.json`). See [hyperjs.ai](https://hyperjs.ai) for the full registry.\n\n## Usage\n\n```ts\nimport { Hyper } from \"@hyper/core\"\nimport { cspPlugin } from \"@hyper/csp\"\n\nexport default new Hyper()\n  .use(cspPlugin({ strictApi: true }))\n  .listen(3000)\n```\n\n## Docs\n\nSee the [main README](../../README.md) and [docs/](../../docs) for guides and integration recipes.\n\n## License\n\nMIT\n",
  "registryDeps": [
    "core"
  ],
  "peerDeps": {},
  "optionalPeerDeps": {},
  "files": [
    {
      "path": "csp/index.ts",
      "contents": "/**\n * @hyper/csp — Content-Security-Policy + strict-by-default siblings.\n *\n * Most Hyper deployments serve JSON APIs and never render HTML, so the\n * default policy is extremely restrictive:\n *\n *   default-src 'none'; frame-ancestors 'none'; base-uri 'none';\n *   form-action 'none'; object-src 'none'; upgrade-insecure-requests\n *\n * Consumers that serve HTML can provide a tighter HTML-shaped policy\n * using `cspPlugin({ directives: {...} })`. Per-response nonces are\n * minted when `noncePlaceholder` is set — the handler can read the\n * nonce from `ctx.cspNonce` and inject it into inline `<script>` tags.\n */\n\nimport type { HyperPlugin } from \"@hyper/core\"\n\nexport interface CspConfig {\n  /** Emit `Content-Security-Policy-Report-Only` instead of the enforcing header. */\n  readonly reportOnly?: boolean\n  /** Whether to add a fresh per-response nonce to script-src/style-src. */\n  readonly nonce?: boolean\n  /** Key-value directive map. Values are arrays of allowed sources. */\n  readonly directives?: Partial<Record<CspDirective, readonly string[]>>\n  /** Report endpoint; if set, emits `report-to` + a Report-To header stub. */\n  readonly reportUri?: string\n  /** Extra response headers. */\n  readonly headers?: {\n    readonly permissionsPolicy?: string\n    readonly referrerPolicy?: string\n    readonly crossOriginEmbedderPolicy?: \"require-corp\" | \"unsafe-none\" | \"credentialless\"\n    readonly crossOriginOpenerPolicy?: \"same-origin\" | \"same-origin-allow-popups\" | \"unsafe-none\"\n    readonly crossOriginResourcePolicy?: \"same-site\" | \"same-origin\" | \"cross-origin\"\n  }\n}\n\nexport type CspDirective =\n  | \"default-src\"\n  | \"base-uri\"\n  | \"connect-src\"\n  | \"font-src\"\n  | \"form-action\"\n  | \"frame-ancestors\"\n  | \"frame-src\"\n  | \"img-src\"\n  | \"manifest-src\"\n  | \"media-src\"\n  | \"object-src\"\n  | \"script-src\"\n  | \"script-src-elem\"\n  | \"script-src-attr\"\n  | \"style-src\"\n  | \"style-src-elem\"\n  | \"style-src-attr\"\n  | \"worker-src\"\n  | \"upgrade-insecure-requests\"\n  | \"block-all-mixed-content\"\n\nconst API_DEFAULT: Partial<Record<CspDirective, readonly string[]>> = {\n  \"default-src\": [\"'none'\"],\n  \"frame-ancestors\": [\"'none'\"],\n  \"base-uri\": [\"'none'\"],\n  \"form-action\": [\"'none'\"],\n  \"object-src\": [\"'none'\"],\n  \"upgrade-insecure-requests\": [],\n}\n\ndeclare module \"@hyper/core\" {\n  interface AppContext {\n    readonly cspNonce?: string\n  }\n}\n\nexport function cspPlugin(config: CspConfig = {}): HyperPlugin {\n  const directives: Record<string, readonly string[]> = {\n    ...API_DEFAULT,\n    ...(config.directives as Record<string, readonly string[]> | undefined),\n  }\n  const header = config.reportOnly\n    ? \"content-security-policy-report-only\"\n    : \"content-security-policy\"\n\n  return {\n    name: \"@hyper/csp\",\n    request: {\n      before({ ctx }) {\n        if (config.nonce) {\n          const nonce = randomNonce()\n          ;(ctx as { cspNonce?: string }).cspNonce = nonce\n        }\n      },\n      after({ ctx, res }) {\n        const merged: Record<string, readonly string[]> = { ...directives }\n        if (config.nonce) {\n          const n = (ctx as { cspNonce?: string }).cspNonce\n          if (n) {\n            merged[\"script-src\"] = dedupe([...(merged[\"script-src\"] ?? []), `'nonce-${n}'`])\n            merged[\"style-src\"] = dedupe([...(merged[\"style-src\"] ?? []), `'nonce-${n}'`])\n          }\n        }\n        if (config.reportUri) {\n          merged[\"report-uri\"] = [config.reportUri]\n        }\n        const value = serialize(merged)\n        if (value) res.headers.set(header, value)\n        if (config.reportUri && !res.headers.has(\"report-to\")) {\n          res.headers.set(\n            \"report-to\",\n            `{\"group\":\"csp\",\"max_age\":10886400,\"endpoints\":[{\"url\":\"${config.reportUri}\"}]}`,\n          )\n        }\n        const h = config.headers\n        if (h?.permissionsPolicy) res.headers.set(\"permissions-policy\", h.permissionsPolicy)\n        if (h?.referrerPolicy) res.headers.set(\"referrer-policy\", h.referrerPolicy)\n        if (h?.crossOriginEmbedderPolicy) {\n          res.headers.set(\"cross-origin-embedder-policy\", h.crossOriginEmbedderPolicy)\n        }\n        if (h?.crossOriginOpenerPolicy) {\n          res.headers.set(\"cross-origin-opener-policy\", h.crossOriginOpenerPolicy)\n        }\n        if (h?.crossOriginResourcePolicy) {\n          res.headers.set(\"cross-origin-resource-policy\", h.crossOriginResourcePolicy)\n        }\n      },\n    },\n  }\n}\n\nfunction serialize(directives: Record<string, readonly string[]>): string {\n  const parts: string[] = []\n  for (const [name, values] of Object.entries(directives)) {\n    if (values.length === 0) {\n      parts.push(name)\n    } else {\n      parts.push(`${name} ${values.join(\" \")}`)\n    }\n  }\n  return parts.join(\"; \")\n}\n\nfunction dedupe(values: readonly string[]): readonly string[] {\n  return Array.from(new Set(values))\n}\n\nfunction randomNonce(): string {\n  const buf = new Uint8Array(16)\n  crypto.getRandomValues(buf)\n  let s = \"\"\n  for (let i = 0; i < buf.length; i++) s += String.fromCharCode(buf[i]!)\n  return btoa(s).replace(/=+$/, \"\")\n}\n",
      "sha256": "7a372546a274b87d0031d88155fed8932ac8943745fd90011c6493c6f322db95"
    }
  ],
  "subpaths": {}
}