Skip to content
Open
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 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', {
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.`);
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