rcf-lite 0.27.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +10 -0
- package/bin/rcf.js +2 -0
- package/blueprints/application-account-settings/CHANGELOG.md +35 -6
- package/blueprints/application-account-settings/README.md +1 -1
- package/blueprints/application-account-settings/blueprint.json +4 -2
- package/blueprints/application-account-settings/contributions/adrs/adr-2602-application-account-settings-security-surface-shape.json +1 -1
- package/blueprints/application-account-settings/contributions/probes/probe-utils.mjs +177 -0
- package/blueprints/application-account-settings/contributions/probes/profile-form-autocomplete.mjs +67 -0
- package/blueprints/application-account-settings/contributions/probes/run-profile-form-autocomplete.mjs +5 -0
- package/blueprints/application-account-settings/contributions/probes/run-sessions-adapter-uniform.mjs +5 -0
- package/blueprints/application-account-settings/contributions/probes/run-sessions-surface-shape.mjs +5 -0
- package/blueprints/application-account-settings/contributions/probes/run-shell-tablist-per-capability.mjs +5 -0
- package/blueprints/application-account-settings/contributions/probes/run-theme-radiogroup.mjs +5 -0
- package/blueprints/application-account-settings/contributions/probes/sessions-adapter-uniform.mjs +130 -0
- package/blueprints/application-account-settings/contributions/probes/sessions-surface-shape.mjs +70 -0
- package/blueprints/application-account-settings/contributions/probes/shell-tablist-per-capability.mjs +60 -0
- package/blueprints/application-account-settings/contributions/probes/theme-radiogroup.mjs +126 -0
- package/blueprints/application-admin-console/CHANGELOG.md +32 -6
- package/blueprints/application-admin-console/README.md +1 -1
- package/blueprints/application-admin-console/blueprint.json +4 -2
- package/blueprints/application-admin-console/contributions/adrs/adr-2201-application-admin-console-capability-vocabulary.json +1 -1
- package/blueprints/application-admin-console/contributions/probes/audit-log-surface.mjs +79 -0
- package/blueprints/application-admin-console/contributions/probes/org-switcher-surface.mjs +83 -0
- package/blueprints/application-admin-console/contributions/probes/permission-matrix-grid.mjs +67 -0
- package/blueprints/application-admin-console/contributions/probes/probe-utils.mjs +177 -0
- package/blueprints/application-admin-console/contributions/probes/run-audit-log-surface.mjs +5 -0
- package/blueprints/application-admin-console/contributions/probes/run-org-switcher-surface.mjs +5 -0
- package/blueprints/application-admin-console/contributions/probes/run-permission-matrix-grid.mjs +5 -0
- package/blueprints/application-admin-console/contributions/probes/run-sign-in-access-gated-surface.mjs +5 -0
- package/blueprints/application-admin-console/contributions/probes/run-users-directory-surface.mjs +5 -0
- package/blueprints/application-admin-console/contributions/probes/sign-in-access-gated-surface.mjs +117 -0
- package/blueprints/application-admin-console/contributions/probes/users-directory-surface.mjs +145 -0
- package/blueprints/application-api-rest/CHANGELOG.md +16 -8
- package/blueprints/application-api-rest/README.md +3 -3
- package/blueprints/application-api-rest/blueprint.json +3 -2
- package/blueprints/application-api-rest/contributions/adrs/adr-302-application-api-rest-auth-model.json +1 -1
- package/blueprints/application-api-rest/contributions/probes/cursor-pagination-round-trip.mjs +154 -0
- package/blueprints/application-api-rest/contributions/probes/health-probes-distinct.mjs +124 -0
- package/blueprints/application-api-rest/contributions/probes/probe-utils.mjs +177 -0
- package/blueprints/application-api-rest/contributions/probes/problem-details-on-error.mjs +77 -0
- package/blueprints/application-api-rest/contributions/probes/request-id-echoed.mjs +76 -0
- package/blueprints/application-api-rest/contributions/probes/run-cursor-pagination-round-trip.mjs +5 -0
- package/blueprints/application-api-rest/contributions/probes/run-health-probes-distinct.mjs +5 -0
- package/blueprints/application-api-rest/contributions/probes/run-problem-details-on-error.mjs +5 -0
- package/blueprints/application-api-rest/contributions/probes/run-request-id-echoed.mjs +5 -0
- package/blueprints/application-api-rest/docs/topics.md +1 -1
- package/blueprints/application-charts/CHANGELOG.md +35 -9
- package/blueprints/application-charts/README.md +6 -6
- package/blueprints/application-charts/blueprint.json +5 -3
- package/blueprints/application-charts/contributions/adrs/adr-1901-application-charts-engine-choice.json +1 -1
- package/blueprints/application-charts/contributions/probes/keyboard-traversal.mjs +151 -0
- package/blueprints/application-charts/contributions/probes/non-colour-distinction.mjs +67 -0
- package/blueprints/application-charts/contributions/probes/probe-utils.mjs +177 -0
- package/blueprints/application-charts/contributions/probes/run-keyboard-traversal.mjs +5 -0
- package/blueprints/application-charts/contributions/probes/run-non-colour-distinction.mjs +5 -0
- package/blueprints/application-charts/contributions/probes/run-text-alternative-table.mjs +5 -0
- package/blueprints/application-charts/contributions/probes/text-alternative-table.mjs +139 -0
- package/blueprints/application-charts/contributions/requirements/application-charts-req-001.json +1 -1
- package/blueprints/application-charts/contributions/requirements/application-charts-req-002.json +1 -1
- package/blueprints/application-charts/contributions/user-stories/application-charts-us-18101.json +5 -5
- package/blueprints/application-charts/contributions/user-stories/application-charts-us-18106.json +1 -1
- package/blueprints/application-charts/guide/application-charts.md +1 -1
- package/blueprints/application-dashboard/CHANGELOG.md +20 -10
- package/blueprints/application-dashboard/README.md +6 -6
- package/blueprints/application-dashboard/assets/guidance/dashboard-design.md +2 -2
- package/blueprints/application-dashboard/blueprint.json +3 -2
- package/blueprints/application-dashboard/contributions/adrs/adr-2001-application-dashboard-primary-kpi-enum.json +1 -1
- package/blueprints/application-dashboard/contributions/adrs/adr-2002-application-dashboard-timeframe-presets.json +1 -1
- package/blueprints/application-dashboard/contributions/probes/export-handle-formats.mjs +75 -0
- package/blueprints/application-dashboard/contributions/probes/four-tile-states.mjs +75 -0
- package/blueprints/application-dashboard/contributions/probes/primary-kpi-top-left.mjs +58 -0
- package/blueprints/application-dashboard/contributions/probes/probe-utils.mjs +177 -0
- package/blueprints/application-dashboard/contributions/probes/run-export-handle-formats.mjs +4 -0
- package/blueprints/application-dashboard/contributions/probes/run-four-tile-states.mjs +4 -0
- package/blueprints/application-dashboard/contributions/probes/run-primary-kpi-top-left.mjs +4 -0
- package/blueprints/application-dashboard/contributions/probes/run-shell-five-regions.mjs +4 -0
- package/blueprints/application-dashboard/contributions/probes/shell-five-regions.mjs +93 -0
- package/blueprints/application-dashboard/contributions/requirements/application-dashboard-req-002.json +1 -1
- package/blueprints/application-dashboard/contributions/requirements/application-dashboard-req-003.json +1 -1
- package/blueprints/application-dashboard/contributions/tacs/tac-2002-application-dashboard-filter-chrome.json +1 -1
- package/blueprints/application-dashboard/contributions/tacs/tac-2003-application-dashboard-export-handle.json +2 -2
- package/blueprints/application-dashboard/contributions/user-stories/application-dashboard-us-19101.json +2 -2
- package/blueprints/application-dashboard/contributions/user-stories/application-dashboard-us-19102.json +2 -2
- package/blueprints/application-dashboard/contributions/user-stories/application-dashboard-us-19103.json +1 -1
- package/blueprints/application-dashboard/contributions/user-stories/application-dashboard-us-19104.json +1 -1
- package/blueprints/application-dashboard/contributions/user-stories/application-dashboard-us-19105.json +1 -1
- package/blueprints/application-dashboard/contributions/user-stories/application-dashboard-us-19106.json +2 -2
- package/blueprints/application-dashboard/contributions/user-stories/application-dashboard-us-19107.json +5 -5
- package/blueprints/application-dashboard/docs/topics.md +2 -2
- package/blueprints/application-dashboard/guide/application-dashboard.md +2 -2
- package/blueprints/application-datatable/CHANGELOG.md +19 -9
- package/blueprints/application-datatable/README.md +2 -2
- package/blueprints/application-datatable/blueprint.json +3 -2
- package/blueprints/application-datatable/contributions/probes/apg-table-shape.mjs +57 -0
- package/blueprints/application-datatable/contributions/probes/four-states-regions.mjs +80 -0
- package/blueprints/application-datatable/contributions/probes/probe-utils.mjs +177 -0
- package/blueprints/application-datatable/contributions/probes/run-apg-table-shape.mjs +4 -0
- package/blueprints/application-datatable/contributions/probes/run-four-states-regions.mjs +4 -0
- package/blueprints/application-datatable/contributions/probes/run-search-adapter-round-trip.mjs +4 -0
- package/blueprints/application-datatable/contributions/probes/run-sort-adapter-round-trip.mjs +4 -0
- package/blueprints/application-datatable/contributions/probes/search-adapter-round-trip.mjs +58 -0
- package/blueprints/application-datatable/contributions/probes/sort-adapter-round-trip.mjs +60 -0
- package/blueprints/application-datatable/contributions/requirements/application-datatable-req-006.json +1 -1
- package/blueprints/application-datatable/docs/topics.md +1 -1
- package/blueprints/application-empty-error-states/CHANGELOG.md +33 -3
- package/blueprints/application-empty-error-states/blueprint.json +4 -2
- package/blueprints/application-empty-error-states/contributions/probes/empty-list-and-no-search.mjs +66 -0
- package/blueprints/application-empty-error-states/contributions/probes/error-boundary-alert.mjs +65 -0
- package/blueprints/application-empty-error-states/contributions/probes/forbidden-and-server-error.mjs +167 -0
- package/blueprints/application-empty-error-states/contributions/probes/not-found-and-recovery.mjs +61 -0
- package/blueprints/application-empty-error-states/contributions/probes/permission-denied-and-offline.mjs +155 -0
- package/blueprints/application-empty-error-states/contributions/probes/probe-utils.mjs +177 -0
- package/blueprints/application-empty-error-states/contributions/probes/run-empty-list-and-no-search.mjs +5 -0
- package/blueprints/application-empty-error-states/contributions/probes/run-error-boundary-alert.mjs +5 -0
- package/blueprints/application-empty-error-states/contributions/probes/run-forbidden-and-server-error.mjs +5 -0
- package/blueprints/application-empty-error-states/contributions/probes/run-not-found-and-recovery.mjs +5 -0
- package/blueprints/application-empty-error-states/contributions/probes/run-permission-denied-and-offline.mjs +5 -0
- package/blueprints/application-error-handling/CHANGELOG.md +45 -6
- package/blueprints/application-error-handling/blueprint.json +3 -2
- package/blueprints/application-error-handling/contributions/probes/category-vocabulary.mjs +90 -0
- package/blueprints/application-error-handling/contributions/probes/probe-utils.mjs +177 -0
- package/blueprints/application-error-handling/contributions/probes/record-shape-adr-1701.mjs +78 -0
- package/blueprints/application-error-handling/contributions/probes/run-category-vocabulary.mjs +5 -0
- package/blueprints/application-error-handling/contributions/probes/run-record-shape-adr-1701.mjs +5 -0
- package/blueprints/application-error-handling/contributions/probes/run-two-boundaries-registered.mjs +5 -0
- package/blueprints/application-error-handling/contributions/probes/two-boundaries-registered.mjs +343 -0
- package/blueprints/application-file-upload/CHANGELOG.md +28 -6
- package/blueprints/application-file-upload/README.md +1 -1
- package/blueprints/application-file-upload/blueprint.json +3 -2
- package/blueprints/application-file-upload/contributions/probes/assertive-completion-slot.mjs +78 -0
- package/blueprints/application-file-upload/contributions/probes/chunked-transport-endpoints.mjs +173 -0
- package/blueprints/application-file-upload/contributions/probes/per-file-progressbar.mjs +124 -0
- package/blueprints/application-file-upload/contributions/probes/probe-utils.mjs +177 -0
- package/blueprints/application-file-upload/contributions/probes/run-assertive-completion-slot.mjs +4 -0
- package/blueprints/application-file-upload/contributions/probes/run-chunked-transport-endpoints.mjs +4 -0
- package/blueprints/application-file-upload/contributions/probes/run-per-file-progressbar.mjs +4 -0
- package/blueprints/application-file-upload/contributions/probes/run-upload-surface-shape.mjs +4 -0
- package/blueprints/application-file-upload/contributions/probes/upload-surface-shape.mjs +55 -0
- package/blueprints/application-file-upload/contributions/requirements/application-file-upload-req-001.json +1 -1
- package/blueprints/application-file-upload/contributions/requirements/application-file-upload-req-002.json +1 -1
- package/blueprints/application-file-upload/contributions/requirements/application-file-upload-req-003.json +1 -1
- package/blueprints/application-file-upload/contributions/requirements/application-file-upload-req-004.json +1 -1
- package/blueprints/application-file-upload/contributions/requirements/application-file-upload-req-005.json +1 -1
- package/blueprints/application-file-upload/probe-packs/application-file-upload.pack.mjs +1 -1
- package/blueprints/application-forms-wizard/CHANGELOG.md +20 -6
- package/blueprints/application-forms-wizard/README.md +2 -2
- package/blueprints/application-forms-wizard/blueprint.json +3 -2
- package/blueprints/application-forms-wizard/contributions/probes/draft-persistence-round-trip.mjs +110 -0
- package/blueprints/application-forms-wizard/contributions/probes/probe-utils.mjs +177 -0
- package/blueprints/application-forms-wizard/contributions/probes/run-draft-persistence-round-trip.mjs +4 -0
- package/blueprints/application-forms-wizard/contributions/probes/run-step-page-shape.mjs +4 -0
- package/blueprints/application-forms-wizard/contributions/probes/run-task-list-surface.mjs +4 -0
- package/blueprints/application-forms-wizard/contributions/probes/step-page-shape.mjs +61 -0
- package/blueprints/application-forms-wizard/contributions/probes/task-list-surface.mjs +109 -0
- package/blueprints/application-forms-wizard/contributions/requirements/application-forms-wizard-req-001.json +1 -1
- package/blueprints/application-forms-wizard/contributions/requirements/application-forms-wizard-req-002.json +1 -1
- package/blueprints/application-forms-wizard/contributions/requirements/application-forms-wizard-req-003.json +1 -1
- package/blueprints/application-forms-wizard/contributions/requirements/application-forms-wizard-req-004.json +1 -1
- package/blueprints/application-forms-wizard/contributions/requirements/application-forms-wizard-req-005.json +1 -1
- package/blueprints/application-forms-wizard/probe-packs/application-forms-wizard.pack.mjs +1 -1
- package/blueprints/application-notifications-in-app/CHANGELOG.md +38 -5
- package/blueprints/application-notifications-in-app/README.md +4 -4
- package/blueprints/application-notifications-in-app/blueprint.json +4 -2
- package/blueprints/application-notifications-in-app/contributions/adrs/adr-2101-application-notifications-in-app-live-region-role-mapping.json +1 -1
- package/blueprints/application-notifications-in-app/contributions/probes/centre-acknowledge-round-trip.mjs +210 -0
- package/blueprints/application-notifications-in-app/contributions/probes/live-region-preseeding.mjs +93 -0
- package/blueprints/application-notifications-in-app/contributions/probes/probe-utils.mjs +177 -0
- package/blueprints/application-notifications-in-app/contributions/probes/run-centre-acknowledge-round-trip.mjs +5 -0
- package/blueprints/application-notifications-in-app/contributions/probes/run-live-region-preseeding.mjs +5 -0
- package/blueprints/application-notifications-in-app/contributions/probes/run-toast-contract.mjs +5 -0
- package/blueprints/application-notifications-in-app/contributions/probes/toast-contract.mjs +92 -0
- package/blueprints/application-notifications-in-app/contributions/tacs/tac-2101-application-notifications-in-app-live-region.json +3 -3
- package/blueprints/application-notifications-in-app/contributions/tacs/tac-2102-application-notifications-in-app-centre.json +1 -1
- package/blueprints/application-notifications-in-app/contributions/user-stories/application-notifications-in-app-us-20101.json +1 -1
- package/blueprints/application-notifications-in-app/contributions/user-stories/application-notifications-in-app-us-20102.json +1 -1
- package/blueprints/application-notifications-in-app/contributions/user-stories/application-notifications-in-app-us-20103.json +1 -1
- package/blueprints/application-notifications-in-app/contributions/user-stories/application-notifications-in-app-us-20105.json +1 -1
- package/blueprints/application-notifications-in-app/guide/application-notifications-in-app.md +3 -3
- package/blueprints/application-onboarding-tour/CHANGELOG.md +31 -5
- package/blueprints/application-onboarding-tour/README.md +1 -1
- package/blueprints/application-onboarding-tour/blueprint.json +5 -3
- package/blueprints/application-onboarding-tour/contributions/probes/checklist-anchor-open.mjs +81 -0
- package/blueprints/application-onboarding-tour/contributions/probes/completion-persistence.mjs +159 -0
- package/blueprints/application-onboarding-tour/contributions/probes/first-run-detection.mjs +67 -0
- package/blueprints/application-onboarding-tour/contributions/probes/probe-utils.mjs +177 -0
- package/blueprints/application-onboarding-tour/contributions/probes/run-checklist-anchor-open.mjs +5 -0
- package/blueprints/application-onboarding-tour/contributions/probes/run-completion-persistence.mjs +5 -0
- package/blueprints/application-onboarding-tour/contributions/probes/run-first-run-detection.mjs +5 -0
- package/blueprints/application-onboarding-tour/contributions/probes/run-stepper-role-dialog.mjs +5 -0
- package/blueprints/application-onboarding-tour/contributions/probes/stepper-role-dialog.mjs +45 -0
- package/blueprints/application-spa/CHANGELOG.md +20 -11
- package/blueprints/application-spa/README.md +9 -10
- package/blueprints/application-spa/blueprint.json +3 -2
- package/blueprints/application-spa/contributions/adrs/adr-202-application-spa-theming.json +1 -1
- package/blueprints/application-spa/contributions/adrs/adr-206-application-spa-iconography.json +1 -1
- package/blueprints/application-spa/contributions/probes/designed-empty-state.mjs +72 -0
- package/blueprints/application-spa/contributions/probes/probe-utils.mjs +177 -0
- package/blueprints/application-spa/contributions/probes/route-inventory-published.mjs +185 -0
- package/blueprints/application-spa/contributions/probes/run-designed-empty-state.mjs +5 -0
- package/blueprints/application-spa/contributions/probes/run-route-inventory-published.mjs +5 -0
- package/blueprints/application-spa/contributions/probes/run-shell-nav-present.mjs +5 -0
- package/blueprints/application-spa/contributions/probes/shell-nav-present.mjs +69 -0
- package/blueprints/application-spa/contributions/tacs/tac-207-application-spa-token-adherence-probe.json +1 -1
- package/blueprints/application-spa/contributions/tacs/tac-208-application-spa-icon-adherence-probe.json +2 -2
- package/blueprints/application-spa/contributions/tacs/tac-209-application-spa-csp-styled-adherence-probe.json +3 -3
- package/blueprints/application-spa/contributions/tacs/tac-210-application-spa-external-dependency-provisioning-probe.json +2 -2
- package/blueprints/application-spa/contributions/tacs/tac-211-application-spa-core-flow-e2e-probe.json +2 -2
- package/blueprints/application-spa/contributions/user-stories/application-spa-us-1131.json +1 -1
- package/blueprints/application-spa/docs/topics.md +1 -1
- package/blueprints/delivery-ci-workflows/CHANGELOG.md +31 -11
- package/blueprints/delivery-ci-workflows/README.md +3 -3
- package/blueprints/delivery-ci-workflows/blueprint.json +3 -2
- package/blueprints/delivery-ci-workflows/contributions/adrs/adr-702-delivery-ci-workflows-strict-coverage-gate.json +2 -2
- package/blueprints/delivery-ci-workflows/contributions/probes/node-gate-entrypoint.mjs +76 -0
- package/blueprints/delivery-ci-workflows/contributions/probes/probe-utils.mjs +56 -0
- package/blueprints/delivery-ci-workflows/contributions/probes/real-account-github-actions-run-record.mjs +139 -0
- package/blueprints/delivery-ci-workflows/contributions/probes/run-node-gate-entrypoint.mjs +3 -0
- package/blueprints/delivery-ci-workflows/contributions/probes/run-real-account-github-actions-run-record.mjs +3 -0
- package/blueprints/delivery-ci-workflows/contributions/probes/run-workflow-template-shape.mjs +3 -0
- package/blueprints/delivery-ci-workflows/contributions/probes/workflow-template-shape.mjs +94 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-004.json +1 -1
- package/blueprints/delivery-ci-workflows/docs/topics.md +2 -3
- package/blueprints/deploy-cloudflare-workers/CHANGELOG.md +4 -4
- package/blueprints/deploy-cloudflare-workers/README.md +3 -3
- package/blueprints/deploy-cloudflare-workers/contributions/adrs/adr-1306-deploy-cloudflare-workers-spa-shape.json +2 -2
- package/blueprints/deploy-cloudflare-workers/docs/topics.md +2 -2
- package/blueprints/deploy-hetzner-server/CHANGELOG.md +98 -7
- package/blueprints/deploy-hetzner-server/blueprint.json +7 -6
- package/blueprints/deploy-hetzner-server/contributions/probes/cloud-init-render-lint.mjs +46 -21
- package/blueprints/deploy-hetzner-server/contributions/probes/hcloud-dry-run-mock.mjs +344 -83
- package/blueprints/deploy-hetzner-server/contributions/probes/manifest-schema-validate.mjs +211 -42
- package/blueprints/deploy-hetzner-server/contributions/probes/probe-utils.mjs +140 -22
- package/blueprints/deploy-hetzner-server/contributions/probes/real-account-cloud-init-hardened.mjs +111 -30
- package/blueprints/deploy-hetzner-server/contributions/probes/real-account-snapshot-on-demand.mjs +158 -30
- package/blueprints/deploy-hetzner-server/contributions/probes/real-account-throwaway-server-provision.mjs +154 -32
- package/blueprints/edge-cloudflare-access/CHANGELOG.md +6 -5
- package/blueprints/edge-cloudflare-access/blueprint.json +1 -1
- package/blueprints/edge-cloudflare-access/contributions/probes/admin-console-gate-surface.mjs +9 -1
- package/blueprints/edge-cloudflare-access/docs/topics.md +1 -1
- package/blueprints/edge-cloudflare-rate-limiting/CHANGELOG.md +4 -7
- package/blueprints/edge-cloudflare-rate-limiting/docs/topics.md +1 -1
- package/blueprints/edge-cloudflare-tunnel/CHANGELOG.md +4 -6
- package/blueprints/edge-cloudflare-tunnel/README.md +1 -2
- package/blueprints/edge-cloudflare-tunnel/docs/topics.md +1 -1
- package/blueprints/edge-cloudflare-turnstile/CHANGELOG.md +2 -3
- package/blueprints/edge-cloudflare-turnstile/docs/topics.md +1 -1
- package/blueprints/email-smtp-resend/CHANGELOG.md +24 -8
- package/blueprints/email-smtp-resend/README.md +2 -2
- package/blueprints/email-smtp-resend/blueprint.json +3 -2
- package/blueprints/email-smtp-resend/contributions/probes/probe-utils.mjs +50 -0
- package/blueprints/email-smtp-resend/contributions/probes/real-account-resend-send.mjs +50 -0
- package/blueprints/email-smtp-resend/contributions/probes/run-real-account-resend-send.mjs +3 -0
- package/blueprints/email-smtp-resend/contributions/probes/run-smtp-round-trip.mjs +3 -0
- package/blueprints/email-smtp-resend/contributions/probes/run-unverified-sender-refusal.mjs +3 -0
- package/blueprints/email-smtp-resend/contributions/probes/smtp-round-trip.mjs +43 -0
- package/blueprints/email-smtp-resend/contributions/probes/unverified-sender-refusal.mjs +99 -0
- package/blueprints/email-smtp-resend/docs/topics.md +2 -3
- package/blueprints/jobs-background/CHANGELOG.md +66 -5
- package/blueprints/jobs-background/README.md +2 -2
- package/blueprints/jobs-background/blueprint.json +3 -2
- package/blueprints/jobs-background/contributions/probes/apply-time-override.mjs +38 -10
- package/blueprints/jobs-background/contributions/probes/apply-time-refusal.mjs +28 -6
- package/blueprints/jobs-background/contributions/probes/event-secrecy.mjs +16 -2
- package/blueprints/jobs-background/contributions/probes/fake-clock-cron.mjs +102 -21
- package/blueprints/jobs-background/contributions/probes/probe-utils.mjs +10 -2
- package/blueprints/jobs-background/contributions/probes/retry-and-fail-real-account.mjs +355 -0
- package/blueprints/jobs-background/contributions/probes/retry-and-fail.mjs +33 -2
- package/blueprints/jobs-background/contributions/probes/run-retry-and-fail-real-account.mjs +12 -0
- package/blueprints/jobs-background/docs/topics.md +1 -1
- package/blueprints/jobs-background/guide/jobs-background.md +1 -1
- package/blueprints/messaging-queue-cloudflare/CHANGELOG.md +25 -19
- package/blueprints/messaging-queue-cloudflare/README.md +12 -12
- package/blueprints/messaging-queue-cloudflare/blueprint.json +1 -1
- package/blueprints/messaging-queue-cloudflare/contributions/adrs/adr-3001-messaging-queue-cloudflare-adapter.json +2 -2
- package/blueprints/messaging-queue-cloudflare/contributions/adrs/adr-3002-messaging-queue-cloudflare-delivery-semantics.json +1 -1
- package/blueprints/messaging-queue-cloudflare/contributions/probes/real-account-concurrency-smoke.mjs +180 -12
- package/blueprints/messaging-queue-cloudflare/contributions/requirements/messaging-queue-cloudflare-req-006.json +1 -1
- package/blueprints/messaging-queue-cloudflare/docs/topics.md +2 -2
- package/blueprints/messaging-queue-cloudflare/guide/messaging-queue-cloudflare.md +2 -2
- package/blueprints/object-storage-s3/CHANGELOG.md +75 -4
- package/blueprints/object-storage-s3/blueprint.json +3 -2
- package/blueprints/object-storage-s3/contributions/probes/event-secrecy.mjs +73 -12
- package/blueprints/object-storage-s3/contributions/probes/facade-round-trip.mjs +55 -7
- package/blueprints/object-storage-s3/contributions/probes/hetzner-object-storage-round-trip.mjs +102 -18
- package/blueprints/object-storage-s3/contributions/probes/multipart-upload.mjs +105 -36
- package/blueprints/object-storage-s3/contributions/probes/presigned-url.mjs +104 -55
- package/blueprints/object-storage-s3/contributions/probes/probe-utils.mjs +129 -7
- package/blueprints/object-storage-s3/contributions/probes/put-get-round-trip.mjs +80 -18
- package/blueprints/object-storage-s3/contributions/probes/r2-real-account-smoke.mjs +291 -37
- package/blueprints/object-storage-s3/contributions/probes/run-event-secrecy.mjs +8 -2
- package/blueprints/object-storage-s3/contributions/probes/run-facade-round-trip.mjs +5 -5
- package/blueprints/object-storage-s3/contributions/probes/run-hetzner-object-storage-round-trip.mjs +8 -5
- package/blueprints/object-storage-s3/contributions/probes/run-multipart-upload.mjs +8 -2
- package/blueprints/object-storage-s3/contributions/probes/run-presigned-url.mjs +8 -2
- package/blueprints/object-storage-s3/contributions/probes/run-put-get-round-trip.mjs +6 -3
- package/blueprints/object-storage-s3/contributions/probes/run-r2-real-account-smoke.mjs +10 -2
- package/blueprints/object-storage-s3/contributions/requirements/object-storage-s3-req-006.json +1 -1
- package/blueprints/object-storage-s3/docs/topics.md +1 -1
- package/blueprints/object-storage-s3/guide/object-storage-s3.md +1 -1
- package/blueprints/observability-essentials/CHANGELOG.md +23 -7
- package/blueprints/observability-essentials/README.md +2 -2
- package/blueprints/observability-essentials/assets/probe-samples/probe-response-baseline.v2.json +1 -1
- package/blueprints/observability-essentials/blueprint.json +3 -2
- package/blueprints/observability-essentials/contributions/probes/liveness-probe.mjs +84 -0
- package/blueprints/observability-essentials/contributions/probes/metrics-endpoint.mjs +96 -0
- package/blueprints/observability-essentials/contributions/probes/probe-utils.mjs +52 -0
- package/blueprints/observability-essentials/contributions/probes/readiness-probe.mjs +171 -0
- package/blueprints/observability-essentials/contributions/probes/run-liveness-probe.mjs +3 -0
- package/blueprints/observability-essentials/contributions/probes/run-metrics-endpoint.mjs +3 -0
- package/blueprints/observability-essentials/contributions/probes/run-readiness-probe.mjs +3 -0
- package/blueprints/observability-essentials/docs/topics.md +2 -3
- package/blueprints/observability-logging/CHANGELOG.md +29 -4
- package/blueprints/observability-logging/README.md +2 -2
- package/blueprints/observability-logging/blueprint.json +3 -2
- package/blueprints/observability-logging/contributions/probes/correlation-id-flow.mjs +173 -0
- package/blueprints/observability-logging/contributions/probes/line-shape-and-fields.mjs +82 -0
- package/blueprints/observability-logging/contributions/probes/probe-utils.mjs +47 -0
- package/blueprints/observability-logging/contributions/probes/redaction-boundary.mjs +130 -0
- package/blueprints/observability-logging/contributions/probes/run-correlation-id-flow.mjs +3 -0
- package/blueprints/observability-logging/contributions/probes/run-line-shape-and-fields.mjs +3 -0
- package/blueprints/observability-logging/contributions/probes/run-redaction-boundary.mjs +3 -0
- package/blueprints/observability-probe-endpoints/CHANGELOG.md +23 -8
- package/blueprints/observability-probe-endpoints/README.md +3 -3
- package/blueprints/observability-probe-endpoints/blueprint.json +3 -2
- package/blueprints/observability-probe-endpoints/contributions/probes/kubernetes-startup-enabled.mjs +80 -0
- package/blueprints/observability-probe-endpoints/contributions/probes/partial-profile-refusal.mjs +101 -0
- package/blueprints/observability-probe-endpoints/contributions/probes/probe-utils.mjs +49 -0
- package/blueprints/observability-probe-endpoints/contributions/probes/profile-boot-materialisation.mjs +69 -0
- package/blueprints/observability-probe-endpoints/contributions/probes/run-kubernetes-startup-enabled.mjs +3 -0
- package/blueprints/observability-probe-endpoints/contributions/probes/run-partial-profile-refusal.mjs +3 -0
- package/blueprints/observability-probe-endpoints/contributions/probes/run-profile-boot-materialisation.mjs +3 -0
- package/blueprints/observability-probe-endpoints/docs/topics.md +2 -2
- package/blueprints/persistence-data-d1/CHANGELOG.md +16 -3
- package/blueprints/persistence-data-d1/README.md +2 -2
- package/blueprints/persistence-data-d1/blueprint.json +3 -2
- package/blueprints/persistence-data-d1/contributions/adrs/adr-1404-persistence-data-d1-store-boundary.json +1 -1
- package/blueprints/persistence-data-d1/contributions/probes/facade-round-trip.mjs +140 -0
- package/blueprints/persistence-data-d1/contributions/probes/migrations-forward-only.mjs +66 -0
- package/blueprints/persistence-data-d1/contributions/probes/probe-utils.mjs +51 -0
- package/blueprints/persistence-data-d1/contributions/probes/real-account-d1-round-trip.mjs +133 -0
- package/blueprints/persistence-data-d1/contributions/probes/run-facade-round-trip.mjs +3 -0
- package/blueprints/persistence-data-d1/contributions/probes/run-migrations-forward-only.mjs +3 -0
- package/blueprints/persistence-data-d1/contributions/probes/run-real-account-d1-round-trip.mjs +3 -0
- package/blueprints/persistence-data-d1/docs/topics.md +2 -3
- package/blueprints/persistence-data-postgres/CHANGELOG.md +84 -4
- package/blueprints/persistence-data-postgres/README.md +2 -2
- package/blueprints/persistence-data-postgres/blueprint.json +3 -2
- package/blueprints/persistence-data-postgres/contributions/adrs/adr-2801-persistence-data-postgres-driver.json +1 -1
- package/blueprints/persistence-data-postgres/contributions/probes/facade-round-trip.mjs +87 -12
- package/blueprints/persistence-data-postgres/contributions/probes/migration-apply.mjs +245 -19
- package/blueprints/persistence-data-postgres/contributions/probes/pool-posture-smoke.mjs +109 -26
- package/blueprints/persistence-data-postgres/contributions/probes/prepared-statement-scan.mjs +151 -38
- package/blueprints/persistence-data-postgres/contributions/probes/probe-utils.mjs +24 -6
- package/blueprints/persistence-data-postgres/contributions/probes/recovery-restore-round-trip.mjs +261 -48
- package/blueprints/persistence-data-postgres/contributions/probes/transaction-atomicity.mjs +76 -20
- package/blueprints/persistence-data-postgres/contributions/requirements/persistence-data-postgres-req-003.json +1 -1
- package/blueprints/persistence-data-postgres/docs/topics.md +1 -1
- package/blueprints/persistence-data-sqlite/CHANGELOG.md +16 -2
- package/blueprints/persistence-data-sqlite/README.md +2 -2
- package/blueprints/persistence-data-sqlite/blueprint.json +3 -2
- package/blueprints/persistence-data-sqlite/contributions/probes/boot-open-migrate.mjs +90 -0
- package/blueprints/persistence-data-sqlite/contributions/probes/facade-round-trip.mjs +82 -0
- package/blueprints/persistence-data-sqlite/contributions/probes/probe-utils.mjs +83 -0
- package/blueprints/persistence-data-sqlite/contributions/probes/run-boot-open-migrate.mjs +5 -0
- package/blueprints/persistence-data-sqlite/contributions/probes/run-facade-round-trip.mjs +5 -0
- package/blueprints/persistence-data-sqlite/contributions/probes/run-wal-checkpoint.mjs +5 -0
- package/blueprints/persistence-data-sqlite/contributions/probes/wal-checkpoint.mjs +137 -0
- package/blueprints/persistence-data-sqlite/docs/topics.md +2 -3
- package/blueprints/platform-cloudflare-cron-triggers/CHANGELOG.md +5 -5
- package/blueprints/platform-cloudflare-cron-triggers/README.md +2 -3
- package/blueprints/platform-cloudflare-cron-triggers/docs/topics.md +1 -1
- package/blueprints/platform-cloudflare-durable-objects/CHANGELOG.md +6 -10
- package/blueprints/platform-cloudflare-durable-objects/docs/topics.md +1 -1
- package/blueprints/platform-cloudflare-kv/CHANGELOG.md +16 -12
- package/blueprints/platform-cloudflare-kv/README.md +2 -4
- package/blueprints/platform-cloudflare-kv/blueprint.json +2 -2
- package/blueprints/platform-cloudflare-kv/docs/topics.md +1 -1
- package/blueprints/platform-docker-compose-host/CHANGELOG.md +92 -5
- package/blueprints/platform-docker-compose-host/blueprint.json +2 -2
- package/blueprints/platform-docker-compose-host/contributions/probes/caddyfile-validate.mjs +116 -29
- package/blueprints/platform-docker-compose-host/contributions/probes/compose-config-lint.mjs +181 -109
- package/blueprints/platform-docker-compose-host/contributions/probes/probe-utils.mjs +146 -26
- package/blueprints/platform-docker-compose-host/contributions/probes/real-account-minimal-stack-up.mjs +238 -24
- package/blueprints/platform-docker-compose-host/contributions/probes/real-account-reload-burst.mjs +207 -21
- package/blueprints/platform-docker-compose-host/contributions/probes/secrets-as-files-scan.mjs +246 -42
- package/blueprints/platform-docker-compose-host/docs/topics.md +1 -1
- package/blueprints/security-auth-clerk/CHANGELOG.md +18 -6
- package/blueprints/security-auth-clerk/README.md +3 -3
- package/blueprints/security-auth-clerk/assets/wiring/workers-wrangler-toml-shape.md +1 -1
- package/blueprints/security-auth-clerk/blueprint.json +3 -2
- package/blueprints/security-auth-clerk/contributions/probes/hosted-identity-ui-config.mjs +61 -0
- package/blueprints/security-auth-clerk/contributions/probes/probe-utils.mjs +188 -0
- package/blueprints/security-auth-clerk/contributions/probes/real-account-principal-directory-round-trip.mjs +216 -0
- package/blueprints/security-auth-clerk/contributions/probes/real-account-session-inventory.mjs +260 -0
- package/blueprints/security-auth-clerk/contributions/probes/role-model-adapter.mjs +53 -0
- package/blueprints/security-auth-clerk/contributions/probes/run-hosted-identity-ui-config.mjs +4 -0
- package/blueprints/security-auth-clerk/contributions/probes/run-real-account-principal-directory-round-trip.mjs +4 -0
- package/blueprints/security-auth-clerk/contributions/probes/run-real-account-session-inventory.mjs +4 -0
- package/blueprints/security-auth-clerk/contributions/probes/run-role-model-adapter.mjs +4 -0
- package/blueprints/security-auth-clerk/docs/topics.md +2 -3
- package/blueprints/security-auth-keycloak/CHANGELOG.md +18 -7
- package/blueprints/security-auth-keycloak/README.md +2 -2
- package/blueprints/security-auth-keycloak/blueprint.json +3 -2
- package/blueprints/security-auth-keycloak/contributions/probes/discovery-shape.mjs +68 -0
- package/blueprints/security-auth-keycloak/contributions/probes/introspection-shape.mjs +88 -0
- package/blueprints/security-auth-keycloak/contributions/probes/jwt-verifier-shape.mjs +86 -0
- package/blueprints/security-auth-keycloak/contributions/probes/probe-utils.mjs +180 -0
- package/blueprints/security-auth-keycloak/contributions/probes/real-account-realm-round-trip.mjs +91 -0
- package/blueprints/security-auth-keycloak/contributions/probes/role-adapter-shape.mjs +69 -0
- package/blueprints/security-auth-keycloak/contributions/probes/run-discovery-shape.mjs +4 -0
- package/blueprints/security-auth-keycloak/contributions/probes/run-introspection-shape.mjs +4 -0
- package/blueprints/security-auth-keycloak/contributions/probes/run-jwt-verifier-shape.mjs +4 -0
- package/blueprints/security-auth-keycloak/contributions/probes/run-real-account-realm-round-trip.mjs +4 -0
- package/blueprints/security-auth-keycloak/contributions/probes/run-role-adapter-shape.mjs +4 -0
- package/blueprints/security-auth-keycloak/docs/topics.md +1 -2
- package/blueprints/security-auth-magic-link/CHANGELOG.md +15 -4
- package/blueprints/security-auth-magic-link/README.md +2 -2
- package/blueprints/security-auth-magic-link/assets/principal-registry-samples/single-address.md +1 -1
- package/blueprints/security-auth-magic-link/blueprint.json +3 -2
- package/blueprints/security-auth-magic-link/contributions/probes/probe-utils.mjs +174 -0
- package/blueprints/security-auth-magic-link/contributions/probes/real-account-magic-link-send.mjs +139 -0
- package/blueprints/security-auth-magic-link/contributions/probes/run-real-account-magic-link-send.mjs +4 -0
- package/blueprints/security-auth-magic-link/contributions/probes/run-token-entropy-shape.mjs +4 -0
- package/blueprints/security-auth-magic-link/contributions/probes/run-token-issue-verify.mjs +4 -0
- package/blueprints/security-auth-magic-link/contributions/probes/token-entropy-shape.mjs +60 -0
- package/blueprints/security-auth-magic-link/contributions/probes/token-issue-verify.mjs +90 -0
- package/blueprints/security-auth-magic-link/docs/topics.md +1 -1
- package/blueprints/security-auth-oauth2/CHANGELOG.md +16 -5
- package/blueprints/security-auth-oauth2/README.md +2 -2
- package/blueprints/security-auth-oauth2/blueprint.json +3 -2
- package/blueprints/security-auth-oauth2/contributions/probes/authorisation-code-flow-shape.mjs +259 -0
- package/blueprints/security-auth-oauth2/contributions/probes/pkce-challenge-shape.mjs +58 -0
- package/blueprints/security-auth-oauth2/contributions/probes/probe-utils.mjs +190 -0
- package/blueprints/security-auth-oauth2/contributions/probes/provider-adapter-shape.mjs +64 -0
- package/blueprints/security-auth-oauth2/contributions/probes/real-account-authorisation-code-flow.mjs +93 -0
- package/blueprints/security-auth-oauth2/contributions/probes/run-authorisation-code-flow-shape.mjs +4 -0
- package/blueprints/security-auth-oauth2/contributions/probes/run-pkce-challenge-shape.mjs +4 -0
- package/blueprints/security-auth-oauth2/contributions/probes/run-provider-adapter-shape.mjs +4 -0
- package/blueprints/security-auth-oauth2/contributions/probes/run-real-account-authorisation-code-flow.mjs +4 -0
- package/blueprints/security-auth-oauth2/contributions/probes/run-session-bridge-shape.mjs +4 -0
- package/blueprints/security-auth-oauth2/contributions/probes/session-bridge-shape.mjs +78 -0
- package/blueprints/security-auth-oauth2/docs/topics.md +1 -2
- package/blueprints/security-secrets-management/CHANGELOG.md +15 -4
- package/blueprints/security-secrets-management/README.md +2 -2
- package/blueprints/security-secrets-management/blueprint.json +3 -2
- package/blueprints/security-secrets-management/contributions/probes/add-recipient-rotation.mjs +107 -0
- package/blueprints/security-secrets-management/contributions/probes/encrypt-decrypt-round-trip.mjs +122 -0
- package/blueprints/security-secrets-management/contributions/probes/key-rotation.mjs +93 -0
- package/blueprints/security-secrets-management/contributions/probes/mismatched-key-refusal.mjs +74 -0
- package/blueprints/security-secrets-management/contributions/probes/probe-utils.mjs +214 -0
- package/blueprints/security-secrets-management/contributions/probes/run-add-recipient-rotation.mjs +4 -0
- package/blueprints/security-secrets-management/contributions/probes/run-encrypt-decrypt-round-trip.mjs +4 -0
- package/blueprints/security-secrets-management/contributions/probes/run-key-rotation.mjs +4 -0
- package/blueprints/security-secrets-management/contributions/probes/run-mismatched-key-refusal.mjs +4 -0
- package/blueprints/security-secrets-management/docs/topics.md +2 -3
- package/fixtures/canary-manifest.json +9 -9
- package/guidance/harness-template.md +25 -0
- package/guidance/managed/agent-instructions-block.hash +1 -1
- package/guidance/managed/agent-instructions-block.md +25 -0
- package/package.json +2 -2
- package/rcf/adrs/adr-4101-feedback-store-gitignored.json +20 -0
- package/rcf/adrs/adr-4102-fingerprint-and-labels.json +25 -0
- package/rcf/adrs/adr-4103-redaction-rule-based.json +20 -0
- package/rcf/adrs/adr-4104-destination-on-library.json +25 -0
- package/rcf/adrs/adr-4105-gh-ambient-auth.json +20 -0
- package/rcf/adrs/adr-4106-bundled-entries-terminal.json +20 -0
- package/rcf/adrs/adr-4107-stop-is-ask-event.json +30 -0
- package/rcf/adrs/adr-4108-hooks-in-committed-config.json +25 -0
- package/rcf/adrs/adr-4109-referee-validate-adr-crosslinks.json +25 -0
- package/rcf/adrs/adr-4110-referee-coverage-strict-by-default.json +25 -0
- package/rcf/adrs/adr-4111-referee-finalise-refusals.json +20 -0
- package/rcf/adrs/adr-4112-referee-finalise-verified-invariant.json +30 -0
- package/rcf/code-nodes/cn-600.json +45 -0
- package/rcf/code-nodes/cn-601.json +18 -0
- package/rcf/code-nodes/cn-602.json +15 -0
- package/rcf/code-nodes/cn-603.json +12 -0
- package/rcf/code-nodes/cn-604.json +14 -0
- package/rcf/code-nodes/cn-605.json +17 -0
- package/rcf/code-nodes/cn-606.json +12 -0
- package/rcf/code-nodes/cn-607.json +12 -0
- package/rcf/code-nodes/cn-608.json +14 -0
- package/rcf/code-nodes/cn-609.json +17 -0
- package/rcf/code-nodes/cn-620.json +18 -0
- package/rcf/code-nodes/cn-621.json +14 -0
- package/rcf/code-nodes/cn-622.json +15 -0
- package/rcf/code-nodes/cn-623.json +16 -0
- package/rcf/code-nodes/cn-624.json +14 -0
- package/rcf/code-nodes/cn-625.json +15 -0
- package/rcf/code-nodes/cn-626.json +13 -0
- package/rcf/code-nodes/cn-627.json +13 -0
- package/rcf/code-nodes/cn-628.json +14 -0
- package/rcf/code-nodes/cn-629.json +12 -0
- package/rcf/code-nodes/cn-630.json +12 -0
- package/rcf/code-nodes/cn-631.json +13 -0
- package/rcf/code-nodes/cn-632.json +13 -0
- package/rcf/fbs/fbs-180.json +45 -0
- package/rcf/fbs/fbs-181.json +45 -0
- package/rcf/fbs/fbs-182.json +42 -0
- package/rcf/fbs/fbs-183.json +50 -0
- package/rcf/fbs/fbs-184.json +49 -0
- package/rcf/fbs/fbs-185.json +34 -0
- package/rcf/fbs/fbs-186.json +16 -0
- package/rcf/fbs/fbs-187.json +17 -0
- package/rcf/fbs/fbs-188.json +17 -0
- package/rcf/fbs/fbs-189.json +17 -0
- package/rcf/requirements/req-155.json +13 -0
- package/rcf/requirements/req-156.json +13 -0
- package/rcf/requirements/req-157.json +13 -0
- package/rcf/requirements/req-158.json +13 -0
- package/rcf/requirements/req-159.json +13 -0
- package/rcf/requirements/req-160.json +13 -0
- package/rcf/requirements/req-161.json +13 -0
- package/rcf/requirements/req-162.json +13 -0
- package/rcf/requirements/req-163.json +13 -0
- package/rcf/requirements/req-164.json +13 -0
- package/rcf/requirements/req-165.json +13 -0
- package/rcf/tacs/tac-4101-feedback-cli.json +77 -0
- package/rcf/tacs/tac-4102-feedback-store.json +50 -0
- package/rcf/tacs/tac-4103-feedback-redact.json +39 -0
- package/rcf/tacs/tac-4104-feedback-fingerprint.json +33 -0
- package/rcf/tacs/tac-4105-feedback-render.json +45 -0
- package/rcf/tacs/tac-4106-feedback-destination.json +39 -0
- package/rcf/tacs/tac-4107-feedback-gh.json +43 -0
- package/rcf/tacs/tac-4108-feedback-hook.json +46 -0
- package/rcf/tacs/tac-4109-referee-validate-adr-crosslinks.json +20 -0
- package/rcf/tacs/tac-4110-referee-coverage-mode-selector.json +21 -0
- package/rcf/tacs/tac-4111-referee-finalise-per-ac-refusals.json +21 -0
- package/rcf/tacs/tac-4112-referee-finalise-verified-invariant.json +22 -0
- package/rcf/test-suites/ts-140.json +9 -9
- package/rcf/test-suites/ts-150.json +9 -9
- package/rcf/test-suites/ts-185.json +54 -0
- package/rcf/test-suites/ts-186.json +58 -0
- package/rcf/test-suites/ts-187.json +44 -0
- package/rcf/test-suites/ts-188.json +166 -0
- package/rcf/test-suites/ts-189.json +38 -0
- package/rcf/test-suites/ts-190.json +30 -0
- package/rcf/test-suites/ts-191.json +51 -0
- package/rcf/test-suites/ts-192.json +37 -0
- package/rcf/test-suites/ts-193.json +60 -0
- package/rcf/test-suites/ts-194.json +67 -0
- package/rcf/test-suites/ts-195.json +52 -0
- package/rcf/test-suites/ts-196.json +58 -0
- package/rcf/test-suites/ts-197.json +66 -0
- package/rcf/test-suites/ts-198.json +87 -0
- package/rcf/test-suites/ts-199.json +51 -0
- package/rcf/test-suites/ts-200.json +30 -0
- package/rcf/test-suites/ts-201.json +38 -0
- package/rcf/test-suites/ts-202.json +38 -0
- package/rcf/test-suites/ts-203.json +38 -0
- package/rcf/user-stories/us-15501.json +60 -0
- package/rcf/user-stories/us-15502.json +33 -0
- package/rcf/user-stories/us-15503.json +33 -0
- package/rcf/user-stories/us-15601.json +51 -0
- package/rcf/user-stories/us-15701.json +42 -0
- package/rcf/user-stories/us-15801.json +42 -0
- package/rcf/user-stories/us-15802.json +33 -0
- package/rcf/user-stories/us-15901.json +33 -0
- package/rcf/user-stories/us-15902.json +51 -0
- package/rcf/user-stories/us-16001.json +51 -0
- package/rcf/user-stories/us-16101.json +42 -0
- package/rcf/user-stories/us-16102.json +33 -0
- package/rcf/user-stories/us-16103.json +42 -0
- package/rcf/user-stories/us-16104.json +42 -0
- package/rcf/user-stories/us-16201.json +33 -0
- package/rcf/user-stories/us-16301.json +36 -0
- package/rcf/user-stories/us-16401.json +45 -0
- package/rcf/user-stories/us-16501.json +45 -0
- package/rcf/user-stories/us-16502.json +45 -0
- package/releases/releases.yaml +11 -1
- package/src/blueprint/library-loader.js +48 -0
- package/src/blueprint/library-registry.js +13 -0
- package/src/browser-verify/pack-runner.js +7 -12
- package/src/build/mark.js +1 -1
- package/src/cli/blueprint-library.js +149 -0
- package/src/cli/coverage.js +49 -24
- package/src/cli/doctor.js +195 -7
- package/src/cli/feedback.js +2204 -0
- package/src/cli/finalise.js +158 -1
- package/src/cli/help.js +6 -0
- package/src/cli/init.js +63 -0
- package/src/cli/update.js +21 -1
- package/src/core/store/walker.js +33 -0
- package/src/core/store/writer.js +23 -0
- package/src/feedback/destination.js +361 -0
- package/src/feedback/fingerprint.js +132 -0
- package/src/feedback/gh.js +376 -0
- package/src/feedback/hook.js +183 -0
- package/src/feedback/labels.js +87 -0
- package/src/feedback/redact-allowlist.json +14 -0
- package/src/feedback/redact.js +1108 -0
- package/src/feedback/render.js +267 -0
- package/src/feedback/store.js +604 -0
- package/src/finalise/index.js +4 -0
- package/src/finalise/ingest.js +61 -0
- package/src/finalise/ship-without-eval.js +19 -7
- package/src/mcp/tools.js +8 -2
- package/src/setup/feedback-hooks.js +397 -0
- package/src/setup/managed-gitignore.js +3 -0
- package/src/view/html-page.js +5 -5
- package/src/view/style.css +2 -2
|
@@ -0,0 +1,1108 @@
|
|
|
1
|
+
// Feedback redactor (TAC-4103-feedback-redact, ADR-4103). Slice 2
|
|
2
|
+
// (FBS-181) ships the rules of design section 5 (eight in slice 2, plus rule 9 url-credential added round 4 for R3b), applied to any
|
|
3
|
+
// string that would leave the machine (issue title, body, evidence
|
|
4
|
+
// pointers). Pure, deterministic, no network, no LLM.
|
|
5
|
+
//
|
|
6
|
+
// Every replacement lands on a ledger of { rule, before, after, count }
|
|
7
|
+
// entries so `rcf feedback preview` can show the operator exactly what
|
|
8
|
+
// was stripped. Rule 8 is the second-pass refusal: if the redacted
|
|
9
|
+
// output still matches rule 5 (a secret-shaped run of characters), we
|
|
10
|
+
// return residual { line, snippet } markers so the caller can refuse
|
|
11
|
+
// the entry rather than send it.
|
|
12
|
+
//
|
|
13
|
+
// The rules, in canonical order (also the ledger's rule-name
|
|
14
|
+
// vocabulary): `project-root`, `absolute-path`, `email`, `hostname`,
|
|
15
|
+
// `private-ip`, `secret-token`, `operator-identity`, `size-shape`,
|
|
16
|
+
// `url-credential` (design amendment R3b, added fix round 4).
|
|
17
|
+
// Rule 6 in the design (operator identity) is applied AFTER rule 5
|
|
18
|
+
// (secret-token) so a name that looks like a random token is redacted
|
|
19
|
+
// as a token first; both are load-bearing and this ordering keeps the
|
|
20
|
+
// secret-shape check the last line of defence, which is what the
|
|
21
|
+
// residual-match refusal in rule 8 leans on.
|
|
22
|
+
|
|
23
|
+
import { readFileSync } from 'node:fs';
|
|
24
|
+
import { dirname, resolve } from 'node:path';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
|
|
27
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
const BUNDLED_ALLOWLIST = JSON.parse(
|
|
29
|
+
readFileSync(resolve(here, 'redact-allowlist.json'), 'utf8'),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
/** Body size cap (bytes). Mirrors src/cli/feedback.js:BODY_CAP_BYTES. */
|
|
33
|
+
export const BODY_CAP_BYTES = 8 * 1024;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Open secret-key vocabulary (design amendment R3a). A key/value pair
|
|
37
|
+
* is treated as a secret when the key, right-bounded at a word edge so
|
|
38
|
+
* `keyword` and `authors` do not fold, matches any stem below. False
|
|
39
|
+
* positives (`mypw=1`) are acceptable per design; false negatives are
|
|
40
|
+
* not. Consumed in exactly three places so the first pass, the residual
|
|
41
|
+
* pass and the whole-text safeguard scan share ONE vocabulary:
|
|
42
|
+
*
|
|
43
|
+
* 1. `kv-secret` early-guard regex in `redact()` (before path pass).
|
|
44
|
+
* 2. `kv-secret` entry in `secretPatterns()` (rule 5 primary pass).
|
|
45
|
+
* 3. `matchesSecretKeyLoose()` used by the rule-9 URL-credential
|
|
46
|
+
* scanner for query-parameter names.
|
|
47
|
+
*
|
|
48
|
+
* The list intentionally covers the full shortname surface the HQ gate
|
|
49
|
+
* on PR #221 (F-P0-A) named as leaking: `pw`, `pwd`, `pass`, `key`,
|
|
50
|
+
* `auth`, plus the long list from R3a. Word-boundary detail:
|
|
51
|
+
* `stem\b` at the right side only. That fails inside `keyword`
|
|
52
|
+
* (`y` -> `w`, no boundary) but succeeds inside `dbpassword`
|
|
53
|
+
* (`d` -> `=`, boundary), matching the round 3 gate probes.
|
|
54
|
+
*/
|
|
55
|
+
export const SECRET_KEY_VOCABULARY = Object.freeze([
|
|
56
|
+
// Long forms first so alternation prefers them (does not change
|
|
57
|
+
// correctness under `\b`, but keeps the ledger `before` samples
|
|
58
|
+
// stable for the operator).
|
|
59
|
+
//
|
|
60
|
+
// `authorization` is intentionally NOT in this list. The
|
|
61
|
+
// `Authorization:` header shape is handled by a line-anchored
|
|
62
|
+
// pattern in `secretPatterns()` that folds the WHOLE header line
|
|
63
|
+
// (scheme + credential + any tail), which is what the operator
|
|
64
|
+
// wants for the header case (F-slice-2-02). Adding `authorization`
|
|
65
|
+
// to the kv vocabulary would preempt that pattern: the guard would
|
|
66
|
+
// fold `Authorization: Basic` alone and leave the credential.
|
|
67
|
+
'passphrase',
|
|
68
|
+
'clientsecret',
|
|
69
|
+
'client_secret',
|
|
70
|
+
'client-secret',
|
|
71
|
+
'privatekey',
|
|
72
|
+
'private_key',
|
|
73
|
+
'private-key',
|
|
74
|
+
'accesskey',
|
|
75
|
+
'access_key',
|
|
76
|
+
'access-key',
|
|
77
|
+
'apikey',
|
|
78
|
+
'api_key',
|
|
79
|
+
'api-key',
|
|
80
|
+
'credential',
|
|
81
|
+
'credentials',
|
|
82
|
+
'signature',
|
|
83
|
+
'password',
|
|
84
|
+
'passwd',
|
|
85
|
+
'session',
|
|
86
|
+
'cookie',
|
|
87
|
+
'secret',
|
|
88
|
+
'bearer',
|
|
89
|
+
'creds',
|
|
90
|
+
'token',
|
|
91
|
+
'salt',
|
|
92
|
+
'cred',
|
|
93
|
+
'pass',
|
|
94
|
+
'auth',
|
|
95
|
+
'pwd',
|
|
96
|
+
'sig',
|
|
97
|
+
'pw',
|
|
98
|
+
'key',
|
|
99
|
+
// `hash` is intentionally NOT in this vocabulary. Per design R3a
|
|
100
|
+
// the `hash` stem is only a secret when the value is 16+ chars.
|
|
101
|
+
// The specific `hash`-with-long-value pattern is a separate regex
|
|
102
|
+
// handled inside `secretPatterns()` so a bare `hash=abc` (short
|
|
103
|
+
// digest displayed for humans) does not fold.
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
// Build the alternation source ONCE at module load. Regex-escapes each
|
|
107
|
+
// stem (currently none carry regex metacharacters, but the escape keeps
|
|
108
|
+
// the constant safe if vocabulary is extended by extension in future).
|
|
109
|
+
const SECRET_KEY_ALTERNATION = SECRET_KEY_VOCABULARY.map(escapeReStatic).join('|');
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Regex source for the vocabulary above, right-bounded with `\b` so
|
|
113
|
+
* `keyword` and `authors` do not fold but `dbpassword` still does.
|
|
114
|
+
* Callers wrap this in a larger regex with their own value pattern.
|
|
115
|
+
*/
|
|
116
|
+
export function secretKeyRegexSource() {
|
|
117
|
+
return `(?:${SECRET_KEY_ALTERNATION})\\b`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Value-shape character class (design amendment R4). Common secret /
|
|
122
|
+
* URL-safe alphabet plus punctuation that turns up in passwords. Used
|
|
123
|
+
* by the R4 separator scanner to bound the value token; the length
|
|
124
|
+
* gate and mixed-classes check are applied by `looksLikeSecretValue`.
|
|
125
|
+
*/
|
|
126
|
+
const R4_VALUE_CHAR_CLASS = `[A-Za-z0-9._~+/=@!#$%^&*?\\-]`;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* R4 separator: one of the prose bridges `is` / `was` / `set to`
|
|
130
|
+
* bounded by whitespace, OR up to 40 units of a non-word non-newline
|
|
131
|
+
* character or an HTML numeric entity (`=`, `=`). Ordering
|
|
132
|
+
* matters: the entity alternative is tried first so the digits inside
|
|
133
|
+
* (`61`) do not stop the run at the leading `&#`. The prose bridge
|
|
134
|
+
* captures markdown-table pipes, bullet-list dashes, arrows,
|
|
135
|
+
* whitespace, tabs and `:` / `=` alongside the R3a shape.
|
|
136
|
+
*/
|
|
137
|
+
const R4_SEPARATOR_SRC =
|
|
138
|
+
'(?:\\s+(?:is|was|set\\s+to)\\s+|(?:&#(?:\\d+|x[0-9a-fA-F]+);|[^\\w\\n]){1,40})';
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* R4 combined regex source: `<vocab-key>\b<separator><value-shape>`.
|
|
142
|
+
* The value shape is bounded by the R4 character class so `\S{8,}`
|
|
143
|
+
* greed does not consume adjacent structural punctuation (e.g. the
|
|
144
|
+
* closing pipe of a markdown table cell). Post-match, callers apply
|
|
145
|
+
* `looksLikeSecretValue` to gate on 2+ character classes so plain
|
|
146
|
+
* English words (`required`, `enabled`) do not fold.
|
|
147
|
+
*/
|
|
148
|
+
const R4_REGEX_SRC =
|
|
149
|
+
`\\b(?:${SECRET_KEY_ALTERNATION})\\b${R4_SEPARATOR_SRC}(${R4_VALUE_CHAR_CLASS}{8,})`;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Round 5 (design amendment R4): does a candidate value shape look
|
|
153
|
+
* like a secret? Guards R4 against over-redaction of ordinary prose:
|
|
154
|
+
* requires 8+ chars AND at least two character classes among {upper,
|
|
155
|
+
* lower, digit, symbol}. Rejects single-class runs like `required`
|
|
156
|
+
* (all lower) and `12345678` (all digits) so a sentence like
|
|
157
|
+
* `the password is required` does not fold the trailing word.
|
|
158
|
+
*
|
|
159
|
+
* @param {string} value
|
|
160
|
+
*/
|
|
161
|
+
function looksLikeSecretValue(value) {
|
|
162
|
+
if (typeof value !== 'string') return false;
|
|
163
|
+
if (value.length < 8) return false;
|
|
164
|
+
let classes = 0;
|
|
165
|
+
if (/[A-Z]/.test(value)) classes += 1;
|
|
166
|
+
if (/[a-z]/.test(value)) classes += 1;
|
|
167
|
+
if (/\d/.test(value)) classes += 1;
|
|
168
|
+
if (/[^A-Za-z0-9]/.test(value)) classes += 1;
|
|
169
|
+
return classes >= 2;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Round 5 (design amendment R4): scan `text` for vocabulary keys
|
|
174
|
+
* separated from a value-shape token by any separator. Returns the
|
|
175
|
+
* match ranges the redactor should fold (and the residual scan should
|
|
176
|
+
* flag). Skips matches whose value fails `looksLikeSecretValue` so
|
|
177
|
+
* ordinary prose survives.
|
|
178
|
+
*
|
|
179
|
+
* @param {string} text
|
|
180
|
+
* @returns {Array<{start: number, end: number, match: string, value: string}>}
|
|
181
|
+
*/
|
|
182
|
+
function scanR4Leaks(text) {
|
|
183
|
+
const re = new RegExp(R4_REGEX_SRC, 'gi');
|
|
184
|
+
const out = [];
|
|
185
|
+
let m;
|
|
186
|
+
while ((m = re.exec(text)) !== null) {
|
|
187
|
+
const value = m[1];
|
|
188
|
+
if (!looksLikeSecretValue(value)) {
|
|
189
|
+
// Guard against zero-width infinite loops on future pattern
|
|
190
|
+
// changes; the fixed regex above cannot match zero-length.
|
|
191
|
+
if (re.lastIndex === m.index) re.lastIndex += 1;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
out.push({ start: m.index, end: m.index + m[0].length, match: m[0], value });
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Case-insensitive test: does `name`, after lower-casing and stripping
|
|
201
|
+
* separators, MATCH any vocabulary stem? Used by the rule-9 URL-
|
|
202
|
+
* credential scanner to decide whether a URL query parameter name
|
|
203
|
+
* (`?apiKey=...`, `?client-secret=...`) is secret-carrying. Distinct
|
|
204
|
+
* from the regex above because query-parameter names are already
|
|
205
|
+
* isolated by the URL parser, so we do not need a boundary check.
|
|
206
|
+
*
|
|
207
|
+
* @param {string} name
|
|
208
|
+
*/
|
|
209
|
+
export function matchesSecretKeyLoose(name) {
|
|
210
|
+
if (typeof name !== 'string' || name.length === 0) return false;
|
|
211
|
+
const norm = name.toLowerCase().replace(/[_\-\s]/g, '');
|
|
212
|
+
for (const stem of SECRET_KEY_VOCABULARY) {
|
|
213
|
+
if (norm === stem.replace(/[_\-]/g, '')) return true;
|
|
214
|
+
}
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Known webhook-style hosts where the credential lives in the URL path
|
|
220
|
+
* (design amendment R3b). Format: `host[/pathPrefix]`. When a URL's
|
|
221
|
+
* host matches (exact or as a suffix under a subdomain), any path
|
|
222
|
+
* segments AFTER the recognisable prefix are folded to
|
|
223
|
+
* `<url-credential>` because the token IS the path.
|
|
224
|
+
*/
|
|
225
|
+
const WEBHOOK_HOST_PATH_PREFIXES = Object.freeze([
|
|
226
|
+
{ host: 'hooks.slack.com', prefix: '/services' },
|
|
227
|
+
{ host: 'discord.com', prefix: '/api/webhooks' },
|
|
228
|
+
{ host: 'discordapp.com', prefix: '/api/webhooks' },
|
|
229
|
+
{ host: 'hooks.zapier.com', prefix: '' },
|
|
230
|
+
{ host: 'api.telegram.org', prefix: '/bot' },
|
|
231
|
+
]);
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Presigned-URL query parameter names (design amendment R3b). Lower-
|
|
235
|
+
* cased on match. The rule-9 scanner also consults the open vocabulary
|
|
236
|
+
* above via `matchesSecretKeyLoose`, so `?token=` and `?api_key=` fold
|
|
237
|
+
* without appearing here; this list carries the presigned-only names
|
|
238
|
+
* that would not otherwise trip the vocabulary.
|
|
239
|
+
*/
|
|
240
|
+
const PRESIGNED_QUERY_NAMES = Object.freeze(new Set([
|
|
241
|
+
'x-amz-signature',
|
|
242
|
+
'x-amz-credential',
|
|
243
|
+
'x-amz-security-token',
|
|
244
|
+
'x-amz-date',
|
|
245
|
+
'sas',
|
|
246
|
+
'se',
|
|
247
|
+
'sv',
|
|
248
|
+
'sp',
|
|
249
|
+
'access_token',
|
|
250
|
+
]));
|
|
251
|
+
|
|
252
|
+
// Small local escape helper used at module-init time before the main
|
|
253
|
+
// `escapeRe` is defined below. Cheap and duplicates one line to keep
|
|
254
|
+
// module ordering simple.
|
|
255
|
+
function escapeReStatic(s) {
|
|
256
|
+
return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* @typedef {object} RedactionContext
|
|
261
|
+
* @property {string | string[]} [projectRoot] absolute path (redacted to `<project>`);
|
|
262
|
+
* an array carries every spelling that names the same root (the as-typed
|
|
263
|
+
* spelling AND its realpath, e.g. `/tmp/foo` and `/private/tmp/foo` on macOS)
|
|
264
|
+
* so a path expressed either way is stripped (design 5 rule 1, F-slice-2-06).
|
|
265
|
+
* @property {string} [projectName] manifest projectName (redacted to `<project>`)
|
|
266
|
+
* @property {string} [operatorName] identity ## Name line, or null
|
|
267
|
+
* @property {string | string[]} [gitRemote] git remote url or URLs (redacted to `<project-remote>`);
|
|
268
|
+
* an array carries every remote a `git remote -v` walk names (F-slice-2-07).
|
|
269
|
+
* @property {string[]} [allowHosts] extension list merged with the bundled allowlist
|
|
270
|
+
*/
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* @typedef {object} LedgerRow
|
|
274
|
+
* @property {string} rule
|
|
275
|
+
* @property {string} before
|
|
276
|
+
* @property {string} after
|
|
277
|
+
* @property {number} count
|
|
278
|
+
*/
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* @typedef {object} ResidualHit
|
|
282
|
+
* @property {number} line 1-indexed line number in redacted output
|
|
283
|
+
* @property {string} snippet matched substring (already redacted string)
|
|
284
|
+
* @property {string} pattern short label for the pattern family
|
|
285
|
+
*/
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* @typedef {object} RedactResult
|
|
289
|
+
* @property {string} text redacted string
|
|
290
|
+
* @property {LedgerRow[]} ledger ordered by rule application
|
|
291
|
+
* @property {ResidualHit[]} residual rule-8 second-pass hits (empty on ok)
|
|
292
|
+
*/
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Redact one string with the rules from design section 5 (eight core plus rule 9 url-credential).
|
|
296
|
+
* Deterministic; safe to call repeatedly; no I/O beyond the bundled
|
|
297
|
+
* allowlist read at module load.
|
|
298
|
+
*
|
|
299
|
+
* @param {string} input
|
|
300
|
+
* @param {RedactionContext} [context]
|
|
301
|
+
* @returns {RedactResult}
|
|
302
|
+
*/
|
|
303
|
+
export function redact(input, context = {}) {
|
|
304
|
+
if (typeof input !== 'string') input = String(input ?? '');
|
|
305
|
+
const ledger = [];
|
|
306
|
+
let text = input;
|
|
307
|
+
|
|
308
|
+
// Rule 1: project root -> <project>. Applied first so absolute paths
|
|
309
|
+
// under it keep their relative tail rather than getting basename-only
|
|
310
|
+
// treatment from rule 2. Accepts either a single string or an array
|
|
311
|
+
// of spellings (typed AND realpath) so a path expressed either way
|
|
312
|
+
// is stripped (F-slice-2-06).
|
|
313
|
+
const projectRootSpellings = normaliseRootSpellings(context.projectRoot);
|
|
314
|
+
for (const spelling of projectRootSpellings) {
|
|
315
|
+
const norm = spelling.replace(/[/\\]+$/, '');
|
|
316
|
+
text = replaceAndLog(text, escapedLiteral(norm), '<project>', 'project-root', ledger);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Rule 1.5: KV-secret early guard runs BEFORE the absolute-path
|
|
320
|
+
// pass. A body like `/tmp/cache password=Abc12345` was previously
|
|
321
|
+
// consumed by the terminal-directory extension of the path regex
|
|
322
|
+
// (the space plus `password` folded into the path match), so the
|
|
323
|
+
// `password` key label vanished into `<path>` and the kv-secret
|
|
324
|
+
// pass then saw a bare `=Abc12345` with no key label to bind. The
|
|
325
|
+
// guard redacts every `keyname=value` pair first, so the path
|
|
326
|
+
// sweep only sees `/tmp/cache <redacted:secret>` and cannot swallow
|
|
327
|
+
// the key. Round 3: R-P0-3 P0 regression from round 2.
|
|
328
|
+
//
|
|
329
|
+
// Round 4 (F-P0-A): vocabulary comes from the shared
|
|
330
|
+
// SECRET_KEY_VOCABULARY constant so this guard, the rule-5 first-
|
|
331
|
+
// pass entry and the residual-scan safeguard cover the same set.
|
|
332
|
+
const kvSecretGuardRe = new RegExp(
|
|
333
|
+
`${secretKeyRegexSource()}["'\\s]*[:=][ \\t]*(?:"(?:\\\\.|[^"\\\\\\n])+"|'(?:\\\\.|[^'\\\\\\n])+'|[^\\s,;)}\\]]+)`,
|
|
334
|
+
'gi',
|
|
335
|
+
);
|
|
336
|
+
text = replaceEachDistinct(text, kvSecretGuardRe, '<redacted:secret>', 'secret-token', ledger);
|
|
337
|
+
// Round 4: `hash` is redacted only when the value is 16+ chars
|
|
338
|
+
// (design R3a). Handled as its own regex so a short `hash=abc` shape
|
|
339
|
+
// (public commit digest displayed for humans) does not fold.
|
|
340
|
+
const kvHashGuardRe = /\bhash["'\s]*[:=][ \t]*(?:"(?:\\.|[^"\\\n]){16,}"|'(?:\\.|[^'\\\n]){16,}'|[^\s,;)}\]]{16,})/gi;
|
|
341
|
+
text = replaceEachDistinct(text, kvHashGuardRe, '<redacted:secret>', 'secret-token', ledger);
|
|
342
|
+
|
|
343
|
+
// Rule 2: other absolute paths -> <path>/<basename>. POSIX and
|
|
344
|
+
// Windows shapes. Path segments allow letters, digits, dots,
|
|
345
|
+
// hyphens, underscores, tildes, pluses, colons, at-signs and
|
|
346
|
+
// spaces. A non-terminal segment carrying spaces (e.g.
|
|
347
|
+
// `/Users/john doe/work` or `/Users/john doe/work` with a double
|
|
348
|
+
// space) is accepted when it is followed by another separator, so
|
|
349
|
+
// any width of whitespace between tokens folds. A terminal
|
|
350
|
+
// (no-separator) segment accepts a space continuation of any width
|
|
351
|
+
// (`[ \t]+`) so `/Users/john doe` (double space) also folds; a
|
|
352
|
+
// sentence like `in /Users/john doe end here` still folds only
|
|
353
|
+
// `/Users/john doe` because the continuation is a single word,
|
|
354
|
+
// not the rest of the sentence (F-slice-2-06).
|
|
355
|
+
// Terminal-segment shape: a first word with a `.` looks like a
|
|
356
|
+
// filename (no space continuation - space starts a sentence again,
|
|
357
|
+
// as in `/foo.js line 42`); a first word without a `.` looks like
|
|
358
|
+
// a directory (allow one continuation - `john doe` is one dir on
|
|
359
|
+
// macOS). This split avoids swallowing the trailing sentence when
|
|
360
|
+
// the path ends in a filename.
|
|
361
|
+
const posixAbsRe = /(?<![A-Za-z0-9_/-])\/(?:Users|home|tmp|var|opt|private|etc|root)(?:\/(?:[A-Za-z0-9._+@~-]+(?:\s+[A-Za-z0-9._+@~-]+)*(?=\/)|[A-Za-z0-9_+@~-]+(?:[ \t]+[A-Za-z0-9._+@~-]+)?|[A-Za-z0-9._+@~-]+))+/g;
|
|
362
|
+
text = replaceRegex(text, posixAbsRe, (match) => {
|
|
363
|
+
const bn = match.split('/').filter(Boolean).pop() ?? 'file';
|
|
364
|
+
// A basename with a space is almost always a username directory
|
|
365
|
+
// (e.g. macOS `Users/First Last`); dropping the basename keeps
|
|
366
|
+
// the personal name off the wire (F-slice-2-06 terminal case).
|
|
367
|
+
if (/\s/.test(bn)) return '<path>';
|
|
368
|
+
return `<path>/${bn}`;
|
|
369
|
+
}, 'absolute-path', ledger);
|
|
370
|
+
const winAbsRe = /(?<![A-Za-z0-9])[A-Z]:\\(?:(?:[A-Za-z0-9._+@~-]+(?:\s+[A-Za-z0-9._+@~-]+)*(?=\\))|[A-Za-z0-9_+@~-]+(?:[ \t]+[A-Za-z0-9._+@~-]+)?|[A-Za-z0-9._+@~-]+)(?:\\(?:(?:[A-Za-z0-9._+@~-]+(?:\s+[A-Za-z0-9._+@~-]+)*(?=\\))|[A-Za-z0-9_+@~-]+(?:[ \t]+[A-Za-z0-9._+@~-]+)?|[A-Za-z0-9._+@~-]+))+/g;
|
|
371
|
+
text = replaceRegex(text, winAbsRe, (match) => {
|
|
372
|
+
const bn = match.split('\\').pop() ?? 'file';
|
|
373
|
+
if (/\s/.test(bn)) return '<path>';
|
|
374
|
+
return `<path>\\${bn}`;
|
|
375
|
+
}, 'absolute-path', ledger);
|
|
376
|
+
|
|
377
|
+
// Operator identity as a literal substitution happens BEFORE the
|
|
378
|
+
// shape-based rules 3-6 so a git remote of the form
|
|
379
|
+
// `git@github.com:owner/repo.git` is not first consumed by the email
|
|
380
|
+
// rule. The operator-name literal likewise takes precedence over
|
|
381
|
+
// any accidental shape overlaps. Accepts either a single remote or
|
|
382
|
+
// an array (F-slice-2-07: `git remote -v` yields multiple).
|
|
383
|
+
const gitRemotes = Array.isArray(context.gitRemote)
|
|
384
|
+
? context.gitRemote
|
|
385
|
+
: (typeof context.gitRemote === 'string' && context.gitRemote.length > 0 ? [context.gitRemote] : []);
|
|
386
|
+
for (const gr of gitRemotes) {
|
|
387
|
+
text = replaceAndLog(text, escapedLiteral(gr), '<project-remote>', 'operator-identity', ledger);
|
|
388
|
+
}
|
|
389
|
+
if (context.operatorName) {
|
|
390
|
+
const opName = context.operatorName.trim();
|
|
391
|
+
if (opName.length > 0) {
|
|
392
|
+
text = replaceAndLog(text, escapedLiteral(opName), '<operator>', 'operator-identity', ledger);
|
|
393
|
+
for (const tok of opName.split(/\s+/)) {
|
|
394
|
+
if (tok.length > 3) {
|
|
395
|
+
text = replaceAndLog(text, `\\b${escapeRe(tok)}\\b`, '<operator>', 'operator-identity', ledger);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (context.projectName) {
|
|
401
|
+
const pn = context.projectName.trim();
|
|
402
|
+
if (pn.length > 0) {
|
|
403
|
+
text = replaceAndLog(text, `\\b${escapeRe(pn)}\\b`, '<project>', 'operator-identity', ledger);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Rule 3: emails -> <email>. RFC-5322 lite; case-insensitive.
|
|
408
|
+
const emailRe = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
|
|
409
|
+
text = replaceRegex(text, emailRe, () => '<email>', 'email', ledger);
|
|
410
|
+
|
|
411
|
+
// Rule 4: hostnames and URLs whose registrable domain is not on the
|
|
412
|
+
// allowlist. URLs keep their path suffix; bare hostnames collapse.
|
|
413
|
+
// Loopback (localhost, 127.0.0.1) and everything on the allowlist
|
|
414
|
+
// survive; every other public hostname or URL loses its host.
|
|
415
|
+
const allowHosts = new Set([
|
|
416
|
+
...BUNDLED_ALLOWLIST.hosts,
|
|
417
|
+
...(Array.isArray(context.allowHosts) ? context.allowHosts : []),
|
|
418
|
+
].map((h) => h.toLowerCase()));
|
|
419
|
+
|
|
420
|
+
// Rule 9: URL-embedded credentials (design amendment R3b, added
|
|
421
|
+
// round 4 for F-P0-B). Runs BEFORE the rule-4 hostname pass so a
|
|
422
|
+
// Slack/Discord webhook URL whose path IS the credential has that
|
|
423
|
+
// path folded to `<url-credential>` before the host itself is
|
|
424
|
+
// folded to `<host>` and the path suffix would otherwise leave the
|
|
425
|
+
// machine intact. Same for `user:pass@host` userinfo, presigned
|
|
426
|
+
// query tokens, and any query parameter whose name matches the
|
|
427
|
+
// shared secret-key vocabulary. The ledger names this rule
|
|
428
|
+
// `url-credential`; false positives are visible in the disclosure.
|
|
429
|
+
text = redactUrlCredentials(text, ledger);
|
|
430
|
+
|
|
431
|
+
// URLs first (http/https), then bare hostnames.
|
|
432
|
+
const urlRe = /https?:\/\/([A-Za-z0-9.-]+)((?::[0-9]+)?(?:\/[^\s)]*)?)/g;
|
|
433
|
+
text = replaceRegex(text, urlRe, (_m, host, tail) => {
|
|
434
|
+
if (isHostAllowed(host, allowHosts)) return `https://${host}${tail}`;
|
|
435
|
+
return `<host>${tail}`;
|
|
436
|
+
}, 'hostname', ledger);
|
|
437
|
+
// Bare hostnames: a word that looks like a domain (has a dot,
|
|
438
|
+
// ends with a 2+ letter TLD) and is not on the allowlist. Skip if
|
|
439
|
+
// preceded by @ (already handled by rule 3), a slash or backslash
|
|
440
|
+
// (path fragment; the winAbsRe substitution leaves `<path>\file.ext`
|
|
441
|
+
// which must not fold to `<host>`), a digit-dot (IP handled by
|
|
442
|
+
// rule 5b), or another word character (a longer host suffix).
|
|
443
|
+
const bareHostRe = /(?<![@/\\\w.])(?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,}(?![\w.-])/g;
|
|
444
|
+
text = replaceRegex(text, bareHostRe, (host) => {
|
|
445
|
+
if (isHostAllowed(host, allowHosts)) return host;
|
|
446
|
+
return '<host>';
|
|
447
|
+
}, 'hostname', ledger);
|
|
448
|
+
|
|
449
|
+
// Rule 5: private and link-local IPs -> <ip>. Loopback survives.
|
|
450
|
+
// Deliberately named `private-ip` in the ledger vocabulary.
|
|
451
|
+
const privateIpRe = /\b(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|169\.254\.\d{1,3}\.\d{1,3})\b/g;
|
|
452
|
+
text = replaceRegex(text, privateIpRe, () => '<ip>', 'private-ip', ledger);
|
|
453
|
+
// Any other public IPv4 outside loopback also collapses.
|
|
454
|
+
const publicIpRe = /\b(?!127\.)(?!0\.)(?:\d{1,3}\.){3}\d{1,3}\b/g;
|
|
455
|
+
text = replaceRegex(text, publicIpRe, () => '<ip>', 'private-ip', ledger);
|
|
456
|
+
|
|
457
|
+
// F-slice-2-05: IPv6 is also part of rule 4 (design 5 explicitly
|
|
458
|
+
// names fc00::/7). Match unique-local (fc00::/7), link-local
|
|
459
|
+
// (fe80::/10) and generic IPv6 shapes (including :: compression),
|
|
460
|
+
// but keep loopback `::1`. Runs after IPv4 so a `::ffff:10.0.0.1`
|
|
461
|
+
// mapped-address form still folds sensibly.
|
|
462
|
+
// F-slice-2-05: preserve loopback in every legal spelling - `::1`,
|
|
463
|
+
// the fully expanded `0:0:0:0:0:0:0:1`, and every zero-padded
|
|
464
|
+
// variant of it (`0000:0000:0000:0000:0000:0000:0000:0001`, etc).
|
|
465
|
+
// A single `::1` matcher would let the expanded forms fall through
|
|
466
|
+
// to the general ipv6 pattern and get redacted against design 5
|
|
467
|
+
// rule 4's loopback exemption.
|
|
468
|
+
const ipv6LoopbackRe = /(?<![0-9A-Fa-f:])(?:::1|(?:0{1,4}:){7}0{0,3}1)(?![0-9A-Fa-f:])/g;
|
|
469
|
+
const ipv6Re = /(?<![0-9A-Fa-f:])(?:(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){1,7}:|(?:[0-9A-Fa-f]{1,4}:){1,6}:[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){1,5}(?::[0-9A-Fa-f]{1,4}){1,2}|(?:[0-9A-Fa-f]{1,4}:){1,4}(?::[0-9A-Fa-f]{1,4}){1,3}|(?:[0-9A-Fa-f]{1,4}:){1,3}(?::[0-9A-Fa-f]{1,4}){1,4}|(?:[0-9A-Fa-f]{1,4}:){1,2}(?::[0-9A-Fa-f]{1,4}){1,5}|[0-9A-Fa-f]{1,4}:(?::[0-9A-Fa-f]{1,4}){1,6}|:(?::[0-9A-Fa-f]{1,4}){1,7})(?![0-9A-Fa-f:])/g;
|
|
470
|
+
// Protect loopback markers before the general pattern runs.
|
|
471
|
+
const LOOPBACK_TOKEN = 'IPV6LO';
|
|
472
|
+
const savedLoopback = [];
|
|
473
|
+
text = text.replace(ipv6LoopbackRe, (m) => {
|
|
474
|
+
savedLoopback.push(m);
|
|
475
|
+
return `${LOOPBACK_TOKEN}${savedLoopback.length - 1}${LOOPBACK_TOKEN}`;
|
|
476
|
+
});
|
|
477
|
+
text = replaceRegex(text, ipv6Re, () => '<ip>', 'private-ip', ledger);
|
|
478
|
+
text = text.replace(new RegExp(`${LOOPBACK_TOKEN}(\\d+)${LOOPBACK_TOKEN}`, 'g'), (_m, i) => savedLoopback[Number(i)]);
|
|
479
|
+
|
|
480
|
+
// Rule 8a-early: dash normalisation runs BEFORE the rule-6 secret
|
|
481
|
+
// pass so em-dash-delimited shapes (e.g. `—BEGIN PRIVATE KEY—`)
|
|
482
|
+
// reach the secret patterns as ASCII forms. Doing this later would
|
|
483
|
+
// let a PEM block with em-dashes on the delimiters slip both the
|
|
484
|
+
// first-pass PEM regex (needs `-`) AND the residual pass because
|
|
485
|
+
// the residual scan runs after this normalisation - R-P0-2.
|
|
486
|
+
// Normalise dashes: em (—), en (–), figure (‒),
|
|
487
|
+
// horizontal bar (―), hyphen (‐), non-breaking hyphen
|
|
488
|
+
// (‑), minus sign (−). All become ASCII hyphen.
|
|
489
|
+
const dashRe = /[‐-―−]/g;
|
|
490
|
+
let dashes = 0;
|
|
491
|
+
text = text.replace(dashRe, () => {
|
|
492
|
+
dashes += 1;
|
|
493
|
+
return '-';
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// Rule 6: secret-looking strings -> <redacted:secret>. This block is
|
|
497
|
+
// the last line of defence and rule 8's residual check re-runs it.
|
|
498
|
+
text = redactSecrets(text, ledger);
|
|
499
|
+
|
|
500
|
+
// Rule 8a-late: size / shape. Strip control characters; truncate to
|
|
501
|
+
// BODY_CAP_BYTES with an explicit marker. Dash normalisation is now
|
|
502
|
+
// in 8a-early above; the size-shape ledger row still totals both.
|
|
503
|
+
const before = text;
|
|
504
|
+
let ctrl = 0;
|
|
505
|
+
text = text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, () => {
|
|
506
|
+
ctrl += 1;
|
|
507
|
+
return '';
|
|
508
|
+
});
|
|
509
|
+
let truncated = 0;
|
|
510
|
+
if (Buffer.byteLength(text, 'utf8') > BODY_CAP_BYTES) {
|
|
511
|
+
text = truncateToBytes(text, BODY_CAP_BYTES - 32);
|
|
512
|
+
text = `${text}\n[truncated by rcf feedback]\n`;
|
|
513
|
+
truncated = 1;
|
|
514
|
+
}
|
|
515
|
+
if (ctrl > 0 || dashes > 0 || truncated > 0) {
|
|
516
|
+
ledger.push({
|
|
517
|
+
rule: 'size-shape',
|
|
518
|
+
before: `${before.length} chars, ${ctrl} control, ${dashes} dashes${truncated ? ', over cap' : ''}`,
|
|
519
|
+
after: `${text.length} chars`,
|
|
520
|
+
count: ctrl + dashes + truncated,
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Rule 8b: second-pass residual secret check. Any surviving match
|
|
525
|
+
// for the rule-5 pattern family names the failing line and lets
|
|
526
|
+
// `submit` refuse without sending. `preview` shows the same list so
|
|
527
|
+
// the agent can rewrite the body.
|
|
528
|
+
const residual = findResidualSecrets(text);
|
|
529
|
+
|
|
530
|
+
return { text, ledger, residual };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Detect residual secrets in already-redacted text. Wider than the
|
|
535
|
+
* first-pass `redactSecrets` set: the residual check drops the word-
|
|
536
|
+
* boundary anchors so a token buried inside a longer identifier
|
|
537
|
+
* (a shape the first pass would skip) still trips the refusal. Used
|
|
538
|
+
* by `preview` for the disclosure line and by `submit` (slice 4) for
|
|
539
|
+
* the refusal.
|
|
540
|
+
*
|
|
541
|
+
* @param {string} text
|
|
542
|
+
* @returns {ResidualHit[]}
|
|
543
|
+
*/
|
|
544
|
+
export function findResidualSecrets(text) {
|
|
545
|
+
// F-slice-2-04 / R-P0-2: the residual pass runs the SAME pattern
|
|
546
|
+
// family the first pass used, plus the word-boundary-loosened
|
|
547
|
+
// token variants so a secret buried inside a longer identifier
|
|
548
|
+
// still trips the refusal. Scans the WHOLE text with each pattern
|
|
549
|
+
// (not line-by-line) so multiline shapes such as a PEM block are
|
|
550
|
+
// matched: a per-line loop could never see a BEGIN..END pair on
|
|
551
|
+
// its own line. Every rule-5 family is represented: what leaves
|
|
552
|
+
// the machine cannot include a shape the first pass would have
|
|
553
|
+
// caught even after later normalisation.
|
|
554
|
+
const primaries = secretPatterns();
|
|
555
|
+
const looseTokens = [
|
|
556
|
+
{ name: 'github-token', re: /(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}/g },
|
|
557
|
+
{ name: 'github-pat', re: /github_pat_[A-Za-z0-9_]{20,}/g },
|
|
558
|
+
{ name: 'aws-access', re: /AKIA[0-9A-Z]{16}/g },
|
|
559
|
+
{ name: 'stripe', re: /sk_(?:live|test)_[A-Za-z0-9]{20,}/g },
|
|
560
|
+
{ name: 'openai', re: /sk-[A-Za-z0-9]{20,}/g },
|
|
561
|
+
{ name: 'slack', re: /xox[abp]-[A-Za-z0-9-]{10,}/g },
|
|
562
|
+
{ name: 'jwt', re: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g },
|
|
563
|
+
{ name: 'bearer', re: /Bearer\s+[A-Za-z0-9._~+/=-]{4,}/g },
|
|
564
|
+
];
|
|
565
|
+
const residualPatterns = [...primaries, ...looseTokens];
|
|
566
|
+
/** @type {ResidualHit[]} */
|
|
567
|
+
const hits = [];
|
|
568
|
+
const seen = new Set();
|
|
569
|
+
// Round 3: also scan a diacritic-stripped shadow copy so a key
|
|
570
|
+
// label carrying a Latin-1 supplement letter (`pássword=Abc12345`,
|
|
571
|
+
// `sécret=X`, homoglyph variants) still trips the residual pass and
|
|
572
|
+
// submit refuses fail-closed. The shadow scan preserves byte
|
|
573
|
+
// offsets by mapping combining marks and diacritics to their base
|
|
574
|
+
// ASCII counterparts one codepoint at a time; every non-ASCII
|
|
575
|
+
// codepoint that is not a diacritic marker collapses to a single
|
|
576
|
+
// `_` placeholder so line and column offsets survive intact.
|
|
577
|
+
const shadow = stripDiacritics(text);
|
|
578
|
+
const scans = [
|
|
579
|
+
{ source: text, label: '' },
|
|
580
|
+
...(shadow !== text ? [{ source: shadow, label: 'unicode-key' }] : []),
|
|
581
|
+
];
|
|
582
|
+
for (const { source, label } of scans) {
|
|
583
|
+
for (const { name, re } of residualPatterns) {
|
|
584
|
+
re.lastIndex = 0;
|
|
585
|
+
let m;
|
|
586
|
+
while ((m = re.exec(source)) !== null) {
|
|
587
|
+
// Line number = 1 + count of newlines before m.index. Uses
|
|
588
|
+
// `text` (not `source`) because the shadow preserves newline
|
|
589
|
+
// positions, so the raw text's line count is authoritative
|
|
590
|
+
// for what the operator sees on stdout.
|
|
591
|
+
let line = 1;
|
|
592
|
+
for (let i = 0; i < m.index; i += 1) {
|
|
593
|
+
if (text.charCodeAt(i) === 10) line += 1;
|
|
594
|
+
}
|
|
595
|
+
// Snippet is drawn from the RAW text so the operator sees the
|
|
596
|
+
// actual bytes at the offending line, not the placeholder.
|
|
597
|
+
const rawSnippet = text.slice(m.index, m.index + m[0].length);
|
|
598
|
+
const snippet = rawSnippet.replace(/\s+/g, ' ').slice(0, 80);
|
|
599
|
+
const pattern = label ? `${label}:${name}` : name;
|
|
600
|
+
const key = `${pattern}|${line}|${snippet}`;
|
|
601
|
+
if (!seen.has(key)) {
|
|
602
|
+
seen.add(key);
|
|
603
|
+
hits.push({ line, snippet, pattern });
|
|
604
|
+
}
|
|
605
|
+
// Guard against zero-width matches (defensive; no such pattern
|
|
606
|
+
// in the set today, but a future pattern with an optional
|
|
607
|
+
// group could hang the loop otherwise).
|
|
608
|
+
if (re.lastIndex === m.index) re.lastIndex += 1;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
// Round 5 (design amendment R4): also scan for a vocabulary key
|
|
613
|
+
// labelling a value-shape token via a non-`:=` separator (markdown
|
|
614
|
+
// table pipe, prose bridge, whitespace, tab, hyphen, arrow, HTML
|
|
615
|
+
// entity). The residual pass and the primary pass share
|
|
616
|
+
// `scanR4Leaks` so the mixed-classes gate is the same on both
|
|
617
|
+
// sides: what the redactor deemed benign under R4 does not
|
|
618
|
+
// resurface as a refusal here.
|
|
619
|
+
for (const scan of scans) {
|
|
620
|
+
const r4Hits = scanR4Leaks(scan.source);
|
|
621
|
+
for (const hit of r4Hits) {
|
|
622
|
+
let line = 1;
|
|
623
|
+
for (let i = 0; i < hit.start; i += 1) {
|
|
624
|
+
if (text.charCodeAt(i) === 10) line += 1;
|
|
625
|
+
}
|
|
626
|
+
const rawSnippet = text.slice(hit.start, hit.end);
|
|
627
|
+
const snippet = rawSnippet.replace(/\s+/g, ' ').slice(0, 80);
|
|
628
|
+
const pattern = scan.label ? `${scan.label}:kv-secret-r4` : 'kv-secret-r4';
|
|
629
|
+
const key = `${pattern}|${line}|${snippet}`;
|
|
630
|
+
if (!seen.has(key)) {
|
|
631
|
+
seen.add(key);
|
|
632
|
+
hits.push({ line, snippet, pattern });
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
hits.sort((a, b) => a.line - b.line || a.pattern.localeCompare(b.pattern));
|
|
637
|
+
return hits;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Return a copy of `input` where every non-ASCII codepoint that carries
|
|
642
|
+
* a base ASCII letter (via NFKD decomposition) is folded to that letter,
|
|
643
|
+
* every remaining non-ASCII codepoint collapses to `_`, and every ASCII
|
|
644
|
+
* character passes through untouched. The output has the same length in
|
|
645
|
+
* codepoints as the input, so `String.prototype.replace` match indices
|
|
646
|
+
* carry over one-for-one for line-number arithmetic in the residual pass.
|
|
647
|
+
* Fail-closed: on any exception, return the input unchanged so the raw
|
|
648
|
+
* scan is still authoritative.
|
|
649
|
+
*
|
|
650
|
+
* @param {string} input
|
|
651
|
+
* @returns {string}
|
|
652
|
+
*/
|
|
653
|
+
function stripDiacritics(input) {
|
|
654
|
+
try {
|
|
655
|
+
let out = '';
|
|
656
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
657
|
+
const c = input.charCodeAt(i);
|
|
658
|
+
if (c < 128) { out += input[i]; continue; }
|
|
659
|
+
// Preserve UTF-16 code-unit alignment. A high surrogate plus a
|
|
660
|
+
// low surrogate is one non-BMP codepoint; emit two `_` so the
|
|
661
|
+
// shadow has the same `.length` and regex offsets carry over
|
|
662
|
+
// unchanged for line-number arithmetic.
|
|
663
|
+
if (c >= 0xD800 && c <= 0xDBFF) {
|
|
664
|
+
out += '__';
|
|
665
|
+
i += 1;
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
// BMP non-ASCII: NFKD fold to the first ASCII letter or digit
|
|
669
|
+
// in the decomposition (e.g. `á` -> `a` + combining acute).
|
|
670
|
+
// If nothing ASCII remains, collapse to `_` so the placeholder
|
|
671
|
+
// holds the offset without introducing pattern shapes of its own.
|
|
672
|
+
const decomposed = input[i].normalize('NFKD');
|
|
673
|
+
let picked = null;
|
|
674
|
+
for (const dch of decomposed) {
|
|
675
|
+
const cc = dch.charCodeAt(0);
|
|
676
|
+
if ((cc >= 48 && cc <= 57) || (cc >= 65 && cc <= 90) || (cc >= 97 && cc <= 122)) {
|
|
677
|
+
picked = dch;
|
|
678
|
+
break;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
out += picked ?? '_';
|
|
682
|
+
}
|
|
683
|
+
return out;
|
|
684
|
+
} catch {
|
|
685
|
+
return input;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Merge the bundled allowlist with the per-project extension.
|
|
691
|
+
* Public helper so callers can preview the merged set (doctor,
|
|
692
|
+
* `feedback status` in slice 5) without re-implementing the merge.
|
|
693
|
+
*
|
|
694
|
+
* @param {string[]} [extend]
|
|
695
|
+
* @returns {string[]}
|
|
696
|
+
*/
|
|
697
|
+
export function allowedHosts(extend = []) {
|
|
698
|
+
return [...new Set([
|
|
699
|
+
...BUNDLED_ALLOWLIST.hosts,
|
|
700
|
+
...(Array.isArray(extend) ? extend : []),
|
|
701
|
+
].map((h) => h.toLowerCase()))];
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Rule 9 (design amendment R3b): fold URL-embedded credentials.
|
|
706
|
+
* Called from `redact()` before the rule-4 hostname pass so the
|
|
707
|
+
* webhook path IS folded before the host would collapse and leave
|
|
708
|
+
* the credential-carrying tail intact. Every replacement lands on
|
|
709
|
+
* the ledger as one `url-credential` row per distinct before-URL
|
|
710
|
+
* so the operator sees exactly what was stripped.
|
|
711
|
+
*
|
|
712
|
+
* The URL regex is deliberately looser than the rule-4 hostname
|
|
713
|
+
* regex: it captures optional userinfo, host with optional port,
|
|
714
|
+
* an optional path, and an optional query. A match that leaves the
|
|
715
|
+
* URL unchanged (no credential detected) is treated as a no-op and
|
|
716
|
+
* the ledger stays clean.
|
|
717
|
+
*
|
|
718
|
+
* @param {string} input
|
|
719
|
+
* @param {LedgerRow[]} ledger
|
|
720
|
+
* @returns {string}
|
|
721
|
+
*/
|
|
722
|
+
function redactUrlCredentials(input, ledger) {
|
|
723
|
+
// Optional userinfo (`user:pass@`), host with optional port,
|
|
724
|
+
// optional path (up to `?`, `#` or whitespace), optional query.
|
|
725
|
+
const re = /https?:\/\/(?:([^\s@/]+)@)?([A-Za-z0-9.-]+(?::[0-9]+)?)(\/[^\s?#)]*)?(\?[^\s#)]*)?/g;
|
|
726
|
+
const counts = new Map();
|
|
727
|
+
const afterFor = new Map();
|
|
728
|
+
const out = input.replace(re, (match, userinfo, hostAndPort, path, query) => {
|
|
729
|
+
const rewritten = rewriteOneUrlForCredentials(match, userinfo, hostAndPort, path, query);
|
|
730
|
+
if (rewritten === match) return match;
|
|
731
|
+
counts.set(match, (counts.get(match) ?? 0) + 1);
|
|
732
|
+
afterFor.set(match, rewritten);
|
|
733
|
+
return rewritten;
|
|
734
|
+
});
|
|
735
|
+
for (const [before, count] of counts) {
|
|
736
|
+
ledger.push({
|
|
737
|
+
rule: 'url-credential',
|
|
738
|
+
before,
|
|
739
|
+
after: afterFor.get(before) ?? '',
|
|
740
|
+
count,
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
return out;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Per-match URL rewriter. Extracted from `redactUrlCredentials` so the
|
|
748
|
+
* inner logic is straight-line and unit-testable. Returns the original
|
|
749
|
+
* `match` string when no credential shape was detected (caller treats
|
|
750
|
+
* that as a no-op and does not append a ledger row).
|
|
751
|
+
*
|
|
752
|
+
* @param {string} match
|
|
753
|
+
* @param {string | undefined} userinfo
|
|
754
|
+
* @param {string} hostAndPort
|
|
755
|
+
* @param {string | undefined} path
|
|
756
|
+
* @param {string | undefined} query
|
|
757
|
+
* @returns {string}
|
|
758
|
+
*/
|
|
759
|
+
function rewriteOneUrlForCredentials(match, userinfo, hostAndPort, path, query) {
|
|
760
|
+
let credRedacted = false;
|
|
761
|
+
let out = match.slice(0, match.indexOf('://') + 3);
|
|
762
|
+
|
|
763
|
+
if (userinfo) {
|
|
764
|
+
// Drop the userinfo entirely so the rule-4 hostname pass that
|
|
765
|
+
// runs next still sees a well-formed URL and can fold a non-
|
|
766
|
+
// allowlisted host to `<host>`. Keeping a `<url-credential>@`
|
|
767
|
+
// marker in front of the host would leave the regex looking at
|
|
768
|
+
// a `<` first char and the host would leak. The ledger row for
|
|
769
|
+
// this URL still names `url-credential` so the operator sees
|
|
770
|
+
// exactly what was stripped (before -> after diff).
|
|
771
|
+
credRedacted = true;
|
|
772
|
+
}
|
|
773
|
+
out += hostAndPort;
|
|
774
|
+
|
|
775
|
+
let pathOut = path ?? '';
|
|
776
|
+
if (path) {
|
|
777
|
+
const hostOnly = hostAndPort.toLowerCase().split(':')[0];
|
|
778
|
+
let webhookHit = false;
|
|
779
|
+
for (const wh of WEBHOOK_HOST_PATH_PREFIXES) {
|
|
780
|
+
if (hostOnly === wh.host || hostOnly.endsWith(`.${wh.host}`)) {
|
|
781
|
+
const prefix = wh.prefix;
|
|
782
|
+
if (prefix === '' || path === prefix || path.startsWith(`${prefix}/`)) {
|
|
783
|
+
const suffix = path.slice(prefix.length);
|
|
784
|
+
if (suffix.length > 1) {
|
|
785
|
+
pathOut = `${prefix}/<url-credential>`;
|
|
786
|
+
credRedacted = true;
|
|
787
|
+
webhookHit = true;
|
|
788
|
+
}
|
|
789
|
+
break;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (!webhookHit) {
|
|
794
|
+
const parts = path.split('/');
|
|
795
|
+
for (let i = 1; i < parts.length; i += 1) {
|
|
796
|
+
if (looksHighEntropyPath(parts[i])) {
|
|
797
|
+
parts[i] = '<url-credential>';
|
|
798
|
+
credRedacted = true;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
pathOut = parts.join('/');
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
out += pathOut;
|
|
805
|
+
|
|
806
|
+
let queryOut = '';
|
|
807
|
+
if (query) {
|
|
808
|
+
const kvs = query.slice(1).split('&').map((pair) => {
|
|
809
|
+
if (!pair.includes('=')) return pair;
|
|
810
|
+
const eq = pair.indexOf('=');
|
|
811
|
+
const k = pair.slice(0, eq);
|
|
812
|
+
const v = pair.slice(eq + 1);
|
|
813
|
+
const kLower = k.toLowerCase();
|
|
814
|
+
if (PRESIGNED_QUERY_NAMES.has(kLower) || matchesSecretKeyLoose(k)) {
|
|
815
|
+
if (v.length > 0) {
|
|
816
|
+
credRedacted = true;
|
|
817
|
+
return `${k}=<url-credential>`;
|
|
818
|
+
}
|
|
819
|
+
return pair;
|
|
820
|
+
}
|
|
821
|
+
if (looksHighEntropyPath(v)) {
|
|
822
|
+
credRedacted = true;
|
|
823
|
+
return `${k}=<url-credential>`;
|
|
824
|
+
}
|
|
825
|
+
return pair;
|
|
826
|
+
});
|
|
827
|
+
queryOut = `?${kvs.join('&')}`;
|
|
828
|
+
}
|
|
829
|
+
out += queryOut;
|
|
830
|
+
|
|
831
|
+
return credRedacted ? out : match;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Entropy heuristic for URL path or query values: 16+ chars of the
|
|
836
|
+
* URL-safe alphabet with at least two character classes present. The
|
|
837
|
+
* webhook token `AAAAAAAAAAAAAAAAAAAAAAAA` (single class) will NOT
|
|
838
|
+
* trip this on its own; the webhook host-prefix rule catches it. The
|
|
839
|
+
* threshold is lower than the general `entropy-blob` (32) because a
|
|
840
|
+
* URL segment carrying a credential is usually shorter than a raw
|
|
841
|
+
* secret in prose.
|
|
842
|
+
*
|
|
843
|
+
* @param {string} value
|
|
844
|
+
*/
|
|
845
|
+
function looksHighEntropyPath(value) {
|
|
846
|
+
if (typeof value !== 'string') return false;
|
|
847
|
+
if (value.length < 16) return false;
|
|
848
|
+
if (!/^[A-Za-z0-9._~+/=-]+$/.test(value)) return false;
|
|
849
|
+
let classes = 0;
|
|
850
|
+
if (/[A-Z]/.test(value)) classes += 1;
|
|
851
|
+
if (/[a-z]/.test(value)) classes += 1;
|
|
852
|
+
if (/\d/.test(value)) classes += 1;
|
|
853
|
+
if (/[._~+/=-]/.test(value)) classes += 1;
|
|
854
|
+
return classes >= 2;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// -- internal helpers -----------------------------------------------------
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* Fold a `projectRoot` context value (string, array of strings, or
|
|
861
|
+
* nullish) into a deduped array of spellings, longest first, so
|
|
862
|
+
* `/private/tmp/foo` is stripped before `/tmp/foo` would leave the
|
|
863
|
+
* trailing tail untouched.
|
|
864
|
+
*
|
|
865
|
+
* @param {string | string[] | null | undefined} value
|
|
866
|
+
* @returns {string[]}
|
|
867
|
+
*/
|
|
868
|
+
function normaliseRootSpellings(value) {
|
|
869
|
+
if (!value) return [];
|
|
870
|
+
const raw = Array.isArray(value) ? value : [value];
|
|
871
|
+
const seen = new Set();
|
|
872
|
+
const out = [];
|
|
873
|
+
for (const s of raw) {
|
|
874
|
+
if (typeof s !== 'string' || s.length === 0) continue;
|
|
875
|
+
if (seen.has(s)) continue;
|
|
876
|
+
seen.add(s);
|
|
877
|
+
out.push(s);
|
|
878
|
+
}
|
|
879
|
+
out.sort((a, b) => b.length - a.length);
|
|
880
|
+
return out;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function replaceAndLog(text, pattern, replacement, rule, ledger) {
|
|
884
|
+
const re = pattern instanceof RegExp ? pattern : new RegExp(pattern, 'g');
|
|
885
|
+
let count = 0;
|
|
886
|
+
let sample = null;
|
|
887
|
+
const out = text.replace(re, (match) => {
|
|
888
|
+
count += 1;
|
|
889
|
+
if (sample === null) sample = match;
|
|
890
|
+
return typeof replacement === 'function' ? replacement(match) : replacement;
|
|
891
|
+
});
|
|
892
|
+
if (count > 0) {
|
|
893
|
+
ledger.push({
|
|
894
|
+
rule,
|
|
895
|
+
before: String(sample),
|
|
896
|
+
after: typeof replacement === 'function' ? replacement(sample) : replacement,
|
|
897
|
+
count,
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
return out;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function replaceRegex(text, re, replacer, rule, ledger) {
|
|
904
|
+
let count = 0;
|
|
905
|
+
let sampleBefore = null;
|
|
906
|
+
let sampleAfter = null;
|
|
907
|
+
const out = text.replace(re, (match, ...args) => {
|
|
908
|
+
const after = replacer(match, ...args);
|
|
909
|
+
if (after === match) return match;
|
|
910
|
+
count += 1;
|
|
911
|
+
if (sampleBefore === null) {
|
|
912
|
+
sampleBefore = match;
|
|
913
|
+
sampleAfter = after;
|
|
914
|
+
}
|
|
915
|
+
return after;
|
|
916
|
+
});
|
|
917
|
+
if (count > 0) {
|
|
918
|
+
ledger.push({ rule, before: sampleBefore, after: sampleAfter, count });
|
|
919
|
+
}
|
|
920
|
+
return out;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* Variant of `replaceRegex` that logs ONE ledger row per distinct
|
|
925
|
+
* before-sample the pattern matched, not one row per pattern call.
|
|
926
|
+
* Two `password=A` and `password=B` hits in a single field both land
|
|
927
|
+
* as their own rows so the operator disclosure ledger names every
|
|
928
|
+
* secret that was stripped (F-slice-2-09 within-field case).
|
|
929
|
+
*
|
|
930
|
+
* @param {string} text
|
|
931
|
+
* @param {RegExp} re a /g flag is required
|
|
932
|
+
* @param {string} replacement the after text (constant)
|
|
933
|
+
* @param {string} rule ledger row rule name
|
|
934
|
+
* @param {LedgerRow[]} ledger
|
|
935
|
+
* @returns {string}
|
|
936
|
+
*/
|
|
937
|
+
function replaceEachDistinct(text, re, replacement, rule, ledger) {
|
|
938
|
+
const counts = new Map();
|
|
939
|
+
const out = text.replace(re, (match) => {
|
|
940
|
+
counts.set(match, (counts.get(match) ?? 0) + 1);
|
|
941
|
+
return replacement;
|
|
942
|
+
});
|
|
943
|
+
for (const [before, count] of counts) {
|
|
944
|
+
ledger.push({ rule, before, after: replacement, count });
|
|
945
|
+
}
|
|
946
|
+
return out;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function escapeRe(s) {
|
|
950
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function escapedLiteral(s) {
|
|
954
|
+
return escapeRe(s);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function isHostAllowed(host, allowSet) {
|
|
958
|
+
const h = host.toLowerCase();
|
|
959
|
+
if (allowSet.has(h)) return true;
|
|
960
|
+
// Match by registrable-domain suffix: `sub.github.com` matches
|
|
961
|
+
// `github.com` when the allowlist has `github.com`.
|
|
962
|
+
for (const allowed of allowSet) {
|
|
963
|
+
if (h === allowed) return true;
|
|
964
|
+
if (h.endsWith(`.${allowed}`)) return true;
|
|
965
|
+
}
|
|
966
|
+
return false;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function truncateToBytes(text, maxBytes) {
|
|
970
|
+
const buf = Buffer.from(text, 'utf8');
|
|
971
|
+
if (buf.length <= maxBytes) return text;
|
|
972
|
+
// Truncate at a UTF-8 boundary. Buffer.toString('utf8') on a slice
|
|
973
|
+
// that lands mid-codepoint would emit replacement chars; walk back
|
|
974
|
+
// to the last complete codepoint.
|
|
975
|
+
let cut = maxBytes;
|
|
976
|
+
while (cut > 0 && (buf[cut] & 0xc0) === 0x80) cut -= 1;
|
|
977
|
+
return buf.slice(0, cut).toString('utf8');
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Secret patterns are the closed vocabulary rule 5 (secret-token in the
|
|
981
|
+
// ledger) applies. Kept as a module-scope helper so `findResidualSecrets`
|
|
982
|
+
// runs the same set the redactor used.
|
|
983
|
+
//
|
|
984
|
+
// F-slice-2-01: `kv-secret` must catch short and quoted values too;
|
|
985
|
+
// any well-known key name paired with any non-empty value is a secret.
|
|
986
|
+
// F-slice-2-02: `authorization` matches to end of line so the header
|
|
987
|
+
// value AND the credential fold together.
|
|
988
|
+
// F-slice-2-03: `pem` matches the whole BEGIN..END block, not just
|
|
989
|
+
// the BEGIN delimiter, so the key payload leaves with it.
|
|
990
|
+
function secretPatterns() {
|
|
991
|
+
return [
|
|
992
|
+
{ name: 'github-token', re: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}\b/g },
|
|
993
|
+
{ name: 'github-pat', re: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g },
|
|
994
|
+
{ name: 'aws-access', re: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
995
|
+
{ name: 'stripe', re: /\bsk_(?:live|test)_[A-Za-z0-9]{20,}\b/g },
|
|
996
|
+
{ name: 'openai', re: /\bsk-[A-Za-z0-9]{20,}\b/g },
|
|
997
|
+
{ name: 'slack', re: /\bxox[abp]-[A-Za-z0-9-]{10,}\b/g },
|
|
998
|
+
{ name: 'jwt', re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g },
|
|
999
|
+
// R-P0-2: broaden PEM to accept 1..5 dashes on each delimiter so a
|
|
1000
|
+
// block whose fences were folded by dash normalisation earlier in
|
|
1001
|
+
// the pass (em-dash `—BEGIN...—` -> `-BEGIN...-`) still
|
|
1002
|
+
// matches. The eight-rule ordering keeps this the last line of
|
|
1003
|
+
// defence before the residual scan.
|
|
1004
|
+
{ name: 'pem', re: /-{1,5}BEGIN (?:[A-Z0-9 ]+ )?PRIVATE KEY-{1,5}[\s\S]*?-{1,5}END (?:[A-Z0-9 ]+ )?PRIVATE KEY-{1,5}/g },
|
|
1005
|
+
{ name: 'bearer', re: /\bBearer\s+[A-Za-z0-9._~+/=-]{4,}/g },
|
|
1006
|
+
{ name: 'authorization', re: /^[ \t]*authorization[ \t]*:[^\n]+/gim },
|
|
1007
|
+
// Well-known key names paired with ANY non-empty value are
|
|
1008
|
+
// secrets by convention. The value class stays permissive so
|
|
1009
|
+
// short and quoted values (e.g. "password": "abc") fold too.
|
|
1010
|
+
// R-P0-1: allow tab (and any whitespace) between key and the
|
|
1011
|
+
// `:` / `=` separator; the previous ["' ]* class dropped tabs
|
|
1012
|
+
// and let `password\t=Abc12345` slip past the redactor.
|
|
1013
|
+
// F-slice-2-01: quoted values with an embedded escaped quote
|
|
1014
|
+
// (`"ab\"cd"`) previously truncated at the first `"`; the value
|
|
1015
|
+
// alternatives now consume `\<any>` escape sequences too.
|
|
1016
|
+
// Round 4 (F-P0-A): vocabulary comes from the shared
|
|
1017
|
+
// SECRET_KEY_VOCABULARY constant so the first-pass, early-guard
|
|
1018
|
+
// and residual-scan safeguard cover the same set.
|
|
1019
|
+
{
|
|
1020
|
+
name: 'kv-secret',
|
|
1021
|
+
re: new RegExp(
|
|
1022
|
+
`${secretKeyRegexSource()}["'\\s]*[:=][ \\t]*(?:"(?:\\\\.|[^"\\\\\\n])+"|'(?:\\\\.|[^'\\\\\\n])+'|[^\\s,;)}\\]]+)`,
|
|
1023
|
+
'gi',
|
|
1024
|
+
),
|
|
1025
|
+
},
|
|
1026
|
+
// Round 4 (R3a): `hash` fires only when the value is 16+ chars.
|
|
1027
|
+
{ name: 'kv-hash-long', re: /\bhash["'\s]*[:=][ \t]*(?:"(?:\\.|[^"\\\n]){16,}"|'(?:\\.|[^'\\\n]){16,}'|[^\s,;)}\]]{16,})/gi },
|
|
1028
|
+
// Round 4 (R3b): URL-embedded credentials caught at residual time.
|
|
1029
|
+
// Slack/Discord webhook path shape and userinfo shape. The first-
|
|
1030
|
+
// pass rule 9 handles these; this residual pattern is the belt-
|
|
1031
|
+
// and-braces backstop for a body that reassembled a webhook URL
|
|
1032
|
+
// from parts the primary scan did not treat as one URL.
|
|
1033
|
+
{ name: 'webhook-path', re: /\/(?:services|api\/webhooks|bot)\/[A-Za-z0-9._~+/=-]{20,}/g },
|
|
1034
|
+
{ name: 'url-userinfo', re: /https?:\/\/[^\s\/@]+:[^\s\/@]+@[A-Za-z0-9.-]+/g },
|
|
1035
|
+
// High-entropy blob. Anchored to non-word boundaries so it does not
|
|
1036
|
+
// fold with the specific patterns above; excludes 7/12/40-char
|
|
1037
|
+
// hex-only sha values (git object shape).
|
|
1038
|
+
{ name: 'entropy-blob', re: /\b(?![0-9a-f]{7}\b)(?![0-9a-f]{12}\b)(?![0-9a-f]{40}\b)(?=[A-Za-z0-9_-]{32,})(?=.*[A-Z])(?=.*[a-z])(?=.*\d)[A-Za-z0-9_-]{32,}\b/g },
|
|
1039
|
+
];
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/**
|
|
1043
|
+
* Round 5 (design amendment R4): apply the R4 scanner and fold each
|
|
1044
|
+
* hit to `<redacted:secret>`. Ledger rows land under the canonical
|
|
1045
|
+
* `secret-token` rule name so operator disclosure stays consistent
|
|
1046
|
+
* with the primary kv-secret pass; `replaceEachDistinct` in the
|
|
1047
|
+
* caller-side ledger fold sums counts across identical before-samples.
|
|
1048
|
+
*
|
|
1049
|
+
* @param {string} text
|
|
1050
|
+
* @param {LedgerRow[]} ledger
|
|
1051
|
+
* @returns {string}
|
|
1052
|
+
*/
|
|
1053
|
+
function redactR4Separators(text, ledger) {
|
|
1054
|
+
const hits = scanR4Leaks(text);
|
|
1055
|
+
if (hits.length === 0) return text;
|
|
1056
|
+
// Splice from the right so earlier indices stay valid.
|
|
1057
|
+
const counts = new Map();
|
|
1058
|
+
let out = text;
|
|
1059
|
+
for (let i = hits.length - 1; i >= 0; i -= 1) {
|
|
1060
|
+
const { start, end, match } = hits[i];
|
|
1061
|
+
counts.set(match, (counts.get(match) ?? 0) + 1);
|
|
1062
|
+
out = `${out.slice(0, start)}<redacted:secret>${out.slice(end)}`;
|
|
1063
|
+
}
|
|
1064
|
+
for (const [before, count] of counts) {
|
|
1065
|
+
ledger.push({ rule: 'secret-token', before, after: '<redacted:secret>', count });
|
|
1066
|
+
}
|
|
1067
|
+
return out;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function redactSecrets(input, ledger) {
|
|
1071
|
+
let text = input;
|
|
1072
|
+
for (const { re } of secretPatterns()) {
|
|
1073
|
+
text = replaceEachDistinct(text, re, '<redacted:secret>', 'secret-token', ledger);
|
|
1074
|
+
}
|
|
1075
|
+
// Round 5 (design amendment R4): a vocabulary key labelling a value
|
|
1076
|
+
// via any separator (markdown table pipe, whitespace, tab, hyphen,
|
|
1077
|
+
// arrow, HTML entity, or the prose bridges is / was / set to) is a
|
|
1078
|
+
// secret. Runs AFTER the primary rule-5 loop so `key=value` and
|
|
1079
|
+
// `Authorization:` shapes already folded (canonical rule names
|
|
1080
|
+
// stay visible in the ledger), and AFTER the dash normalisation in
|
|
1081
|
+
// rule 8a-early so em-dash and other Unicode separators reach this
|
|
1082
|
+
// pass as ASCII hyphens. False positives are visible in the ledger
|
|
1083
|
+
// (design says false positives are acceptable; a labelled value
|
|
1084
|
+
// reaching the body is not) but the mixed-classes gate in
|
|
1085
|
+
// `looksLikeSecretValue` keeps ordinary prose intact.
|
|
1086
|
+
text = redactR4Separators(text, ledger);
|
|
1087
|
+
// F-slice-2-09: keep every distinct before/after pair so the
|
|
1088
|
+
// operator disclosure ledger shows exactly what was stripped.
|
|
1089
|
+
// `replaceEachDistinct` already writes one row per distinct
|
|
1090
|
+
// before-sample WITHIN a single pattern, so two different
|
|
1091
|
+
// `password=A` / `password=B` hits in the same field are two rows.
|
|
1092
|
+
// The fold below dedupes across DIFFERENT patterns that produced
|
|
1093
|
+
// the same before text (e.g. a token caught by both a specific
|
|
1094
|
+
// pattern and the loose entropy-blob) and sums their counts.
|
|
1095
|
+
const folded = [];
|
|
1096
|
+
for (const row of ledger) {
|
|
1097
|
+
if (row.rule !== 'secret-token') {
|
|
1098
|
+
folded.push(row);
|
|
1099
|
+
continue;
|
|
1100
|
+
}
|
|
1101
|
+
const twin = folded.find((r) => r.rule === 'secret-token' && r.before === row.before);
|
|
1102
|
+
if (twin) twin.count += row.count;
|
|
1103
|
+
else folded.push({ ...row });
|
|
1104
|
+
}
|
|
1105
|
+
ledger.length = 0;
|
|
1106
|
+
for (const r of folded) ledger.push(r);
|
|
1107
|
+
return text;
|
|
1108
|
+
}
|