Developer-Dashboard

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

    digest so the Docker Hub build has immutable container inputs.
  - Add a regression assertion for the base-image digest and document the
    immutable image dependency contract.

5.81  2026-10-10

  - Problem 47: add a GitHub Actions Docker Hub build for current master source.
  - Run install.sh, build a fresh Dist::Zilla archive, and install/test that
    exact archive so the image does not ship an older MetaCPAN copy.
  - Publish versioned and latest tags for linux/amd64 and linux/arm64 using
    pinned workflow actions and configured Docker Hub credentials.
  - Document the master-source build and Apple Silicon/Linux arm64 behavior.

5.80  2026-10-10

  - Reissue Problem 46 verification with final 5.79 blank-container install
    and image-version evidence, and record the outstanding repository-level
    Scorecard gates separately from the implementation results.

5.79  2026-10-10

Changes  view on Meta::CPAN


  - Problem 25 follow-up: keep help handling within internal CLI dispatch and
    pass native help flags through to dotted external skill commands.
  - Add a Docker regression fixture for `d2 tira.tasklist.prune --help` and
    document the internal-versus-skill help ownership boundary.

5.50  2026-10-03

  - Problem 35: configure Dist::Zilla's UploadToCPAN releaser so an explicit `dzil release`
    has a real PAUSE upload action; keep the upload separate from `dzil build`
    and document that credentials belong outside the repository.
  - Add a release metadata regression test for the configured Releaser plugin.

5.49  2026-10-03

  - Problem 12, 32, 33, and 34 delivery follow-up: publish the final Docker,
    coverage, package-build, and blank-container integration evidence in the
    numbered problem report.
  - Refresh the release archive to include the completed problem status index.

5.48  2026-10-03

Changes  view on Meta::CPAN

    skill-namespaced web route with static-file containment asserted (DD-416),
    and skill installs whose repo name escapes the skills root (DD-426).
  - Security: every page-derived value the web layer places inside an HTML
    attribute is now escaped for attribute context (DD-421), and saved
    bookmark link segments are percent-encoded for URL context (DD-423).
  - Security: state-changing requests are rejected when Origin or Referer is
    foreign, on every web tier (DD-422).
  - Security: the transient-token denial now depends on the token alone, so a
    file value riding along can no longer bypass the 403 (DD-425).
  - Security: release tarballs no longer carry dogfood QA evidence, ticket
    worktrees (DD-414), or the Hermes runtime state root and its credentials
    (DD-432).
  - Feature: first-class self-upgrade - dashboard upgrade and d2 upgrade run
    the canonical platform installer (DD-386).
  - Feature: a real /favicon.ico is served instead of a 404 on every page
    load (DD-407).
  - Collectors: a timed-out command's full process subtree is terminated
    (DD-388); blocked Windows command collectors are interrupted at their
    timeout (DD-389); the atomic writer uses a per-process pending name so
    concurrent workers stop clobbering each other (DD-418); log append and
    rotation share a per-log lock (DD-399) and rotation cuts on entry

README.md  view on Meta::CPAN

replaces the version that the bootstrap installer may have fetched from
MetaCPAN, so the image contains the current repository code.

The regular container/release gates run the full test and coverage suites
before publishing. The multi-platform image build skips rerunning those suites
under QEMU for each target architecture; the tarball itself is still installed
and its install is verified separately in a blank Docker environment.

The workflow selects the GitHub Actions environment `release`, whose
secrets `DOCKER_HUB_USER` and `DOCKER_HUB_TOKEN` provide the Docker Hub
credentials. It pushes two tags under `DOCKER_HUB_USER/developer-dashboard`:
the distribution version read from `dist.ini` (for example `5.88`) and
`latest`.

A manual workflow dispatch also builds the current `master`
branch. The image targets `linux/amd64` and
`linux/arm64`; Docker does not support a macOS-kernel image platform, while
Apple Silicon can run the Linux arm64 image using Docker Desktop's Linux VM.

The Ubuntu Linux base image is pinned by its multi-platform manifest digest,
and every GitHub Action dependency is pinned to a full commit SHA.

README.md  view on Meta::CPAN

hex digest under `secret` plus an `ajax` array of exact saved Ajax route
paths such as `/ajax/stream.txt` or
`/ajax/example-skill/status.json`. When a non-admin remote request targets one
of those registered `/ajax/...` paths, the caller can send
`X-DD-API-Key: NAME` and `X-DD-API-Secret: RAW-SECRET`.

Developer Dashboard
hashes the raw secret with SHA-256, compares it to the stored digest, and
executes the saved Ajax handler when they match.

Missing or wrong credentials
for a registered API route return `403` with the JSON body
`{"status":"forbidden"}`.

Existing helper-session auth still works on the
same saved Ajax routes, so browser workflows and machine callers can coexist on
one handler without adding a second copy of the route. Like the rest of
`DD-OOP-LAYERS`, runtime `config/api.json` files merge from home to the
deepest active child layer, and installed skills contribute their own layered
`config/api.json` fragments for skill-local saved Ajax routes.

doc/integration-test-plan.md  view on Meta::CPAN

- TT-backed collector icons render from stdout JSON and stay rendered through later config-sync reads instead of reverting to raw `[% ... %]` text
- the web service serves the root editor on `127.0.0.1:7890`
- the browser can load both the editor and a saved fake-project bookmark page from the fake project bookmark directory
- the browser sees sorted shared `nav/*.tt` fragments above the main page body on that fake-project bookmark page
- the browser top-right status strip shows configured collector icons and does not leave stale renamed collector indicators behind
- nested `DD-OOP-LAYERS` collector prompts do not let a child-layer placeholder `missing` state override a healthy inherited parent-layer collector indicator when the child config adds no collector override
- under `DD-OOP-LAYERS`, `dashboard path add` writes only the new child-layer alias delta into the deepest child `config/config.json` instead of copying inherited parent config domains into that file
- bookmark pages can use `fetch_value()`, `stream_value()`, and `stream_data()` helpers against saved `/ajax/...` endpoints on first render
- the installed `/ajax/<file>` route streams early output chunks promptly enough to prove browser-visible progress instead of silent buffering
- skill pages that ship `config/routes.json` emit their declared canonical custom ajax paths, while the smart `/ajax/<repo-name>/...` route still works as the parent compatibility resolver and custom paths stay fallback-only before a normal `404`
- layered `config/api.json` files under both runtime roots and installed skills can authorize selected saved `/ajax/...` routes for machine callers, those callers must present matching `X-DD-API-Key` and `X-DD-API-Secret` headers, helper-session auth...
- the built-in `dashboard api` command can list that effective merged registry, hash raw secrets from `--secret` or `--maybe-secret` before saving them, add and remove exact saved ajax routes, and write only to the deepest writable `config/api.json` ...
- non-loopback access produces `401` with an empty body and without a login page until a helper user exists in the active runtime
- under `dashboard serve --ssl`, plain `http://HOST:PORT/...` requests on the public listener return a same-port `307` redirect to `https://HOST:PORT/...`, the generated cert advertises SAN coverage for `localhost`, `127.0.0.1`, and `::1`, and a brow...
- after a helper user exists, non-loopback access produces the helper login page
- helper logout removes both the helper session and the helper account
- `dashboard stop` leaves no active listener on port `7890`
- `dashboard stop` and `dashboard restart` still control the real serving pid
  when the web process has renamed itself into a `starman master` listener
  shape, so container lifecycle checks stay attached to the active listener
- interactive `dashboard stop` and `dashboard restart` runs print the full lifecycle task board on `stderr` before work begins, so managed shutdown and startup waits stay visible instead of looking hung

doc/integration-test-plan.md  view on Meta::CPAN


The separate Docker Hub image workflow builds from the master checkout, runs
`install.sh`, builds the Dist::Zilla source archive, installs that exact
archive into the runtime image with `cpanm --notest`, and publishes version
and `latest` tags for Linux amd64 and arm64. Full test and coverage gates must
pass before publication; rerunning the full suite under QEMU for each target
architecture duplicated that work and caused the 120-minute workflow timeout.
The exact tarball still receives a tested `cpanm` install in the blank Docker
environment above. The workflow contract is covered by
`t/228-dockerhub-image-workflow.t`; Docker Hub publication itself requires
GitHub-managed credentials and is verified in the Actions run.

The run passes when:

- the container exits `0`
- the app under test comes only from the host-built tarball
- the installed `dashboard` CLI completes the scripted fake-project flow from the mounted tarball install
- Chromium verifies the editor, saved bookmark page, outsider disabled-access page, and helper login page. Before a helper user exists, the remote request must remain a body-free 401; headless Chromium may render its generic `HTTP ERROR 401` page or ...
- the web lifecycle and helper browser flow behave as expected

doc/problem-48-performance-research.md  view on Meta::CPAN


| Chapter | Applicability and evidence |
| --- | --- |
| V1 Architecture | Applicable: caches are bounded/scoped and preserve explicit freshness boundaries; direct route parity benchmarks and mutation tests. |
| V2 Authentication | Applicable, Level 3: PBKDF2 native adapter retains iteration count and Digest::SHA output; `t/651` checks native/Perl parity and unavailable/error paths. |
| V3 Session Management | No session lifecycle change; existing session and web-security tests pass in the complete suite. |
| V4 Access Control | No authorization rule change; protected-route/auth regressions remain green. |
| V5 Validation, Sanitization and Encoding | Applicable: XS escape paths preserve UTF-8/taint and match Perl output; `t/665` and static-file/security tests. |
| V6 Stored Cryptography | Applicable, Level 3: FFI inputs and OpenSSL ABI bounds are validated; native failure is visible; no reduced work factor or password-result cache. |
| V7 Error Handling and Logging | Applicable: failed native calls and filesystem I/O remain visible; warnings are fatal in tests; close and race failures have explicit assertions. |
| V8 Data Protection | Applicable: no cache persists credentials; status reuse retains high-resolution metadata checks and permission behavior is covered by existing tests. |
| V9 Communication | No transport/TLS behavior changed; `t/17-web-server-ssl.t` passed. |
| V10 Malicious Code | Applicable: XS is distribution-built C with no runtime compiler or shell execution; forbidden-import scan found no production imports/calls. |
| V11 Business Logic | Applicable: response reuse occurs only after unchanged config, inventory, and file-signature checks; changed persisted state is returned on the next request. |
| V12 Files and Resources | Applicable: paths derive from configured roots and discovered names; inventory changes, atomic replacement, missing roots, and I/O errors are tested. |
| V13 API and Web Service | Applicable: `/system/status` response caching preserves exact bytes and invalidates on persisted changes; web-security route tests passed. |
| V14 Configuration | Applicable: collector configuration signature invalidates cached work; current configuration remains authoritative and no new setting was introduced. |

Top 10 cross-check: A01/A07 access-control and identity rules were unchanged
and remain covered by auth tests; A02 cryptographic output and work factor
are pinned by `t/651`; A03 injection/encoding is covered by XS parity and web

doc/problem-report.md  view on Meta::CPAN

`cpanm`. It also guards that GitHub automation files, excluded from the CPAN
archive, do not make the packaged test suite fail, and that the shared Docker
context re-includes the tarball needed by the repository's local image build.

Implementation: `.github/workflows/dockerhub-image.yml` checks out `master`,
validates the X.XX distribution version, logs in to Docker Hub, and uses
Buildx/QEMU to publish a multi-platform manifest. Its Dockerfile runs
`install.sh`, installs the required Dist::Zilla plugins, builds the current
checkout, and tests/reinstalls the exact versioned tarball so an equal-version
MetaCPAN copy cannot shadow it. `.dockerignore` keeps local runtime state,
credentials, and build artifacts out of the image context. Detailed usage and
platform expectations are documented in the repository manual.

Verification: the full repository suite passed in the isolated Docker dev
container (259 files, 23,312 tests), with statement, subroutine, branch, and
condition coverage each measured at 100%. The Docker image build also passed;
its packaged cpanm install ran without `--notest` and passed (259 files, 16,985
tests), and an isolated Compose smoke test reported `d2 version` 5.81. The
focused release/security container gate passed (8 files, 6,438 tests), covering
web update/static/SSL checks, release metadata, this workflow contract,
kwalitee, POD syntax, and CPAN security metadata. `perlsec` was reviewed, and

doc/problem-report.md  view on Meta::CPAN

workflow was triggered by the first push and its final result is being checked;
5.83 will run the multi-platform publish after the package and local image
gates pass and it is pushed.

Security review for Problems 46-47 (ASVS 5.0): V1, V4, V5, V7, V8, V10,
V11, V12, and V14 were applicable. The workspace service name is fixed, user
aliases remain argv values rather than shell source, the private overlay rejects
symlinks and non-owner directories, and startup/exec errors remain visible.
The image workflow requests read-only repository contents, pins every external
action to a full commit SHA and the Ubuntu base to its multi-platform digest,
and passes Docker Hub credentials only to the login action from the `release`
environment; credentials are not build arguments or context files. V2, V3, V6,
V9, and V13 (authentication, sessions, cryptography, transport, and application
API) have no changed runtime surface. OWASP A03, A04, A05, A08, and A09 were
reviewed for command construction, container reuse, workflow configuration,
source/image integrity, and visible failure reporting. A01, A02, A06, A07, and
A10 do not gain a changed access-control, cryptography, dependency, identity,
or request-forgery surface. Required security scans and web/static/SSL tests
passed in Docker; post-push Scorecard governance findings are tracked separately
from code-side security controls.

#### Problem 47 timeout correction (2026-10-10; release version 5.84)

doc/problem-report.txt  view on Meta::CPAN

main user documentation. Focused Docker verification passes
`t/103-collectorrunner-coverage.t` (335 tests). The clean full coverage gate
passes 257 files / 22,259 tests with 100.0% statement, branch, condition, and
subroutine coverage.

Problem 27/28 shared delivery verification:
Docker reproduction/verification: `d2 docker compose --project-name dd-problem2728-regression -f /tmp/dd-p27-28-compose.yml up --build --abort-on-container-exit --exit-code-from regression regression` passed the focused CLI/open-file/collector regres...

Full host regression: `prove -lr t` passed (257 files, 22,806 tests). Its tarball-dependent guard was skipped because the tarball had not yet been built. After packaging, `t/44-smart-router-two-stage.t` was retried and skipped with the exact reason `...

Post-version targeted checks: `prove -lv t/15-release-metadata.t t/98-cli-openfile-coverage.t t/103-collectorrunner-coverage.t t/69-cli-complete-coverage.t` passed (4 files, 5,928 tests). Required web/security checks passed (3 files, 459 tests). Requ...

Release verification: the first 5.40 archive audit found root `cover_db_*` databases in the tarball; the previous gather rule excluded only the exact `cover_db` name. Added a red-first release-metadata assertion and extended both gather exclusion and...

Delivery commit: the Problem 27/28 code was committed as `78d6573d`. The 5.41 archive-hygiene follow-up is committed separately. No push was requested.

Problem 31: Honor Docker service markers at every layer (status: complete; release 5.44; selected-home marker write rule reconfirmed 5.45)
Expected happy path: any active `config/docker/<service>/disabled.yml` marker
disables that service; any `develop.yml` marker enables its development overlay.
`docker enable <service>` removes every matching `disabled.yml`, and
`docker development disable <service>` removes every matching `develop.yml`.

doc/problem-report.txt  view on Meta::CPAN

same known warnings for temporary fixture files removed before digesting.
The 5.49 archive was built, `d2 docker.images.build` completed, and a fresh
image returned version 5.49. The blank-container rerun installed that archive
using `cpanm` without `--notest`, passed its distribution suite, and ended with
“Blank-environment integration run passed.” Thus the 5.49 release gates and
problem fixes are committed; no push was performed.

Problem 35: Configure a real Dist::Zilla releaser for dzil release
Expected outcome: `dzil release` must find a real Dist::Zilla Releaser plugin
instead of exiting with “you can't release without any Releaser plugins.” The
release action must be an explicit PAUSE upload, and no credentials may be
stored in the repository.
Reproduction: run `dzil release` with the existing dist.ini. It exits before
building or uploading because the config contains build/gather plugins only
and no class with the Dist::Zilla::Role::Releaser role.
Red test: `t/15-release-metadata.t` asserted that dist.ini contains the real
`[UploadToCPAN]` stanza; before the fix this assertion failed (1 of 5,396).
Fix: configure `[UploadToCPAN]`, document that `dzil release` uploads to PAUSE,
and keep credentials in user-owned `~/.dzil/config.ini` or `~/.pause` only.
The test now passes. Host `dzil authordeps` lists
`Dist::Zilla::Plugin::UploadToCPAN`; direct Dist::Zilla configuration
inspection finds `UploadToCPAN (Dist::Zilla::Plugin::UploadToCPAN)` under
`-Releaser`; and `dzil build` succeeds. We did not execute `dzil release`,
because that would upload externally.
The initial full gate found stale 5.49 artifacts and correctly failed the
single-archive hygiene assertion. `dzil clean` was run against the prior
version configuration to clean generated artifacts; after rebuilding only
5.50, `t/36-release-kwalitee.t` passed 7/7. The clean 5.50 full Docker gate
passed 257 files / 22,733 tests and all four library metrics at 100.0% (45,335

doc/problem-report.txt  view on Meta::CPAN

existing layered disable/development rules, then runs a final config when
service overlays were selected. Existing base/project/service/addon/mode
precedence is maintained. Compose-returned service names must be non-empty
single path segments; `.`/`..` and names containing path separators are
rejected before filesystem lookup. Dry-run remains non-executing and is
documented as a source-based preview.

Security review for this change: the command still passes argument vectors to
`system` and does not compose a shell string; config-derived service names only
select existing runtime-root paths. No new dependencies, SQL, HTTP routes,
credentials, or authentication behavior were introduced. The fresh full Docker
gate passed 258 files / 22,609 tests at 100.0% statement, branch, condition,
and subroutine coverage (45,787 detail rows and no stale uncoverable
annotations). The required focused web-security trio passed 459 tests.
`perlsec` was reviewed from the container's `perlsec.pod` with `pod2text`
because `perldoc` has no formatter. The 5.65 archive passed CPANTS kwalitee
(100%, 7/7 assertions) and POD syntax (348 source files, 355 assertions).
`dzil build` produced `Developer-Dashboard-5.65.tar.gz`; its image reported
version 5.65 in a one-off Compose run. A blank-environment container installed
the archive with `cpanm` without `--notest` and passed the full integration
script. Required security searches found no Problem 40 shell-string execution

doc/problem-report.txt  view on Meta::CPAN

5.81 push started the Docker Hub workflow; its completion and the 5.83
multi-platform run remain to be verified after the package and local image gates
pass.

Security review for Problems 46-47 (ASVS 5.0): V1, V4, V5, V7, V8, V10, V11,
V12, and V14 were applicable. The workspace service name is fixed, user aliases
remain argv values rather than shell source, the private overlay rejects
symlinks and non-owner directories, and startup/exec errors remain visible.
The image workflow requests read-only repository contents, pins every external
action to a full commit SHA and the Ubuntu base to its multi-platform digest,
and passes Docker Hub credentials only to the login action from the `release`
environment; credentials are not build arguments or context files. V2, V3, V6,
V9, and V13 (authentication, sessions, cryptography, transport, and application
API) have no changed runtime surface. OWASP A03, A04, A05, A08, and A09 were
reviewed for command construction, container reuse, workflow configuration,
source/image integrity, and visible failure reporting. A01, A02, A06, A07, and
A10 do not gain a changed access-control, cryptography, dependency, identity,
or request-forgery surface. Required security scans and web/static/SSL tests
passed in Docker; post-push Scorecard governance findings are tracked separately
from code-side security controls.

Problem 47 timeout correction (2026-10-10; release version 5.84)

doc/security.md  view on Meta::CPAN

- home-runtime directories under `~/.developer-dashboard` are created and tightened to `0700`
- home-runtime files under `~/.developer-dashboard` are written and tightened to `0600`, while owner-executable scripts stay at `0700`
- helper access requires a stored helper account
- helper usernames are restricted to safe filename characters
- helper passwords must be at least 8 characters long
- helper user files and helper session files are written with `0600` permissions
- helper sessions are bound to the originating remote address
- helper sessions expire automatically after 12 hours
- session cookies use `HttpOnly` and `SameSite=Strict`
- HTTP responses add `Content-Security-Policy`, `X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`, and `Cache-Control: no-store`
- state-changing requests (`POST`, `PUT`, `DELETE`, `PATCH`) are refused with an empty-bodied `403` when `Origin` or `Referer` names anything other than this dashboard or a trusted local alias; the check runs before trust-tier classification, so it c...
- that `Origin` check cannot defend a `GET`, so the same choke point additionally refuses, **on every method**, any request the browser labelled `Sec-Fetch-Site: cross-site` or `same-site` unless the accompanying `Origin`/`Referer` names this dashboa...
- every page-derived value placed inside a quoted HTML attribute (the page chrome's play, source, and share URLs, the bookmark editor's form action, nav ids, and the host link) is escaped in attribute context, so both quote characters are neutralised...
- generated saved-bookmark links percent-encode every path segment of the bookmark id (URL-context output encoding, with `/` separators kept raw), so ids carrying `#`, `?`, `%`, spaces, or quote characters stay valid, reachable URLs and reach the att...

## Cross-Site Request Defense

The dashboard's automatic loopback-admin tier authorizes on the remote address
alone. There is no cookie in that decision, so `SameSite=Strict` protects
nothing there: any page the operator happens to visit can aim a request at
`127.0.0.1` and it arrives already authorized. Two independent checks close

doc/security.md  view on Meta::CPAN

`actions/checkout` survives and `actions-setup-perl` v1.31.3 does not: it fails
with `Error: unable to get latest version`. The `Setup Perl` step therefore
failed on every CI run for ten days, skipping the suite, the coverage gate and
both dependency audits, while the guardrail test read the comments and certified
the migration as complete. A version floor read from a comment cannot detect a
comment that lies, which is why the resolving check is a separate gate.

Runtime declarations are part of this review. Every pinned action now declares
`node24`, so the jobs no longer set `FORCE_JAVASCRIPT_ACTIONS_TO_NODE24`,
GitHub's transitional shim for rerouting `node20` actions. Steps that handle
credentials — the GHCR login in particular — are held on `node24`-native release
lines, and `t/34-scorecard-guardrails.t` gates those version floors rather than
only the pin format.

## Release Provenance

A `vX.XX` tag push publishes the distribution tarball, its SHA-256 checksum, and
a detached GPG signature. Those three assets prove integrity and authorship, but
they say nothing about *where the artifact came from*, so the release also
carries SLSA build provenance generated by `actions/attest-build-provenance`.

doc/update-and-release.md  view on Meta::CPAN

Recovering a tag this way still runs the full suite and the coverage gate
against the tagged tree, so a tag whose code cannot pass those gates stays
unpublishable by design — that is the second, separate reason `v4.24` cannot
be recovered, and it is not something the dispatch path is meant to bypass.

The GitHub-hosted CPAN upload workflow is deliberately manual-only. Do not
wire tag pushes to automatic PAUSE uploads here; ordinary `vX.XX` tags are for
the signed GitHub release path, while CPAN publication stays an explicit
`workflow_dispatch`, local `dashboard pause-release`, or local `dzil release`
operator action. The repository configures `Dist::Zilla::Plugin::UploadToCPAN`
as the real releaser for `dzil release`. Configure credentials only in
`~/.dzil/config.ini` or `~/.pause`, never in tracked repository files. Run
`dzil release` only after all release gates pass and an operator explicitly
intends to upload to PAUSE.

## OWASP Gate

The repository also treats OWASP as a hard release gate.

This is not limited to a baseline ASVS spot check anymore. Before build,
release, or security-sensitive closeout, complete the full OWASP ASVS 5.0.0

integration/windows/run-qemu-windows-smoke.sh  view on Meta::CPAN

  # Input: the configured Dockur workdir path.
  # Output: deletes the persisted storage, shared, and OEM subdirectories.
  rm -rf \
    "$WINDOWS_DOCKUR_WORKDIR/storage" \
    "$WINDOWS_DOCKUR_WORKDIR/shared" \
    "$WINDOWS_DOCKUR_WORKDIR/oem"
}

run_prepared_qemu_smoke() {
  # Purpose: boot a prepared Windows image and run the Strawberry smoke over SSH.
  # Input: a prepared qcow2 image, SSH credentials, and a built tarball.
  # Output: exits zero only when the in-guest Windows smoke passes.
  if [[ -z "$WINDOWS_IMAGE" ]]; then
    echo "WINDOWS_IMAGE is required and must point to a prepared Windows qcow2 image" >&2
    exit 1
  fi

  if [[ ! -f "$WINDOWS_IMAGE" ]]; then
    echo "Windows image does not exist: $WINDOWS_IMAGE" >&2
    exit 1
  fi

lib/Developer/Dashboard.pm  view on Meta::CPAN

replaces the version that the bootstrap installer may have fetched from
MetaCPAN, so the image contains the current repository code.

The regular container/release gates run the full test and coverage suites
before publishing. The multi-platform image build skips rerunning those suites
under QEMU for each target architecture; the tarball itself is still installed
and its install is verified separately in a blank Docker environment.

The workflow selects the GitHub Actions environment C<release>, whose
secrets C<DOCKER_HUB_USER> and C<DOCKER_HUB_TOKEN> provide the Docker Hub
credentials. It pushes two tags under C<DOCKER_HUB_USER/developer-dashboard>:
the distribution version read from C<dist.ini> (for example C<5.88>) and
C<latest>.

A manual workflow dispatch also builds the current C<master>
branch. The image targets C<linux/amd64> and
C<linux/arm64>; Docker does not support a macOS-kernel image platform, while
Apple Silicon can run the Linux arm64 image using Docker Desktop's Linux VM.

The Ubuntu Linux base image is pinned by its multi-platform manifest digest,
and every GitHub Action dependency is pinned to a full commit SHA.

lib/Developer/Dashboard.pm  view on Meta::CPAN

hex digest under C<secret> plus an C<ajax> array of exact saved Ajax route
paths such as C</ajax/stream.txt> or
C</ajax/example-skill/status.json>. When a non-admin remote request targets one
of those registered C</ajax/...> paths, the caller can send
C<X-DD-API-Key: NAME> and C<X-DD-API-Secret: RAW-SECRET>.

Developer Dashboard
hashes the raw secret with SHA-256, compares it to the stored digest, and
executes the saved Ajax handler when they match.

Missing or wrong credentials
for a registered API route return C<403> with the JSON body
C<{"status":"forbidden"}>.

Existing helper-session auth still works on the
same saved Ajax routes, so browser workflows and machine callers can coexist on
one handler without adding a second copy of the route. Like the rest of
C<DD-OOP-LAYERS>, runtime C<config/api.json> files merge from home to the
deepest active child layer, and installed skills contribute their own layered
C<config/api.json> fragments for skill-local saved Ajax routes.

lib/Developer/Dashboard/PageDocument.pm  view on Meta::CPAN

  if (!window.__dashboardAjaxSingletons) window.__dashboardAjaxSingletons = {};
  if (window.__dashboardAjaxSingletons[name]) return;
  window.__dashboardAjaxSingletons[name] = true;
  window.addEventListener('pagehide', function() {
    let url = '/ajax/singleton/stop?singleton=' + encodeURIComponent(name);
    if (navigator.sendBeacon) {
      navigator.sendBeacon(url, '');
      return;
    }
    if (window.fetch) {
      fetch(url, { method: 'POST', keepalive: true, credentials: 'same-origin' }).catch(function () {});
    }
  });
}
function dashboard_target_nodes(target) {
  if (!target) return [];
  if (typeof target === 'string') return Array.prototype.slice.call(document.querySelectorAll(target));
  if (target instanceof Element) return [target];
  if (target.length && typeof target !== 'string') return Array.prototype.slice.call(target);
  return [];
}

lib/Developer/Dashboard/PageDocument.pm  view on Meta::CPAN

    if (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA') {
      node.value = rendered;
      return;
    }
    node.textContent = rendered;
  });
  return rendered;
}
function fetch_value(url, target, options, formatter) {
  if (!url || !window.fetch) return Promise.resolve('');
  let settings = Object.assign({ credentials: 'same-origin' }, (options && options.fetch) || {});
  return window.fetch(url, settings).then(function(response) {
    if (!response.ok) throw new Error('Request failed with status ' + response.status);
    if (options && options.type === 'json') return response.text();
    return response.text();
  }).then(function(value) {
    return dashboard_write_target(target, value, options || {}, formatter);
  });
}
function dashboard_stream_settings(options) {
  let fetchOptions = (options && options.fetch) || {};
  let method = fetchOptions.method || options.method || 'GET';
  let body = typeof fetchOptions.body !== 'undefined' ? fetchOptions.body : (typeof options.body !== 'undefined' ? options.body : null);
  let headers = fetchOptions.headers || options.headers || {};
  let credentials = fetchOptions.credentials || options.credentials || 'same-origin';
  return {
    method: method,
    body: body,
    headers: headers,
    credentials: credentials
  };
}
function stream_data(url, target, options, formatter) {
  if (!url) return Promise.resolve('');
  if (!window.XMLHttpRequest) return fetch_value(url, target, options, formatter);
  let settings = dashboard_stream_settings(options || {});
  return new Promise(function(resolve, reject) {
    let xhr = new XMLHttpRequest();
    xhr.open(settings.method, url, true);
    xhr.withCredentials = settings.credentials !== 'omit';
    Object.keys(settings.headers || {}).forEach(function(name) {
      xhr.setRequestHeader(name, settings.headers[name]);
    });
    xhr.onprogress = function () {
      dashboard_write_target(target, xhr.responseText, options || {}, formatter);
    };
    xhr.onload = function () {
      if (xhr.status < 200 || xhr.status >= 300) {
        reject(new Error('Request failed with status ' + xhr.status));
        return;

lib/Developer/Dashboard/Web/App.pm  view on Meta::CPAN

# forbidden header name, so page script can neither forge nor suppress it.
# `same-origin` is the dashboard's own page and `none` is a user-initiated load
# (typed URL, bookmark, browser start-up), so both stay outside the check.
my %FOREIGN_FETCH_SITES = map { $_ => 1 } qw(cross-site same-site);

# _csrf_rejection_response(%args)
# Cross-site request forgery choke point for every request. Browsers
# unconditionally attach an Origin header to cross-site state-changing
# requests (and legacy flows carry a Referer), so a foreign Origin/Referer
# identifies an attack regardless of trust tier: the loopback-admin shortcut,
# an ambient helper session cookie, and even valid machine credentials must all
# refuse to act on it. Requests carrying neither header are allowed because
# non-browser machine clients (curl, registered x-dd-api-key consumers) send
# neither, while a hostile page cannot suppress the Origin header on a
# cross-site state-changing request. A GET needs the separate fetch-metadata
# check, which runs first and on every method.
# Input: normalized request method and headers (host, origin, referer,
# sec-fetch-site).
# Output: 403 empty-body response array reference when the browser context is
# foreign, otherwise undef and the request proceeds to tier dispatch.
sub _csrf_rejection_response {

lib/Developer/Dashboard/Web/App.pm  view on Meta::CPAN


Cross-site request forgery defense: every state-changing request (POST, PUT,
DELETE, PATCH) passes one origin check before any trust-tier dispatch. When
the request carries an C<Origin> header (or, absent that, a C<Referer>), its
authority must equal the request's own C<Host> header or name a trusted local
alias — a numeric loopback literal, the localhost hostname family, or a
configured C<web.ssl_subject_alt_names> entry, the same alias semantics the
loopback-admin trust check applies. Anything else, including the opaque
C<Origin: null>, is refused with an empty 403 on every tier: the
loopback-admin shortcut, helper sessions, and the machine API tier alike,
because browsers attach ambient credentials and loopback reachability to
cross-site requests automatically. Requests with neither header keep working,
since non-browser machine clients send neither while browsers always attach
C<Origin> to cross-site state-changing requests. The check also holds behind
the SSL front-proxy, which forwards TLS bytes unmodified, so the backend
compares against the browser's own C<Host> header.

That origin check cannot defend a C<GET>, because browsers omit C<Origin> on
same-origin GETs and a hostile page can drop its C<Referer> with a referrer
policy — yet C</ajax/E<lt>fileE<gt>> runs an operator-written saved handler as
a child process from a plain GET, on a tier that needs no cookie. So the same

t/08-web-update-coverage.t  view on Meta::CPAN

    print {$skill_api} qq|{"skill-client":{"secret":"@{[ sha256_hex('skill-secret') ]}","ajax":["/ajax/dancer-route-skill/bar"]}}|;
    close $skill_api or die "Unable to close skill api.json: $!";

    my $api_host = 'dashboard-helper.example:7890';
    my ( $registered_no_api_code, $registered_no_api_type, $registered_no_api_body ) = @{ $app->handle(
        path        => '/ajax/stream.txt',
        query       => 'type=text',
        remote_addr => '127.0.0.1',
        headers     => { host => $api_host },
    ) };
    is( $registered_no_api_code, 403, 'registered remote ajax route rejects requests that omit API credentials' );
    like( $registered_no_api_type, qr/application\/json/, 'registered remote ajax route returns JSON when API credentials are missing' );
    is_deeply( json_decode( decode_body_text($registered_no_api_body) ), { status => 'forbidden' }, 'registered remote ajax route returns the explicit forbidden JSON payload when API credentials are missing' );

    my ( $registered_bad_api_code, undef, $registered_bad_api_body ) = @{ $app->handle(
        path        => '/ajax/stream.txt',
        query       => 'type=text',
        remote_addr => '127.0.0.1',
        headers     => {
            host            => $api_host,
            'x-dd-api-key'    => 'runtime-client',
            'x-dd-api-secret' => 'wrong-secret',
        },

t/08-web-update-coverage.t  view on Meta::CPAN

    my ( $skill_api_code, $skill_api_type, $skill_api_body ) = @{ $app->handle(
        path        => '/ajax/dancer-route-skill/bar',
        query       => '',
        remote_addr => '127.0.0.1',
        headers     => {
            host              => $api_host,
            'x-dd-api-key'    => 'skill-client',
            'x-dd-api-secret' => 'skill-secret',
        },
    ) };
    is( $skill_api_code, 200, 'installed skill config/api.json can authorize its ajax routes through API credentials' );
    like( $skill_api_type, qr/application\/json/, 'skill API auth keeps the skill ajax response content type' );
    is( decode_body_text( drain_stream_body($skill_api_body) ), qq|{"route":"dancer-top"}\n|, 'installed skill API auth streams the skill ajax response body' );

    my $psgi_app = Developer::Dashboard::Web::DancerApp->build_psgi_app( app => $app );
    Local::PSGITest::test_psgi $psgi_app, sub {
        my ($cb) = @_;
        my $res = $cb->(
            GET(
                'http://127.0.0.1/ajax/stream.txt?type=text',
                Host              => $api_host,

t/101-web-app-coverage.t  view on Meta::CPAN

        actions  => $actions,
        auth     => $auth,
        config   => $api_config,
        pages    => $store,
        prompt   => $prompt,
        runtime  => $runtime,
        sessions => $sessions,
    );
    my $ah = 'helper.example:7890';

    # registered ajax route, no credentials -> forbidden (api path, no session)
    is( $app_api->handle( path => '/ajax/demo.json', query => 'type=json', remote_addr => '203.0.113.7', headers => { host => $ah } )->[0], 403, 'registered api route without creds -> 403' );
    # registered ajax route with valid credentials -> 200
    is( $app_api->handle( path => '/ajax/demo.json', query => 'type=json', remote_addr => '203.0.113.7', headers => { host => $ah, 'x-dd-api-key' => 'good', 'x-dd-api-secret' => 'sekret' } )->[0], 200, 'registered api route with creds -> 200' );
    # unregistered route, no helper users yet -> helper access disabled (401 empty)
    is( $app_api->handle( path => '/app/welcome', query => '', remote_addr => '203.0.113.7', headers => { host => $ah } )->[0], 401, 'outsider before helper users -> 401' );

    # create a helper user; now outsiders get a login page
    $auth->add_user( username => 'helper', password => 'helper-pass-123', role => 'helper' );
    is( $app_api->handle( path => '/app/welcome', query => 'x=1', remote_addr => '203.0.113.7', headers => { host => $ah } )->[0], 401, 'outsider after helper users -> login 401' );

    # bad + good login
    is( $app_api->handle( path => '/login', method => 'POST', body => 'username=helper&password=wrong', remote_addr => '203.0.113.7', headers => { host => $ah } )->[0], 401, 'bad login -> 401' );

t/105-web-app-coverage-2.t  view on Meta::CPAN

        actions  => $actions,
        auth     => $auth,
        config   => $api_config,
        pages    => $store,
        prompt   => $prompt,
        runtime  => $runtime,
        sessions => $sessions,
    );
    my $ah = 'helper.example:7890';

    # registered ajax route, no credentials -> forbidden (api path, no session)
    is( $app_api->handle( path => '/ajax/demo.json', query => 'type=json', remote_addr => '203.0.113.7', headers => { host => $ah } )->[0], 403, 'registered api route without creds -> 403' );
    # registered ajax route with valid credentials -> 200
    is( $app_api->handle( path => '/ajax/demo.json', query => 'type=json', remote_addr => '203.0.113.7', headers => { host => $ah, 'x-dd-api-key' => 'good', 'x-dd-api-secret' => 'sekret' } )->[0], 200, 'registered api route with creds -> 200' );
    # unregistered route, no helper users yet -> helper access disabled (401 empty)
    is( $app_api->handle( path => '/app/welcome', query => '', remote_addr => '203.0.113.7', headers => { host => $ah } )->[0], 401, 'outsider before helper users -> 401' );

    # create a helper user; now outsiders get a login page
    $auth->add_user( username => 'helper', password => 'helper-pass-123', role => 'helper' );
    is( $app_api->handle( path => '/app/welcome', query => 'x=1', remote_addr => '203.0.113.7', headers => { host => $ah } )->[0], 401, 'outsider after helper users -> login 401' );

    # bad + good login
    is( $app_api->handle( path => '/login', method => 'POST', body => 'username=helper&password=wrong', remote_addr => '203.0.113.7', headers => { host => $ah } )->[0], 401, 'bad login -> 401' );

t/130-csrf-origin-defense.t  view on Meta::CPAN

            host        => $HELPER_HOST,
            origin      => 'http://evil.example',
        );
        is( $proxied_foreign_code, 403, 'a foreign-Origin login POST through the SSL front-proxy is rejected' );
    }
}

# ---------------------------------------------------------------------------
# 3. Ajax x-dd-api-key machine tier. Machine clients send no Origin/Referer
#    and must keep working; a browser-carried foreign Origin must be rejected
#    even when the request presents valid API credentials.
# ---------------------------------------------------------------------------
{
    wfile(
        File::Spec->catfile( $paths->dashboards_root, 'legacy-ajax' ),
        "TITLE: LA\n:--------------------------------------------------------------------------------:\nBOOKMARK: legacy-ajax\n:--------------------------------------------------------------------------------:\nHTML: <script>var configs={};</script>\...
        0644,
    );
    request( path => '/app/legacy-ajax', remote_addr => '127.0.0.1', host => $ADMIN_HOST );

    my $api_json = sprintf '{"machine":{"secret":"%s","ajax":["/ajax/demo.json"]}}', sha256_hex('sekret');

t/130-csrf-origin-defense.t  view on Meta::CPAN

    my ($machine_code) = request(
        app         => $api_app,
        path        => '/ajax/demo.json',
        method      => 'POST',
        query       => 'type=json',
        remote_addr => '203.0.113.7',
        host        => $HELPER_HOST,
        api_key     => 'machine',
        api_secret  => 'sekret',
    );
    is( $machine_code, 200, 'a headerless machine POST with valid api credentials still works' );

    my ( $code, $body ) = request(
        app         => $api_app,
        path        => '/ajax/demo.json',
        method      => 'POST',
        query       => 'type=json',
        remote_addr => '203.0.113.7',
        host        => $HELPER_HOST,
        api_key     => 'machine',
        api_secret  => 'sekret',
        origin      => 'http://evil.example',
    );
    is( $code, 403, 'foreign-Origin POST is rejected on the api tier even with valid credentials' );
    is( $body, '', 'the api-tier cross-site rejection body is empty, not the api-forbidden JSON' );

    ($code) = request(
        app         => $api_app,
        path        => '/ajax/demo.json',
        method      => 'POST',
        query       => 'type=json',
        remote_addr => '203.0.113.7',
        host        => $HELPER_HOST,
        api_key     => 'machine',

t/130-csrf-origin-defense.t  view on Meta::CPAN

        app         => $api_app,
        path        => '/ajax/demo.json',
        method      => 'POST',
        query       => 'type=json',
        remote_addr => '203.0.113.7',
        host        => $HELPER_HOST,
        api_key     => 'machine',
        api_secret  => 'sekret',
        origin      => 'http://helper.example:7890',
    );
    is( $code, 200, 'a same-origin POST with valid api credentials still works' );
}

# ---------------------------------------------------------------------------
# 4. Configured SAN aliases are permitted origins. web.ssl_subject_alt_names
#    entries are the operator's declared alternate names for this dashboard,
#    so a POST whose Origin names one of them is same-site, mirroring the
#    loopback-alias semantics the trust tier already applies to Host.
# ---------------------------------------------------------------------------
{
    $config->save_global_web_settings( ssl_subject_alt_names => ['mybox.local'] );

t/130-csrf-origin-defense.t  view on Meta::CPAN

t/130-csrf-origin-defense.t - cross-site requests cannot change dashboard state

=head1 PURPOSE

This test is the cross-site request forgery contract for the web layer. Every
state-changing request (POST, PUT, DELETE, PATCH) that carries a foreign
browser context — an Origin header naming another site, an opaque
C<Origin: null>, or, when Origin is absent, a Referer naming another site —
must be refused with an empty 403 before any tier logic runs. The check must
hold identically on the loopback-admin shortcut, on an authenticated helper
session, and on the ajax machine tier even when valid API credentials are
presented, because a browser attaches cookies and the ambient loopback trust
automatically to cross-site requests.

=head1 WHY IT EXISTS

DD-422 closed the gap DD-421 documented: the loopback-admin tier authorizes
with no cookie at all, so any web page an operator visited could fire
state-changing requests at 127.0.0.1 and they would execute with admin trust.
Browsers always attach an Origin header to cross-site state-changing
requests, so rejecting foreign origins at one choke point blocks that class

t/142-action-pin-provenance.t  view on Meta::CPAN

node24. C<actions/checkout> survives that; C<shogo82148/actions-setup-perl>
v1.31.3 fails with C<Error: unable to get latest version>. The C<Setup Perl>
step failed on every CI run for ten days - skipping the suite, the coverage gate
and both dependency audits - while the guardrail test certified the migration as
complete. C<script/audit-action-pins> is the check that resolves the SHA, and
this file is what keeps that check honest.

=head1 WHEN TO USE

Run it whenever C<script/audit-action-pins> changes, and as part of the full
suite. It needs no network and no credentials.

=head1 HOW TO USE

    prove -lv t/142-action-pin-provenance.t
    prove -lr t

=head1 WHAT USES IT

The full C<prove -lr t> gate, and the CI workflow that runs it.

t/228-dockerhub-image-workflow.t  view on Meta::CPAN

__END__

=head1 NAME

t/228-dockerhub-image-workflow.t - enforce the Docker Hub image build and publish contract

=head1 PURPOSE

This repository-only test locks down the Docker Hub publication path for the
latest checked-out master source. It checks that the GitHub workflow uses the configured Docker Hub
credentials, publishes version and latest tags for Linux amd64 and arm64, and
that the Dockerfile runs the repository installer, creates a Dist::Zilla
archive, and installs that archive into the resulting CLI image.

=head1 WHY IT EXISTS

The CPAN release artifact can lag behind the code on master. A workflow that
simply installs the MetaCPAN package would therefore publish an image with old
code while labeling it current. These assertions keep the image build anchored
to the checkout and its freshly generated tarball.

t/34-scorecard-guardrails.t  view on Meta::CPAN

like( $codeql, qr/uses:\s*github\/codeql-action\/analyze\@[0-9a-f]{40}/, 'CodeQL analyze action is pinned by full SHA' );

my $package_workflow = _slurp('.github/workflows/package-ghcr.yml');
unlike( $package_workflow, qr/^permissions:\s*$(?:\n^[^\n]*:\s*write\s*$)+/ms, 'packaging workflow does not use top-level write permissions' );
like( $package_workflow, qr/jobs:\n\s+package:\n(?:.+\n)*?\s+permissions:\n(?:.+\n)*?\s+packages:\s*write\b/ms, 'packaging workflow grants package publish access only at the job level' );
like( $package_workflow, qr/ghcr\.io/i, 'packaging workflow publishes to GHCR' );
like( $package_workflow, qr/uses:\s*docker\/build-push-action\@[0-9a-f]{40}/, 'docker build-push action is pinned by full SHA' );
like( $package_workflow, qr/uses:\s*docker\/login-action\@[0-9a-f]{40}/, 'docker login action is pinned by full SHA' );

# The login step is the only one in the pipeline that handles registry
# credentials, so its execution must not depend on GitHub rerouting a stale
# runtime for it: the docker/login-action v3 line declares node20 while the v4
# line declares node24 natively. Keep the pin on v4+, and keep an auditable
# vX.Y.Z comment beside the SHA so a reviewer can tell which upstream release the
# 40-hex pin resolves to without a network round-trip. DD-449 removed the
# FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 shim these jobs used to set, because it is
# documented as removable once no pinned action still declares node20, and none
# does - script/audit-action-pins is what keeps that true.
my ($login_pin_version) = $package_workflow =~ m{uses:\s*docker/login-action\@[0-9a-f]{40}\s+\#\s*v(\d+(?:\.\d+){2})\b};
ok( defined $login_pin_version, 'docker login action pin carries an auditable vX.Y.Z comment beside its SHA' )
  or diag('no "# vX.Y.Z" comment found beside the docker/login-action SHA pin');

t/651-crypto-pbkdf2-ffi.t  view on Meta::CPAN


=head1 PURPOSE

This test verifies the optional OpenSSL FFI implementation of
PBKDF2-HMAC-SHA256 against the byte-for-byte behavior currently used by helper
account authentication.

=head1 WHY IT EXISTS

The FFI path collapses hundreds of thousands of Perl-to-XS HMAC calls into one
native call. A digest mismatch would invalidate existing helper credentials,
so the exact digest, iteration count, and Perl byte-string behavior are
security-sensitive compatibility requirements. The suite also supplies a
deterministic fake FFI boundary so backend setup, buffer validation, digest
parity, and explicit native errors are exercised even when optional FFI modules
or system libcrypto are absent from the test environment.

=head1 WHEN TO USE

Run this test when changing the FFI adapter, password derivation, library
discovery, or the supported native fallback behavior.



( run in 2.006 seconds using v1.01-cache-2.11-cpan-3f962061493 )