ci(security): flag <30d-old packages introduced by a PR (#136) #137
No reviewers
Labels
No labels
area:auth
area:ci
area:db
area:infra
area:native
area:pwa
area:service
epic
feature
foundation
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
james/carol!137
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "136-package-age-check"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Closes #136. Adds a soft-signal CI check that flags newly-published npm packages introduced by a PR. This is a distinct threat from ADR-0009's version-age quarantine — it targets typosquats, dependency-confusion attacks, and brand-new malicious packages, not compromised releases of established deps.
ADR-0009's Renovate
minimumReleaseAge: "7 days"gates version release dates. ADR-0021 (new in this PR) gates package creation dates. A new transitive that flows in via a feature PR's lockfile resolution previously slid past every existing gate; this check catches it.How it works
package_agesruns on every PR.package-lock.json+ base viagit show <baseRef>:package-lock.json.https://registry.npmjs.org/<pkg>for each new package'stime.createdfield..dep-policy.json's allowlist.Never fails the workflow. The sticky comment is the entire forcing function. Per ADR-0019's soft-signal rationale, hard gates create false-positive friction without buying enough.
Sticky comment behaviour
<!-- deps-comment -->Allowlist (
.dep-policy.json)The CI check matches only on
name; the other fields are reviewer breadcrumbs. New packages get accepted in the same PR that introduces them — the reviewer's act of accepting is visible in the diff.Files
New:
scripts/lib/package-ages.mjs— pure logic. All I/O at the edges (onlyfetchPackageMetatouches the network, with an injectable fetch impl).scripts/lib/package-ages.d.mts— type declarations so TS tests don't need@ts-expect-errorcasts.scripts/ci/check-package-ages.mjs— CI wrapper: lockfile diff + registry queries + Markdown body +flag=<bool>for the workflow.scripts/ci/post-deps-comment.mjs— Forgejo upsert, scanner-pattern delete-on-clean..dep-policy.json— empty starter allowlist.tests/scripts/package-ages.test.ts— 23 unit tests for the pure lib.docs/adr/0021-package-age-policy.md— policy ADR.Modified:
.forgejo/workflows/pr.yml— newpackage_agesjob (mirrors the coverage job shape).docs/ci.md,docs/adr/README.md,CLAUDE.md— cross-references.Decisions documented in ADR-0021 (and re-stated as "out of scope" in #136)
Test plan
npm run typecheck— clean.npm run lint— clean.actionlint .forgejo/workflows/pr.yml— clean.npm test— 380 passed / 86 skipped (was 357 / 86; +23 net new on the pure lib).extractPackagesFromLockfile: scoped names (@scope/name), nested transitives, deduplication, malformed input.newPackages: set difference, version bumps NOT flagged as new, empty base = everything new.fetchPackageMeta: URL encoding for scoped names, 404 handling, missingtime.createdhandling.flagOffenders: threshold boundary (exactly 30d not flagged, 1ms inside flagged), allowlist hits, sort youngest-first, defence-in-depth against malformed allowlist entries.npm run build— succeeds.flag=false, no body file written (no new packages on this branch)..dep-policy.json, re-run, confirm the comment is deleted.Acceptance criteria (from #136)
.dep-policy.jsonin the same PR makes the comment disappear on the next CI run.Closes #136.
🤖 Generated with Claude Code
📊 Test coverage
Patch coverage: no testable lines changed.
Overall (
app/,lib/,db/, excluding UI per ADR-0019):Soft thresholds per ADR-0019. Coverage is informational and does not block merge.
8b9cd4db38ce9768005b