Skip to content

export handleHttpResponseHeaders hook #39

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ node_modules
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
/coverage
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ yarn.lock

# Changesets
.changeset

# Code coverage
/coverage
25 changes: 10 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,38 +12,33 @@ Adds HTTP headers to page responses from any SvelteKit web application enhancing
npm install @faranglao/sveltekit-security-headers
```

### Getting Started
## Getting Started

To add HTTP Security Response Headers to a SvelteKit application follow these steps:

1. Install the `@faranglao/sveltekit-security-headers` package using `npm install @faranglao/sveltekit-security-headers`.

1. Add or update [src/hooks.server.ts](./src/hooks.server.ts) file:
2. Add the `handleHttpResponseHeaders` Hook in [src/hooks.server.ts](./src/hooks.server.ts):

- Scenario: no previous Hooks defined in `src/hooks.server.ts`:
- Scenario 1: no previous `handle` Hook defined in `src/hooks.server.ts`:

```ts
import type { Handle } from '@sveltejs/kit';
import { HttpResponseHeaders } from '@faranglao/sveltekit-security-headers';
import { handleHttpResponseHeaders } from '@faranglao/sveltekit-security-headers';

export const handle: Handle = HttpResponseHeaders.handle;
export const handle: Handle = handleHttpResponseHeaders;
```

- Scenario: existing Hooks defined in `src/hooks.server.ts`:
- Scenario 2: existing `handle` Hook defined in `src/hooks.server.ts`:

Use [the sequence helper function](https://kit.svelte.dev/docs/modules#sveltejs-kit-hooks) to wrap the existing hook and `HttpResponseHeaders.handle` hook as shown below.
Use [the sequence helper function](https://kit.svelte.dev/docs/modules#sveltejs-kit-hooks) to wrap the existing hook and `handleHttpResponseHeaders` handler as shown below.

```ts
import type { Handle } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit/hooks';
import { HttpResponseHeaders } from '@faranglao/sveltekit-security-headers';

export const handle: Handle = sequence(async ({ event, resolve }) => {
// Do something with the inbound request
const response = await resolve(event);
// Do something with the outbound response before returning it
return response;
}, HttpResponseHeaders.handle);
import { handleHttpResponseHeaders } from '@faranglao/sveltekit-security-headers';

export const handle: Handle = sequence(existingHook, handleHttpResponseHeaders);
```

Then run the web application using `npm run dev` or `npm run build && npm run preview`.
Expand Down
34 changes: 31 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"package": "svelte-kit sync && svelte-package && publint",
"prepublishOnly": "npm run package",
"test": "npm run test:integration && npm run test:unit",
"coverage": "vitest run --coverage",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .",
Expand Down Expand Up @@ -71,7 +72,8 @@
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^5.1.1",
"vitest": "^1.2.0"
"vitest": "^1.2.0",
"vitest-mock-extended": "^1.3.1"
},
"svelte": "./dist/index.js",
"types": "./dist/index.d.ts"
Expand Down
10 changes: 10 additions & 0 deletions src/hooks.server.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { describe, it, expect } from 'vitest';
import { handle } from './hooks.server.js';

describe('Server Hooks', () => {
describe('handle function', () => {
it('is defined', () => {
expect(handle).toBeDefined();
});
});
});
21 changes: 8 additions & 13 deletions src/hooks.server.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
// Scenario: no previous Hooks defined in src/hooks.server.ts
import { HttpResponseHeaders } from './lib/headers.js';
// src/hooks.server.ts
import { handleHttpResponseHeaders } from './lib/hook.js';
import type { Handle } from '@sveltejs/kit';

export const handle: Handle = HttpResponseHeaders.handle;

// Scenario: existing Hooks defined in src/hooks.server.ts:
// import { HttpResponseHeaders } from "$lib/headers.js";
// import type { Handle } from "@sveltejs/kit";
// import sequence for scenario 2
// import { sequence } from "@sveltejs/kit/hooks";

// export const handle: Handle = sequence( async ( { event, resolve } ) => {
// // Do something with the inbound request
// const response = await resolve( event );
// // Do something with the response before returning it
// return response;
// }, HttpResponseHeaders.handle);
// scenario 1: no previous handle Hook defined
export const handle: Handle = handleHttpResponseHeaders;

// scenario 2: existing handle Hook defined
// export const handle: Handle = sequence( existingHook, handleHttpResponseHeaders );
13 changes: 4 additions & 9 deletions src/lib/headers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { Handle } from '@sveltejs/kit';
import type { SecurityHeader } from './types.js';

const Rules = {
Expand All @@ -10,7 +9,10 @@ const Rules = {
]
};

const applySecurityHeaders = (headers: Headers, securityHeaders: SecurityHeader[]) => {
const applySecurityHeaders = (
headers: Headers,
securityHeaders: SecurityHeader[] = Rules.SecurityHeaders
) => {
securityHeaders.forEach((header) => {
if (header.value !== undefined) {
const currentValue = headers.get(header.name);
Expand All @@ -32,14 +34,7 @@ const applySecurityHeaders = (headers: Headers, securityHeaders: SecurityHeader[
});
};

const applySecurityHeadersHandler: Handle = async ({ event, resolve }) => {
const response = await resolve(event);
applySecurityHeaders(response.headers, Rules.SecurityHeaders);
return response;
};

export const HttpResponseHeaders = {
handle: applySecurityHeadersHandler,
applySecurityHeaders,
Rules
};
33 changes: 33 additions & 0 deletions src/lib/hook.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { describe, it, expect, beforeEach } from 'vitest';
import type { RequestEvent } from '@sveltejs/kit';
import { mockDeep, type DeepMockProxy } from 'vitest-mock-extended';
import { handleHttpResponseHeaders } from './hook.js';

describe('handleHttpResponseHeaders', () => {
let mockEvent: DeepMockProxy<RequestEvent>;
let mockResponse: DeepMockProxy<Response>;

beforeEach(() => {
mockEvent = mockDeep<RequestEvent>();
mockResponse = mockDeep<Response>();
});

it('should apply security headers to response', async () => {
const event = mockEvent;
const resolve = () => mockResponse;

await handleHttpResponseHeaders({ event, resolve });

expect(mockResponse.headers.set).toHaveBeenCalledWith('X-Frame-Options', 'DENY');
expect(mockResponse.headers.set).toHaveBeenCalledWith('X-Content-Type-Options', 'nosniff');
expect(mockResponse.headers.set).toHaveBeenCalledWith(
'Referrer-Policy',
'strict-origin-when-cross-origin'
);
expect(mockResponse.headers.set).toHaveBeenCalledWith(
'Permissions-Policy',
'geolocation=(), camera=(), microphone=()'
);
});
});
8 changes: 8 additions & 0 deletions src/lib/hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { Handle } from '@sveltejs/kit';
import { HttpResponseHeaders } from './headers.js';

export const handleHttpResponseHeaders: Handle = async ({ event, resolve }) => {
const response = await resolve(event);
HttpResponseHeaders.applySecurityHeaders(response.headers);
return response;
};
2 changes: 1 addition & 1 deletion src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// Reexport your entry components here

export { HttpResponseHeaders } from './headers.js';
export { handleHttpResponseHeaders } from './hook.js';
11 changes: 11 additions & 0 deletions src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@
npm install {npmPackage}
</code></pre>

<h2>Getting Started</h2>

<p>
Add the <code>handleHttpResponseHeaders</code> Hook in <code>src/hooks.server.ts</code> file.
Refer to the repo
<a
href="https://github.com/kevinobee/sveltekit-security-headers?tab=readme-ov-file#getting-started"
>README</a
> for code snippets.
</p>

<p>
Then run the web application using <code>npm run dev</code> or
<code>npm run build && npm run preview</code>.
Expand Down
28 changes: 5 additions & 23 deletions tests/http.headers.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { expect, test, type Response, type Page } from '@playwright/test';

test.describe.configure({ mode: 'serial' });
import { expect, test } from '@playwright/test';

test.describe('HTTP Security Response Headers', () => {
const homepage = '/';
Expand All @@ -11,28 +9,12 @@ test.describe('HTTP Security Response Headers', () => {
{ name: 'Referrer-Policy', recommendedValue: 'strict-origin-when-cross-origin' },
{ name: 'Permissions-Policy', recommendedValue: 'geolocation=(), camera=(), microphone=()' }
];
let response: Response | null;

let page: Page;

test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
response = await page.goto(homepage);
});

test.afterAll(async () => {
await page.close();
});

expectedSecurityHeaders.forEach(({ name, recommendedValue }) => {
test.describe(name, () => {
test('header is returned', async () => {
expect(await response?.headerValue(name)).not.toBeNull();
});

test('header is recommended value', async () => {
expect(await response?.headerValue(name)).toBe(recommendedValue);
});
test(`${name} header is set to '${recommendedValue}'`, async ({ page }) => {
const response = await page.goto(homepage);
expect(await response?.headerValue(name)).not.toBeNull();
expect(await response?.headerValue(name)).toBe(recommendedValue);
});
});
});