From 1badf0387bfd12239660d42df983f975342eb773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Correa=20Casablanca?= Date: Sun, 11 Feb 2024 10:15:43 +0100 Subject: [PATCH 1/2] feat!: improve public api Signed-off-by: Andres Correa Casablanca --- .editorconfig | 15 ++++++++ README.md | 53 ++++++++++++++------------ biome.json | 9 +---- main.d.ts | 23 +++++++++++- main.mjs | 42 +++++++++++---------- package.json | 101 ++++++++++++++++++++++++++------------------------ tsconfig.json | 36 ++++++++++++++++++ 7 files changed, 179 insertions(+), 100 deletions(-) create mode 100644 .editorconfig create mode 100644 tsconfig.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1ba9813 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true + +[*.{js,mjs,json,ts}] +charset = utf-8 +indent_style = tab +indent_size = 2 +trim_trailing_whitespace = true + +[*.md] +indent_style = space +indent_size = 2 diff --git a/README.md b/README.md index 8213725..318c5ae 100644 --- a/README.md +++ b/README.md @@ -14,25 +14,17 @@ hook performs 3 steps: them later for other purposes, such as configuring your `Content-Security-Policy` headers. -### Known limitations - -- For now, the SRI hashes calculation is done only for inlined resources. This - will be solved in future releases. -- For now, this integration only works for generated static content (the - exported subresource integrity hashes could be used in dynamic contexts, but - that does not cover the whole SSG use case) - ## How to install ```bash # With NPM -npm install @kindspells/astro-sri-csp +npm install --save-dev @kindspells/astro-sri-csp # With Yarn -yarn add @kindspells/astro-sri-csp +yarn add --dev @kindspells/astro-sri-csp # With PNPM -pnpm add @kindspells/astro-sri-csp +pnpm add --save-dev @kindspells/astro-sri-csp ``` ## How to use @@ -40,30 +32,45 @@ pnpm add @kindspells/astro-sri-csp In your `astro.config.mjs` file: ```javascript -import { join } from 'node:path' +import { resolve } from 'node:path' import { defineConfig } from 'astro/config' import { sriCSP } from '@kindspells/astro-sri-csp' const rootDir = new URL('.', import.meta.url).pathname -// In this example we set dist/client because we assume a "hybrid" output, and -// in that case it makes no sense to traverse the server-side generated code. -// If your site is 100% static, we shouldn't add the 'client' part. -const distDir = join(rootDir, 'dist', 'client') - -// This is the path where we'll generate the module containing the SRI hashes -// for your scripts and styles. There's no need to pass this parameter if you -// don't need this data, but it can be useful to configure your CSP policies. -const sriHashesModule = join(rootDir, 'src', 'utils', 'sriHashes.mjs') - export default defineConfig({ integrations: [ - sriCSP(distDir, sriHashesModule) + sriCSP({ + // This parameter is only "necessary" if you are working on something like + // "hybrid" output mode. It allows you to constrain the directories and + // files scan to just the generated client-side files (therefore saving + // time). + distDir: 'client', + + // This is the path where we'll generate the module containing the SRI + // hashes for your scripts and styles. There's no need to pass this + // parameter if you don't need this data, but it can be useful to + // configure your CSP policies. + sriHashesModule: resolve(rootDir, 'src', 'utils', 'sriHashes.mjs'), + }) ] }) ``` +## Known limitations + +- For now, the SRI hashes calculation is done only for inlined resources. This + will be solved in future releases. + +- For now, this integration only works for generated static content (the + exported subresource integrity hashes could be used in dynamic contexts, but + that does not cover the whole SSG use case) + +- The SRI hashes will be regenerated only when running `astro build`. This means + that if you need them to be up to date when you run `astro dev`, then you will + have to manually run `astro build`. + ## License This library is released under [MIT License](LICENSE). diff --git a/biome.json b/biome.json index 8f40e47..efece0f 100644 --- a/biome.json +++ b/biome.json @@ -16,13 +16,8 @@ "indentWidth": 2, "lineWidth": 80, "lineEnding": "lf", - "ignore": [ - "./.sst/**/*", - "./dist/**/*", - "./node_modules/**/*", - "./src/content/emailTemplates/**/*" - ], - "include": ["./*.json", "./*.js", "./*.mjs", "./*.d.ts"] + "ignore": ["./node_modules/**/*"], + "include": ["./*.json", "./*.mjs", "./*.d.ts"] }, "javascript": { "formatter": { diff --git a/main.d.ts b/main.d.ts index 5f16d28..d2a034e 100644 --- a/main.d.ts +++ b/main.d.ts @@ -1,6 +1,25 @@ +export type SriCspOptions = { + /** + * It specifies a directory _inside_ the dist/build directory. This is useful + * when working on "hybrid" output mode, as it allows to just scan the + * client-side assets, and not the server-side ones. + */ + distDir?: string | undefined + + /** + * Specifies the path for the auto-generated module that will contain the SRI + * hashes. Note that: + * - The generated module will be an ESM module + * - The generated module should be treated as source code, and not as a build + * artifact. + */ + sriHashesModule?: string | undefined +} + +export type StrictSriCspOptions = SriCspOptions & { distDir: string } + export function sriCSP( - distDir: string, - hashesOutputModule?: string | undefined, + sriCspOptions: SriCspOptions, ): import('astro').AstroIntegration export default sriCSP diff --git a/main.mjs b/main.mjs index 845e030..ebdb733 100644 --- a/main.mjs +++ b/main.mjs @@ -6,7 +6,8 @@ import { createHash } from 'node:crypto' import { readFile, readdir, stat, writeFile } from 'node:fs/promises' -import { extname, join } from 'node:path' +import { extname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' /** * @param {string} data @@ -109,7 +110,7 @@ const scanDirectory = async dirPath => { const inlineStyleHashes = /** @type {Set} */ (new Set()) for (const file of await readdir(dirPath)) { - const filePath = join(dirPath, file) + const filePath = resolve(dirPath, file) const stats = await stat(filePath) const hashes = stats.isDirectory() @@ -168,26 +169,25 @@ const arraysEqual = (a, b) => { } /** - * @param {string} distDir - * @param {string | undefined} hashesOutputModule + * @param {import('./main.d.ts').StrictSriCspOptions} sriCspOptions */ -const generateSRIHashes = async (distDir, hashesOutputModule) => { +const generateSRIHashes = async ({ distDir, sriHashesModule }) => { const hashes = await scanDirectory(distDir) const inlineScriptHashes = Array.from(hashes.inlineScriptHashes).sort() const inlineStyleHashes = Array.from(hashes.inlineStyleHashes).sort() - if (!hashesOutputModule) { + if (!sriHashesModule) { return } let persistHashes = false - if (await doesFileExist(hashesOutputModule)) { + if (await doesFileExist(sriHashesModule)) { const hashesModule = /** @type {{ inlineScriptHashes?: string[] | undefined inlineStyleHashes?: string[] | undefined - }} */ (await import(hashesOutputModule)) + }} */ (await import(sriHashesModule)) persistHashes = !arraysEqual(inlineScriptHashes, hashesModule.inlineScriptHashes ?? []) || @@ -205,22 +205,24 @@ const generateSRIHashes = async (distDir, hashesOutputModule) => { inlineStyleHashes, )})\n` - await writeFile(hashesOutputModule, hashesFileContent) + await writeFile(sriHashesModule, hashesFileContent) } } /** - * @param {string} distDir - * @param {string | undefined} hashesOutputModule + * @param {import('./main.d.ts').SriCspOptions} sriCspOptions + * @returns {import('astro').AstroIntegration} */ -export const sriCSP = (distDir, hashesOutputModule) => ({ - name: 'scp-sri-postbuild', - hooks: { - 'astro:build:done': async () => - await generateSRIHashes(distDir, hashesOutputModule), - 'astro:server:setup': async () => - await generateSRIHashes(distDir, hashesOutputModule), - }, -}) +export const sriCSP = sriCspOptions => + /** @type {import('astro').AstroIntegration} */ ({ + name: 'scp-sri-postbuild', + hooks: { + 'astro:build:done': async ({ dir }) => + await generateSRIHashes({ + distDir: resolve(fileURLToPath(dir), sriCspOptions.distDir ?? ''), + sriHashesModule: sriCspOptions.sriHashesModule, + }), + }, + }) export default sriCSP diff --git a/package.json b/package.json index 9631e10..75b02d7 100644 --- a/package.json +++ b/package.json @@ -1,50 +1,55 @@ { - "name": "@kindspells/astro-sri-csp", - "version": "0.1.8", - "description": "An Astro plugin to compute and inject SRI hashes for script and style tags", - "private": false, - "type": "module", - "main": "main.mjs", - "types": "main.d.ts", - "exports": { - "types": "./main.d.ts", - "default": "./main.mjs" - }, - "files": [ - "main.d.ts", - "main.mjs" - ], - "scripts": { - "format": "biome format --write .", - "lint": "biome lint ." - }, - "keywords": [ - "astro", - "astro-component", - "astro-integration", - "code-generation", - "csp", - "content-security-policy", - "security", - "sri", - "subresource-integrity", - "withastro" - ], - "author": "KindSpells Labs S.L.", - "license": "MIT", - "devDependencies": { - "@biomejs/biome": "^1.5.3", - "astro": "^4.3.5" - }, - "repository": "github:KindSpells/astro-sri-csp", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/kindspells-labs" - }, - { - "type": "individual", - "url": "https://ko-fi.com/coderspirit" - } - ] + "name": "@kindspells/astro-sri-csp", + "version": "0.2.0", + "description": "An Astro plugin to compute and inject SRI hashes for script and style tags", + "private": false, + "type": "module", + "main": "main.mjs", + "types": "main.d.ts", + "exports": { + "types": "./main.d.ts", + "default": "./main.mjs" + }, + "files": ["main.d.ts", "main.mjs"], + "scripts": { + "format": "biome format --write .", + "lint": "pnpm run lint:biome && pnpm run lint:tsc", + "lint:biome": "biome lint .", + "lint:tsc": "tsc -p ." + }, + "keywords": [ + "astro", + "astro-component", + "astro-integration", + "code-generation", + "csp", + "content-security-policy", + "security", + "sri", + "subresource-integrity", + "withastro" + ], + "author": "KindSpells Labs S.L.", + "license": "MIT", + "devDependencies": { + "@biomejs/biome": "^1.5.3", + "@types/node": "^20.11.17", + "astro": "^4.3.5", + "typescript": "^5.3.3" + }, + "repository": "github:KindSpells/astro-sri-csp", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/kindspells-labs" + }, + { + "type": "individual", + "url": "https://ko-fi.com/coderspirit" + } + ], + "packageManager": "pnpm@8.15.1", + "engines": { + "node": ">= 18.0.0" + } } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..21e9859 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,36 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "Node16", + "moduleResolution": "Node16", + + "baseUrl": ".", + "noEmit": true, + + "allowJs": true, + "checkJs": true, + + "isolatedModules": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "noImplicitThis": true, + "useUnknownInCatchVariables": true, + "alwaysStrict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + + "skipLibCheck": true + }, + "include": ["*.mjs", "*.d.ts", "*.ts"] +} From 30f8b6be4dde4e2420524bbe424b6dbe779cac65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Correa=20Casablanca?= Date: Sun, 11 Feb 2024 10:26:47 +0100 Subject: [PATCH 2/2] fix: regression Signed-off-by: Andres Correa Casablanca --- README.md | 6 ------ main.d.ts | 7 ------- main.mjs | 2 +- package.json | 2 +- 4 files changed, 2 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 318c5ae..9ff6b34 100644 --- a/README.md +++ b/README.md @@ -42,12 +42,6 @@ const rootDir = new URL('.', import.meta.url).pathname export default defineConfig({ integrations: [ sriCSP({ - // This parameter is only "necessary" if you are working on something like - // "hybrid" output mode. It allows you to constrain the directories and - // files scan to just the generated client-side files (therefore saving - // time). - distDir: 'client', - // This is the path where we'll generate the module containing the SRI // hashes for your scripts and styles. There's no need to pass this // parameter if you don't need this data, but it can be useful to diff --git a/main.d.ts b/main.d.ts index d2a034e..4e0ee38 100644 --- a/main.d.ts +++ b/main.d.ts @@ -1,11 +1,4 @@ export type SriCspOptions = { - /** - * It specifies a directory _inside_ the dist/build directory. This is useful - * when working on "hybrid" output mode, as it allows to just scan the - * client-side assets, and not the server-side ones. - */ - distDir?: string | undefined - /** * Specifies the path for the auto-generated module that will contain the SRI * hashes. Note that: diff --git a/main.mjs b/main.mjs index ebdb733..e622f91 100644 --- a/main.mjs +++ b/main.mjs @@ -219,7 +219,7 @@ export const sriCSP = sriCspOptions => hooks: { 'astro:build:done': async ({ dir }) => await generateSRIHashes({ - distDir: resolve(fileURLToPath(dir), sriCspOptions.distDir ?? ''), + distDir: fileURLToPath(dir), sriHashesModule: sriCspOptions.sriHashesModule, }), }, diff --git a/package.json b/package.json index 75b02d7..83381e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kindspells/astro-sri-csp", - "version": "0.2.0", + "version": "0.2.1", "description": "An Astro plugin to compute and inject SRI hashes for script and style tags", "private": false, "type": "module",