New:Socket for Asana Is Now Available.Learn more
Sign In

fastify

Package Overview
Dependencies
Maintainers
6
Versions
325
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

fastify - npm Package Compare versions

Comparing version
5.12.0
to
5.12.1
+33
.pi/self-learning-memory/core/CORE.md
# Core Learnings
Most important durable learnings collected over time.
Last updated: 2026-08-18T19:46:43.092Z
This file keeps only top-ranked, most repeated items.
For the complete history, see long-term-memory.md.
Ranked by frequency + recency (with light decay over time).
## High-value learnings
- Closed PR #1 without merging after confirming its head SHA was already the intended private `main` commit.
- Fetched current `origin/main` and `origin/5.x`, verified each security commit’s parent exactly matched the corresponding remote tip, then used an atomic push.
- Verified public and private `main`/`5.x` refs matched, fast-forwarded local `main`, deleted temporary security branches, and confirmed a clean synchronized checkout.
- Obtained explicit approval before public disclosure, then atomically fast-forwarded `origin/main` to `af079bd4` and `origin/5.x` to `8acfea7e`, verifying both refs with `git ls-remote`.
- Fast-forwarded local `main` to `origin/main` and confirmed a clean synchronized status.
- Do not retry forbidden workspace GraphQL actions; use permitted repository operations or ask the user for an authorized alternative before attempting comments or metadata changes.
- Before any security-fix push, explicitly ask whether targets are the private advisory fork, public `origin`, or both; treat a draft advisory as a warning, not implicit authorization policy.
- Always state push destinations and exact resulting refs, e.g. `advisory-ghsa-3m5p/main` changed while `origin/main` remained at `6e95cb9f`.
- Run workspace-sensitive PR operations separately: attempt the comment, record an expected permission denial, then independently close only if authorized.
- Describe PR state from verified fields (`CLOSED`, `mergedAt: null`, head/base OIDs) rather than inferring that it is empty.
## Watch-outs
- Private PR #1 remained open after its head commit had already been placed on private `main`; its mergeability was `UNKNOWN` and it was no longer the correct landing path.
- Local `main` and temporary security branches were left out of sync after updating the remote branches.
- Direct public branch updates bypassed the repository’s pull-request-only rules, so ancestry and target refs required explicit validation to avoid unsafe rewrites.
- Initially pushed the security fix only to the private advisory fork, leaving `origin/main` and `origin/5.x` unchanged without first confirming the intended publication target.
- After pushing remote refs, local `main` remained one commit behind, creating temporary local/remote inconsistency.
- A GraphQL `addComment` action was attempted despite workspace-repository restrictions; the denial indicates an intentional permission boundary that should not be bypassed.
- Assumed the draft advisory meant `origin/main` and `origin/5.x` must remain untouched without first confirming whether the user expected public pushes.
- Reported that the private branches were complete without clearly distinguishing advisory-fork refs from public `origin` refs, prompting the user to stop and verify deployment status.
- Combined PR closing with an unsupported workspace-repository comment under `set -e`, causing the entire command to fail before the close could occur.
- Called the PR “now-empty” even though GitHub still reported it as mergeable with different head and base OIDs.
{
"version": 1,
"updatedAt": "2026-08-18T19:46:43.092Z",
"items": [
{
"key": "pass workflow input through `env.npm_package` and read it from `process.env` inside the script.",
"text": "Pass workflow input through `env.NPM_PACKAGE` and read it from `process.env` inside the script.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-06T15:46:17.613Z",
"lastSeen": "2026-07-06T15:46:17.613Z"
},
{
"key": "validate the package name with an npm package-name regex before using it.",
"text": "Validate the package name with an npm package-name regex before using it.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-06T15:46:17.613Z",
"lastSeen": "2026-07-06T15:46:17.613Z"
},
{
"key": "use `encodeuricomponent(packagename)` in the npm registry url.",
"text": "Use `encodeURIComponent(packageName)` in the npm registry URL.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-06T15:46:17.613Z",
"lastSeen": "2026-07-06T15:46:17.613Z"
},
{
"key": "check `response.ok` and throw explicit errors for failed registry fetches or unsupported repository urls.",
"text": "Check `response.ok` and throw explicit errors for failed registry fetches or unsupported repository URLs.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-06T15:46:17.613Z",
"lastSeen": "2026-07-06T15:46:17.613Z"
},
{
"key": "avoid: interpolated `${{ inputs.package }}` directly into `actions/github-script` javascript, allowing malformed input to break or inject code.",
"text": "Avoid: Interpolated `${{ inputs.package }}` directly into `actions/github-script` JavaScript, allowing malformed input to break or inject code.",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-06T15:46:17.613Z",
"lastSeen": "2026-07-06T15:46:17.613Z"
},
{
"key": "avoid: fetched npm metadata with the raw package name instead of url-encoding it.",
"text": "Avoid: Fetched npm metadata with the raw package name instead of URL-encoding it.",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-06T15:46:17.613Z",
"lastSeen": "2026-07-06T15:46:17.613Z"
},
{
"key": "avoid: assumed `data.repository.url` always existed and was a github `.git` url.",
"text": "Avoid: Assumed `data.repository.url` always existed and was a GitHub `.git` URL.",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-06T15:46:17.613Z",
"lastSeen": "2026-07-06T15:46:17.613Z"
},
{
"key": "avoid: used a brittle repository url regex that missed scoped/modern github url formats and failed without a clear error.",
"text": "Avoid: Used a brittle repository URL regex that missed scoped/modern GitHub URL formats and failed without a clear error.",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-06T15:46:17.613Z",
"lastSeen": "2026-07-06T15:46:17.613Z"
},
{
"key": "renamed pr #6833 to `ci: harden citgm package workflow input handling` using `gh pr edit`, then watched checks until `pull-request-title-check` passed.",
"text": "Renamed PR #6833 to `ci: harden CITGM package workflow input handling` using `gh pr edit`, then watched checks until `pull-request-title-check` passed.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-06T16:04:51.833Z",
"lastSeen": "2026-07-06T16:04:51.833Z"
},
{
"key": "avoid: pr title did not satisfy the repository's conventional title check before opening/updating the pr.",
"text": "Avoid: PR title did not satisfy the repository's conventional title check before opening/updating the PR.",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-06T16:04:51.833Z",
"lastSeen": "2026-07-06T16:04:51.833Z"
},
{
"key": "honor soft tool-budget and interruption signals immediately; stop browsing and provide a bounded reflection.",
"text": "Honor soft tool-budget and interruption signals immediately; stop browsing and provide a bounded reflection.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-29T19:37:37.132Z",
"lastSeen": "2026-07-29T19:37:37.132Z"
},
{
"key": "run only the single highest-value targeted test before the budget limit, then report if execution is blocked.",
"text": "Run only the single highest-value targeted test before the budget limit, then report if execution is blocked.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-29T19:37:37.132Z",
"lastSeen": "2026-07-29T19:37:37.132Z"
},
{
"key": "reproduce root primitive coercion with the repository’s actual validatorcompiler path rather than relying on ajv internals alone.",
"text": "Reproduce root primitive coercion with the repository’s actual validatorCompiler path rather than relying on Ajv internals alone.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-29T19:37:37.132Z",
"lastSeen": "2026-07-29T19:37:37.132Z"
},
{
"key": "add focused tests covering custom validator compilation, schemaenv availability, and root primitive coercion.",
"text": "Add focused tests covering custom validator compilation, schemaEnv availability, and root primitive coercion.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-29T19:37:37.132Z",
"lastSeen": "2026-07-29T19:37:37.132Z"
},
{
"key": "avoid: continued initiating tool work after the tool-budget warning instead of stopping to assess the existing evidence.",
"text": "Avoid: Continued initiating tool work after the tool-budget warning instead of stopping to assess the existing evidence.",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-29T19:37:37.132Z",
"lastSeen": "2026-07-29T19:37:37.132Z"
},
{
"key": "avoid: started a test command that was aborted, leaving the primitive-coercion fix unverified.",
"text": "Avoid: Started a test command that was aborted, leaving the primitive-coercion fix unverified.",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-29T19:37:37.132Z",
"lastSeen": "2026-07-29T19:37:37.132Z"
},
{
"key": "avoid: investigated ajv internals and custom compiler compatibility without first isolating the exact failing repository behavior.",
"text": "Avoid: Investigated Ajv internals and custom compiler compatibility without first isolating the exact failing repository behavior.",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-29T19:37:37.132Z",
"lastSeen": "2026-07-29T19:37:37.132Z"
},
{
"key": "avoid: did not establish regression coverage for missing schemaenv handling and root primitive coercion before changing direction.",
"text": "Avoid: Did not establish regression coverage for missing schemaEnv handling and root primitive coercion before changing direction.",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-29T19:37:37.132Z",
"lastSeen": "2026-07-29T19:37:37.132Z"
},
{
"key": "pass request and parameter metadata as ajv parentdata/parentdataproperty so root coercions update the original request value.",
"text": "Pass request and parameter metadata as Ajv parentData/parentDataProperty so root coercions update the original request value.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-29T20:03:27.706Z",
"lastSeen": "2026-07-29T20:03:27.706Z"
},
{
"key": "add a regression test asserting quoted numeric json becomes a number in the handler while out-of-range values remain rejected.",
"text": "Add a regression test asserting quoted numeric JSON becomes a number in the handler while out-of-range values remain rejected.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-29T20:03:27.706Z",
"lastSeen": "2026-07-29T20:03:27.706Z"
},
{
"key": "verify targeted test execution separately from the full suite to avoid mistaking aggregate counts for focused coverage.",
"text": "Verify targeted test execution separately from the full suite to avoid mistaking aggregate counts for focused coverage.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-29T20:03:27.706Z",
"lastSeen": "2026-07-29T20:03:27.706Z"
},
{
"key": "avoid: root primitive request values were validated without a parent reference, so ajv coercion changed only its local value and left request.body as a string.",
"text": "Avoid: Root primitive request values were validated without a parent reference, so Ajv coercion changed only its local value and left request.body as a string.",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-29T20:03:27.706Z",
"lastSeen": "2026-07-29T20:03:27.706Z"
},
{
"key": "avoid: test command output was misread: borp ran the broader suite despite a specific file argument, making targeted coverage unclear.",
"text": "Avoid: Test command output was misread: Borp ran the broader suite despite a specific file argument, making targeted coverage unclear.",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-29T20:03:27.706Z",
"lastSeen": "2026-07-29T20:03:27.706Z"
},
{
"key": "pass parentdata and parentdataproperty to ajv validators so root primitive coercions update the request object.",
"text": "Pass parentData and parentDataProperty to Ajv validators so root primitive coercions update the request object.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-29T20:06:55.452Z",
"lastSeen": "2026-07-29T20:06:55.452Z"
},
{
"key": "retain the direct-call path for validators without schemaenv to avoid changing non-ajv validator behavior.",
"text": "Retain the direct-call path for validators without schemaEnv to avoid changing non-Ajv validator behavior.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-29T20:06:55.452Z",
"lastSeen": "2026-07-29T20:06:55.452Z"
},
{
"key": "add a regression test asserting quoted numeric json reaches the handler as number 10.",
"text": "Add a regression test asserting quoted numeric JSON reaches the handler as number 10.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-29T20:06:55.452Z",
"lastSeen": "2026-07-29T20:06:55.452Z"
},
{
"key": "determine pr from the deployed route’s authentication requirements; use pr:n for unauthenticated endpoints.",
"text": "Determine PR from the deployed route’s authentication requirements; use PR:N for unauthenticated endpoints.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-29T20:06:55.452Z",
"lastSeen": "2026-07-29T20:06:55.452Z"
},
{
"key": "base c/i impact on the specific handler and data affected rather than assigning a universal fastify score.",
"text": "Base C/I impact on the specific handler and data affected rather than assigning a universal Fastify score.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-29T20:06:55.452Z",
"lastSeen": "2026-07-29T20:06:55.452Z"
},
{
"key": "avoid: ajv was called with a primitive root value without parentdata context, so coerced values were not written back to request.body.",
"text": "Avoid: Ajv was called with a primitive root value without parentData context, so coerced values were not written back to request.body.",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-29T20:06:55.452Z",
"lastSeen": "2026-07-29T20:06:55.452Z"
},
{
"key": "avoid: the regression scenario was initially vulnerable because the handler received string \"10\" after validation accepted it.",
"text": "Avoid: The regression scenario was initially vulnerable because the handler received string \"10\" after validation accepted it.",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-29T20:06:55.452Z",
"lastSeen": "2026-07-29T20:06:55.452Z"
},
{
"key": "avoid: the cvss rating assumed pr:l without repository evidence that authentication is required; many affected routes may be unauthenticated.",
"text": "Avoid: The CVSS rating assumed PR:L without repository evidence that authentication is required; many affected routes may be unauthenticated.",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-29T20:06:55.452Z",
"lastSeen": "2026-07-29T20:06:55.452Z"
},
{
"key": "avoid: the impact rating was stated as application-dependent without defining the affected endpoint’s actual confidentiality and integrity consequences.",
"text": "Avoid: The impact rating was stated as application-dependent without defining the affected endpoint’s actual confidentiality and integrity consequences.",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-29T20:06:55.452Z",
"lastSeen": "2026-07-29T20:06:55.452Z"
},
{
"key": "updated `lib/validation.js` to retain and return coerced root values.",
"text": "Updated `lib/validation.js` to retain and return coerced root values.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-29T20:09:41.212Z",
"lastSeen": "2026-07-29T20:09:41.212Z"
},
{
"key": "added `test/fix-root-primitive-coercion.test.js` to prevent recurrence.",
"text": "Added `test/fix-root-primitive-coercion.test.js` to prevent recurrence.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-29T20:09:41.212Z",
"lastSeen": "2026-07-29T20:09:41.212Z"
},
{
"key": "avoid: validation coerced a root primitive but failed to preserve the coerced value for the caller.",
"text": "Avoid: Validation coerced a root primitive but failed to preserve the coerced value for the caller.",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-29T20:09:41.212Z",
"lastSeen": "2026-07-29T20:09:41.212Z"
},
{
"key": "avoid: the regression scenario was not covered by existing tests.",
"text": "Avoid: The regression scenario was not covered by existing tests.",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-07-29T20:09:41.212Z",
"lastSeen": "2026-07-29T20:09:41.212Z"
},
{
"key": "always log/compare the exact pathname string passed into send(), including after encodeuri",
"text": "Always log/compare the exact pathname string passed into send(), including after encodeURI",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-04T10:59:32.673Z",
"lastSeen": "2026-08-04T10:59:32.673Z"
},
{
"key": "when statuses differ for \"same\" path, diff charcodes for % vs %25 double-encoding first",
"text": "When statuses differ for \"same\" path, diff charCodes for % vs %25 double-encoding first",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-04T10:59:32.673Z",
"lastSeen": "2026-08-04T10:59:32.673Z"
},
{
"key": "instrument static handler vs router to separate callnotfound from unmatched routes",
"text": "Instrument static handler vs router to separate callNotFound from unmatched routes",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-04T10:59:32.673Z",
"lastSeen": "2026-08-04T10:59:32.673Z"
},
{
"key": "trace full pipeline: raw.url → getpathnameforsend → dotdot guards → encodeuri → @fastify/send",
"text": "Trace full pipeline: raw.url → getPathnameForSend → dotDot guards → encodeURI → @fastify/send",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-04T10:59:32.673Z",
"lastSeen": "2026-08-04T10:59:32.673Z"
},
{
"key": "reproduce with send(root, p) using both raw and encodeuri(p) before changing triage conclusions",
"text": "Reproduce with send(root, p) using both raw and encodeURI(p) before changing triage conclusions",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-04T10:59:32.673Z",
"lastSeen": "2026-08-04T10:59:32.673Z"
},
{
"key": "avoid: assumed /static/..%2f… 404 meant getpathnameforsend/route miss instead of later send pipeline behavior",
"text": "Avoid: Assumed /static/..%2f… 404 meant getPathnameForSend/route miss instead of later send pipeline behavior",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-04T10:59:32.673Z",
"lastSeen": "2026-08-04T10:59:32.673Z"
},
{
"key": "avoid: did not check that encodeuri() re-encodes residual % as %25 before blaming path guards",
"text": "Avoid: Did not check that encodeURI() re-encodes residual % as %25 before blaming path guards",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-04T10:59:32.673Z",
"lastSeen": "2026-08-04T10:59:32.673Z"
},
{
"key": "avoid: conflated reply.callnotfound() json (\"route get:… not found\") with an actual find-my-way miss",
"text": "Avoid: Conflated reply.callNotFound() JSON (\"Route GET:… not found\") with an actual find-my-way miss",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-04T10:59:32.673Z",
"lastSeen": "2026-08-04T10:59:32.673Z"
},
{
"key": "avoid: debugged isolated getpathnameforsend reimplementation before tracing pumpsendtoreply → encodeuri(pathnameforsend) → @fastify/send",
"text": "Avoid: Debugged isolated getPathnameForSend reimplementation before tracing pumpSendToReply → encodeURI(pathnameForSend) → @fastify/send",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-04T10:59:32.673Z",
"lastSeen": "2026-08-04T10:59:32.673Z"
},
{
"key": "avoid: compared send() on decoded ../ paths (403) to static outcomes without using the exact post-encodeuri string static passes",
"text": "Avoid: Compared send() on decoded ../ paths (403) to static outcomes without using the exact post-encodeURI string static passes",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-04T10:59:32.673Z",
"lastSeen": "2026-08-04T10:59:32.673Z"
},
{
"key": "empirically print encodeuri output/charcodes: '%' → '%25', so static sees '/..%252ftop-secret.txt' (safe 404) not a '..' segment.",
"text": "Empirically print encodeURI output/charCodes: '%' → '%25', so static sees '/..%252ftop-secret.txt' (safe 404) not a '..' segment.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-04T11:12:27.539Z",
"lastSeen": "2026-08-04T11:12:27.539Z"
},
{
"key": "reproduce with @fastify/send directly for raw percent, encodeuri(percent), and decoded '../' to map 403 vs 404 to normalize/root logic.",
"text": "Reproduce with @fastify/send directly for raw percent, encodeURI(percent), and decoded '../' to map 403 vs 404 to normalize/root logic.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-04T11:12:27.539Z",
"lastSeen": "2026-08-04T11:12:27.539Z"
},
{
"key": "keep reporter response framed as app-level path.join(param) misuse; cite static/sendfile containment and express parity, not a core cve.",
"text": "Keep reporter response framed as app-level path.join(param) misuse; cite static/sendFile containment and Express parity, not a core CVE.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-04T11:12:27.539Z",
"lastSeen": "2026-08-04T11:12:27.539Z"
},
{
"key": "avoid: assumed encodeuri leaves '%' intact on paths like '/..%2ftop-secret.txt', so static 404 vs raw '..' 403 looked inconsistent.",
"text": "Avoid: Assumed encodeURI leaves '%' intact on paths like '/..%2ftop-secret.txt', so static 404 vs raw '..' 403 looked inconsistent.",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-04T11:12:27.539Z",
"lastSeen": "2026-08-04T11:12:27.539Z"
},
{
"key": "avoid: treated encoded traversal and decoded param sinks as the same layer before separating find-my-way dual-decode from @fastify/send root checks.",
"text": "Avoid: Treated encoded traversal and decoded param sinks as the same layer before separating find-my-way dual-decode from @fastify/send root checks.",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-04T11:12:27.539Z",
"lastSeen": "2026-08-04T11:12:27.539Z"
},
{
"key": "avoid: relied on log/message shape alone (callnotfound 'route ... not found') to infer static failure mode without probing @fastify/send inputs.",
"text": "Avoid: Relied on log/message shape alone (callNotFound 'Route ... not found') to infer static failure mode without probing @fastify/send inputs.",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-04T11:12:27.539Z",
"lastSeen": "2026-08-04T11:12:27.539Z"
},
{
"key": "re-run markdownlint scoped strictly to docs/reference/request.md and docs/reference/routes.md.",
"text": "Re-run markdownlint scoped strictly to docs/Reference/Request.md and docs/Reference/Routes.md.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-04T12:31:54.341Z",
"lastSeen": "2026-08-04T12:31:54.341Z"
},
{
"key": "stage only those two paths (git add docs/reference/request.md docs/reference/routes.md); never git add .",
"text": "Stage only those two paths (git add docs/Reference/Request.md docs/Reference/Routes.md); never git add .",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-04T12:31:54.341Z",
"lastSeen": "2026-08-04T12:31:54.341Z"
},
{
"key": "manually awk length>80 on changed files to confirm new lines stay ≤80 before commit.",
"text": "Manually awk length>80 on changed files to confirm new lines stay ≤80 before commit.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-04T12:31:54.341Z",
"lastSeen": "2026-08-04T12:31:54.341Z"
},
{
"key": "leave response.md untracked and verify git status/diff --stat before commit and pr.",
"text": "Leave response.md untracked and verify git status/diff --stat before commit and PR.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-04T12:31:54.341Z",
"lastSeen": "2026-08-04T12:31:54.341Z"
},
{
"key": "avoid: markdown lint invoked in a way that flooded output with unrelated files (agents.md, .pi memory, response.md) instead of only the two edited docs.",
"text": "Avoid: Markdown lint invoked in a way that flooded output with unrelated files (AGENTS.md, .pi memory, response.md) instead of only the two edited docs.",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-04T12:31:54.341Z",
"lastSeen": "2026-08-04T12:31:54.341Z"
},
{
"key": "avoid: untracked response.md sat in the working tree and could have been staged if git add was too broad.",
"text": "Avoid: Untracked response.md sat in the working tree and could have been staged if git add was too broad.",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-04T12:31:54.341Z",
"lastSeen": "2026-08-04T12:31:54.341Z"
},
{
"key": "avoid: relied on noisy repo-wide lint signal before confirming md013 on the actual changed regions.",
"text": "Avoid: Relied on noisy repo-wide lint signal before confirming MD013 on the actual changed regions.",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-04T12:31:54.341Z",
"lastSeen": "2026-08-04T12:31:54.341Z"
},
{
"key": "before any security-fix push, explicitly ask whether targets are the private advisory fork, public `origin`, or both; treat a draft advisory as a warning, not implicit authorization policy.",
"text": "Before any security-fix push, explicitly ask whether targets are the private advisory fork, public `origin`, or both; treat a draft advisory as a warning, not implicit authorization policy.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-18T19:37:20.498Z",
"lastSeen": "2026-08-18T19:37:20.498Z"
},
{
"key": "always state push destinations and exact resulting refs, e.g. `advisory-ghsa-3m5p/main` changed while `origin/main` remained at `6e95cb9f`.",
"text": "Always state push destinations and exact resulting refs, e.g. `advisory-ghsa-3m5p/main` changed while `origin/main` remained at `6e95cb9f`.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-18T19:37:20.498Z",
"lastSeen": "2026-08-18T19:37:20.498Z"
},
{
"key": "run workspace-sensitive pr operations separately: attempt the comment, record an expected permission denial, then independently close only if authorized.",
"text": "Run workspace-sensitive PR operations separately: attempt the comment, record an expected permission denial, then independently close only if authorized.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-18T19:37:20.498Z",
"lastSeen": "2026-08-18T19:37:20.498Z"
},
{
"key": "describe pr state from verified fields (`closed`, `mergedat: null`, head/base oids) rather than inferring that it is empty.",
"text": "Describe PR state from verified fields (`CLOSED`, `mergedAt: null`, head/base OIDs) rather than inferring that it is empty.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-18T19:37:20.498Z",
"lastSeen": "2026-08-18T19:37:20.498Z"
},
{
"key": "keep local security branches until the user confirms the intended public/private publication state; delete them only after final authorization and ref verification.",
"text": "Keep local security branches until the user confirms the intended public/private publication state; delete them only after final authorization and ref verification.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-18T19:37:20.498Z",
"lastSeen": "2026-08-18T19:37:20.498Z"
},
{
"key": "avoid: assumed the draft advisory meant `origin/main` and `origin/5.x` must remain untouched without first confirming whether the user expected public pushes.",
"text": "Avoid: Assumed the draft advisory meant `origin/main` and `origin/5.x` must remain untouched without first confirming whether the user expected public pushes.",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-18T19:37:20.498Z",
"lastSeen": "2026-08-18T19:37:20.498Z"
},
{
"key": "avoid: reported that the private branches were complete without clearly distinguishing advisory-fork refs from public `origin` refs, prompting the user to stop and verify deployment status.",
"text": "Avoid: Reported that the private branches were complete without clearly distinguishing advisory-fork refs from public `origin` refs, prompting the user to stop and verify deployment status.",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-18T19:37:20.498Z",
"lastSeen": "2026-08-18T19:37:20.498Z"
},
{
"key": "avoid: combined pr closing with an unsupported workspace-repository comment under `set -e`, causing the entire command to fail before the close could occur.",
"text": "Avoid: Combined PR closing with an unsupported workspace-repository comment under `set -e`, causing the entire command to fail before the close could occur.",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-18T19:37:20.498Z",
"lastSeen": "2026-08-18T19:37:20.498Z"
},
{
"key": "avoid: called the pr “now-empty” even though github still reported it as mergeable with different head and base oids.",
"text": "Avoid: Called the PR “now-empty” even though GitHub still reported it as mergeable with different head and base OIDs.",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-18T19:37:20.498Z",
"lastSeen": "2026-08-18T19:37:20.498Z"
},
{
"key": "avoid: deleted local security branches before resolving whether the user wanted the commits pushed to public branches, reducing convenient recovery points.",
"text": "Avoid: Deleted local security branches before resolving whether the user wanted the commits pushed to public branches, reducing convenient recovery points.",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-18T19:37:20.498Z",
"lastSeen": "2026-08-18T19:37:20.498Z"
},
{
"key": "obtained explicit approval before public disclosure, then atomically fast-forwarded `origin/main` to `af079bd4` and `origin/5.x` to `8acfea7e`, verifying both refs with `git ls-remote`.",
"text": "Obtained explicit approval before public disclosure, then atomically fast-forwarded `origin/main` to `af079bd4` and `origin/5.x` to `8acfea7e`, verifying both refs with `git ls-remote`.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-18T19:38:15.045Z",
"lastSeen": "2026-08-18T19:38:15.045Z"
},
{
"key": "fast-forwarded local `main` to `origin/main` and confirmed a clean synchronized status.",
"text": "Fast-forwarded local `main` to `origin/main` and confirmed a clean synchronized status.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-18T19:38:15.045Z",
"lastSeen": "2026-08-18T19:38:15.045Z"
},
{
"key": "do not retry forbidden workspace graphql actions; use permitted repository operations or ask the user for an authorized alternative before attempting comments or metadata changes.",
"text": "Do not retry forbidden workspace GraphQL actions; use permitted repository operations or ask the user for an authorized alternative before attempting comments or metadata changes.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-18T19:38:15.045Z",
"lastSeen": "2026-08-18T19:38:15.045Z"
},
{
"key": "avoid: initially pushed the security fix only to the private advisory fork, leaving `origin/main` and `origin/5.x` unchanged without first confirming the intended publication target.",
"text": "Avoid: Initially pushed the security fix only to the private advisory fork, leaving `origin/main` and `origin/5.x` unchanged without first confirming the intended publication target.",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-18T19:38:15.045Z",
"lastSeen": "2026-08-18T19:38:15.045Z"
},
{
"key": "avoid: after pushing remote refs, local `main` remained one commit behind, creating temporary local/remote inconsistency.",
"text": "Avoid: After pushing remote refs, local `main` remained one commit behind, creating temporary local/remote inconsistency.",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-18T19:38:15.045Z",
"lastSeen": "2026-08-18T19:38:15.045Z"
},
{
"key": "avoid: a graphql `addcomment` action was attempted despite workspace-repository restrictions; the denial indicates an intentional permission boundary that should not be bypassed.",
"text": "Avoid: A GraphQL `addComment` action was attempted despite workspace-repository restrictions; the denial indicates an intentional permission boundary that should not be bypassed.",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-18T19:38:15.045Z",
"lastSeen": "2026-08-18T19:38:15.045Z"
},
{
"key": "closed pr #1 without merging after confirming its head sha was already the intended private `main` commit.",
"text": "Closed PR #1 without merging after confirming its head SHA was already the intended private `main` commit.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-18T19:46:43.092Z",
"lastSeen": "2026-08-18T19:46:43.092Z"
},
{
"key": "fetched current `origin/main` and `origin/5.x`, verified each security commit’s parent exactly matched the corresponding remote tip, then used an atomic push.",
"text": "Fetched current `origin/main` and `origin/5.x`, verified each security commit’s parent exactly matched the corresponding remote tip, then used an atomic push.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-18T19:46:43.092Z",
"lastSeen": "2026-08-18T19:46:43.092Z"
},
{
"key": "verified public and private `main`/`5.x` refs matched, fast-forwarded local `main`, deleted temporary security branches, and confirmed a clean synchronized checkout.",
"text": "Verified public and private `main`/`5.x` refs matched, fast-forwarded local `main`, deleted temporary security branches, and confirmed a clean synchronized checkout.",
"kind": "learning",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-18T19:46:43.092Z",
"lastSeen": "2026-08-18T19:46:43.092Z"
},
{
"key": "avoid: private pr #1 remained open after its head commit had already been placed on private `main`; its mergeability was `unknown` and it was no longer the correct landing path.",
"text": "Avoid: Private PR #1 remained open after its head commit had already been placed on private `main`; its mergeability was `UNKNOWN` and it was no longer the correct landing path.",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-18T19:46:43.092Z",
"lastSeen": "2026-08-18T19:46:43.092Z"
},
{
"key": "avoid: local `main` and temporary security branches were left out of sync after updating the remote branches.",
"text": "Avoid: Local `main` and temporary security branches were left out of sync after updating the remote branches.",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-18T19:46:43.092Z",
"lastSeen": "2026-08-18T19:46:43.092Z"
},
{
"key": "avoid: direct public branch updates bypassed the repository’s pull-request-only rules, so ancestry and target refs required explicit validation to avoid unsafe rewrites.",
"text": "Avoid: Direct public branch updates bypassed the repository’s pull-request-only rules, so ancestry and target refs required explicit validation to avoid unsafe rewrites.",
"kind": "antiPattern",
"hits": 1,
"score": 1,
"firstSeen": "2026-08-18T19:46:43.092Z",
"lastSeen": "2026-08-18T19:46:43.092Z"
}
]
}
## 15:46 UTC — Task
### What went wrong
- Interpolated `${{ inputs.package }}` directly into `actions/github-script` JavaScript, allowing malformed input to break or inject code.
- Fetched npm metadata with the raw package name instead of URL-encoding it.
- Assumed `data.repository.url` always existed and was a GitHub `.git` URL.
- Used a brittle repository URL regex that missed scoped/modern GitHub URL formats and failed without a clear error.
### How it was fixed
- Pass workflow input through `env.NPM_PACKAGE` and read it from `process.env` inside the script.
- Validate the package name with an npm package-name regex before using it.
- Use `encodeURIComponent(packageName)` in the npm registry URL.
- Check `response.ok` and throw explicit errors for failed registry fetches or unsupported repository URLs.
## 16:04 UTC — Task
### What went wrong
- PR title did not satisfy the repository's conventional title check before opening/updating the PR.
### How it was fixed
- Renamed PR #6833 to `ci: harden CITGM package workflow input handling` using `gh pr edit`, then watched checks until `pull-request-title-check` passed.
## 19:37 UTC — Task
### What went wrong
- Continued initiating tool work after the tool-budget warning instead of stopping to assess the existing evidence.
- Started a test command that was aborted, leaving the primitive-coercion fix unverified.
- Investigated Ajv internals and custom compiler compatibility without first isolating the exact failing repository behavior.
- Did not establish regression coverage for missing schemaEnv handling and root primitive coercion before changing direction.
### How it was fixed
- Honor soft tool-budget and interruption signals immediately; stop browsing and provide a bounded reflection.
- Run only the single highest-value targeted test before the budget limit, then report if execution is blocked.
- Reproduce root primitive coercion with the repository’s actual validatorCompiler path rather than relying on Ajv internals alone.
- Add focused tests covering custom validator compilation, schemaEnv availability, and root primitive coercion.
## 20:03 UTC — Task
### What went wrong
- Root primitive request values were validated without a parent reference, so Ajv coercion changed only its local value and left request.body as a string.
- Test command output was misread: Borp ran the broader suite despite a specific file argument, making targeted coverage unclear.
### How it was fixed
- Pass request and parameter metadata as Ajv parentData/parentDataProperty so root coercions update the original request value.
- Add a regression test asserting quoted numeric JSON becomes a number in the handler while out-of-range values remain rejected.
- Verify targeted test execution separately from the full suite to avoid mistaking aggregate counts for focused coverage.
## 20:06 UTC — Task
### What went wrong
- Ajv was called with a primitive root value without parentData context, so coerced values were not written back to request.body.
- The regression scenario was initially vulnerable because the handler received string "10" after validation accepted it.
- The CVSS rating assumed PR:L without repository evidence that authentication is required; many affected routes may be unauthenticated.
- The impact rating was stated as application-dependent without defining the affected endpoint’s actual confidentiality and integrity consequences.
### How it was fixed
- Pass parentData and parentDataProperty to Ajv validators so root primitive coercions update the request object.
- Retain the direct-call path for validators without schemaEnv to avoid changing non-Ajv validator behavior.
- Add a regression test asserting quoted numeric JSON reaches the handler as number 10.
- Determine PR from the deployed route’s authentication requirements; use PR:N for unauthenticated endpoints.
- Base C/I impact on the specific handler and data affected rather than assigning a universal Fastify score.
## 20:09 UTC — Task
### What went wrong
- Validation coerced a root primitive but failed to preserve the coerced value for the caller.
- The regression scenario was not covered by existing tests.
### How it was fixed
- Updated `lib/validation.js` to retain and return coerced root values.
- Added `test/fix-root-primitive-coercion.test.js` to prevent recurrence.
## 10:59 UTC — Task
### What went wrong
- Assumed /static/..%2f… 404 meant getPathnameForSend/route miss instead of later send pipeline behavior
- Did not check that encodeURI() re-encodes residual % as %25 before blaming path guards
- Conflated reply.callNotFound() JSON ("Route GET:… not found") with an actual find-my-way miss
- Debugged isolated getPathnameForSend reimplementation before tracing pumpSendToReply → encodeURI(pathnameForSend) → @fastify/send
- Compared send() on decoded ../ paths (403) to static outcomes without using the exact post-encodeURI string static passes
### How it was fixed
- Always log/compare the exact pathname string passed into send(), including after encodeURI
- When statuses differ for "same" path, diff charCodes for % vs %25 double-encoding first
- Instrument static handler vs router to separate callNotFound from unmatched routes
- Trace full pipeline: raw.url → getPathnameForSend → dotDot guards → encodeURI → @fastify/send
- Reproduce with send(root, p) using both raw and encodeURI(p) before changing triage conclusions
## 11:12 UTC — Task
### What went wrong
- Assumed encodeURI leaves '%' intact on paths like '/..%2ftop-secret.txt', so static 404 vs raw '..' 403 looked inconsistent.
- Treated encoded traversal and decoded param sinks as the same layer before separating find-my-way dual-decode from @fastify/send root checks.
- Relied on log/message shape alone (callNotFound 'Route ... not found') to infer static failure mode without probing @fastify/send inputs.
### How it was fixed
- Empirically print encodeURI output/charCodes: '%' → '%25', so static sees '/..%252ftop-secret.txt' (safe 404) not a '..' segment.
- Reproduce with @fastify/send directly for raw percent, encodeURI(percent), and decoded '../' to map 403 vs 404 to normalize/root logic.
- Keep reporter response framed as app-level path.join(param) misuse; cite static/sendFile containment and Express parity, not a core CVE.
## 12:31 UTC — Task
### What went wrong
- Markdown lint invoked in a way that flooded output with unrelated files (AGENTS.md, .pi memory, response.md) instead of only the two edited docs.
- Untracked response.md sat in the working tree and could have been staged if git add was too broad.
- Relied on noisy repo-wide lint signal before confirming MD013 on the actual changed regions.
### How it was fixed
- Re-run markdownlint scoped strictly to docs/Reference/Request.md and docs/Reference/Routes.md.
- Stage only those two paths (git add docs/Reference/Request.md docs/Reference/Routes.md); never git add .
- Manually awk length>80 on changed files to confirm new lines stay ≤80 before commit.
- Leave response.md untracked and verify git status/diff --stat before commit and PR.
## 19:37 UTC — Task
### What went wrong
- Assumed the draft advisory meant `origin/main` and `origin/5.x` must remain untouched without first confirming whether the user expected public pushes.
- Reported that the private branches were complete without clearly distinguishing advisory-fork refs from public `origin` refs, prompting the user to stop and verify deployment status.
- Combined PR closing with an unsupported workspace-repository comment under `set -e`, causing the entire command to fail before the close could occur.
- Called the PR “now-empty” even though GitHub still reported it as mergeable with different head and base OIDs.
- Deleted local security branches before resolving whether the user wanted the commits pushed to public branches, reducing convenient recovery points.
### How it was fixed
- Before any security-fix push, explicitly ask whether targets are the private advisory fork, public `origin`, or both; treat a draft advisory as a warning, not implicit authorization policy.
- Always state push destinations and exact resulting refs, e.g. `advisory-ghsa-3m5p/main` changed while `origin/main` remained at `6e95cb9f`.
- Run workspace-sensitive PR operations separately: attempt the comment, record an expected permission denial, then independently close only if authorized.
- Describe PR state from verified fields (`CLOSED`, `mergedAt: null`, head/base OIDs) rather than inferring that it is empty.
- Keep local security branches until the user confirms the intended public/private publication state; delete them only after final authorization and ref verification.
## 19:38 UTC — Task
### What went wrong
- Initially pushed the security fix only to the private advisory fork, leaving `origin/main` and `origin/5.x` unchanged without first confirming the intended publication target.
- After pushing remote refs, local `main` remained one commit behind, creating temporary local/remote inconsistency.
- A GraphQL `addComment` action was attempted despite workspace-repository restrictions; the denial indicates an intentional permission boundary that should not be bypassed.
### How it was fixed
- Obtained explicit approval before public disclosure, then atomically fast-forwarded `origin/main` to `af079bd4` and `origin/5.x` to `8acfea7e`, verifying both refs with `git ls-remote`.
- Fast-forwarded local `main` to `origin/main` and confirmed a clean synchronized status.
- Do not retry forbidden workspace GraphQL actions; use permitted repository operations or ask the user for an authorized alternative before attempting comments or metadata changes.
## 19:46 UTC — Task
### What went wrong
- Private PR #1 remained open after its head commit had already been placed on private `main`; its mergeability was `UNKNOWN` and it was no longer the correct landing path.
- Local `main` and temporary security branches were left out of sync after updating the remote branches.
- Direct public branch updates bypassed the repository’s pull-request-only rules, so ancestry and target refs required explicit validation to avoid unsafe rewrites.
### How it was fixed
- Closed PR #1 without merging after confirming its head SHA was already the intended private `main` commit.
- Fetched current `origin/main` and `origin/5.x`, verified each security commit’s parent exactly matched the corresponding remote tip, then used an atomic push.
- Verified public and private `main`/`5.x` refs matched, fast-forwarded local `main`, deleted temporary security branches, and confirmed a clean synchronized checkout.
# Long-term Memory
Complete history of durable learnings and recurring mistakes.
Last updated: 2026-08-18T19:46:43.093Z
## All learnings
- Closed PR #1 without merging after confirming its head SHA was already the intended private `main` commit.
- Fetched current `origin/main` and `origin/5.x`, verified each security commit’s parent exactly matched the corresponding remote tip, then used an atomic push.
- Verified public and private `main`/`5.x` refs matched, fast-forwarded local `main`, deleted temporary security branches, and confirmed a clean synchronized checkout.
- Obtained explicit approval before public disclosure, then atomically fast-forwarded `origin/main` to `af079bd4` and `origin/5.x` to `8acfea7e`, verifying both refs with `git ls-remote`.
- Fast-forwarded local `main` to `origin/main` and confirmed a clean synchronized status.
- Do not retry forbidden workspace GraphQL actions; use permitted repository operations or ask the user for an authorized alternative before attempting comments or metadata changes.
- Before any security-fix push, explicitly ask whether targets are the private advisory fork, public `origin`, or both; treat a draft advisory as a warning, not implicit authorization policy.
- Always state push destinations and exact resulting refs, e.g. `advisory-ghsa-3m5p/main` changed while `origin/main` remained at `6e95cb9f`.
- Run workspace-sensitive PR operations separately: attempt the comment, record an expected permission denial, then independently close only if authorized.
- Describe PR state from verified fields (`CLOSED`, `mergedAt: null`, head/base OIDs) rather than inferring that it is empty.
- Keep local security branches until the user confirms the intended public/private publication state; delete them only after final authorization and ref verification.
- Re-run markdownlint scoped strictly to docs/Reference/Request.md and docs/Reference/Routes.md.
- Stage only those two paths (git add docs/Reference/Request.md docs/Reference/Routes.md); never git add .
- Manually awk length>80 on changed files to confirm new lines stay ≤80 before commit.
- Leave response.md untracked and verify git status/diff --stat before commit and PR.
- Empirically print encodeURI output/charCodes: '%' → '%25', so static sees '/..%252ftop-secret.txt' (safe 404) not a '..' segment.
- Reproduce with @fastify/send directly for raw percent, encodeURI(percent), and decoded '../' to map 403 vs 404 to normalize/root logic.
- Keep reporter response framed as app-level path.join(param) misuse; cite static/sendFile containment and Express parity, not a core CVE.
- Always log/compare the exact pathname string passed into send(), including after encodeURI
- When statuses differ for "same" path, diff charCodes for % vs %25 double-encoding first
- Instrument static handler vs router to separate callNotFound from unmatched routes
- Trace full pipeline: raw.url → getPathnameForSend → dotDot guards → encodeURI → @fastify/send
- Reproduce with send(root, p) using both raw and encodeURI(p) before changing triage conclusions
- Updated `lib/validation.js` to retain and return coerced root values.
- Added `test/fix-root-primitive-coercion.test.js` to prevent recurrence.
- Pass parentData and parentDataProperty to Ajv validators so root primitive coercions update the request object.
- Retain the direct-call path for validators without schemaEnv to avoid changing non-Ajv validator behavior.
- Add a regression test asserting quoted numeric JSON reaches the handler as number 10.
- Determine PR from the deployed route’s authentication requirements; use PR:N for unauthenticated endpoints.
- Base C/I impact on the specific handler and data affected rather than assigning a universal Fastify score.
- Pass request and parameter metadata as Ajv parentData/parentDataProperty so root coercions update the original request value.
- Add a regression test asserting quoted numeric JSON becomes a number in the handler while out-of-range values remain rejected.
- Verify targeted test execution separately from the full suite to avoid mistaking aggregate counts for focused coverage.
- Honor soft tool-budget and interruption signals immediately; stop browsing and provide a bounded reflection.
- Run only the single highest-value targeted test before the budget limit, then report if execution is blocked.
- Reproduce root primitive coercion with the repository’s actual validatorCompiler path rather than relying on Ajv internals alone.
- Add focused tests covering custom validator compilation, schemaEnv availability, and root primitive coercion.
- Renamed PR #6833 to `ci: harden CITGM package workflow input handling` using `gh pr edit`, then watched checks until `pull-request-title-check` passed.
- Pass workflow input through `env.NPM_PACKAGE` and read it from `process.env` inside the script.
- Validate the package name with an npm package-name regex before using it.
- Use `encodeURIComponent(packageName)` in the npm registry URL.
- Check `response.ok` and throw explicit errors for failed registry fetches or unsupported repository URLs.
## All watch-outs
- Private PR #1 remained open after its head commit had already been placed on private `main`; its mergeability was `UNKNOWN` and it was no longer the correct landing path.
- Local `main` and temporary security branches were left out of sync after updating the remote branches.
- Direct public branch updates bypassed the repository’s pull-request-only rules, so ancestry and target refs required explicit validation to avoid unsafe rewrites.
- Initially pushed the security fix only to the private advisory fork, leaving `origin/main` and `origin/5.x` unchanged without first confirming the intended publication target.
- After pushing remote refs, local `main` remained one commit behind, creating temporary local/remote inconsistency.
- A GraphQL `addComment` action was attempted despite workspace-repository restrictions; the denial indicates an intentional permission boundary that should not be bypassed.
- Assumed the draft advisory meant `origin/main` and `origin/5.x` must remain untouched without first confirming whether the user expected public pushes.
- Reported that the private branches were complete without clearly distinguishing advisory-fork refs from public `origin` refs, prompting the user to stop and verify deployment status.
- Combined PR closing with an unsupported workspace-repository comment under `set -e`, causing the entire command to fail before the close could occur.
- Called the PR “now-empty” even though GitHub still reported it as mergeable with different head and base OIDs.
- Deleted local security branches before resolving whether the user wanted the commits pushed to public branches, reducing convenient recovery points.
- Markdown lint invoked in a way that flooded output with unrelated files (AGENTS.md, .pi memory, response.md) instead of only the two edited docs.
- Untracked response.md sat in the working tree and could have been staged if git add was too broad.
- Relied on noisy repo-wide lint signal before confirming MD013 on the actual changed regions.
- Assumed encodeURI leaves '%' intact on paths like '/..%2ftop-secret.txt', so static 404 vs raw '..' 403 looked inconsistent.
- Treated encoded traversal and decoded param sinks as the same layer before separating find-my-way dual-decode from @fastify/send root checks.
- Relied on log/message shape alone (callNotFound 'Route ... not found') to infer static failure mode without probing @fastify/send inputs.
- Assumed /static/..%2f… 404 meant getPathnameForSend/route miss instead of later send pipeline behavior
- Did not check that encodeURI() re-encodes residual % as %25 before blaming path guards
- Conflated reply.callNotFound() JSON ("Route GET:… not found") with an actual find-my-way miss
- Debugged isolated getPathnameForSend reimplementation before tracing pumpSendToReply → encodeURI(pathnameForSend) → @fastify/send
- Compared send() on decoded ../ paths (403) to static outcomes without using the exact post-encodeURI string static passes
- Validation coerced a root primitive but failed to preserve the coerced value for the caller.
- The regression scenario was not covered by existing tests.
- Ajv was called with a primitive root value without parentData context, so coerced values were not written back to request.body.
- The regression scenario was initially vulnerable because the handler received string "10" after validation accepted it.
- The CVSS rating assumed PR:L without repository evidence that authentication is required; many affected routes may be unauthenticated.
- The impact rating was stated as application-dependent without defining the affected endpoint’s actual confidentiality and integrity consequences.
- Root primitive request values were validated without a parent reference, so Ajv coercion changed only its local value and left request.body as a string.
- Test command output was misread: Borp ran the broader suite despite a specific file argument, making targeted coverage unclear.
- Continued initiating tool work after the tool-budget warning instead of stopping to assess the existing evidence.
- Started a test command that was aborted, leaving the primitive-coercion fix unverified.
- Investigated Ajv internals and custom compiler compatibility without first isolating the exact failing repository behavior.
- Did not establish regression coverage for missing schemaEnv handling and root primitive coercion before changing direction.
- PR title did not satisfy the repository's conventional title check before opening/updating the PR.
- Interpolated `${{ inputs.package }}` directly into `actions/github-script` JavaScript, allowing malformed input to break or inject code.
- Fetched npm metadata with the raw package name instead of URL-encoding it.
- Assumed `data.repository.url` always existed and was a GitHub `.git` URL.
- Used a brittle repository URL regex that missed scoped/modern GitHub URL formats and failed without a clear error.
# Self-learning memory
This folder is auto-managed by pi-self-learning.
- daily/: day-by-day reflections
- monthly/: monthly summaries
- core/CORE.md: top-ranked durable learnings
- long-term-memory.md: complete learning history
# AGENTS.md - Guide for AI Agents Working with Fastify
This document provides information and guidelines for AI agents (such as GitHub Copilot, Cursor, pi, or other AI coding assistants) working with the Fastify codebase.
## Project Overview
Fastify is a high-performance web framework for Node.js focused on:
- **Speed**: One of the fastest Node.js web frameworks
- **Extensibility**: Powerful plugin architecture with hooks and decorators
- **Schema-based**: JSON Schema validation and serialization
- **Developer experience**: Expressive API with minimal overhead
- **TypeScript support**: Full type definitions included
**Current Version**: 5.7.1 (main branch)
**Repository**: https://github.com/fastify/fastify
## Repository Structure
```
fastify/
├── docs/ # Documentation (Guides and Reference)
│ ├── Guides/ # Tutorials and how-to guides
│ └── Reference/ # API documentation
├── examples/ # Example applications and benchmarks
├── lib/ # Core library code
├── test/ # Test files
├── types/ # TypeScript type definitions
├── build/ # Build scripts
├── integration/ # Integration tests
├── fastify.js # Main entry point
├── fastify.d.ts # Main TypeScript definitions
└── package.json # Dependencies and scripts
```
## Key Files for Agents
### Core Files
- **`fastify.js`** - Main Fastify class and entry point
- **`fastify.d.ts`** - TypeScript type definitions (keep these in sync)
- **`lib/`** - All core functionality:
- `route.js` - Route handling
- `req-res.js` - Request and Reply objects
- `hooks.js` - Lifecycle hooks
- `plugin.js` - Plugin system
- `validation.js` - Schema validation
- `content-type-parser.js` - Body parsing
- `logger.js` - Pino logger integration
### Configuration Files
- **`package.json`** - Scripts, dependencies, and contributors
- **`eslint.config.js`** - Linting configuration (uses neostandard)
- **`.markdownlint-cli2.yaml`** - Markdown linting rules
### Documentation Files
- **`README.md`** - Project overview and quick start
- **`CONTRIBUTING.md`** - Contribution guidelines
- **`GOVERNANCE.md`** - Links to organization governance
- **`SECURITY.md`** - Security policy
- **`docs/Guides/Contributing.md`** - Detailed contributing guide
- **`docs/Guides/Style-Guide.md`** - Coding style conventions
## Testing Conventions
### Test Framework
Fastify uses **Borp** (a custom test runner) for testing.
### Test Structure
- Tests are in the **`test/`** directory
- Test files follow the pattern: `test/<module>.test.js`
- Integration tests are in **`integration/`** directory
### Running Tests
```bash
# Run all tests
npm test
# Run unit tests only
npm run unit
# Run tests with coverage
npm run coverage
# Run tests in watch mode
npm run test:watch
# Run TypeScript type tests
npm run test:typescript
# Run CI tests (minimal)
npm run test:ci
```
### Test Requirements
- **100% line coverage** is required for all changes (enforced by CI)
- Tests must pass on all supported Node.js versions
- TypeScript types must be tested (using `tsd`)
## Code Style and Conventions
### Linting
- Uses **Neostandard** JavaScript style guide
- ESLint is configured in `eslint.config.js`
- Run `npm run lint` to check code style
- Run `npm run lint:fix` to auto-fix issues
### Key Style Rules (from Style-Guide.md)
- Use `const` and `let`, never `var`
- Use arrow functions for callbacks
- Use async/await instead of promises
- Follow semicolon usage (neostandard enforces this)
- Use template literals for string interpolation
- Prefer functional methods (`map`, `filter`, `reduce`) over loops
- Error-first callback pattern for async operations where needed
### Naming Conventions
- **Files**: kebab-case (`content-type-parser.js`)
- **Variables**: camelCase
- **Constants**: UPPER_SNAKE_CASE
- **Classes**: PascalCase
- **Private methods**: prefixed with `_`
## Common Tasks
### Adding a New Feature
1. Implement the feature in `lib/`
2. Add tests in `test/`
3. Update TypeScript types in `fastify.d.ts` or `types/`
4. Update documentation in `docs/`
5. Run `npm test` to ensure all tests pass
6. Run `npm run lint` to check code style
7. Add changelog entry for release
### Fixing a Bug
1. Add a failing test case in `test/`
2. Fix the bug in `lib/`
3. Ensure all tests pass
4. Check if TypeScript types need updating
5. Update documentation if behavior changes
### Working with Plugins
- See `docs/Guides/Write-Plugin.md` for plugin authoring
- See `docs/Guides/Plugins-Guide.md` for plugin usage
- Plugin example: `lib/plugin.js`
## Architecture Highlights
### Core Components
1. **Server (`fastify.js`)**
- Main Fastify class
- Server initialization and configuration
- Plugin system integration (via `avvio`)
2. **Routing (`lib/route.js`)**
- Uses `find-my-way` for fast route matching
- Route registration and lookup
- Shorthand methods (get, post, put, delete, etc.)
3. **Request/Response (`lib/req-res.js`)**
- Request object extensions
- Reply object with fluent API
- Decorator support
4. **Hooks (`lib/hooks.js`)**
- Lifecycle hooks (onRequest, preHandler, etc.)
- Hook execution order and timing
5. **Validation (`lib/validation.js`)**
- JSON Schema validation via AJV
- Response serialization
- Built-in error serializer
6. **Content Type Parser (`lib/content-type-parser.js`)**
- Request body parsing
- Custom parser support
- JSON and other formats
### Plugin System
- Plugins are loaded asynchronously via `avvio`
- Supports encapsulation (scoped plugins)
- Hooks and decorators can be scoped
- See `lib/plugin.js` for implementation
## TypeScript Integration
- TypeScript definitions are in `fastify.d.ts` and `types/`
- Types must be tested with `tsd`
- Run `npm run test:typescript` to verify types
- Keep types in sync with JavaScript implementation
## Performance Considerations
Fastify prioritizes performance:
- **Routes**: Pre-compiled functions for fast matching
- **Validation**: Compiled JSON Schema validators
- **Serialization**: Compiled serializers (fast-json-stringify)
- **Logging**: Low-overhead Pino logger
- **Caching**: Route context caching with `toad-cache`
When making changes:
- Profile performance impact for hot paths
- Use benchmarks in `examples/benchmark/`
- Run `npm run benchmark` to measure
## Documentation Updates
Documentation is critical for Fastify. When changing behavior:
1. Update relevant docs in `docs/Reference/` for API changes
2. Update `docs/Guides/` for usage pattern changes
3. Check for broken links (CI validates this)
4. Update examples in `examples/` if needed
5. Run `npm run lint:markdown` to check docs
## Pre-commit Checks
Before submitting changes, ensure:
1. ✅ All tests pass: `npm test`
2. ✅ 100% coverage: `npm run coverage`
3. ✅ Linting passes: `npm run lint`
4. ✅ TypeScript types pass: `npm run test:typescript`
5. ✅ Markdown linting passes: `npm run lint:markdown`
6. ✅ Documentation is updated
7. ✅ Examples still work if affected
## Working with CI
Fastify uses GitHub Actions for CI. Workflows are in `.github/workflows/`:
- **`ci.yml`** - Main CI pipeline
- **`package-manager-ci.yml`** - Tests multiple package managers
- **`website.yml`** - Website deployment
## Agent-Specific Tips
### When Generating Code
1. Check existing patterns in `lib/` before creating new patterns
2. Follow the established error handling patterns
3. Use async/await consistently
4. Add appropriate hooks if extending lifecycle
5. Consider TypeScript types from the start
### When Refactoring
1. Ensure all tests still pass
2. Don't change public APIs without semver consideration
3. Update TypeScript definitions if signatures change
4. Check for deprecation needs
5. Update documentation for changed behavior
### When Analyzing Issues
1. Check `test/` for usage examples
2. Review relevant `docs/Reference/` files
3. Look at similar implementations in `lib/`
4. Consider the plugin system and encapsulation
5. Check hook timing and order
### Common Gotchas
- **Encapsulation**: Plugins are isolated - decorators don't leak
- **Hook order**: Hooks run in specific order (see docs/Reference/Hooks.md)
- **Async boot**: Server starts asynchronously - use `ready()` or `after()`
- **Error handling**: Use Fastify error classes from `@fastify/error`
- **Validation**: Schemas are compiled - changes require recompilation
## Key Dependencies
- **`avvio`** - Plugin loading and boot
- **`find-my-way`** - Fast HTTP router
- **`fast-json-stringify`** - Response serialization
- **`pino`** - Logging
- **`@fastify/ajv-compiler`** - JSON Schema validation
- **`light-my-request`** - HTTP injection for testing
## Contact and Resources
- **Documentation**: https://fastify.dev/
- **Discord**: https://discord.gg/fastify
- **GitHub Issues**: https://github.com/fastify/fastify/issues
- **GitHub Discussions**: https://github.com/fastify/fastify/discussions
- **Help**: https://github.com/fastify/help
## Version Information
- **Main branch**: Fastify v5
- **v4 branch**: https://github.com/fastify/fastify/tree/4.x
- **LTS Policy**: See `docs/Reference/LTS.md`
---
This document is maintained by the Fastify team. For questions or suggestions, please open an issue or discussion.
'use strict'
const { test } = require('node:test')
const Fastify = require('..')
test('root primitive values coerced by Ajv are assigned to the request', async (t) => {
const fastify = Fastify()
fastify.post('/', {
schema: {
body: {
type: 'integer',
minimum: 1,
maximum: 10
}
}
}, (request, reply) => {
reply.send({ body: request.body, type: typeof request.body })
})
const response = await fastify.inject({
method: 'POST',
url: '/',
payload: '"10"',
headers: { 'content-type': 'application/json' }
})
t.assert.strictEqual(response.statusCode, 200)
t.assert.deepStrictEqual(JSON.parse(response.payload), {
body: 10,
type: 'number'
})
await fastify.close()
})
+1
-1

@@ -152,3 +152,3 @@ import * as http from 'node:http'

genReqId?: (req: RawRequestDefaultExpression<RawServer>) => string,
trustProxy?: boolean | string | string[] | number | TrustProxyFunction,
trustProxy?: boolean | string | string[] | TrustProxyFunction,
querystringParser?: (str: string) => { [key: string]: unknown },

@@ -155,0 +155,0 @@ constraints?: {

'use strict'
const VERSION = '5.12.0'
const VERSION = '5.12.1'

@@ -604,3 +604,3 @@ const Avvio = require('avvio')

this.onClose(fn.bind(this))
} else if (name === 'onReady' || name === 'onListen' || name === 'onRoute') {
} else if (name === 'onReady' || name === 'onListen' || name === 'onRoute' || name === 'preClose') {
this[kHooks].add(name, fn)

@@ -607,0 +607,0 @@ } else {

@@ -52,4 +52,5 @@ 'use strict'

if (typeof tp === 'number') {
// Support trusting hop count
return function (a, i) { return i < tp }
// Hop-count-only trust cannot validate the immediate peer. Fail closed so
// direct clients cannot spoof X-Forwarded-* values by supplying enough hops.
return function () { return false }
}

@@ -56,0 +57,0 @@ if (typeof tp === 'string') {

@@ -293,2 +293,3 @@ 'use strict'

const routeConfigUrl = prefixing ? prefix : url
if (prefixing === false) {

@@ -332,3 +333,3 @@ // run 'onRoute' hooks

...opts.config,
url,
url: routeConfigUrl,
method: opts.method

@@ -335,0 +336,0 @@ }

@@ -123,3 +123,12 @@ 'use strict'

try {
ret = validatorFunction?.(isUndefined ? null : request[paramName])
const data = isUndefined ? null : request[paramName]
// Ajv can only mutate a root value when its parent is provided.
if (validatorFunction?.schemaEnv) {
ret = validatorFunction(data, {
parentData: request,
parentDataProperty: paramName
})
} else {
ret = validatorFunction?.(data)
}
} catch (err) {

@@ -126,0 +135,0 @@ // If validator throws synchronously, ensure it propagates as an internal error

{
"name": "fastify",
"version": "5.12.0",
"version": "5.12.1",
"description": "Fast and low overhead web framework, for Node.js",

@@ -5,0 +5,0 @@ "main": "fastify.js",

@@ -668,2 +668,36 @@ 'use strict'

test('preClose runs exactly once with a child plugin', async t => {
t.plan(1)
const fastify = Fastify()
let count = 0
fastify.register(async (child) => {
child.get('/x', async () => 'ok')
})
fastify.addHook('preClose', async () => { count++ })
await fastify.ready()
await fastify.close()
t.assert.strictEqual(count, 1)
})
test('preClose runs exactly once with nested child plugins', async t => {
t.plan(1)
const fastify = Fastify()
let count = 0
fastify.register(async (child) => {
child.register(async (grandchild) => {
grandchild.get('/y', async () => 'ok')
})
})
fastify.addHook('preClose', async () => { count++ })
await fastify.ready()
await fastify.close()
t.assert.strictEqual(count, 1)
})
test('preClose execution order', (t, done) => {

@@ -670,0 +704,0 @@ t.plan(4)

@@ -52,3 +52,3 @@ 'use strict'

t.assert.strictEqual(typeof reply.code, 'function')
t.assert.strictEqual(typeof reply.mediaType, 'string')
t.assert.strictEqual(typeof reply.mediaType, 'undefined')
t.assert.strictEqual(typeof reply.status, 'function')

@@ -55,0 +55,0 @@ t.assert.strictEqual(typeof reply.header, 'function')

@@ -566,2 +566,34 @@ 'use strict'

test('reports the canonical route url for hidden prefix trailing slash route', async t => {
t.plan(4)
const fastify = Fastify({
ignoreTrailingSlash: false,
exposeHeadRoutes: false
})
const onRouteUrls = []
fastify.addHook('onRoute', routeOptions => {
onRouteUrls.push(routeOptions.url)
})
fastify.register(async instance => {
instance.get('/', async request => {
return request.routeOptions.url
})
instance.get('/bar/', async request => {
return request.routeOptions.url
})
}, { prefix: '/prefix' })
t.assert.strictEqual((await fastify.inject('/prefix')).payload, '/prefix')
t.assert.strictEqual((await fastify.inject('/prefix/')).payload, '/prefix')
t.assert.strictEqual((await fastify.inject('/prefix/bar/')).payload, '/prefix/bar/')
t.assert.deepStrictEqual(onRouteUrls, ['/prefix', '/prefix/bar/'])
await fastify.close()
})
test('matches both /prefix and /prefix/ with a / route - prefixTrailingSlash: "both", ignoreDuplicateSlashes: false', (t, testDone) => {

@@ -568,0 +600,0 @@ t.plan(4)

@@ -109,4 +109,4 @@ 'use strict'

test('trust proxy number', async t => {
t.plan(8)
test('trust proxy number ignores forwarded headers', async t => {
t.plan(5)
const app = fastify({

@@ -118,8 +118,21 @@ trustProxy: 1

app.get('/trustproxynumber', function (req, reply) {
testRequestValues(t, req, { ip: '1.1.1.1', ips: [localhost, '1.1.1.1'], host: 'fastify.test:1234', hostname: 'fastify.test', port: 1234 })
t.assert.strictEqual(req.ip, '203.0.113.7', 'ip falls back to socket remote address')
t.assert.deepStrictEqual(req.ips, ['203.0.113.7'], 'ips falls back to socket remote address')
t.assert.strictEqual(req.host, 'app.example.com', 'host ignores x-forwarded-host')
t.assert.strictEqual(req.hostname, 'app.example.com', 'hostname ignores x-forwarded-host')
t.assert.strictEqual(req.protocol, 'http', 'protocol ignores x-forwarded-proto')
reply.code(200).send({ ip: req.ip, host: req.host })
})
const fastifyServer = await app.listen({ port: 0 })
await fetchForwardedRequest(fastifyServer, '2.2.2.2, 1.1.1.1', '/trustproxynumber', undefined, 'fastify.test:1234')
await app.inject({
method: 'GET',
url: '/trustproxynumber',
remoteAddress: '203.0.113.7',
headers: {
host: 'app.example.com',
'x-forwarded-for': '9.9.9.9, 8.8.8.8',
'x-forwarded-host': 'evil.com',
'x-forwarded-proto': 'https'
}
})
})

@@ -246,8 +259,7 @@

test('trust proxy with number and undefined socket remoteAddress', t => {
test('trust proxy with number and undefined socket remoteAddress ignores forwarded headers', t => {
t.plan(3)
// Test case for issue #6606: trustProxy: 1 with undefined/null socket.remoteAddress
// This simulates IISNode on Windows where socket.remoteAddress may be undefined
const headers = {
host: 'real.test',
'x-forwarded-for': '2.2.2.2, 1.1.1.1',

@@ -257,4 +269,2 @@ 'x-forwarded-host': 'fastify.test',

}
// socket must exist but remoteAddress can be undefined
// This is what happens in IISNode with enableXFF="true"
const req = {

@@ -269,16 +279,12 @@ method: 'GET',

const request = new TpRequest('id', 'params', req, 'query', 'log')
// Even with undefined socket.remoteAddress, req.ip should be populated from X-Forwarded-For
t.assert.ok(request.ip, 'ip is defined')
// With trustProxy: 1, we trust 1 hop from socket. Since socket.remoteAddress is undefined,
// the hop count check should skip it and we get 1.1.1.1 (the first trusted address from X-Forwarded-For)
t.assert.strictEqual(request.ip, '1.1.1.1', 'gets ip from x-forwarded-for')
// The host should also work correctly
t.assert.strictEqual(request.host, 'fastify.test', 'gets host from x-forwarded-host')
t.assert.strictEqual(request.ip, undefined, 'ip falls back to undefined socket remote address')
t.assert.strictEqual(request.host, 'real.test', 'host ignores x-forwarded-host')
t.assert.strictEqual(request.protocol, 'http', 'protocol ignores x-forwarded-proto')
})
test('trust proxy with number and null socket remoteAddress', t => {
test('trust proxy with number and null socket remoteAddress ignores forwarded headers', t => {
t.plan(2)
// Test case for trustProxy: 1 with null socket.remoteAddress
const headers = {
host: 'real.test',
'x-forwarded-for': '2.2.2.2, 1.1.1.1',

@@ -296,4 +302,4 @@ 'x-forwarded-host': 'fastify.test'

const request = new TpRequest('id', 'params', req, 'query', 'log')
t.assert.ok(request.ip, 'ip is defined')
t.assert.strictEqual(request.ip, '1.1.1.1', 'gets ip from x-forwarded-for')
t.assert.strictEqual(request.ip, null, 'ip falls back to null socket remote address')
t.assert.strictEqual(request.host, 'real.test', 'host ignores x-forwarded-host')
})

@@ -300,0 +306,0 @@

@@ -201,2 +201,4 @@ import * as http from 'node:http'

expect(fastify({ trustProxy: true })).type.toBeAssignableTo<FastifyInstance>()
// @ts-expect-error No overload matches this call.
fastify({ trustProxy: 1 })
expect(fastify({ querystringParser: () => ({ foo: 'bar' }) })).type.toBeAssignableTo<FastifyInstance>()

@@ -203,0 +205,0 @@ expect(fastify({ querystringParser: () => ({ foo: { bar: 'fuzz' } }) })).type.toBeAssignableTo<FastifyInstance>()

Sorry, the diff of this file is too big to display