Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
* Bump @octokit/rest to ^22.0.1. Refs STCLI-280.
* Commit `yarn.lock` to avoid future supply chain attacks. Refs STCLI-281.
* *BREAKING* bump `engines.node` to v22. Refs STCLI-XXX.
* Add capability to attach a Content-Security-Policy to locally served, existing build. Refs STCLI-286.

## [4.0.0](https://github.com/folio-org/stripes-cli/tree/v4.0.0) (2025-02-24)
[Full Changelog](https://github.com/folio-org/stripes-cli/compare/v3.2.0...v4.0.0)
Expand Down
4 changes: 4 additions & 0 deletions lib/commands/serve.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ module.exports = {
type: 'string',
conflicts: 'configFile',
})
.option('csp-file', {
Comment thread
JohnC-80 marked this conversation as resolved.
Outdated
describe: 'Path to a text file containing a Content-Security-Policy header value to apply when serving',
type: 'string',
})
.option('mirage [scenario]', {
describe: 'Enable Mirage Server when available and optionally specify a scenario',
type: 'string',
Expand Down
16 changes: 16 additions & 0 deletions lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,25 @@ function start(dir, options) {
const app = express();
app.use(express.json());
app.use(cors());
if (options.cspFile) {
const cspFilePath = path.resolve(options.cspFile);
if (!fs.existsSync(cspFilePath)) {
console.error(`CSP file "${options.cspFile}" does not exist.`);
Comment thread
zburke marked this conversation as resolved.
Outdated
return;
}
const cspValue = fs.readFileSync(cspFilePath, 'utf8').trim();
app.use((req, res, next) => {
res.set('Content-Security-Policy-Report-Only', cspValue);
next();
});
}
app.use(express.static(dir, {}));
app.use(logger('tiny'));
const resolvedPath = path.resolve(dir);

app.get('*', (req, res) => {
res.sendFile(path.join(resolvedPath, 'index.html'));
});
const server = http.createServer(app);

// Perform some basic checks to ensure we have a directory with something to serve
Expand Down
62 changes: 62 additions & 0 deletions test/server.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const fs = require('fs');
const http = require('http');
const os = require('os');
const path = require('path');
const expect = require('chai').expect;
const server = require('../lib/server');

describe('The server module', function () {
beforeEach(function () {
this.tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'stripes-cli-server-test-'));
fs.writeFileSync(path.join(this.tmpDir, 'index.html'), '<html></html>');
this.httpCreateServerSpy = this.sandbox.spy(http, 'createServer');
});

afterEach(function () {
fs.rmSync(this.tmpDir, { recursive: true, force: true });
});

describe('csp-file option', function () {
it('applies the CSP header from the supplied file when serving', function (done) {
const cspValue = "default-src 'self'";
const cspFilePath = path.join(this.tmpDir, 'csp.txt');
fs.writeFileSync(cspFilePath, `${cspValue}\n`);

server.start(this.tmpDir, { port: 0, cspFile: cspFilePath });

const httpServer = this.httpCreateServerSpy.returnValues[0];
httpServer.on('listening', () => {
const { port } = httpServer.address();
http.get({ hostname: 'localhost', port, path: '/' }, (res) => {
expect(res.headers['content-security-policy-report-only']).to.equal(cspValue);
res.resume();
httpServer.close(done);
});
});
});

it('does not set a CSP header when the option is omitted', function (done) {
server.start(this.tmpDir, { port: 0 });

const httpServer = this.httpCreateServerSpy.returnValues[0];
httpServer.on('listening', () => {
const { port } = httpServer.address();
http.get({ hostname: 'localhost', port, path: '/' }, (res) => {
expect(res.headers['content-security-policy-report-only']).to.be.undefined;
res.resume();
httpServer.close(done);
});
});
});

it('logs an error and does not start the server when the csp file does not exist', function () {
this.sandbox.spy(console, 'error');
const missingCspPath = path.join(this.tmpDir, 'missing-csp.txt');

server.start(this.tmpDir, { port: 0, cspFile: missingCspPath });

expect(console.error).to.have.been.calledWithMatch(`CSP file "${missingCspPath}" does not exist.`);
expect(this.httpCreateServerSpy).to.not.have.been.called;
});
});
});
Loading