| # 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 |
+290
| # 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?: { |
+2
-2
| '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 { |
+3
-2
@@ -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') { |
+2
-1
@@ -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 @@ } |
+10
-1
@@ -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 |
+1
-1
| { | ||
| "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", |
+34
-0
@@ -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) |
+27
-21
@@ -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
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
2984609
2.52%371
2.77%68060
1.24%