@schalkneethling/toolkit 0.5.0 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs.map +1 -1
- package/hooks/auto-approve-safe-commands/hook.mjs +5 -1
- package/hooks/auto-approve-safe-commands/hook.mts +7 -6
- package/hooks/block-dangerous-commands/hook.mjs +3 -3
- package/hooks/block-dangerous-commands/hook.mts +10 -22
- package/package.json +9 -9
- package/skills/css-tokens/SKILL.md +1 -1
- package/skills/css-tokens/references/tokens.css +6 -10
- package/skills/frontend-security/SKILL.md +3 -0
- package/skills/frontend-security/references/csp-configuration.md +68 -51
- package/skills/frontend-security/references/csrf-protection.md +74 -70
- package/skills/frontend-security/references/dom-security.md +36 -29
- package/skills/frontend-security/references/file-upload-security.md +101 -69
- package/skills/frontend-security/references/framework-patterns.md +42 -40
- package/skills/frontend-security/references/input-validation.md +36 -31
- package/skills/frontend-security/references/jwt-security.md +68 -84
- package/skills/frontend-security/references/nodejs-npm-security.md +63 -55
- package/skills/frontend-security/references/xss-prevention.md +38 -36
- package/skills/frontend-testing/SKILL.md +31 -38
- package/skills/frontend-testing/references/accessibility-testing.md +56 -62
- package/skills/frontend-testing/references/aria-snapshots.md +35 -34
- package/skills/frontend-testing/references/locator-strategies.md +37 -40
- package/skills/frontend-testing/references/visual-regression.md +29 -23
- package/skills/npm-publishing-best-practices/SKILL.md +316 -0
- package/skills/semantic-html/SKILL.md +5 -21
- package/skills/semantic-html/references/heading-patterns.md +1 -5
|
@@ -5,6 +5,7 @@ Visual regression testing (VRT) catches unintended visual changes by comparing s
|
|
|
5
5
|
## When to Use VRT
|
|
6
6
|
|
|
7
7
|
**Good candidates:**
|
|
8
|
+
|
|
8
9
|
- Design system components
|
|
9
10
|
- Critical landing pages
|
|
10
11
|
- Complex layouts (grids, dashboards)
|
|
@@ -12,6 +13,7 @@ Visual regression testing (VRT) catches unintended visual changes by comparing s
|
|
|
12
13
|
- Cross-browser visual consistency
|
|
13
14
|
|
|
14
15
|
**Poor candidates:**
|
|
16
|
+
|
|
15
17
|
- Pages with frequently changing dynamic content
|
|
16
18
|
- Components with animations
|
|
17
19
|
- Time-dependent displays
|
|
@@ -26,7 +28,7 @@ import { test, expect } from "@playwright/test";
|
|
|
26
28
|
|
|
27
29
|
test("homepage looks correct", async ({ page }) => {
|
|
28
30
|
await page.goto("/");
|
|
29
|
-
|
|
31
|
+
|
|
30
32
|
await expect(page).toHaveScreenshot();
|
|
31
33
|
});
|
|
32
34
|
```
|
|
@@ -38,7 +40,7 @@ First run creates the baseline. Subsequent runs compare against it.
|
|
|
38
40
|
```javascript
|
|
39
41
|
test("hero section renders correctly", async ({ page }) => {
|
|
40
42
|
await page.goto("/");
|
|
41
|
-
|
|
43
|
+
|
|
42
44
|
await expect(page).toHaveScreenshot("homepage-hero.png");
|
|
43
45
|
});
|
|
44
46
|
```
|
|
@@ -50,7 +52,7 @@ More stable than full-page screenshots:
|
|
|
50
52
|
```javascript
|
|
51
53
|
test("product card renders correctly", async ({ page }) => {
|
|
52
54
|
await page.goto("/products/123");
|
|
53
|
-
|
|
55
|
+
|
|
54
56
|
const productCard = page.getByTestId("product-card");
|
|
55
57
|
await expect(productCard).toHaveScreenshot("product-card.png");
|
|
56
58
|
});
|
|
@@ -61,7 +63,7 @@ test("product card renders correctly", async ({ page }) => {
|
|
|
61
63
|
```javascript
|
|
62
64
|
test("full page layout", async ({ page }) => {
|
|
63
65
|
await page.goto("/about");
|
|
64
|
-
|
|
66
|
+
|
|
65
67
|
await expect(page).toHaveScreenshot("about-page-full.png", {
|
|
66
68
|
fullPage: true,
|
|
67
69
|
});
|
|
@@ -77,13 +79,13 @@ Visual tests fail due to timing, rendering, or environment differences. Stabiliz
|
|
|
77
79
|
```javascript
|
|
78
80
|
test("page renders after loading", async ({ page }) => {
|
|
79
81
|
await page.goto("/");
|
|
80
|
-
|
|
82
|
+
|
|
81
83
|
// Wait for network to be idle
|
|
82
84
|
await page.waitForLoadState("networkidle");
|
|
83
|
-
|
|
85
|
+
|
|
84
86
|
// Wait for web fonts to load
|
|
85
87
|
await page.evaluate(() => document.fonts.ready);
|
|
86
|
-
|
|
88
|
+
|
|
87
89
|
await expect(page).toHaveScreenshot();
|
|
88
90
|
});
|
|
89
91
|
```
|
|
@@ -107,7 +109,7 @@ Or in test:
|
|
|
107
109
|
```javascript
|
|
108
110
|
test("static screenshot", async ({ page }) => {
|
|
109
111
|
await page.goto("/");
|
|
110
|
-
|
|
112
|
+
|
|
111
113
|
// Inject CSS to disable animations
|
|
112
114
|
await page.addStyleTag({
|
|
113
115
|
content: `
|
|
@@ -117,7 +119,7 @@ test("static screenshot", async ({ page }) => {
|
|
|
117
119
|
}
|
|
118
120
|
`,
|
|
119
121
|
});
|
|
120
|
-
|
|
122
|
+
|
|
121
123
|
await expect(page).toHaveScreenshot();
|
|
122
124
|
});
|
|
123
125
|
```
|
|
@@ -129,7 +131,7 @@ Hide elements that change between runs:
|
|
|
129
131
|
```javascript
|
|
130
132
|
test("page with masked dynamic content", async ({ page }) => {
|
|
131
133
|
await page.goto("/dashboard");
|
|
132
|
-
|
|
134
|
+
|
|
133
135
|
await expect(page).toHaveScreenshot({
|
|
134
136
|
mask: [
|
|
135
137
|
page.locator("[data-testid='current-time']"),
|
|
@@ -187,9 +189,9 @@ Allow minor pixel differences to reduce flakiness.
|
|
|
187
189
|
export default defineConfig({
|
|
188
190
|
expect: {
|
|
189
191
|
toHaveScreenshot: {
|
|
190
|
-
maxDiffPixels: 100,
|
|
191
|
-
maxDiffPixelRatio: 0.01,
|
|
192
|
-
threshold: 0.2,
|
|
192
|
+
maxDiffPixels: 100, // Allow up to 100 different pixels
|
|
193
|
+
maxDiffPixelRatio: 0.01, // Or 1% of total pixels
|
|
194
|
+
threshold: 0.2, // Color difference threshold (0-1)
|
|
193
195
|
},
|
|
194
196
|
},
|
|
195
197
|
});
|
|
@@ -206,14 +208,14 @@ await expect(page).toHaveScreenshot({
|
|
|
206
208
|
|
|
207
209
|
### Threshold Guidelines
|
|
208
210
|
|
|
209
|
-
| Setting
|
|
210
|
-
|
|
211
|
-
| `threshold`
|
|
212
|
-
| `threshold`
|
|
213
|
-
| `threshold`
|
|
214
|
-
| `maxDiffPixels`
|
|
215
|
-
| `maxDiffPixels`
|
|
216
|
-
| `maxDiffPixelRatio` | 0.01
|
|
211
|
+
| Setting | Value | Use Case |
|
|
212
|
+
| ------------------- | ----- | ---------------------------- |
|
|
213
|
+
| `threshold` | 0.1 | Strict pixel matching |
|
|
214
|
+
| `threshold` | 0.2 | Standard tolerance (default) |
|
|
215
|
+
| `threshold` | 0.3 | Generous for anti-aliasing |
|
|
216
|
+
| `maxDiffPixels` | 50 | Small components |
|
|
217
|
+
| `maxDiffPixels` | 200 | Full pages |
|
|
218
|
+
| `maxDiffPixelRatio` | 0.01 | 1% tolerance |
|
|
217
219
|
|
|
218
220
|
## Updating Baselines
|
|
219
221
|
|
|
@@ -290,6 +292,7 @@ export default defineConfig({
|
|
|
290
292
|
### OS Differences
|
|
291
293
|
|
|
292
294
|
Font rendering varies by OS. Options:
|
|
295
|
+
|
|
293
296
|
- Run visual tests in CI only (consistent environment)
|
|
294
297
|
- Use Docker with consistent fonts
|
|
295
298
|
- Generate baselines in CI, not locally
|
|
@@ -301,7 +304,7 @@ Font rendering varies by OS. Options:
|
|
|
301
304
|
```yaml
|
|
302
305
|
- name: Run visual tests
|
|
303
306
|
run: npx playwright test --project=chromium
|
|
304
|
-
|
|
307
|
+
|
|
305
308
|
- name: Upload diff artifacts on failure
|
|
306
309
|
if: failure()
|
|
307
310
|
uses: actions/upload-artifact@v7
|
|
@@ -317,7 +320,7 @@ For consistent baselines, generate in CI:
|
|
|
317
320
|
```yaml
|
|
318
321
|
- name: Update baselines
|
|
319
322
|
run: npx playwright test --update-snapshots
|
|
320
|
-
|
|
323
|
+
|
|
321
324
|
- name: Commit baselines
|
|
322
325
|
run: |
|
|
323
326
|
git config user.name "CI Bot"
|
|
@@ -360,6 +363,7 @@ npx playwright show-report
|
|
|
360
363
|
```
|
|
361
364
|
|
|
362
365
|
Shows:
|
|
366
|
+
|
|
363
367
|
- Expected (baseline) image
|
|
364
368
|
- Actual (current) image
|
|
365
369
|
- Diff highlighting changed pixels
|
|
@@ -398,12 +402,14 @@ This seems appealing for "cleaner" snapshot names, but it breaks visual testing.
|
|
|
398
402
|
> "Different snapshots are needed for different browsers and platforms due to variations in rendering and fonts."
|
|
399
403
|
|
|
400
404
|
**Why it fails:**
|
|
405
|
+
|
|
401
406
|
- Font rendering differs between macOS, Windows, and Linux
|
|
402
407
|
- Anti-aliasing algorithms vary by OS and browser
|
|
403
408
|
- Subpixel rendering produces different results
|
|
404
409
|
- You'll get constant false positives or false negatives
|
|
405
410
|
|
|
406
411
|
**Keep the default naming:**
|
|
412
|
+
|
|
407
413
|
```
|
|
408
414
|
button-chromium-darwin.png
|
|
409
415
|
button-chromium-linux.png
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: npm-package-publishing
|
|
3
|
+
description: >
|
|
4
|
+
Apply best practices when publishing npm packages, including secure CI/CD workflows, trusted
|
|
5
|
+
publishing via OIDC, GitHub repository hardening, and supply-chain attack prevention. Use this
|
|
6
|
+
skill whenever the user asks about publishing an npm package, setting up a publish workflow,
|
|
7
|
+
configuring GitHub Actions for release automation, managing npm tokens or secrets, setting up
|
|
8
|
+
changesets, or auditing an existing publishing pipeline for security. Also trigger when the user
|
|
9
|
+
mentions publint, OIDC trusted publishing, release automation, or package versioning workflows.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# npm Package Publishing — Best Practices
|
|
13
|
+
|
|
14
|
+
Based on the [e18e publishing guide](https://e18e.dev/docs/publishing.html). Reference it for
|
|
15
|
+
the canonical source; this skill distils the actionable steps.
|
|
16
|
+
|
|
17
|
+
> **Package manager note.** All examples in this skill use `npm` to match the e18e source
|
|
18
|
+
> material, but nothing here is npm-specific. Always use whichever package manager the project
|
|
19
|
+
> already uses — `pnpm`, `yarn`, `bun`, etc. Adapt commands accordingly:
|
|
20
|
+
>
|
|
21
|
+
> | npm | pnpm | yarn |
|
|
22
|
+
> | --------------------------------- | ------------------------------------------------- | ------------------------------------------------- |
|
|
23
|
+
> | `npm ci --ignore-scripts` | `pnpm install --frozen-lockfile --ignore-scripts` | `yarn install --frozen-lockfile --ignore-scripts` |
|
|
24
|
+
> | `npm i -g npm` | `pnpm add -g pnpm` | `yarn set version stable` |
|
|
25
|
+
> | `ignore-scripts=true` in `.npmrc` | `ignore-scripts=true` in `.npmrc` | `enableScripts: false` in `.yarnrc.yml` |
|
|
26
|
+
>
|
|
27
|
+
> Detect the project's package manager by checking for a lockfile (`pnpm-lock.yaml`,
|
|
28
|
+
> `yarn.lock`, `bun.lockb`) or a `packageManager` field in `package.json` before
|
|
29
|
+
> generating any commands or workflow steps.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 1 · Prerequisites
|
|
34
|
+
|
|
35
|
+
### 1.1 · Enforce 2FA everywhere
|
|
36
|
+
|
|
37
|
+
| Account | Location | Recommended method |
|
|
38
|
+
| ------- | -------------------------------------- | ---------------------------------------------------------------------------- |
|
|
39
|
+
| npm | Account → Security | Security key (YubiKey, Touch ID, Windows Hello); fallback: authenticator app |
|
|
40
|
+
| GitHub | Settings → Password and authentication | Same priority order |
|
|
41
|
+
|
|
42
|
+
Use a password manager with generated passwords for both accounts.
|
|
43
|
+
|
|
44
|
+
### 1.2 · Harden GitHub Actions settings
|
|
45
|
+
|
|
46
|
+
`Settings → Actions → General`:
|
|
47
|
+
|
|
48
|
+
- ✅ Require actions to be pinned to a **full-length commit SHA**
|
|
49
|
+
- ✅ Require approval for first-time contributors
|
|
50
|
+
- ✅ Set default workflow permissions to **"Read repository contents and packages"**
|
|
51
|
+
|
|
52
|
+
> If the repository belongs to an organisation, apply these settings at org level for consistency
|
|
53
|
+
> across all repositories.
|
|
54
|
+
|
|
55
|
+
### 1.3 · Configure branch protection
|
|
56
|
+
|
|
57
|
+
`Settings → Branches` → create a ruleset for `main`:
|
|
58
|
+
|
|
59
|
+
- ✅ Require a pull request before merging
|
|
60
|
+
- ✅ Require at least 1 approval
|
|
61
|
+
- ✅ Dismiss stale approvals when new commits are pushed
|
|
62
|
+
- ✅ Require approval of the most recent reviewable push
|
|
63
|
+
|
|
64
|
+
### 1.4 · Remove legacy npm tokens
|
|
65
|
+
|
|
66
|
+
`Settings → Secrets & Variables → Actions`: remove any stored npm tokens. OIDC trusted publishing
|
|
67
|
+
replaces them entirely — no long-lived secrets needed in the repository.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 2 · Trusted Publishing (OIDC)
|
|
72
|
+
|
|
73
|
+
Trusted publishing means GitHub Actions authenticates directly with npm via OIDC — no npm token
|
|
74
|
+
ever touches the repository.
|
|
75
|
+
|
|
76
|
+
### 2.1 · Configure on npmjs.com
|
|
77
|
+
|
|
78
|
+
1. Open your package page → **Settings** tab → **Trusted Publishing** section.
|
|
79
|
+
2. Add a trusted publisher:
|
|
80
|
+
- **Organisation / user**: your GitHub org or username
|
|
81
|
+
- **Repository**: the repository name
|
|
82
|
+
- **Workflow filename**: e.g. `publish.yml`
|
|
83
|
+
3. Check **"Require two-factor authentication and disallow tokens"** — this forces manual publishes
|
|
84
|
+
to use 2FA as well.
|
|
85
|
+
|
|
86
|
+
> For bulk configuration across many packages, use
|
|
87
|
+
> [`open-packages-on-npm`](https://github.com/antfu/open-packages-on-npm) to open each package
|
|
88
|
+
> in a new tab, then the
|
|
89
|
+
> [npm-trusted-publisher userscript](https://github.com/sxzz/userscripts/blob/main/src/npm-trusted-publisher.md)
|
|
90
|
+
> to configure them rapidly.
|
|
91
|
+
|
|
92
|
+
### 2.2 · npm CLI version requirement
|
|
93
|
+
|
|
94
|
+
The publish step **must** use npm CLI ≥ 11.5.1 for automatic OIDC trusted publishing.
|
|
95
|
+
Node.js 24 bundles npm 11.5.1; with older CI images, add a step before publishing:
|
|
96
|
+
|
|
97
|
+
```yaml
|
|
98
|
+
- run: npm i -g npm
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 3 · Standard Publish Workflow
|
|
104
|
+
|
|
105
|
+
Use the [e18e setup-publish template](https://github.com/e18e/setup-publish/blob/main/templates/default.yml)
|
|
106
|
+
as your base, or scaffold it with:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
npx @e18e/setup-publish
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 3.1 · Job structure
|
|
113
|
+
|
|
114
|
+
The workflow **must** separate build from publish. This ensures publish permissions (the OIDC
|
|
115
|
+
token) are never exposed to build-time code.
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
test → build → publish
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 3.2 · Non-negotiable workflow constraints
|
|
122
|
+
|
|
123
|
+
| Constraint | Why |
|
|
124
|
+
| ------------------------------------------------------ | ----------------------------------------------------------- |
|
|
125
|
+
| All actions pinned to full-length commit SHA | Prevents supply-chain attacks via action updates |
|
|
126
|
+
| `npm ci --ignore-scripts` (or `--ignore-scripts` flag) | Prevents malicious lifecycle scripts running during install |
|
|
127
|
+
| Build and publish in **separate jobs** | Isolates publish permissions from arbitrary build code |
|
|
128
|
+
|
|
129
|
+
### 3.3 · Suppress lifecycle scripts project-wide
|
|
130
|
+
|
|
131
|
+
Add to `.npmrc` in the repository:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
ignore-scripts=true
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Also apply globally on developer machines:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
npm config set -g ignore-scripts true
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 3.4 · Creating a release
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
git tag v1.0.0
|
|
147
|
+
git push origin v1.0.0
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Then in GitHub UI: **Releases → Draft a new release** → choose the tag → **Generate release notes**
|
|
151
|
+
→ **Publish release**. This triggers the workflow.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## 4 · Alternative Workflow Strategies
|
|
156
|
+
|
|
157
|
+
### 4.1 · Changesets (recommended for teams)
|
|
158
|
+
|
|
159
|
+
Use the [changesets template](https://github.com/e18e/setup-publish/blob/main/templates/changesets.yml).
|
|
160
|
+
|
|
161
|
+
- Merged PRs automatically update a release pull request
|
|
162
|
+
- Changelog is generated by changesets and included in the release PR
|
|
163
|
+
- Releasing = merging the generated release PR — no manual tagging
|
|
164
|
+
|
|
165
|
+
### 4.2 · changelogithub (changelog from commit messages)
|
|
166
|
+
|
|
167
|
+
Use the [changelogithub template](https://github.com/e18e/setup-publish/blob/main/templates/changelogithub.yml).
|
|
168
|
+
|
|
169
|
+
- Tags are still pushed manually
|
|
170
|
+
- GitHub release + changelog are created automatically on tag push
|
|
171
|
+
- Package is published on tag push
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## 5 · Ongoing Maintenance
|
|
176
|
+
|
|
177
|
+
### 5.1 · Keep dependencies updated
|
|
178
|
+
|
|
179
|
+
Set up **Dependabot** or **Renovate** to receive automated PRs for dependency updates,
|
|
180
|
+
addressing security vulnerabilities promptly.
|
|
181
|
+
|
|
182
|
+
### 5.2 · Keep GitHub Actions updated
|
|
183
|
+
|
|
184
|
+
All actions must be pinned to a commit SHA (not a tag). To migrate existing workflows and keep
|
|
185
|
+
them current:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
npx actions-up
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Run this periodically, or let Dependabot/Renovate manage action updates once SHAs are in place.
|
|
192
|
+
|
|
193
|
+
### 5.3 · Lint workflows for vulnerabilities
|
|
194
|
+
|
|
195
|
+
[`zizmor`](https://github.com/zizmorcore/zizmor) detects template injection vulnerabilities and
|
|
196
|
+
excessive permission scopes in GitHub workflow files:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
zizmor .github/workflows/publish.yml
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Integrate this into CI or run it before merging workflow changes.
|
|
203
|
+
|
|
204
|
+
### 5.4 · Validate package.json and exports
|
|
205
|
+
|
|
206
|
+
[`publint`](https://publint.dev) checks for common publishing issues: missing files, incorrect
|
|
207
|
+
`exports` fields, wrong `main`/`module` paths, and more.
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
npx publint
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Review the [full list of publint rules](https://publint.dev/rules) to understand what it checks.
|
|
214
|
+
Run this locally before tagging a release.
|
|
215
|
+
|
|
216
|
+
### 5.5 · Visualise dependency changes
|
|
217
|
+
|
|
218
|
+
[`multiocular`](https://github.com/multiocular-com/multiocular) shows exactly what code changed
|
|
219
|
+
between dependency versions, helping catch unexpected changes or potential security issues.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## 6 · Further Security Hardening
|
|
224
|
+
|
|
225
|
+
### 6.1 · Use a GitHub environment (important for shared repos)
|
|
226
|
+
|
|
227
|
+
Without a GitHub environment, **any account with write access can trigger a publish** by creating
|
|
228
|
+
a branch and modifying the release workflow — bypassing code review entirely.
|
|
229
|
+
|
|
230
|
+
`Settings → Environments`:
|
|
231
|
+
|
|
232
|
+
1. Create an environment named `publish`.
|
|
233
|
+
2. **Do not** allow administrator bypass of protection rules.
|
|
234
|
+
3. Limit deployment to explicit branch names only (e.g. `main`, `v1`). Do not use wildcards.
|
|
235
|
+
Remove stale branches promptly.
|
|
236
|
+
4. Update the publish job in `publish.yml`:
|
|
237
|
+
|
|
238
|
+
```yaml
|
|
239
|
+
jobs:
|
|
240
|
+
publish:
|
|
241
|
+
environment: publish
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
5. Update the npm trusted publisher settings on `npmjs.com` to include the environment name.
|
|
245
|
+
|
|
246
|
+
> Optionally configure the `publish` environment to require **manual approval** before the job
|
|
247
|
+
> proceeds — providing a human gate even if malicious code reaches a release branch.
|
|
248
|
+
|
|
249
|
+
### 6.2 · Consider hardware security keys _(optional)_
|
|
250
|
+
|
|
251
|
+
Physical security keys (YubiKey, etc.) are significantly more resistant to phishing and credential
|
|
252
|
+
theft than authenticator apps or SMS. They are worth recommending but should never be enforced —
|
|
253
|
+
not everyone has access to one, and a good authenticator app is a perfectly reasonable alternative.
|
|
254
|
+
Mention this as a suggestion, not a requirement.
|
|
255
|
+
|
|
256
|
+
### 6.3 · Protect all long-lived branches and tags
|
|
257
|
+
|
|
258
|
+
Apply branch protection rules not just to `main` but to all long-lived branches and to all tags.
|
|
259
|
+
|
|
260
|
+
### 6.4 · Enable immutable releases
|
|
261
|
+
|
|
262
|
+
Enable [GitHub immutable releases](https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/immutable-releases)
|
|
263
|
+
to prevent modification or deletion of tags and releases after creation.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## 7 · Sole Maintainer Considerations
|
|
268
|
+
|
|
269
|
+
There is an [open feature request](https://github.com/orgs/community/discussions/174507) to
|
|
270
|
+
support 2FA for GitHub environment approvals. Until that lands, trusted publishing carries a risk
|
|
271
|
+
for solo maintainers: a leaked GitHub token could be used to publish through the trusted workflow
|
|
272
|
+
without a 2FA challenge.
|
|
273
|
+
|
|
274
|
+
**Recommendation for sole maintainers**: consider publishing locally with `npm publish` and
|
|
275
|
+
2FA-protected npm access until environment-level 2FA approval is supported. Continue to follow
|
|
276
|
+
all other security recommendations in this document regardless.
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## Quick Reference Checklist
|
|
281
|
+
|
|
282
|
+
Use this when setting up a new package or auditing an existing one.
|
|
283
|
+
|
|
284
|
+
### Account security
|
|
285
|
+
|
|
286
|
+
- [ ] npm 2FA enabled (security key preferred)
|
|
287
|
+
- [ ] GitHub 2FA enabled (security key preferred)
|
|
288
|
+
- [ ] Password manager in use
|
|
289
|
+
|
|
290
|
+
### Repository settings
|
|
291
|
+
|
|
292
|
+
- [ ] Actions pinned to full-length commit SHAs (enforced in settings)
|
|
293
|
+
- [ ] First-time contributor approval required
|
|
294
|
+
- [ ] Default workflow permissions set to read-only
|
|
295
|
+
- [ ] `main` branch protected (PR + review required)
|
|
296
|
+
- [ ] No npm tokens in repository secrets
|
|
297
|
+
|
|
298
|
+
### Trusted publishing
|
|
299
|
+
|
|
300
|
+
- [ ] OIDC trusted publisher configured on npmjs.com
|
|
301
|
+
- [ ] "Require 2FA, disallow tokens" enabled on npm
|
|
302
|
+
- [ ] Publish step uses Node.js ≥ 24.8.0
|
|
303
|
+
- [ ] GitHub environment (`publish`) configured with branch restrictions
|
|
304
|
+
|
|
305
|
+
### Workflow hygiene
|
|
306
|
+
|
|
307
|
+
- [ ] Build and publish are separate jobs
|
|
308
|
+
- [ ] `npm ci --ignore-scripts` used in all install steps
|
|
309
|
+
- [ ] `ignore-scripts=true` in `.npmrc`
|
|
310
|
+
- [ ] `zizmor` passes on all workflow files
|
|
311
|
+
|
|
312
|
+
### Package quality
|
|
313
|
+
|
|
314
|
+
- [ ] `npx publint` passes with no errors
|
|
315
|
+
- [ ] Dependabot or Renovate configured
|
|
316
|
+
- [ ] `actions-up` run to migrate to SHA-pinned actions
|
|
@@ -209,9 +209,7 @@ When in doubt: if the content serves the primary purpose of the page, it belongs
|
|
|
209
209
|
```html
|
|
210
210
|
<!-- Correct: pull quote from the article's own content -->
|
|
211
211
|
<aside aria-label="Pull quote">
|
|
212
|
-
<p>
|
|
213
|
-
"The biggest gains came not from new features, but from removing old ones."
|
|
214
|
-
</p>
|
|
212
|
+
<p>"The biggest gains came not from new features, but from removing old ones."</p>
|
|
215
213
|
</aside>
|
|
216
214
|
|
|
217
215
|
<!-- Use blockquote for genuine external quotations -->
|
|
@@ -462,9 +460,7 @@ HTML's `required` attribute communicates required state to assistive technology,
|
|
|
462
460
|
```html
|
|
463
461
|
<!-- Pattern: asterisk with legend explaining it -->
|
|
464
462
|
<fieldset>
|
|
465
|
-
<legend>
|
|
466
|
-
Contact details <span aria-hidden="true">*</span> required fields
|
|
467
|
-
</legend>
|
|
463
|
+
<legend>Contact details <span aria-hidden="true">*</span> required fields</legend>
|
|
468
464
|
|
|
469
465
|
<label for="name">Full name <span aria-hidden="true">*</span></label>
|
|
470
466
|
<input type="text" id="name" required />
|
|
@@ -486,12 +482,7 @@ When inputs have format hints or helper text, associate them with the input via
|
|
|
486
482
|
Multiple associations are allowed—comma-separated IDs work for both hint and error:
|
|
487
483
|
|
|
488
484
|
```html
|
|
489
|
-
<input
|
|
490
|
-
type="email"
|
|
491
|
-
id="email"
|
|
492
|
-
aria-invalid="true"
|
|
493
|
-
aria-describedby="email-hint email-error"
|
|
494
|
-
/>
|
|
485
|
+
<input type="email" id="email" aria-invalid="true" aria-describedby="email-hint email-error" />
|
|
495
486
|
```
|
|
496
487
|
|
|
497
488
|
### Error Messages
|
|
@@ -505,15 +496,8 @@ Current best practice (due to browser support gaps with `aria-errormessage`):
|
|
|
505
496
|
|
|
506
497
|
```html
|
|
507
498
|
<label for="email">Email</label>
|
|
508
|
-
<input
|
|
509
|
-
|
|
510
|
-
id="email"
|
|
511
|
-
aria-invalid="true"
|
|
512
|
-
aria-describedby="email-error"
|
|
513
|
-
/>
|
|
514
|
-
<p id="email-error" class="error">
|
|
515
|
-
Enter a valid email address, like name@example.com
|
|
516
|
-
</p>
|
|
499
|
+
<input type="email" id="email" aria-invalid="true" aria-describedby="email-error" />
|
|
500
|
+
<p id="email-error" class="error">Enter a valid email address, like name@example.com</p>
|
|
517
501
|
```
|
|
518
502
|
|
|
519
503
|
## Tables
|
|
@@ -139,11 +139,7 @@ function Card({ title, headingLevel = 3, headingClass, children }) {
|
|
|
139
139
|
// Specialised product card - knows its context
|
|
140
140
|
function ProductCard({ product, headingLevel = 3 }) {
|
|
141
141
|
return (
|
|
142
|
-
<Card
|
|
143
|
-
title={product.name}
|
|
144
|
-
headingLevel={headingLevel}
|
|
145
|
-
headingClass="product-card__title"
|
|
146
|
-
>
|
|
142
|
+
<Card title={product.name} headingLevel={headingLevel} headingClass="product-card__title">
|
|
147
143
|
<p className="product-card__price">{product.price}</p>
|
|
148
144
|
<p className="product-card__description">{product.description}</p>
|
|
149
145
|
</Card>
|