diff --git a/.github/workflows/e2e-ddev.yml b/.github/workflows/e2e-ddev.yml new file mode 100644 index 0000000..3a3b27e --- /dev/null +++ b/.github/workflows/e2e-ddev.yml @@ -0,0 +1,66 @@ +name: e2e-ddev + +# Real-ddev e2e suite (tests/e2e/run-ddev.sh, docs/design/ddev-e2e-test.md). +# MANUAL-ONLY for now: the maintainer validates runtime + duration on a real +# runner first; a weekly schedule is the planned phase-3 follow-up (the +# golden-image cache is a local-workspace concept — CI rebuilds warm every +# run, see design doc §6 R6). +# +# Not part of the PR merge gate (e2e.yml keeps that job): this suite +# downloads GBs of images and runs a real ddev project lifecycle. +on: + workflow_dispatch: + inputs: + site_tier: + description: 'Also run the real-site tier (camino fixture, DD10-DD12)' + required: false + default: false + type: boolean + ddev_version: + description: 'Pin a ddev version (e.g. v1.25.3); empty = latest release' + required: false + default: '' + type: string + +permissions: + contents: read + +concurrency: + group: e2e-ddev-${{ github.ref }} + cancel-in-progress: true + +jobs: + e2e-ddev: + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@v7 + - name: Resolve opencode version + id: version + run: | + echo "OC_VERSION=$(curl -fsSL --retry 5 --retry-delay 10 --retry-all-errors --max-time 30 https://api.github.com/repos/anomalyco/opencode/releases/latest | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')" >> "$GITHUB_OUTPUT" + - name: Cache opencode binaries + installer (per version, shared with the other e2e jobs) + uses: actions/cache@v6 + with: + path: tests/e2e/cache + key: opencode-e2e-latest${{ steps.version.outputs.OC_VERSION }} + restore-keys: | + opencode-e2e-latest- + - name: Run real-ddev E2E test + env: + E2E_DDEV_SITE: ${{ inputs.site_tier == true && 'camino' || '' }} + DDEV_VERSION: ${{ inputs.ddev_version }} + run: | + chmod +x ./tests/e2e/run.sh ./tests/e2e/run-docker-rootless.sh ./tests/e2e/run-ddev.sh ./tests/e2e/lib.sh ./tests/check-host.sh ./tests/test-wrapper-validation.sh ./tests/test-jsonc-parser.sh ./tests/test-git-config.sh ./tests/test-container-backend.sh ./tests/test-bypass-guard.sh ./tests/test-ddev-as-opencode.sh ./tests/test-ddev-migrate.sh ./tests/test-ddev-hosts.sh ./tests/test-mkcert-reuse.sh ./tests/test-wsl-exposure.sh ./tests/test-ui.sh ./tests/test-docs.sh ./tests/test-kit-cli.sh ./tests/test-project-paths.sh ./tests/test-workflows.sh ./tests/test-install-args.sh ./tests/test-kit-files.sh ./tests/test-uninstall.sh ./tests/test-status.sh ./tests/test-update-flags.sh ./tests/test-fs-baseline.sh \ + ./files/config.sh ./files/update.sh ./files/install.sh ./files/uninstall.sh ./files/status.sh \ + ./files/opencode-permissions-kit-lib/wrapper ./files/opencode-permissions-kit-lib/kit ./files/opencode-permissions-kit-lib/log.sh ./files/opencode-permissions-kit-lib/ui.sh ./files/opencode-permissions-kit-lib/shell-warn.sh ./files/opencode-permissions-kit-lib/setup-container-backend.sh ./files/opencode-permissions-kit-lib/bin/socket-check.sh ./files/opencode-permissions-kit-lib/ddev-as-opencode.sh ./files/opencode-permissions-kit-lib/ddev-handover.sh ./files/opencode-permissions-kit-lib/ddev-migrate.sh ./files/opencode-permissions-kit-lib/ddev-hosts.sh ./files/opencode-permissions-kit-lib/fs-baseline.sh ./files/opencode-permissions-kit-lib/bin/ddev-as-opencode + ./tests/e2e/run-ddev.sh + - name: Summary + if: always() + env: + E2E_DDEV_SITE: ${{ inputs.site_tier == true && 'camino' || '' }} + DDEV_VERSION: ${{ inputs.ddev_version }} + run: | + echo "### ddev-e2e (run-ddev.sh)" >> "$GITHUB_STEP_SUMMARY" + echo "- site tier: ${E2E_DDEV_SITE:-none}" >> "$GITHUB_STEP_SUMMARY" + echo "- ddev: ${DDEV_VERSION:-latest}" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index e885ec6..52c2748 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -54,7 +54,7 @@ jobs: opencode-e2e-old${{ steps.old.outputs.OLD_VERSION }}- - name: Run E2E test run: | - chmod +x ./tests/e2e/run.sh ./tests/e2e/run-docker-rootless.sh ./tests/e2e/lib.sh ./tests/check-host.sh ./tests/test-wrapper-validation.sh ./tests/test-jsonc-parser.sh ./tests/test-git-config.sh ./tests/test-container-backend.sh ./tests/test-bypass-guard.sh ./tests/test-ddev-as-opencode.sh ./tests/test-ddev-migrate.sh ./tests/test-ddev-hosts.sh ./tests/test-mkcert-reuse.sh ./tests/test-wsl-exposure.sh ./tests/test-ui.sh ./tests/test-docs.sh ./tests/test-kit-cli.sh ./tests/test-project-paths.sh ./tests/test-workflows.sh ./tests/test-install-args.sh ./tests/test-kit-files.sh ./tests/test-uninstall.sh ./tests/test-status.sh ./tests/test-update-flags.sh ./tests/test-fs-baseline.sh \ + chmod +x ./tests/e2e/run.sh ./tests/e2e/run-docker-rootless.sh ./tests/e2e/run-ddev.sh ./tests/e2e/lib.sh ./tests/check-host.sh ./tests/test-wrapper-validation.sh ./tests/test-jsonc-parser.sh ./tests/test-git-config.sh ./tests/test-container-backend.sh ./tests/test-bypass-guard.sh ./tests/test-ddev-as-opencode.sh ./tests/test-ddev-migrate.sh ./tests/test-ddev-hosts.sh ./tests/test-mkcert-reuse.sh ./tests/test-wsl-exposure.sh ./tests/test-ui.sh ./tests/test-docs.sh ./tests/test-kit-cli.sh ./tests/test-project-paths.sh ./tests/test-workflows.sh ./tests/test-install-args.sh ./tests/test-kit-files.sh ./tests/test-uninstall.sh ./tests/test-status.sh ./tests/test-update-flags.sh ./tests/test-fs-baseline.sh \ ./files/config.sh ./files/update.sh ./files/install.sh ./files/uninstall.sh ./files/status.sh \ ./files/opencode-permissions-kit-lib/wrapper ./files/opencode-permissions-kit-lib/kit ./files/opencode-permissions-kit-lib/log.sh ./files/opencode-permissions-kit-lib/ui.sh ./files/opencode-permissions-kit-lib/shell-warn.sh ./files/opencode-permissions-kit-lib/setup-container-backend.sh ./files/opencode-permissions-kit-lib/bin/socket-check.sh ./files/opencode-permissions-kit-lib/ddev-as-opencode.sh ./files/opencode-permissions-kit-lib/ddev-handover.sh ./files/opencode-permissions-kit-lib/ddev-migrate.sh ./files/opencode-permissions-kit-lib/ddev-hosts.sh ./files/opencode-permissions-kit-lib/fs-baseline.sh ./files/opencode-permissions-kit-lib/bin/ddev-as-opencode ./tests/e2e/run.sh @@ -83,7 +83,7 @@ jobs: opencode-e2e-old${{ steps.old.outputs.OLD_VERSION }}- - name: Run docker-rootless E2E test run: | - chmod +x ./tests/e2e/run.sh ./tests/e2e/run-docker-rootless.sh ./tests/e2e/lib.sh ./tests/check-host.sh ./tests/test-update-flags.sh \ + chmod +x ./tests/e2e/run.sh ./tests/e2e/run-docker-rootless.sh ./tests/e2e/run-ddev.sh ./tests/e2e/lib.sh ./tests/check-host.sh ./tests/test-update-flags.sh \ ./tests/test-wrapper-validation.sh ./tests/test-jsonc-parser.sh ./tests/test-git-config.sh ./tests/test-container-backend.sh ./tests/test-bypass-guard.sh ./tests/test-ddev-as-opencode.sh ./tests/test-ddev-migrate.sh ./tests/test-ddev-hosts.sh ./tests/test-mkcert-reuse.sh ./tests/test-wsl-exposure.sh ./tests/test-ui.sh ./tests/test-docs.sh ./tests/test-kit-cli.sh ./tests/test-project-paths.sh ./tests/test-workflows.sh ./tests/test-install-args.sh ./tests/test-kit-files.sh ./tests/test-uninstall.sh ./tests/test-status.sh ./tests/test-fs-baseline.sh \ ./files/config.sh ./files/update.sh ./files/install.sh ./files/uninstall.sh ./files/status.sh \ ./files/opencode-permissions-kit-lib/wrapper ./files/opencode-permissions-kit-lib/kit ./files/opencode-permissions-kit-lib/log.sh ./files/opencode-permissions-kit-lib/ui.sh ./files/opencode-permissions-kit-lib/shell-warn.sh ./files/opencode-permissions-kit-lib/setup-container-backend.sh ./files/opencode-permissions-kit-lib/bin/socket-check.sh ./files/opencode-permissions-kit-lib/ddev-as-opencode.sh ./files/opencode-permissions-kit-lib/ddev-handover.sh ./files/opencode-permissions-kit-lib/ddev-migrate.sh ./files/opencode-permissions-kit-lib/ddev-hosts.sh ./files/opencode-permissions-kit-lib/fs-baseline.sh ./files/opencode-permissions-kit-lib/bin/ddev-as-opencode diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ce1ced4..4c62a1e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,7 +30,7 @@ jobs: - name: Check version consistency run: make check-version - name: Make scripts executable - run: chmod +x ./tests/test-ui.sh ./tests/test-wrapper-validation.sh ./tests/test-jsonc-parser.sh ./tests/test-git-config.sh ./tests/test-container-backend.sh ./tests/test-bypass-guard.sh ./tests/test-ddev-as-opencode.sh ./tests/test-ddev-migrate.sh ./tests/test-ddev-hosts.sh ./tests/test-mkcert-reuse.sh ./tests/test-wsl-exposure.sh ./tests/test-docs.sh ./tests/test-kit-cli.sh ./tests/test-project-paths.sh ./tests/test-workflows.sh ./tests/test-install-args.sh ./tests/test-kit-files.sh ./tests/test-uninstall.sh ./tests/test-status.sh ./tests/test-update-flags.sh ./tests/test-fs-baseline.sh ./tests/check-host.sh ./tests/e2e/run.sh ./tests/e2e/run-docker-rootless.sh ./tests/e2e/lib.sh ./files/config.sh ./files/update.sh ./files/install.sh ./files/uninstall.sh ./files/status.sh ./files/opencode-permissions-kit-lib/wrapper ./files/opencode-permissions-kit-lib/kit ./files/opencode-permissions-kit-lib/log.sh ./files/opencode-permissions-kit-lib/ui.sh ./files/opencode-permissions-kit-lib/shell-warn.sh ./files/opencode-permissions-kit-lib/setup-container-backend.sh ./files/opencode-permissions-kit-lib/bin/socket-check.sh ./files/opencode-permissions-kit-lib/ddev-as-opencode.sh ./files/opencode-permissions-kit-lib/ddev-handover.sh ./files/opencode-permissions-kit-lib/ddev-migrate.sh ./files/opencode-permissions-kit-lib/ddev-hosts.sh ./files/opencode-permissions-kit-lib/fs-baseline.sh ./files/opencode-permissions-kit-lib/bin/ddev-as-opencode + run: chmod +x ./tests/test-ui.sh ./tests/test-wrapper-validation.sh ./tests/test-jsonc-parser.sh ./tests/test-git-config.sh ./tests/test-container-backend.sh ./tests/test-bypass-guard.sh ./tests/test-ddev-as-opencode.sh ./tests/test-ddev-migrate.sh ./tests/test-ddev-hosts.sh ./tests/test-mkcert-reuse.sh ./tests/test-wsl-exposure.sh ./tests/test-docs.sh ./tests/test-kit-cli.sh ./tests/test-project-paths.sh ./tests/test-workflows.sh ./tests/test-install-args.sh ./tests/test-kit-files.sh ./tests/test-uninstall.sh ./tests/test-status.sh ./tests/test-update-flags.sh ./tests/test-fs-baseline.sh ./tests/check-host.sh ./tests/e2e/run.sh ./tests/e2e/run-docker-rootless.sh ./tests/e2e/run-ddev.sh ./tests/e2e/lib.sh ./files/config.sh ./files/update.sh ./files/install.sh ./files/uninstall.sh ./files/status.sh ./files/opencode-permissions-kit-lib/wrapper ./files/opencode-permissions-kit-lib/kit ./files/opencode-permissions-kit-lib/log.sh ./files/opencode-permissions-kit-lib/ui.sh ./files/opencode-permissions-kit-lib/shell-warn.sh ./files/opencode-permissions-kit-lib/setup-container-backend.sh ./files/opencode-permissions-kit-lib/bin/socket-check.sh ./files/opencode-permissions-kit-lib/ddev-as-opencode.sh ./files/opencode-permissions-kit-lib/ddev-handover.sh ./files/opencode-permissions-kit-lib/ddev-migrate.sh ./files/opencode-permissions-kit-lib/ddev-hosts.sh ./files/opencode-permissions-kit-lib/fs-baseline.sh ./files/opencode-permissions-kit-lib/bin/ddev-as-opencode - name: Install shellcheck run: sudo apt-get update -qq && sudo apt-get install -y shellcheck - name: Check contributor host tools diff --git a/Makefile b/Makefile index e589027..7d67c4b 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help test lint check-host test-wrapper test-fs-baseline test-parser test-git-config test-container-backend test-bypass-guard test-ddev-as-opencode test-ddev-migrate test-ddev-hosts test-mkcert-reuse test-wsl-exposure test-ui test-kit-cli test-project-paths test-workflows test-docs test-install-args test-kit-files test-uninstall test-status test-update-flags e2e e2e-rootless e2e-all install-dev clean version check-version +.PHONY: help test lint check-host test-wrapper test-fs-baseline test-parser test-git-config test-container-backend test-bypass-guard test-ddev-as-opencode test-ddev-migrate test-ddev-hosts test-mkcert-reuse test-wsl-exposure test-ui test-kit-cli test-project-paths test-workflows test-docs test-install-args test-kit-files test-uninstall test-status test-update-flags e2e e2e-rootless e2e-ddev e2e-ddev-fresh e2e-all install-dev clean version check-version # Scripts checked by `make lint` (everything shipped in files/). SHELLCHECK_FILES = files/install.sh files/config.sh files/update.sh files/status.sh files/uninstall.sh files/umask.sh \ @@ -49,6 +49,9 @@ help: @echo " make e2e Run end-to-end test (Docker required)" @echo " make e2e-rootless Run docker-rootless daemon end-to-end test (Docker + systemd-in-container required; skips if unavailable)" @echo " make e2e-rootless ARGS=--debug Same, keep the container on failure + dump daemon logs" + @echo " make e2e-ddev Run the real-ddev e2e suite (golden-image cache; first run builds it, see docs/design/ddev-e2e-test.md)" + @echo " make e2e-ddev ARGS='--fresh' Force a golden-image rebuild (new ddev version, recipe bump)" + @echo " make e2e-ddev-fresh Same as e2e-ddev ARGS=--fresh" @echo " make e2e-all Run both e2e suites" @echo " make install-dev Quick dev install (skip prompts)" @echo " make clean Uninstall" @@ -130,6 +133,12 @@ e2e: e2e-rootless: @sh ./tests/e2e/run-docker-rootless.sh $(if $(ARGS),$(ARGS)) +e2e-ddev: + @sh ./tests/e2e/run-ddev.sh $(if $(ARGS),$(ARGS)) + +e2e-ddev-fresh: + @sh ./tests/e2e/run-ddev.sh --fresh + e2e-all: e2e e2e-rootless install-dev: diff --git a/docs/design/ddev-as-user-issues.md b/docs/design/ddev-as-user-issues.md new file mode 100644 index 0000000..65785cc --- /dev/null +++ b/docs/design/ddev-as-user-issues.md @@ -0,0 +1,229 @@ +# IDEA-DDEV-AS-USER: run ddev as the developer again — security inventory + +> Status: **PLANNING RECORD (brainstorming, 2026-08).** No code change. This +> document answers two questions: which security disadvantages arise if ddev +> runs as the developer user again (agent-delegated or developer-only), and +> whether the known `.ddev/commands/host/` gap is the only one — and whether +> it can be closed. Verdict in §6: **not viable**; the host-command gap is one +> instance of a structural class that cannot be sealed without re-introducing +> the hard-deny model the kit deliberately removed. Where wording differs from +> the code, the code wins. + +## 1. Motivation (why the idea keeps coming back) + +ddev-as-opencode (ddev-working.md §11) has real operational cost, all of it +caused by "ddev must run as one user, and that user is not the one who owns +the files ddev touches": + +- the `.ddev/` + settings-dir **handover machinery** (chmod is owner-only), + incl. the bootstrap-root handover/handback dance for fresh TYPO3 clones, +- the **browser-command split** (issue #20: `launch`/`mailpit`/… need Windows + interop the opencode user must not have), +- the **hosts-file bridge** (`ddev-hosts-add`, because the opencode user + cannot run `ddev-hostname.exe`; its scans need vendor/node_modules + exclusions, issue #21), +- the `ddev()` shell function + `export -f` / `BASH_ENV` transports for vendor + scripts (issue #18: `vendor/bin/runTests.sh` fell through to the real ddev + as the developer → chmod EPERM on the opencode-owned `.ddev/`) — dash + scripts, cronjobs and IDE tasks still fall through, +- mode-repair friction on settings files (issue #25: ddev resets + `config/system` to `0755` on every start, stripping `g+w` from + `additional.php` until the next `refresh`), +- the **SSH-key trade-off** (`/home/opencode/.ddev` is agent-readable), +- slow first start (mutagen + image pulls into the rootless daemon) and the + install-time **database migration** from the developer's old daemon + (issue #15: the export/import dance around the handover). + +Running ddev as the developer again would delete most of that machinery — +stock ddev is designed to run as the developer on the developer's machine. +The question is only what it costs. + +### 1.1 The documented evidence: closed `ddev`-labeled GitHub issues + +The kit's issue tracker is the burn-in log of this model. All six closed +`ddev` issues +([#15](https://github.com/steffenmaechtel/opencode-permissions-kit/issues/15), +[#17](https://github.com/steffenmaechtel/opencode-permissions-kit/issues/17), +[#18](https://github.com/steffenmaechtel/opencode-permissions-kit/issues/18), +[#20](https://github.com/steffenmaechtel/opencode-permissions-kit/issues/20), +[#21](https://github.com/steffenmaechtel/opencode-permissions-kit/issues/21), +[#25](https://github.com/steffenmaechtel/opencode-permissions-kit/issues/25)) +map to pain classes, and the mapping matters for the assessment — because +**not every ddev issue is a ddev-*user* issue**: + +| Issue | Symptom | Pain class | Would ddev-as-developer fix it? | +|---|---|---|---| +| #25 | `ddev start` strips `g+w` from `config/system/additional.php` (chmod `0755` on the settings dir) | handover/chmod (two owners) | yes — one owner, chmod is a no-op for the developer. **Already fixed inside the model** by dev-owned projects (`disable_settings_management`) + `refresh` re-applying `g+w` | +| #21 | `ddev-hosts-add` reported hostnames from `.ddev` dirs inside `vendor/` | hosts bridge scan bug | moot — with ddev as the developer the whole hosts bridge would not exist (stock `ddev-hostname.exe` interop works). Kit bug itself is fixed (vendor/node_modules skip) | +| #20 | `ddev launch` failed: WSL interop/wslview denied for the ddev user | browser/interop split | yes — the developer has interop; the split machinery would not exist | +| #18 | `vendor/bin/runTests.sh` (dash) called the real ddev as the developer → chmod EPERM on opencode-owned `.ddev/.webimageBuild` | function-transport gap (two owners, two ddevs) | yes — no function/transports needed, ddev is ddev. Fixed via `export -f`/`BASH_ENV`; residual gaps (cronjobs, IDE tasks, `#!/bin/sh` without bash parent) documented | +| #17 | agent's `git log`: "dubious ownership" on developer-owned `.git` | **UID separation itself**, not ddev | **no** — `.git` stays developer-owned and the agent stays a different UID regardless of who runs ddev; only `safe.directory` handling (kit `git-config`) addresses it | +| #15 | after install, all project databases unreachable on the old daemon | daemon/user migration | yes — containers would stay on the developer's daemon; no export/import. Fixed by the install-time export + `ddev-migrate.sh` import | + +Reading of the table: four of six issues (#25, #20, #18, #15 — plus #21's +whole feature category) are *structural* consequences of "ddev runs as +opencode" and would vanish with ddev-as-developer; one (#17) is a +consequence of UID separation that no ddev-user switch can touch. The +operational case for the switch is therefore real and documented — which is +exactly why §4–§6 weigh it against the security cost instead of dismissing +it. Note also what #18 demonstrates about the current model's failure mode: +when the developer's native ddev meets opencode-owned trees, it fails with +hard EPERM — annoying, but fail-closed. The reverse combination (agent-side +ddev on developer-owned trees) is the one that motivated the handover +machinery in the first place. + +## 2. What the change would mean concretely + +Two variants are usually meant: + +- **A — delegation (the old `delegated` mode):** the agent's `ddev` calls are + re-executed as the developer via a RunAs-developer sudoers rule; the + developer's terminal runs ddev natively. `.ddev/` becomes developer-owned + again; the developer's daemon/context serves the containers. +- **B — developer-only:** the agent loses ddev access entirely; only the + developer runs ddev, natively as themselves. + +Both variants put ddev's *executing* user back to the developer. The +inventory below applies to both; §4.1 explains why even B does not close the +class. + +## 3. The known gap: custom host commands (H3) + +The scenario from the brainstorming, verified against the ddev source: + +1. The agent writes `.ddev/commands/host/evil-cmd` — a tree it can write + (group sharing, setgid, umask 002). +2. `ddev evil-cmd` — **no `ddev restart` needed**: command discovery happens + on every invocation, and ddev even `chmod 0755`s the file for the agent + (`cmd/ddev/cmd/commands.go`, `addCustomCommandsFromDir`). +3. ddev executes the script **directly on the host, as the invoking user** + (`makeHostCmd` → `exec.RunInteractiveCommand(fullPath, …)`). No container + is involved (`cmd/ddev/cmd/commands.go:447-488`). + +With variant A that is arbitrary code execution **as the developer**, outside +every soft rule — this is exactly finding H3 from `docs/security/proof-3.md` +and §1 of `docs/design/ddev-sandbox.md`, the reason sandbox mode (and later +the as-opencode model) exists. + +## 4. Is it the only gap? No. + +The host-command gap is merely the most visible carrier of one structural +problem: **ddev executes agent-authored configuration and scripts with the +authority of the invoking user.** Move that user to the developer and every +carrier becomes a developer-privilege channel. + +### 4.1 The four carriers (same class as H3) + +All verified in the ddev source; all read from files the agent can write: + +| # | Carrier | Mechanism (ddev source) | Trigger | +|---|---|---|---| +| 1 | Custom host commands | `.ddev/commands/host/*` executed on the host (`commands.go:447-488`) | `ddev ` — by the agent (A) **or the developer** | +| 2 | `exec-host` hooks | `.ddev/config.yaml` `hooks:` accept `exec-host: ` tasks run on the host (`pkg/ddevapp/task.go:34-115`; valid tasks `exec`/`exec-host`/`composer`, `config.go:1881-1885`); lifecycle events include pre/post-`start`/`stop`/`restart`/`exec`/`pull`/`push`/… | **automatic** on the next `ddev start`/`restart`/… — by anyone | +| 3 | Add-on actions | `ddev get ` executes `pre_install_actions` / `post_install_actions` / `removal_actions` shell actions (`pkg/ddevapp/addons.go:79-91, 1330-1344`) | `ddev get` / add-on remove — argv, or **from inside a planted hook (2)** | +| 4 | Compose overrides / bind mounts | `.ddev/docker-compose.*.yaml` (and `config.yaml` mounts) are agent-writable; `ddev start` mounts anything the daemon user can read into containers | `ddev start` — by anyone | + +Carrier 2 is the crucial one for the "can we close it" question, for two +reasons: + +- **No invocation needed at fire time.** A planted `exec-host` hook fires on + the *developer's own* next `ddev start` — there is no agent session, no + sudo, no ddev call by the agent involved when it goes off. Variant B (§2) + therefore does **not** close the class: the agent only needs write access + to `.ddev/` — which the group-collaboration baseline grants by design — + and patience. The payload runs as the developer either way. +- **It defeats argv allowlists.** Even a delegation helper that only permits + `ddev start/stop/restart/…` (the H3 "mitigation" sketched in proof-3) + executes the planted hook, the custom command discovery chmod, and the + compose overrides. The payload lives in files, not in argv. + +### 4.2 Consequences beyond the carrier class (variant A) + +- **Developer-UID containers.** The containers serve from the developer's + daemon. On the developer's rootless daemon every container (and its + "root") maps to the developer's host UID: combined with carrier 4, the + whole developer-readable namespace (`~/.ssh`, `.gitconfig`, `.aws`, + browser profiles — `/home/` is `750`, but the container *is* + the developer's UID) becomes readable from `ddev exec` / `ddev ssh`. +- **Root-equivalent socket.** If the developer runs rootful docker (docker + group — the stock setup), agent-driven ddev reaches a root-equivalent + socket: mount `/` into a container → full host compromise. The kit cannot + force the developer's *own* environment to be rootless. +- **Windows interop as the Windows user.** Host commands and `exec-host` + hooks run as the developer and can invoke `powershell.exe` / + `explorer.exe` / any `.exe` via WSL interop — execution on the Windows + side, outside every Linux rule. This is exactly the capability the + browser-command split (issue #20) exists to keep from the agent. +- **`ddev auth ssh` with the real keys.** Delegation gives the agent a path + to import the developer's *actual* SSH keys into the ssh-agent container + (the template's `ddev auth ssh*` deny is soft and re-allowable per + project). +- **A RunAs-developer sudoers rule returns.** The delegation helper *is* an + `opencode ALL=(developer)` rule — the class of rule the current + security-model.md lists as a hard guarantee ("no code path executes as + the developer"). + +### 4.3 Which guarantees survive? None of the three. + +| Guarantee (security-model.md) | ddev as developer | +|---|---| +| Agent ≠ developer | void — every ddev run (A), or the developer's own run over agent-planted files (A+B), executes with developer authority | +| Containers ≠ root | void — developer's daemon; root-equivalent if rootful docker | +| No developer-RunAs | void in A (the delegation rule) | + +## 5. Could the gaps be closed while ddev runs as the developer? + +| Vector | Closing attempt | Why it fails | +|---|---|---| +| Host commands (4.1.1) | make `.ddev/commands/host` developer-owned, agent read-only; argv allowlist in a delegation helper | agent can plant fresh `.ddev` trees in any writable root (new projects); carrier 2 makes even `ddev start` sufficient; allowlists cannot see file payloads | +| `exec-host` hooks (4.1.2) | deny agent writes to `.ddev/config.yaml` | re-introduces hard OS denies over `.ddev/` — the ACL model ddev-working.md removed after it broke ddev and the agent workflow; still scan-based/racy (the PROOF-1/2 gaps); compose overrides are separate files anyway | +| Add-ons (4.1.3) | deny `ddev get` in the allowlist | a planted hook (4.1.2) can run `ddev get` itself; remote payloads (URL/GitHub) bypass local file checks | +| Bind mounts (4.1.4) | seal `.ddev/*.yaml` | same as 4.1.2; sealing ddev's whole extension surface means the agent can no longer do its job (configuring projects *is* agent work) | +| Developer-UID / rootful daemon (4.2) | enforce rootless for the developer | not the kit's environment to control | +| Windows interop (4.2) | — | the developer's normal capability; only reachable because execution happens as the developer | + +**Hybrid idea, considered and rejected:** ddev as the developer *against the +opencode-owned rootless daemon* (keeps containers opencode-UID, fixes the +chmod pain). It leaves every host-side carrier (4.1) and the interop path +open and adds cross-user socket plumbing (XDG runtime dir is `0700`). +Security theater: the biggest class stays wide open while the model gets +more complex. + +**The only theoretical full closure** is an OS-level hard deny on agent +writes to every `.ddev/`, settings dir, and config/compose file *everywhere +the agent can write*, enforced continuously. That is the pre-ddev-working +model — removed because it broke `ddev start` (chmod is owner-only), needed +per-collision carve-outs, and its own proofs showed the scan model leaks. +Re-introducing it contradicts the kit's soft-only decision +(docs/design/ddev-working.md, docs/concepts/security-model.md) and would not +even restore the guarantees: it would just move the breakage back. + +## 6. Assessment + +**Recommendation: no — do not run ddev as the developer, in neither variant.** + +- The `evil-cmd` scenario is **not** the only gap. It is one of four file + carriers of the same class ("agent-authored ddev input executed with the + invoking user's authority"), and the class additionally drags in + developer-UID containers, a possibly root-equivalent socket, Windows + interop, and real-key exfil. +- The class **cannot be closed** while ddev executes as the developer: the + decisive vector (`exec-host` hooks) fires on the developer's own runs, + defeats argv allowlisting, and sealing it means re-introducing the hard + ACL layer the kit just removed — knowing it broke ddev then. +- The pain that motivates the idea is real, and **documented** — §1.1 shows + four of the six closed `ddev` issues would vanish structurally. But the + same table shows the switch is not even a complete pain relief: #17 (git + dubious ownership) is UID-separation pain that no ddev-user change + touches. The pain is *operational*, not structural. The existing answer + inside the as-opencode model is the + **dev-owned projects mode** (`disable_settings_management`, + docs/design/ddev-dev-owned-projects.md): it removes the handover cause — + ddev stops writing/chmodding outside `.ddev/` — without moving the trust + boundary. Remaining friction (browser split, hosts bridge, BASH_ENV + transports) is cosmetic-to-moderate tooling cost, not a security + regression. + +If the operational pain grows further, the direction to explore is reducing +the handover surface and the context gaps (§1) within the one-user model — +not moving ddev's executing user back to the developer. diff --git a/docs/design/ddev-e2e-test.md b/docs/design/ddev-e2e-test.md new file mode 100644 index 0000000..b3407c5 --- /dev/null +++ b/docs/design/ddev-e2e-test.md @@ -0,0 +1,465 @@ +# PLAN-DDEV-E2E: real-ddev e2e suite with a cached golden container + +> Status: **PLANNED — not implemented.** Design record for a third e2e suite +> (`make e2e-ddev`) that runs the REAL ddev against the REAL docker-rootless +> daemon inside the systemd test container, answering the recurring burn-in +> problem: most ddev issues (#18, #20, #21, #25, the §10a TODOs in +> ddev-sandbox.md) only surfaced on productive WSL installs because both +> existing suites exercise ddev through the `fake-ddev` stub — kit plumbing +> only, never ddev itself. The core enabler is a **golden-image cache**: the +> provisioned rootless daemon + ddev + pulled images are snapshotted once via +> `docker commit`, so repeat runs skip the 3–4+ minute download/install phase. +> Where wording differs from the code, the code wins. + +## 1. Problem & goal + +| Today | Gap | +|---|---| +| `make e2e` (run.sh, 250 checks) | ddev is `tests/e2e/fake-ddev` — a logging stub. Nothing of ddev itself runs: no containers, no mutagen, no chmod behavior, no router. | +| `make e2e-rootless` (run-docker-rootless.sh, 54 checks) | REAL inner docker-rootless daemon (systemd container), but ddev is never installed — only raw `docker` probes (alpine). | + +Every issue class from `ddev-as-user-issues.md` §1.1 — settings-dir chmod +(`#25`), the `ddev()` function transports (`#18`), describe-state mismatches, +mutagen, first-start behavior — needs real ddev to reproduce. Goal: + +- `make e2e-ddev` boots the systemd container, provisions the inner + docker-rootless daemon, installs real ddev, and drives a real project + lifecycle as BOTH users (agent: opencode; developer: via the `ddev()` + function and the sudoers helper). +- First run pays the full price (ddev download, image pulls, mutagen — + realistically 5–12 min, dominated by GB-sized ddev images). The finished + environment is committed as a local golden image; repeat runs start from + it and take **~1–3 min**. +- Cache refresh is a non-event: automatic on ddev-version change, manual via + `make e2e-ddev-fresh` (or `--fresh`), optional TTL. + +## 2. Non-goals + +- **Windows-side halves** stay untestable in a container: browser opening + (`ddev launch` → `explorer.exe`), the Windows hosts file + (`ddev-hostname.exe`), mkcert CA reuse from `/mnt/c`. The suite asserts + only the container-side behavior (launch prints URL, interop-blocked as + designed; hosts bridge prints the missing-hostnames report). +- **A broad CMS matrix** stays out — but ONE real, complete site is in scope + as an **opt-in fixture tier** (§7.1): a real TYPO3 with the Camino theme, + installed via composer and a DB dump, proving the full boot path + (settings.php generation, `additional.php` `g+w`, site responds) that + skeletons cannot reach. The default tier stays typo3-typed *skeletons* + (no vendor/, no CMS runtime) because the file/ownership failure class + ddev performs before any CMS code runs; the real-site tier is what the + skeletons cannot see. Additional fixtures later = same recipe. +- **Performance testing** (mutagen sync speed at scale). +- Replacing the two existing suites — they stay the merge gate; e2e-ddev is + the deep-dive suite (see §8). + +## 3. Architecture: reuse the rootless scaffolding + +The suite is `run-docker-rootless.sh` grown a ddev stage — same container, +same knobs: + +- `tests/e2e/Dockerfile.rootless` unchanged (systemd PID 1, uidmap, + slirp4netns, fuse-overlayfs, the nf_tables modules.builtin synthesis). +- `tests/e2e/lib.sh` unchanged: setuid guard + classic-builder self-heal, + host-layout detection, opencode binary cache, `check`/`skip`/`E` helpers. +- New: `tests/e2e/run-ddev.sh` (runner), golden-image helpers (§4), and the + same container-internal adaptations the rootless suite applies (RL2 prep: + fuse-overlayfs daemon.json pin; nested-userns subuid seed when the OUTER + daemon is rootless — the dev-workspace case). + +What the inner environment looks like after warm-up: + +``` +systemd (PID 1, --privileged, --cgroupns=private) +└─ user@.service (linger) # opencode user manager + └─ docker.service (rootless) # fuse-overlayfs storage: + │ # /home/opencode/.local/share/docker + ├─ ddev-router # images pre-pulled (golden): + ├─ ddev--web # ddev-webserver, ddev-dbserver, + └─ ddev--db # ddev-router, ddev-utilities +/usr/local/bin/ddev # real ddev binary +/home/opencode/.ddev # global config + mutagen binary +``` + +## 4. The cache: one golden image via `docker commit` + +Yes to the brainstorming question — a finished "rootless docker + ddev" +environment can be cached, and the natural vehicle is the container itself: +everything expensive lives **inside the container filesystem**, so a commit +captures all of it in one artifact. + +### 4.1 What is cached — and what deliberately is not + +| State | Location | Cached? | Why | +|---|---|---|---| +| docker-ce-rootless-extras (inner daemon) | `/usr/lib/...`, system dirs | yes | ~100 MB download via get.docker.com | +| ddev binary | `/usr/local/bin/ddev` | yes | ~50 MB download | +| inner daemon images (webserver/db/router/utilities) | `/home/opencode/.local/share/docker` | yes | **the big one** — GBs of pulls, the 3–4 min the user sees | +| mutagen binary + global ddev config | `/home/opencode/.ddev` | yes | downloaded on first `ddev start` | +| composer package cache (real-site tier) | `~opencode/.composer/cache` (or ddev's composer cache volume) | yes | `ddev composer install` on warm-up fills it → repeat runs resolve offline | +| real-site fixture master, pristine (§7.1) | `/opt/e2e/fixtures/` inside the image | yes | tests run against a per-run `cp -a` copy; baking the master into the golden image keeps repeat runs offline and fast | +| `opencode` user, subuid/subgid, linger, `systemctl --user enable docker` | system state | yes | makes the daemon auto-start on every cached boot | +| the kit (`/usr/local/lib/opencode-permissions-kit`, sudoers, install.conf) | system state | **no** | the thing under test — freshly installed from the repo bind mount every run | +| project `.ddev/` + project files | bind-mounted host tmp dir | **no** | fresh `e2e_prepare_project` fixture every run (both-owners scenarios need a clean tree) | +| `/run` (sockets, user runtime dirs) | tmpfs | **no** (impossible) | recreated by systemd on boot — this is why nothing needs manual re-animation | + +### 4.2 Build flow (first run / cache miss) + +``` +1. build Dockerfile.rootless (lib.sh, setuid guard applies) +2. start systemd container +3. create opencode user + subuid seed + fuse-overlayfs daemon.json (RL2 prep) +4. provision inner daemon: setup-container-backend.sh path + (or dockerd-rootless-setuptool.sh directly — see §6 R5) +5. install ddev (official install script, pinned version §5) +6. warm up: ddev config --project-type=typo3 --auto + ddev start + on a throwaway project → pulls all images + mutagen, creates none kept +7. teardown warm-up project: ddev delete -Oy + docker system prune -f + (keeps images, drops warm-up containers/volumes/networks) +8. quiesce: sudo -u opencode systemctl --user stop docker.service + (commit must not snapshot a mid-write storage) +9. docker commit --label kit.e2e.ddev.version= --label kit.e2e.format= + --label kit.e2e.built= opencode-e2e-ddev:cached +``` + +### 4.3 Run flow (cache hit) + +``` +1. docker run -d ... opencode-e2e-ddev:cached /sbin/init +2. systemd boots → linger starts user@ → enabled docker.service starts + → socket reappears in /run/user/ (no manual steps) +3. DD0 gate: daemon active, `docker images` non-empty, `ddev version` = , + sudo setuid intact (§6 R2) +4. install the KIT from the repo bind mount (existing-opencode-user path) +5. run the DD catalog (§7) against a fresh project fixture +``` + +First `ddev start` on the cached image only creates the project's containers +from local images — seconds, not minutes. + +## 5. Cache key & invalidation + +Same idioms as `e2e_resolve_cache` (lib.sh): resolve, compare, fall back. + +- **Key = three labels** on the golden image: + `kit.e2e.ddev.version` (e.g. `v1.25.3`), `kit.e2e.format` (bump when the + warm-up recipe changes — the manual escape hatch), `kit.e2e.built` (epoch). +- **Desired version:** env `DDEV_VERSION` (pins win — "test the new release + that just broke things": `make e2e-ddev DDEV_VERSION=v1.26.0`), else + latest from the ddev GitHub release API (tiny request). API unreachable → + reuse the cached image with its label version (offline dev runs work). +- **Rebuild triggers:** label missing (no image) · version mismatch · + `kit.e2e.format` bump · `--fresh` flag / `make e2e-ddev-fresh` · optional + TTL (`kit.e2e.built` older than `E2E_DDEV_TTL` days, default **14** — the + "every few weeks" refresh from the brainstorming, automated; `0` disables). +- **Wipe:** `make e2e-ddev-fresh` is enough; `docker rmi + opencode-e2e-ddev:cached` for the paranoid. The golden image is a local + docker artifact — NOT `tests/e2e/cache/` (that gitignored dir stays + opencode-binaries-only, so CI's existing cache key is untouched). + +## 6. Risks & mitigations + +| # | Risk | Assessment / mitigation | +|---|---|---| +| R1 | committed storage inconsistent (daemon writing during commit) | mandatory quiesce step (§4.2.8); DD0 fails loudly with "restore images" hint if the storage were corrupt — worst case is a `--fresh` rebuild | +| R2 | outer daemon strips setuid (this workspace's rootless daemon + BuildKit) | base build already self-heals via lib.sh's classic-builder guard; `docker commit` does not re-unpack layers, it reuses them + adds the container diff as-is, so setuid survives. DD0 still asserts `-u /usr/bin/sudo` on every cached boot; a surprising loss self-heals container-internally (`chmod u+s`) with a loud note | +| R3 | fuse-overlayfs on-disk format not portable across daemon restarts | it is plain directories/files under the container FS — the daemon re-opens its own storage on boot; DD1 (`docker images` non-empty) is exactly this regression's canary | +| R4 | golden image grows unboundedly (layered diffs, leftover volumes) | prune before commit (§4.2.7); TTL rebuild bounds drift; size printed after commit | +| R5 | warm-up uses kit code to provision → kit bugs poison the cache | acceptable and even desirable (the kit's provisioning IS production path); `kit.e2e.format` is the reset button. Alternative (raw setuptool) kept as fallback if kit provisioning gains checks that assume a fresh machine | +| R6 | CI: golden image is GBs; `actions/cache` caps at 10 GB/repo | CI does NOT commit/save per run. Strategy (§8): weekly scheduled run rebuilds warm (no image cache in CI at all — runner disk is ephemeral anyway), plus `workflow_dispatch` for on-demand deep dives. If per-PR e2e-ddev is ever wanted: `docker save | gzip` ≈ 1.5–2.5 GB into a dedicated actions/cache key (`ddev-e2e-image--`, restore-keyed) — feasible but only worth it once the suite is stable | +| R7 | router ports <1024 under rootless | suite pins the documented rootless answer — `ddev config global --router-http-port 8080 --router-https-port 8443` — instead of the host-wide sysctl; the sysctl question stays covered by unit tests | +| R8 | ddev update checks / instrumentation phone home and add flakiness | warm-up sets `DDEV_NO_INSTRUMENTATION=true` + update-check off in `~opencode/.ddev/global_config.yaml`; offline repeat runs stay green | + +## 7. Test catalog (DD sections) + +Mapped to the documented issue classes (ddev-as-user-issues.md §1.1) — each +row is the regression net for a real burn-in finding: + +| Section | Checks | Issue class | +|---|---|---| +| DD0 | systemd PID 1; inner rootless daemon active; `docker images` non-empty (R3 canary); `ddev version` = label; sudo setuid intact | cache integrity | +| DD1 | kit install from repo bind mount on the warm image (existing `opencode` user path — the upgrade-style install, complementing run.sh's fresh-user path); wrapper + sudoers + `ddev()` shell hook wired for `dev` | install on real state | +| DD2 | agent-side bootstrap: fresh `typo3`-typed project (no vendor/), `sudo -u opencode ddev start` — the bootstrap-root handover: root inode opencode-owned `2755`, start completes without `operation not permitted` (the ddev-working.md §11 chmod error, now with REAL ddev) | #18/#25 EPERM class | +| DD3 | ownership/modes around `ddev start`: `.ddev/` opencode-owned; settings dir chmod'd `0755` by ddev (real behavior); `config.sh refresh` restores `g+w` on settings files | #25 | +| DD4 | dev-owned mode: `ddev-settings on` → start → settings dirs stay dev-owned `2775`, no write outside `.ddev/` | #25 in-model fix | +| DD5 | developer side: `sudo -u dev bash -ic 'ddev ...'` through the `ddev()` function + sudoers helper; `BASH_ENV` transport: nested `bash -c` script calling ddev runs as opencode (uid assert, not just exit code) | #18 | +| DD6 | container identity with REAL ddev containers: `ddev exec` works; web container uid_map maps container root → opencode host UID (§9.1 proof, ddev edition); container reads settings file via bind mount (soft-only goal) | security-model | +| DD7 | lifecycle: `ddev restart`, `ddev stop`, `ddev delete -Oy`, mutagen flush; full-output grep for `operation not permitted` / `EPERM` — zero tolerance across the whole suite log | all chmod classes | +| DD8 | `ddev describe` / `ddev list` report the SAME project state from agent and developer context (the ddev-sandbox §10a mismatch, finally assertable) | §10a TODO | +| DD9 (opt.) | db round-trip: tiny `import-db` → `export-db`; snapshot + restore | db migration (#15 adjacency) | +| DD10–11 (opt-in) | real-site tier, §7.1 below | full boot path | +| DD12 (opt-in) | git flow against a local bare origin, §7.2 below | checkout EPERM class | +| DD13 | `ddev config` on a fresh empty dir (new-project creation), §7.3 below | new-project gap (2026-08-22 burn-in) | +| DD14 | `ddev composer create-project` on the handed-over bootstrap project, §7.3 below | create-project exit 23 (2026-08-22 burn-in) | + +Out of scope per §2: launch/hosts/mkcert Windows halves — asserted only as +"prints URL / reports missing hostnames / interop-blocked as designed". + +### 7.1 Real-site tier: a complete TYPO3 (e.g. Camino) — DD10/DD11 + +Opt-in via `E2E_DDEV_SITE=camino` (empty default → skeleton tier only). A +real site proves what skeletons cannot: TYPO3 actually boots, the +frontend renders, and ddev's TYPO3 settings management behaves on real +files. + +**DD10 — install & boot (agent side):** + +1. `cp -a /opt/e2e/fixtures/camino /var/www/vhosts/camino-e2e` (pristine + master from the golden image; handover would corrupt the master), + then kit-side handover (`config.sh projects add` / handover helper). +2. `ddev start` → `ddev composer install` (composer cache is warm — §4.1) + → `ddev import-db --src=db.sql.gz` → `ddev exec vendor/bin/typo3 + cache:flush` (or equivalent). +3. Assert `curl -f http://camino-e2e.ddev.site:8080/` returns **200** and + contains a fixture marker string — the first end-to-end "the site + really works" assertion the kit has ever had. +4. Assert the REAL `config/system/settings.php` exists, is + `#ddev-generated`-managed, and `additional.php` is group-writable after + `config.sh refresh` (issue #25 on the real file, not a fixture stub). + +**DD11 — two-owner reality on a live site:** developer-side `ddev restart` +through the `ddev()` function while the site runs; edit of +`additional.php` as `dev` (group write — #25 regression); site still 200 +afterwards; `ddev exec` uid assertions from DD6 repeated on the real +project. + +#### Fixture spec (what gets prepared, where it lives) + +| File | Content | Rules | +|---|---|---| +| `composer.json` (+ ideally `composer.lock`) | TYPO3 CMS + Camino theme, versions **pinned** | lock preferred = deterministic warm-up; the theme itself is fetched from packagist at warm-up, so **no theme code is redistributed** in this repo — only the two fixture files | +| `db.sql.gz` | gzip'd dump of the installed site (ddev `export-db` output) | **must not** contain real customer data, credentials, or secrets — a throwaway demo install; keep it small (target < 10 MB gz, hard cap 20 MB); homepage must contain a stable marker string the assertions grep for | +| optional `assets/` | fileadmin/user_upload content, if the theme needs it to render | same size/secrecy rules; referenced paths must exist after import | +| `README.md` | how to regenerate (install steps + `ddev export-db`) | reproducibility — anyone can rebuild the fixture | + +Concrete recipe proven out in the 2026-08-22 burn-in (§7.3): TYPO3 +v14.3.6 + `typo3/theme-camino` v14.3.6, PHP 8.3, `--project-tld local`, +dump via `ddev export-db -f=backup.sql.gz` came out at **85 KB** (budget +is a non-issue), frontend marker route `/camino/` (the site config's +`base:` is relative — works under any hostname). The burn-in's manual +post-install steps (below) are **baked into the fixture master**, never +re-done per run: `trustedHostsPattern` in `config/system/settings.php`, +`public/.htaccess` copied from +`vendor/typo3/cms-install/Resources/Private/FolderStructureTemplateFiles/root-htaccess`. + +**Status: fixture harvested (2026-08-22)** at +`tests/e2e/fixtures/camino/` — `site/` (pristine master, byte-identical +to the burn-in commits except three documented deviations: `name:` line +dropped from `.ddev/config.yaml`, `!/public/index.php` gitignore +exception so clones boot, dump as sidecar `db.sql.gz`) plus the fixture +README with the regeneration recipe. Phase 2b fixture intake is done; +the golden-image warm-up integration remains. + +Location: `tests/e2e/fixtures/camino/` in the repo (shipped — installs +stream from the same repo, and CI needs the files). If the dump ever +breaches the size budget: move `db.sql.gz` out of git, keep a README + +download/mount hint, and let the golden image warm-up fetch it from a +local path (`E2E_DDEV_FIXTURE_DIR`). Fixture changes bump +`kit.e2e.format` → golden rebuild re-runs `composer install` + a +freshness check that the installed vendor tree matches the lock file. + +Warm-up integration: after §4.2.6, the warm-up also clones the fixture +master into `/opt/e2e/fixtures/`, runs `ddev composer install` + the DB +import once, verifies the 200/marker, `ddev delete -Oy` + prune, then +commits — so the golden image contains warm composer cache + the +pristine master (vendor/ included; page-cached `cp -a` makes per-run +copies cheap). + +### 7.2 Git-flow tier: local bare origin — DD12 + +`git checkout` colliding with ddev ownership is a documented burn-in class +(ddev-dev-owned-projects.md §1: fresh-clone `ddev start` EPERM; +`git checkout ` EPERM while the bootstrap root is opencode-owned — +"error: unable to unlink old 'AGENTS.md'", half-switched working tree). None +of it is e2e-tested today: run.sh's git checks use plain directories, never +clone/branch-switch flows. A **local bare repo as origin** makes the real +flow testable offline and deterministically — no GitHub, no network flake. + +**Fixture: `tests/e2e/fixtures/make-bare-origin.sh`** builds the bare repo +per run from the pristine fixture master into the tmp project dir (a bare +repo without vendor/ is tiny; generating it fresh every run avoids yet +another baked artifact and keeps branch content in reviewable script form — +the maintainer's real project bare repo is the template for what the +branches must contain). **Status: generator implemented and verified** +(2026-08-22; ShellCheck-clean, clone + all branch switches tested against +the harvested camino fixture) — reconciled with the real burn-in bare repo: +the tracked set there is `.ddev/config.yaml` + the composer/TYPO3 tree +(11 files, no AGENTS.md), so `feature/top-level` works on README.md/LICENSE +instead. Branch layout: + +| Branch | Differs from `main` in | Exercises | +|---|---|---| +| `main` | — (the installed state, DB-dump-matching) | clone baseline | +| `feature/top-level` | modifies/deletes a top-level tracked file (e.g. `AGENTS.md`) | bootstrap-root unlink — THE checkout EPERM | +| `feature/settings` | modifies tracked files under `config/system/` | settings-dir ownership on file replacement | +| `feature/ddev-tree` | adds/removes a tracked `.ddev/commands/host/` file | opencode-owned `.ddev/` vs git unlink/rename | + +**DD12 checks** (per run: `git clone /tmp/…/origin.git camino-e2e` as dev, +then kit handover, then): + +1. **clone → start (handover mode):** fresh clone, `ddev start` via the + agent — either boots (handover already ran) or prints the ready-made + `config handover` hint (ddev-integration.md's hook promise, finally + asserted). +2. **checkout EPERM, handover mode (documented pain):** with the bootstrap + root opencode-owned, dev-side `git checkout feature/top-level` must fail + with `unable to unlink` — asserting *current* behavior as a regression + tripwire: if this ever changes silently (kit or ddev), DD12 notices. +3. **checkout free, dev-owned mode (the promise):** `ddev-settings on`, + branch-switch through ALL three feature branches as dev — zero EPERM, + working tree fully switched (`git status` clean). This is the + ddev-dev-owned-projects.md §2 guarantee, end-to-end. +4. **agent-side git:** clone + `git pull` as opencode (safe.directory / + dubious ownership, #17 class; kit `git-config on` path) and branch + switch as opencode on developer-owned tracked files (group baseline). +5. **pull-while-running:** `ddev restart` after a checkout that replaced + `.ddev/commands/host/` content — ddev must survive git's file + replacement inside its own tree. + +Runs with the site tier (`E2E_DDEV_SITE=camino` gates it — realistic +tracked-file set) but is generator-driven, so a skeleton variant is one +flag away if the site fixture lags. The 2026-08-22 burn-in validated the +bare-repo mechanics manually (empty bare repo → `mv .git` into the +working tree → add/commit/push; `.git` stays dev-owned as the model +promises) — and confirmed `.ddev/config.yaml` IS a tracked file in a real +project, so the `feature/ddev-tree` branch must include it (already in +the layout above). + +### 7.3 Burn-in findings (2026-08-22) → DD13/DD14 + +A first real new-project install (empty dir → `ddev config` → `ddev +start` → `ddev composer create-project` → `ddev typo3 setup` → Camino +site running, db dumped, bare origin pushed) surfaced two kit-relevant +gaps and validated the designed flows. Both become checks; one is a kit +fix candidate. + +**Finding 1 — `ddev config` on a fresh dir creates `.ddev/` without +`g+w`, and no hint fires (kit gap, fix candidate):** ddev creates +`.ddev/` (and the docroot) opencode-owned with its own modes — group +gets `r-x` only, so the developer **cannot edit `.ddev/config.yaml`** +(hotfix was `sudo chmod 775 -R .ddev/`). The `ddev()` hook's bootstrap +hint cannot fire for `ddev config`: it hooks `start|restart` only and +requires `.ddev/config.yaml` to already exist — chicken-and-egg on +project creation. The settings-file warning ddev prints (`Could not +write settings file … chmod … operation not permitted`) is the bootstrap +EPERM in warning form; config still completes. Fix candidate (own issue ++ PR, not part of this plan): post-`config` hook arm that suggests +`config handover` / `projects add` once `.ddev/` exists. + +**DD13 checks** (asserting today's behavior as tripwires until the fix +lands, then flipped): + +1. fresh empty dir + `ddev config --project-type=typo3 …` (exact burn-in + flags) via the `ddev()` function completes with "Configuration + complete." even with the settings warning; +2. `.ddev/` exists, opencode-owned; `config.yaml` NOT group-writable + (documents the gap — flips to `g+w`-or-hint when fixed); +3. dev-side edit of `.ddev/config.yaml` fails EACCES pre-`refresh`, + works after `config.sh refresh` (the existing repair path); +4. the follow-up `ddev start` prints the bootstrap hint (hook works — + `.ddev/config.yaml` exists by then) and fails EPERM until handover, + then succeeds — the full designed first-start flow of the burn-in. + +**Finding 2 — `ddev composer create-project` exits 23 ("Moving install +to Composer root", partial result):** files landed (composer.json, +packages/, …) but the move was incomplete and the command errored; +recovery via `ddev composer req`/`install` worked. **Root cause +diagnosed (2026-08-22, in-suite):** the move is an `rsync -rltgopD` +inside the web container (`cmd/ddev/cmd/composer-create-project.go`); +`-o/-g` chowns the composer root, which only succeeds when the ddev user +OWNS it. In dev-owned mode (kit default) the root belongs to the +developer, so as opencode rsync dies with `chown "/var/www/html/." +failed: Operation not permitted` → exit 23 — the exact burn-in symptom. +With an opencode-owned (handover-mode) root create-project succeeds. +Consequence: **dev-owned mode × `create-project` are incompatible until +a fix** (kit-side: document the temporary `config handover` workaround +or special-case it; upstream: ddev could drop `-o/-g` when running as +non-root on a foreign-owned root). DD14 asserts both sides as the +regression pair. Reference checkouts for follow-up analysis: +`github/ddev` (tag v1.25.3), `github/composer` (tag 2.10.2) in this +workspace. + +**DD14 checks:** dev-owned root → create-project must FAIL with exit 23 +(tripwire, Finding 2); after switching to handover mode (root +opencode-owned) create-project must exit 0 and land composer.lock + +public/index.php completely. + +**Validated as working (no new checks needed beyond existing DD rows):** +the pre-start hint + handover + green start flow (DD2/DD4), hosts-hint + +`ddev-hosts-add` bridge (DD section §7 out-of-scope note), `ddev typo3 +setup` / `ddev export-db` inside the container (DD9/DD10), and the whole +bare-origin flow (DD12). + +## 8. Integration + +- **Makefile:** `e2e-ddev` (`sh ./tests/e2e/run-ddev.sh $(ARGS)`), + `e2e-ddev-fresh` (`--fresh`), help entries. NOT added to `e2e-all` — + `e2e-all` stays the merge gate; e2e-ddev downloads GBs and must not gate + every PR. +- **CI (phase 3):** `.github/workflows/e2e-ddev.yml` — live since + 2026-08-22 as **manual-only** (`workflow_dispatch` with `site_tier` and + `ddev_version` inputs; the maintainer validates runner runtime first). + Weekly `schedule` is the planned follow-up once duration is known; no + image caching between runs (R6). The chmod-list rule now covers all + THREE workflow files (`tests/test-workflows.sh` enforces it, including + the runner `tests/e2e/run-ddev.sh`). +- **Docs:** this record + a MANUAL.md "troubleshooting with e2e-ddev" note + + README testing mention in the same PR as the runner (repo rule: docs move + with code). + +## 9. Rollout & effort + +| Phase | Content | Estimate | +|---|---|---| +| 1 | runner skeleton + golden-image build/quiesce/commit/boot (§4) + DD0–DD2 | 1–2 d | +| 2 | full catalog DD3–DD9 + `--fresh`/TTL knobs | 1 d | +| 2b | real-site tier: fixture intake (**done 2026-08-22** — `tests/e2e/fixtures/camino/` harvested), remaining: warm-up integration + DD10–DD11 | 1 d | +| 2c | git-flow tier: `make-bare-origin.sh` generator (**done 2026-08-22**, verified) + DD12 | 0.5 d | +| 2d | burn-in findings §7.3: DD13 (config-on-empty-dir tripwires) + DD14 (create-project) — DD13/DD14 need no site fixture, only ddev + composer | 0.5 d | +| 3 | Makefile/CI workflow + test-workflows + docs | 0.5–1 d | +| 4 | burn-in on this workspace (rootful + rootless outer layouts), matrix knob `DDEV_VERSION` for new ddev releases | 0.5 d | + +Expected wall times (workspace, rootless outer daemon): first build 5–12 min +(image pulls dominate; real-site warm-up adds `composer install` + import, +a few more minutes), cached runs 1–3 min (real-site tier: +1–2 min for +copy/import/boot), `--fresh` = first-build cost again. + +## 10. Open questions + +- Warm-up provisioning via the kit vs. raw `dockerd-rootless-setuptool.sh` + (R5) — start kit-path, keep fallback. +- TTL default 14 d vs. manual-only refresh — plan says 14 d; decide after + seeing how often label-rebuilds hit mid-week. +- DD9 depth (db round-trip worth its runtime?) — optional flag initially. +- Should DD2 use a second, `drupal`-typed skeleton for the + `sites/default` handover row, or is typo3 enough for v1? Plan: typo3 only, + drupal behind the same flag as DD9. +- Fixture versioning policy (real-site tier): follow Camino/TYPO3 releases + on fixture refresh, or pin a known-good snapshot and only bump + deliberately? Plan: pin deliberately (a fixture bump is a conscious act + + golden rebuild), track upstream in the fixture README. +- Does the Camino theme license permit its use in fixture context (fetch + via packagist, demo dump in a public repo)? Plan: assume yes for + packagist usage; verify before committing the dump, keep the dump free of + anything beyond theme demo content. +- Bare-origin branch set (§7.2): are the four branches enough, or does the + maintainer's real project bare repo show additional collision files + (e.g. tracked `.ddev/config.yaml` churn, `public/fileadmin` content)? + **Resolved (2026-08-22):** reconciled against the real repo — tracked + set is 11 files incl. `.ddev/config.yaml`; generator covers it + (`feature/ddev-tree` includes config.yaml churn). `fileadmin` is + gitignored in the real project, correctly absent from the fixture. +- DD12.2 asserts today's EPERM as a tripwire — flip it to "must succeed" + the day handover mode is retired in favor of dev-owned-everywhere (then + the check becomes the promise, not the pain). +- DD13's `ddev config` gap (§7.3 Finding 1): fix inside the kit first + (post-`config` hint arm in `ddev-as-opencode.sh`) or ship the e2e + tripwire first and fix later? Plan: file the issue now, ship both — + the tripwire documents current behavior, the fix PR flips it. +- DD14's exit-23 cause is undiagnosed — investigate during phase 1 + burn-in (may be a ddev bug worth reporting upstream, in which case the + check pins ddev's behavior with a version note instead of ours). + **Resolved (2026-08-22):** rsync `-o/-g` chown on the dev-owned + composer root — dev-owned × create-project incompatible until fixed; + DD14 asserts the pair (fail in dev-owned, succeed in handover). + Remaining decision: kit-side workaround (hint/`config handover` + detour) vs. upstream report — tracked as the Finding-2 issue. diff --git a/tests/e2e/fixtures/camino/README.md b/tests/e2e/fixtures/camino/README.md new file mode 100644 index 0000000..54a4008 --- /dev/null +++ b/tests/e2e/fixtures/camino/README.md @@ -0,0 +1,71 @@ +# Fixture: camino — real TYPO3 v14 site with the Camino theme + +The real-site payload for the e2e-ddev suite (`docs/design/ddev-e2e-test.md` +§7.1, DD10/DD11) and the bare-origin generator (§7.2, DD12). Harvested +2026-08-22 from the manual burn-in install (`local/TEST-PROJECT-INSTALL.txt`). + +## Layout + +| Path | Content | +|---|---| +| `site/` | pristine fixture master — the tracked file set of the burn-in project (what a fresh clone contains) | +| `db.sql.gz` | DB dump of the installed site (`ddev export-db`), 85 KB — demo data only | +| `../make-bare-origin.sh` | builds the per-run bare git origin (main + 3 feature branches) from `site/` | + +## Versions (pinned) + +- TYPO3 `typo3/cms-base-distribution` **v14.3.0** (lock resolves CMS to v14.3.6) +- Theme `typo3/theme-camino` **v14.3.6** +- PHP 8.3, apache-fpm, docroot `public`, `project_tld local`, MariaDB 11.8 +- ddev config carries `disable_settings_management: true` (dev-owned mode) + +## Deliberate deviations from the burn-in repo + +Three edits vs. the committed state of the burn-in project, made so the +master works as a cloneable fixture: + +1. `.ddev/config.yaml`: the fixed `name: perm-kit-typo3-example` line is + removed — ddev then derives the project name from the clone directory + (e.g. `camino-e2e` → `camino-e2e.local`). +2. `public/index.php` is included although the TYPO3 `.gitignore` excludes + `/public/*` — the fixture `.gitignore` adds a `!/public/index.php` + exception, because without it a fresh clone cannot boot (the burn-in repo + relied on `create-project` having run in-place first). +3. `backup.sql.gz` is renamed to the sidecar `db.sql.gz` and is NOT tracked + inside the git origin — it is fixture payload, not project content. + +Everything else is byte-identical to the burn-in commits, including +`config/system/settings.php` (`trustedHostsPattern: '.*'`, ddev-standard +db/db credentials) and the site config with its relative `base: /camino/` +(works under any hostname — the frontend marker route is **`/camino/`**). + +## Demo credentials (throwaway, nothing real) + +- TYPO3 backend user `admin` (system maintainer), password from the burn-in + log — irrelevant for e2e (no backend login asserted). +- `settings.php` carries the demo install's `encryptionKey` and install-tool + hash. Regenerate on `ddev typo3 setup` if that ever matters; this fixture + is for file/ownership/boot assertions, not for holding secrets. + +## Regenerating + +```bash +# fresh empty dir, then (dev terminal, kit installed): +ddev config --php-version 8.3 --project-type=typo3 --docroot=public \ + --webserver-type=apache-fpm --project-tld local \ + --web-environment-add="TYPO3_CONTEXT=Development/ddev" --nodejs-version=14.18.1 +sudo opencode-permissions-kit config handover "$PWD" +ddev start +ddev composer create-project "typo3/cms-base-distribution:^14" +ddev composer req typo3/theme-camino +ddev composer install # retry on GitHub CDN 504s (burn-in finding) +ddev typo3 setup --server-type=other --driver=mysqli --host=db --port=3306 \ + --dbname=db --username=db --password=db --create-site .local +# manual steps (see TEST-PROJECT-INSTALL.txt): trustedHostsPattern '.*' in +# config/system/settings.php; copy root-htaccess template to public/.htaccess +ddev launch /camino/ # verify frontend renders +ddev export-db -f=db.sql.gz +``` + +Then re-apply the three deviations above and refresh `site/`. Bump +`kit.e2e.format` in the e2e-ddev runner so cached golden images rebuild. diff --git a/tests/e2e/fixtures/camino/db.sql.gz b/tests/e2e/fixtures/camino/db.sql.gz new file mode 100644 index 0000000..d4a47ba Binary files /dev/null and b/tests/e2e/fixtures/camino/db.sql.gz differ diff --git a/tests/e2e/fixtures/camino/site/.ddev/config.yaml b/tests/e2e/fixtures/camino/site/.ddev/config.yaml new file mode 100644 index 0000000..4f708d1 --- /dev/null +++ b/tests/e2e/fixtures/camino/site/.ddev/config.yaml @@ -0,0 +1,298 @@ +type: typo3 +docroot: public +php_version: "8.3" +webserver_type: apache-fpm +xdebug_enabled: false +additional_hostnames: [] +additional_fqdns: [] +database: + type: mariadb + version: "11.8" +project_tld: local +use_dns_when_possible: true +composer_version: "2" +web_environment: + - TYPO3_CONTEXT=Development/ddev +nodejs_version: 14.18.1 +corepack_enable: false + +# Key features of DDEV's config.yaml: + +# name: # Name of the project, automatically provides +# http://projectname.ddev.site and https://projectname.ddev.site +# If the name is omitted, the project will take the name of the enclosing directory, +# which is useful if you want to have a copy of the project side by side with this one. + +# type: # asterios, backdrop, cakephp, codeigniter, craftcms, drupal, drupal6, drupal7, drupal8, drupal9, drupal10, drupal11, drupal12, generic, joomla, laravel, magento, magento2, php, shopware6, silverstripe, symfony, typo3, wordpress, wp-bedrock +# See https://docs.ddev.com/en/stable/users/quickstart/ for more +# information on the different project types + +# docroot: # Relative path to the directory containing index.php. + +# php_version: "8.4" # PHP version to use, "5.6" through "8.5" + +# You can explicitly specify the webimage but this +# is not recommended, as the images are often closely tied to DDEV's behavior, +# so this can break upgrades. + +# webimage: +# It’s unusual to change this option, and we don’t recommend it without Docker experience and a good reason. +# Typically, this means additions to the existing web image using a .ddev/web-build/Dockerfile.* + +# database: +# type: # mysql, mariadb, postgres +# version: # database version, like "10.11" or "8.0" +# MariaDB versions can be 5.5-10.8, 10.11, 11.4, 11.8, 12.3 +# MySQL versions can be 5.5-8.0, 8.4 +# PostgreSQL versions can be 9-18 + +# router_http_port: # Port to be used for http (defaults to global configuration, usually 80) +# router_https_port: # Port for https (defaults to global configuration, usually 443) + +# xdebug_enabled: false # Set to true to enable Xdebug and "ddev start" or "ddev restart" +# Note that for most people the commands +# "ddev xdebug" to enable Xdebug and "ddev xdebug off" to disable it work better, +# as leaving Xdebug enabled all the time is a big performance hit. + +# xhgui_http_port: "8143" +# xhgui_https_port: "8142" +# The XHGui ports can be changed from the default 8143 and 8142 +# Very rarely used + +# host_xhgui_port: "8142" +# Can be used to change the host binding port of the XHGui +# application. Rarely used; only when port conflict and +# bind_all_ports is used (normally with router disabled) + +# xhprof_mode: [prepend|xhgui|global] +# Default is "xhgui" + +# webserver_type: nginx-fpm, apache-fpm, generic + +# timezone: Europe/Berlin +# If timezone is unset, DDEV will attempt to derive it from the host system timezone +# using the $TZ environment variable or the /etc/localtime symlink. +# This is the timezone used in the containers and by PHP; +# it can be set to any valid timezone, +# see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones +# For example Europe/Dublin or MST7MDT + +# composer_root: +# Relative path to the Composer root directory from the project root. This is +# the directory which contains the composer.json and where all Composer related +# commands are executed. + +# composer_version: "2" +# You can set it to "" or "2" (default) for Composer v2 +# to use the latest major version available at the time your container is built. +# It is also possible to use any other Composer version channel. This includes: +# - 2.2 (latest Composer LTS version) +# - stable +# - preview +# - snapshot +# Alternatively, an explicit Composer version may be specified, for example "2.2.18". +# To reinstall Composer after the image was built, run "ddev utility rebuild". + +# nodejs_version: "24" +# change from the default system Node.js version to any other version. +# See https://docs.ddev.com/en/stable/users/configuration/config/#nodejs_version for more information +# and https://www.npmjs.com/package/n#specifying-nodejs-versions for the full documentation. + +# corepack_enable: false +# Change to 'true' to 'corepack enable' and gain access to latest versions of yarn/pnpm + +# additional_hostnames: +# - somename +# - someothername +# would provide http and https URLs for "somename.ddev.site" +# and "someothername.ddev.site". + +# additional_fqdns: +# - example.com +# - sub1.example.com +# would provide http and https URLs for "example.com" and "sub1.example.com" +# Please take care with this because it can cause great confusion. + +# upload_dirs: "custom/upload/dir" +# +# upload_dirs: +# - custom/upload/dir +# - ../private +# +# would set the destination paths for ddev import-files to /custom/upload/dir +# When Mutagen is enabled this path is bind-mounted so that all the files +# in the upload_dirs don't have to be synced into Mutagen. + +# disable_upload_dirs_warning: false +# If true, turns off the normal warning that says +# "You have Mutagen enabled and your 'php' project type doesn't have upload_dirs set" + +# ddev_version_constraint: "" +# Example: +# ddev_version_constraint: ">= 1.24.8" +# This will enforce that the running ddev version is within this constraint. +# See https://github.com/Masterminds/semver#checking-version-constraints for +# supported constraint formats + +# working_dir: +# web: /var/www/html +# db: /home +# would set the default working directory for the web and db services. +# These values specify the destination directory for ddev ssh and the +# directory in which commands passed into ddev exec are run. + +# omit_containers: [db, ddev-ssh-agent] +# Currently only these containers are supported. Some containers can also be +# omitted globally in the ~/.ddev/global_config.yaml. Note that if you omit +# the "db" container, several standard features of DDEV that access the +# database container will be unusable. In the global configuration it is also +# possible to omit ddev-router, but not here. + +# performance_mode: "global" +# DDEV offers performance optimization strategies to improve the filesystem +# performance depending on your host system. Should be configured globally. +# +# If set, will override the global config. Possible values are: +# - "global": uses the value from the global config. +# - "none": disables performance optimization for this project. +# - "mutagen": enables Mutagen for this project. +# +# See https://docs.ddev.com/en/stable/users/install/performance/#mutagen + +# fail_on_hook_fail: False +# Decide whether 'ddev start' should be interrupted by a failing hook + +# host_https_port: "59002" +# The host port binding for https can be explicitly specified. It is +# dynamic unless otherwise specified. +# This is not used by most people, most people use the *router* instead +# of the localhost port. + +# host_webserver_port: "59001" +# The host port binding for the ddev-webserver can be explicitly specified. It is +# dynamic unless otherwise specified. +# This is not used by most people, most people use the *router* instead +# of the localhost port. + +# host_db_port: "59002" +# The host port binding for the ddev-dbserver can be explicitly specified. It is dynamic +# unless explicitly specified. + +# mailpit_http_port: "8025" +# mailpit_https_port: "8026" +# The Mailpit ports can be changed from the default 8025 and 8026 + +# host_mailpit_port: "8025" +# The mailpit port is not normally bound on the host at all, instead being routed +# through ddev-router, but it can be bound directly to localhost if specified here. + +# webimage_extra_packages: ['php${DDEV_PHP_VERSION}-tidy', 'php${DDEV_PHP_VERSION}-yac'] +# Extra Debian packages that are needed in the webimage can be added here + +# dbimage_extra_packages: [netcat, telnet, sudo] +# Extra Debian packages that are needed in the dbimage can be added here + +# use_dns_when_possible: true +# If the host has internet access and the domain configured can +# successfully be looked up, DNS will be used for hostname resolution +# instead of editing /etc/hosts +# Defaults to true + +# project_tld: ddev.site +# The top-level domain used for project URLs +# The default "ddev.site" allows DNS lookup via a wildcard + +# share_default_provider: ngrok +# The default share provider to use for "ddev share" +# Defaults to global configuration, usually "ngrok" +# Can be "ngrok" or "cloudflared" or the name of a custom provider from .ddev/share-providers/ + +# share_provider_args: --basic-auth username:pass1234 +# Provide extra flags to the share provider script +# See https://docs.ddev.com/en/stable/users/configuration/config/#share_provider_args + +# disable_settings_management: false +# If true, DDEV will not create CMS-specific settings files like +# Drupal's settings.php/settings.ddev.php or TYPO3's additional.php +# In this case the user must provide all such settings. + +# You can inject environment variables into the web container with: +# web_environment: +# - SOMEENV=somevalue +# - SOMEOTHERENV=someothervalue + +# no_project_mount: false +# (Experimental) If true, DDEV will not mount the project into the web container; +# the user is responsible for mounting it manually or via a script. +# This is to enable experimentation with alternate file mounting strategies. +# For advanced users only! + +# bind_all_interfaces: false +# If true, host ports will be bound on all network interfaces, +# not the localhost interface only. This means that ports +# will be available on the local network if the host firewall +# allows it. + +# default_container_timeout: 120 +# The default time that DDEV waits for all containers to become ready can be increased from +# the default 120. This helps in importing huge databases, for example. + +#web_extra_exposed_ports: +#- name: nodejs +# container_port: 3000 +# http_port: 2999 +# https_port: 3000 +#- name: something +# container_port: 4000 +# https_port: 4000 +# http_port: 3999 +# Allows a set of extra ports to be exposed via ddev-router +# Fill in all three fields even if you don’t intend to use the https_port! +# If you don’t add https_port, then it defaults to 0 and ddev-router will fail to start. +# +# The port behavior on the ddev-webserver must be arranged separately, for example +# using web_extra_daemons. +# For example, with a web app on port 3000 inside the container, this config would +# expose that web app on https://.ddev.site:9999 and http://.ddev.site:9998 +# web_extra_exposed_ports: +# - name: myapp +# container_port: 3000 +# http_port: 9998 +# https_port: 9999 + +#web_extra_daemons: +#- name: "http-1" +# command: "/var/www/html/node_modules/.bin/http-server -p 3000" +# directory: /var/www/html +#- name: "http-2" +# command: "/var/www/html/node_modules/.bin/http-server /var/www/html/sub -p 3000" +# directory: /var/www/html + +# override_config: false +# By default, config.*.yaml files are *merged* into the configuration +# But this means that some things can't be overridden +# For example, if you have 'use_dns_when_possible: true' you can't override it with a merge +# and you can't erase existing hooks or all environment variables. +# However, with "override_config: true" in a particular config.*.yaml file, +# 'use_dns_when_possible: false' can override the existing values, and +# hooks: +# post-start: [] +# or +# web_environment: [] +# or +# additional_hostnames: [] +# can have their intended effect. 'override_config' affects only behavior of the +# config.*.yaml file it exists in. + +# Many DDEV commands can be extended to run tasks before or after the +# DDEV command is executed, for example "post-start", "post-import-db", +# "pre-composer", "post-composer" +# See https://docs.ddev.com/en/stable/users/extend/custom-commands/ for more +# information on the commands that can be extended and the tasks you can define +# for them. Example: +#hooks: +# post-start: +# - exec: composer install -d /var/www/html +# opencode permissions kit: dev-owned mode — ddev must not +# chmod/write settings files outside .ddev/ (permanent 2775/664). +disable_settings_management: true diff --git a/tests/e2e/fixtures/camino/site/.gitignore b/tests/e2e/fixtures/camino/site/.gitignore new file mode 100644 index 0000000..7d69d42 --- /dev/null +++ b/tests/e2e/fixtures/camino/site/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +.idea +nbproject +/var/* +!/var/labels +/vendor +/public/* +!/public/.htaccess +!/public/index.php diff --git a/tests/e2e/fixtures/camino/site/LICENSE b/tests/e2e/fixtures/camino/site/LICENSE new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/tests/e2e/fixtures/camino/site/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/tests/e2e/fixtures/camino/site/README.md b/tests/e2e/fixtures/camino/site/README.md new file mode 100644 index 0000000..ad2c471 --- /dev/null +++ b/tests/e2e/fixtures/camino/site/README.md @@ -0,0 +1,66 @@ +# TYPO3 CMS Base Distribution + +Get going quickly with TYPO3 CMS. + +## Prerequisites + +* PHP 8.2 +* [Composer](https://getcomposer.org/download/) + +## Quickstart + +* `composer create-project typo3/cms-base-distribution project-name ^13` +* `cd project-name` + +Note that this distribution installs most, but not all of the TYPO3 CMS core extensions. +Depending on your need you might also want to install other TYPO3 extensions from +[packagist.org](https://packagist.org/?type=typo3-cms-framework). + +### Setup + +To start an interactive installation, you can do so by executing the following +command and then follow the wizard: + +```bash +composer exec typo3 setup +``` + +### Setup unattended (optional) + +If you're a more advanced user, you might want to leverage the unattended installation. +To do this, you need to execute the following command and substitute the arguments +with your own environment configuration. + +```bash +export TYPO3_SETUP_ADMIN_PASSWORD=$(tr -dc "_A-Za-z0-9#=$()/" < /dev/urandom | head -c24) +composer exec -- typo3 setup \ + --no-interaction \ + --server-type=other \ + --driver=sqlite \ + --admin-username=admin \ + --admin-email="info@example.com" \ + --project-name="My TYPO3 Project" \ + --create-site="http://localhost:8000/" +echo "Admin password: ${TYPO3_SETUP_ADMIN_PASSWORD}" +``` + +### Development server + +While it's advised to use a more sophisticated web server such as +Apache 2 or Nginx, you can instantly run the project by using PHPs` built-in +[web server](https://secure.php.net/manual/en/features.commandline.webserver.php). + +* `TYPO3_CONTEXT=Development php -S localhost:8000 -t public` +* open your browser at "http://localhost:8000" + +Please be aware that the built-in web server is single threaded and only meant +to be used for development. + +## Next steps + +* [Getting Started with TYPO3](https://docs.typo3.org/permalink/t3start:start) +* [Create a Site Package](https://docs.typo3.org/permalink/t3sitepackage:start) + +## License + +GPL-2.0 or later diff --git a/tests/e2e/fixtures/camino/site/composer.json b/tests/e2e/fixtures/camino/site/composer.json new file mode 100644 index 0000000..b16ab75 --- /dev/null +++ b/tests/e2e/fixtures/camino/site/composer.json @@ -0,0 +1,49 @@ +{ + "name": "typo3/cms-base-distribution", + "description": "TYPO3 CMS Base Distribution", + "license": "GPL-2.0-or-later", + "type": "project", + "config": { + "allow-plugins": { + "typo3/class-alias-loader": true, + "typo3/cms-composer-installers": true + }, + "platform": { + "php": "8.2.0" + }, + "sort-packages": true + }, + "require": { + "typo3/cms-backend": "^14.3", + "typo3/cms-belog": "^14.3", + "typo3/cms-beuser": "^14.3", + "typo3/cms-core": "^14.3", + "typo3/cms-dashboard": "^14.3", + "typo3/cms-extbase": "^14.3", + "typo3/cms-felogin": "^14.3", + "typo3/cms-filelist": "^14.3", + "typo3/cms-fluid": "^14.3", + "typo3/cms-fluid-styled-content": "^14.3", + "typo3/cms-form": "^14.3", + "typo3/cms-frontend": "^14.3", + "typo3/cms-impexp": "^14.3", + "typo3/cms-info": "^14.3", + "typo3/cms-install": "^14.3", + "typo3/cms-reactions": "^14.3", + "typo3/cms-recycler": "^14.3", + "typo3/cms-rte-ckeditor": "^14.3", + "typo3/cms-seo": "^14.3", + "typo3/cms-setup": "^14.3", + "typo3/cms-sys-note": "^14.3", + "typo3/cms-tstemplate": "^14.3", + "typo3/cms-viewpage": "^14.3", + "typo3/cms-webhooks": "^14.3", + "typo3/theme-camino": "*" + }, + "repositories": { + "packages": { + "type": "path", + "url": "./packages/*" + } + } +} diff --git a/tests/e2e/fixtures/camino/site/composer.lock b/tests/e2e/fixtures/camino/site/composer.lock new file mode 100644 index 0000000..f3850c4 --- /dev/null +++ b/tests/e2e/fixtures/camino/site/composer.lock @@ -0,0 +1,7808 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "1e85162c676e3f717877a266f7155b00", + "packages": [ + { + "name": "bacon/bacon-qr-code", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/Bacon/BaconQrCode.git", + "reference": "4da2233e72eeecd9be3b62e0dc2cc9ed8e2e31c2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/4da2233e72eeecd9be3b62e0dc2cc9ed8e2e31c2", + "reference": "4da2233e72eeecd9be3b62e0dc2cc9ed8e2e31c2", + "shasum": "" + }, + "require": { + "dasprid/enum": "^1.0.3", + "ext-iconv": "*", + "php": "^8.1" + }, + "require-dev": { + "phly/keep-a-changelog": "^2.12", + "phpunit/phpunit": "^10.5.11 || ^11.0.4", + "spatie/phpunit-snapshot-assertions": "^5.1.5", + "spatie/pixelmatch-php": "^1.2.0", + "squizlabs/php_codesniffer": "^3.9" + }, + "suggest": { + "ext-imagick": "to generate QR code images" + }, + "type": "library", + "autoload": { + "psr-4": { + "BaconQrCode\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "BaconQrCode is a QR code generator for PHP.", + "homepage": "https://github.com/Bacon/BaconQrCode", + "support": { + "issues": "https://github.com/Bacon/BaconQrCode/issues", + "source": "https://github.com/Bacon/BaconQrCode/tree/v3.1.1" + }, + "time": "2026-04-05T21:06:35+00:00" + }, + { + "name": "christian-riesen/base32", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/ChristianRiesen/base32.git", + "reference": "be07b4a4a2d6056975934d71e111c64e7d8babf9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ChristianRiesen/base32/zipball/be07b4a4a2d6056975934d71e111c64e7d8babf9", + "reference": "be07b4a4a2d6056975934d71e111c64e7d8babf9", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.64", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Base32\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Riesen", + "email": "chris.riesen@gmail.com", + "homepage": "http://christianriesen.com", + "role": "Developer" + } + ], + "description": "Base32 encoder/decoder according to RFC 4648", + "homepage": "https://github.com/ChristianRiesen/base32", + "keywords": [ + "base32", + "decode", + "encode", + "rfc4648" + ], + "support": { + "issues": "https://github.com/ChristianRiesen/base32/issues", + "source": "https://github.com/ChristianRiesen/base32/tree/2.0.1" + }, + "time": "2026-07-11T21:00:30+00:00" + }, + { + "name": "composer/class-map-generator", + "version": "1.7.3", + "source": { + "type": "git", + "url": "https://github.com/composer/class-map-generator.git", + "reference": "86d8208fc3c649a3a999daf1a63c25201be2990f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/class-map-generator/zipball/86d8208fc3c649a3a999daf1a63c25201be2990f", + "reference": "86d8208fc3c649a3a999daf1a63c25201be2990f", + "shasum": "" + }, + "require": { + "composer/pcre": "^2.1 || ^3.1", + "php": "^7.2 || ^8.0", + "symfony/finder": "^4.4 || ^5.3 || ^6 || ^7 || ^8" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-deprecation-rules": "^1 || ^2", + "phpstan/phpstan-phpunit": "^1 || ^2", + "phpstan/phpstan-strict-rules": "^1.1 || ^2", + "phpunit/phpunit": "^8", + "symfony/filesystem": "^5.4 || ^6 || ^7 || ^8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\ClassMapGenerator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Utilities to scan PHP code and generate class maps.", + "keywords": [ + "classmap" + ], + "support": { + "issues": "https://github.com/composer/class-map-generator/issues", + "source": "https://github.com/composer/class-map-generator/tree/1.7.3" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2026-05-05T09:17:07+00:00" + }, + { + "name": "composer/pcre", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<2.2.2" + }, + "require-dev": { + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.4.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2026-06-07T11:47:49+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.4", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.4" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2025-08-20T19:15:30+00:00" + }, + { + "name": "dasprid/enum", + "version": "1.0.7", + "source": { + "type": "git", + "url": "https://github.com/DASPRiD/Enum.git", + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/b5874fa9ed0043116c72162ec7f4fb50e02e7cce", + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce", + "shasum": "" + }, + "require": { + "php": ">=7.1 <9.0" + }, + "require-dev": { + "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "DASPRiD\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "PHP 7.1 enum implementation", + "keywords": [ + "enum", + "map" + ], + "support": { + "issues": "https://github.com/DASPRiD/Enum/issues", + "source": "https://github.com/DASPRiD/Enum/tree/1.0.7" + }, + "time": "2025-09-16T12:23:56+00:00" + }, + { + "name": "doctrine/dbal", + "version": "4.4.4", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "fb9e0ffe15e1590e24dc61c0c0a23f9a33ee42ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/fb9e0ffe15e1590e24dc61c0c0a23f9a33ee42ce", + "reference": "fb9e0ffe15e1590e24dc61c0c0a23f9a33ee42ce", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1.5", + "php": "^8.2", + "psr/cache": "^1|^2|^3", + "psr/log": "^1|^2|^3" + }, + "require-dev": { + "doctrine/coding-standard": "14.0.0", + "fig/log-test": "^1", + "jetbrains/phpstorm-stubs": "2023.2", + "phpstan/phpstan": "2.1.30", + "phpstan/phpstan-phpunit": "2.0.7", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "11.5.50", + "slevomat/coding-standard": "8.27.1", + "squizlabs/php_codesniffer": "4.0.1", + "symfony/cache": "^6.3.8|^7.0|^8.0", + "symfony/console": "^5.4|^6.3|^7.0|^8.0" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", + "keywords": [ + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "support": { + "issues": "https://github.com/doctrine/dbal/issues", + "source": "https://github.com/doctrine/dbal/tree/4.4.4" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", + "type": "tidelift" + } + ], + "time": "2026-07-21T14:34:40+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.6", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=14" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^14", + "phpstan/phpstan": "1.4.10 || 2.1.30", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.6" + }, + "time": "2026-02-07T07:09:04+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:23:10+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, + { + "name": "egulias/email-validator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2025-03-06T22:45:56+00:00" + }, + { + "name": "enshrined/svg-sanitize", + "version": "0.22.0", + "source": { + "type": "git", + "url": "https://github.com/darylldoyle/svg-sanitizer.git", + "reference": "0afa95ea74be155a7bcd6c6fb60c276c39984500" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/darylldoyle/svg-sanitizer/zipball/0afa95ea74be155a7bcd6c6fb60c276c39984500", + "reference": "0afa95ea74be155a7bcd6c6fb60c276c39984500", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.5 || ^8.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "enshrined\\svgSanitize\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "Daryll Doyle", + "email": "daryll@enshrined.co.uk" + } + ], + "description": "An SVG sanitizer for PHP", + "support": { + "issues": "https://github.com/darylldoyle/svg-sanitizer/issues", + "source": "https://github.com/darylldoyle/svg-sanitizer/tree/0.22.0" + }, + "time": "2025-08-12T10:13:48+00:00" + }, + { + "name": "firebase/php-jwt", + "version": "v7.1.0", + "source": { + "type": "git", + "url": "https://github.com/googleapis/php-jwt.git", + "reference": "b374a5d1a4f1f67fadc2165cdb284645945e2fc0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/googleapis/php-jwt/zipball/b374a5d1a4f1f67fadc2165cdb284645945e2fc0", + "reference": "b374a5d1a4f1f67fadc2165cdb284645945e2fc0", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.4", + "phpfastcache/phpfastcache": "^9.2", + "phpseclib/phpseclib": "~3.0", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psr/cache": "^2.0||^3.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" + }, + "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present", + "phpseclib/phpseclib": "Support PS256 (RSASSA-PSS) signatures" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/googleapis/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "support": { + "issues": "https://github.com/googleapis/php-jwt/issues", + "source": "https://github.com/googleapis/php-jwt/tree/v7.1.0" + }, + "time": "2026-06-11T17:54:14+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.15.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc", + "reference": "ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^2.5.2", + "guzzlehttp/psr7": "^2.13", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.25" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.3", + "guzzlehttp/test-server": "^0.7", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.52 || ^9.6.34", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.15.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2026-08-05T19:48:21+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.5.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "2823687acff28b2dbe67b2508a6b300e2c3fa4ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/2823687acff28b2dbe67b2508a6b300e2c3fa4ce", + "reference": "2823687acff28b2dbe67b2508a6b300e2c3fa4ce", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.52 || ^9.6.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.5.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2026-08-05T19:30:54+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.13.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "dad89620b7a6edb60c15858442eb2e408b45d8f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/dad89620b7a6edb60c15858442eb2e408b45d8f4", + "reference": "dad89620b7a6edb60c15858442eb2e408b45d8f4", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.25" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "1.1.0", + "jshttp/mime-db": "1.54.0.1", + "phpunit/phpunit": "^8.5.52 || ^9.6.34" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.13.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2026-07-16T22:23:49+00:00" + }, + { + "name": "lolli42/finediff", + "version": "1.1.2", + "source": { + "type": "git", + "url": "https://github.com/lolli42/FineDiff.git", + "reference": "38a03ca581ee72d7b20bbb1d89d47c5ecf8b11ba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lolli42/FineDiff/zipball/38a03ca581ee72d7b20bbb1d89d47c5ecf8b11ba", + "reference": "38a03ca581ee72d7b20bbb1d89d47c5ecf8b11ba", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.2" + }, + "replace": { + "cogpowered/finediff": "*" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.89.1", + "phpstan/phpstan": "^2.1.31", + "phpunit/phpunit": "^11.5.43 || ^12.4.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "cogpowered\\FineDiff\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Raymond Hill" + }, + { + "name": "Rob Crowe", + "email": "rob@cogpowered.com" + }, + { + "name": "Christian Kuhn", + "email": "lolli@schwarzbu.ch", + "role": "maintainer" + } + ], + "description": "PHP implementation of a Fine granularity Diff engine", + "homepage": "https://github.com/lolli42/FineDiff", + "keywords": [ + "diff", + "finediff", + "opcode", + "string", + "text" + ], + "support": { + "issues": "https://github.com/lolli42/FineDiff/issues", + "source": "https://github.com/lolli42/FineDiff/tree/1.1.2" + }, + "time": "2025-10-31T12:28:27+00:00" + }, + { + "name": "masterminds/html5", + "version": "2.11.0", + "source": { + "type": "git", + "url": "https://github.com/Masterminds/html5-php.git", + "reference": "a1e7a2f88ee13635d86fc61cfbdf2306a76ddfc7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/a1e7a2f88ee13635d86fc61cfbdf2306a76ddfc7", + "reference": "a1e7a2f88ee13635d86fc61cfbdf2306a76ddfc7", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": ">=7.4" + }, + "require-dev": { + "phpunit/phpunit": "^6 || ^7 || ^8 || ^9 || ^10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Masterminds\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matt Butcher", + "email": "technosophos@gmail.com" + }, + { + "name": "Matt Farina", + "email": "matt@mattfarina.com" + }, + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + } + ], + "description": "An HTML5 parser and serializer.", + "homepage": "http://masterminds.github.io/html5-php", + "keywords": [ + "HTML5", + "dom", + "html", + "parser", + "querypath", + "serializer", + "xml" + ], + "support": { + "issues": "https://github.com/Masterminds/html5-php/issues", + "source": "https://github.com/Masterminds/html5-php/tree/2.11.0" + }, + "time": "2026-08-18T06:18:41+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.8.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/044a6a392ff8ad0d61f14370a5fbbd0a0107152f", + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.8.0" + }, + "time": "2026-07-04T14:30:18+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/7bae67520aa9f5ecc506d646810bd40d9da54582", + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1", + "ext-filter": "*", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^2.0", + "phpstan/phpdoc-parser": "^2.0", + "webmozart/assert": "^1.9.1 || ^2" + }, + "require-dev": { + "mockery/mockery": "~1.3.5 || ~1.6.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^5.26", + "shipmonk/dead-code-detector": "^0.5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.3" + }, + "time": "2026-03-18T20:49:53+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/327a05bbee54120d4786a0dc67aad30226ad4cf9", + "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev", + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/2.0.0" + }, + "time": "2026-01-06T21:53:42+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.3.3", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "fb19eedd2bb67ff8cf7a5502ad329e701d6398a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fb19eedd2bb67ff8cf7a5502ad329e701d6398a3", + "reference": "fb19eedd2bb67ff8cf7a5502ad329e701d6398a3", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.3" + }, + "time": "2026-07-08T07:01:06+00:00" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/http-server-handler", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-handler.git", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side request handler", + "keywords": [ + "handler", + "http", + "http-interop", + "psr", + "psr-15", + "psr-7", + "request", + "response", + "server" + ], + "support": { + "source": "https://github.com/php-fig/http-server-handler/tree/1.0.2" + }, + "time": "2023-04-10T20:06:20+00:00" + }, + { + "name": "psr/http-server-middleware", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-middleware.git", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0", + "psr/http-server-handler": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side middleware", + "keywords": [ + "http", + "http-interop", + "middleware", + "psr", + "psr-15", + "psr-7", + "request", + "response" + ], + "support": { + "issues": "https://github.com/php-fig/http-server-middleware/issues", + "source": "https://github.com/php-fig/http-server-middleware/tree/1.0.2" + }, + "time": "2023-04-11T06:14:47+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "symfony/cache", + "version": "v7.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache.git", + "reference": "f6028442dd1dfa4f88e9c7360d753948d165e938" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache/zipball/f6028442dd1dfa4f88e9c7360d753948d165e938", + "reference": "f6028442dd1dfa4f88e9c7360d753948d165e938", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/cache": "^2.0|^3.0", + "psr/log": "^1.1|^2|^3", + "symfony/cache-contracts": "^3.6", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/service-contracts": "^2.5|^3", + "symfony/var-exporter": "^6.4|^7.0|^8.0" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "ext-relay": "<0.12.1", + "symfony/dependency-injection": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/var-dumper": "<6.4" + }, + "provide": { + "psr/cache-implementation": "2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0", + "symfony/cache-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "cache/integration-tests": "^1.0.3", + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/filesystem": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Cache\\": "" + }, + "classmap": [ + "Traits/ValueWrapper.php" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides extended PSR-6, PSR-16 (and tags) implementations", + "homepage": "https://symfony.com", + "keywords": [ + "caching", + "psr6" + ], + "support": { + "source": "https://github.com/symfony/cache/tree/v7.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-19T08:28:05+00:00" + }, + { + "name": "symfony/cache-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache-contracts.git", + "reference": "9789738bc19af1106dc54d6afba9a0b467516cf2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/9789738bc19af1106dc54d6afba9a0b467516cf2", + "reference": "9789738bc19af1106dc54d6afba9a0b467516cf2", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/cache": "^3.0" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Cache\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to caching", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/cache-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:23:12+00:00" + }, + { + "name": "symfony/clock", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/674fa3b98e21531dd040e613479f5f6fa8f32111", + "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/config", + "version": "v7.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "696e12da8eea497a1a3808d714b43ff67a7df43e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/696e12da8eea497a1a3808d714b43ff67a7df43e", + "reference": "696e12da8eea497a1a3808d714b43ff67a7df43e", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/filesystem": "^7.1|^8.0", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/finder": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "require-dev": { + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/config/tree/v7.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-20T09:55:18+00:00" + }, + { + "name": "symfony/console", + "version": "v7.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "962e18f09ebe68a49039b4c82fc0ea4871824fca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/962e18f09ebe68a49039b4c82fc0ea4871824fca", + "reference": "962e18f09ebe68a49039b4c82fc0ea4871824fca", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2|^8.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T12:09:28+00:00" + }, + { + "name": "symfony/dependency-injection", + "version": "v7.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "f318ac9da5aba0be2cf43ecdd562c05a33bc11c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/f318ac9da5aba0be2cf43ecdd562c05a33bc11c0", + "reference": "f318ac9da5aba0be2cf43ecdd562c05a33bc11c0", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/service-contracts": "^3.6", + "symfony/var-exporter": "^6.4.20|^7.2.5|^8.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2", + "symfony/config": "<6.4", + "symfony/finder": "<6.4", + "symfony/yaml": "<6.4" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "symfony/service-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DependencyInjection\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows you to standardize and centralize the way objects are constructed in your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dependency-injection/tree/v7.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T17:40:08+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:23:12+00:00" + }, + { + "name": "symfony/doctrine-messenger", + "version": "v7.4.15", + "source": { + "type": "git", + "url": "https://github.com/symfony/doctrine-messenger.git", + "reference": "2356265da4dd8e8b689e735dcce33f5fd40506e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/doctrine-messenger/zipball/2356265da4dd8e8b689e735dcce33f5fd40506e5", + "reference": "2356265da4dd8e8b689e735dcce33f5fd40506e5", + "shasum": "" + }, + "require": { + "doctrine/dbal": "^3.6|^4", + "php": ">=8.2", + "symfony/messenger": "^7.2|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "doctrine/persistence": "<1.3" + }, + "require-dev": { + "doctrine/persistence": "^1.3|^2|^3", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0" + }, + "type": "symfony-messenger-bridge", + "autoload": { + "psr-4": { + "Symfony\\Component\\Messenger\\Bridge\\Doctrine\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Doctrine Messenger Bridge", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/doctrine-messenger/tree/v7.4.15" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-29T06:18:55+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v7.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "d269974ee93c61d03620ffee358355bfdb471d66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d269974ee93c61d03620ffee358355bfdb471d66", + "reference": "d269974ee93c61d03620ffee358355bfdb471d66", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T17:40:08+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c7de7a00ffb67842132da02ea92988a39ccd9f4e", + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:23:12+00:00" + }, + { + "name": "symfony/expression-language", + "version": "v7.4.14", + "source": { + "type": "git", + "url": "https://github.com/symfony/expression-language.git", + "reference": "bd5763f92959201816ecc31defdf352d2ea473be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/expression-language/zipball/bd5763f92959201816ecc31defdf352d2ea473be", + "reference": "bd5763f92959201816ecc31defdf352d2ea473be", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/service-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ExpressionLanguage\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an engine that can compile and evaluate expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/expression-language/tree/v7.4.14" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-08T20:24:16+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v7.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "ee7bc7bca4c7079b88e57d5000aeeb20df570c8d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/ee7bc7bca4c7079b88e57d5000aeeb20df570c8d", + "reference": "ee7bc7bca4c7079b88e57d5000aeeb20df570c8d", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v7.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T12:09:28+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "5ce28827081f6d1f0c32eaf3882750f19cb5bbe6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/5ce28827081f6d1f0c32eaf3882750f19cb5bbe6", + "reference": "5ce28827081f6d1f0c32eaf3882750f19cb5bbe6", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T12:09:28+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v7.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "2ebe78c083501dfb9509b31a7aedcae4d60a391f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/2ebe78c083501dfb9509b31a7aedcae4d60a391f", + "reference": "2ebe78c083501dfb9509b31a7aedcae4d60a391f", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v7.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-20T09:55:18+00:00" + }, + { + "name": "symfony/mailer", + "version": "v7.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "b17c9bf3a551d5f635638a3b6c05f06c4dc87584" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/b17c9bf3a551d5f635638a3b6c05f06c4dc87584", + "reference": "b17c9bf3a551d5f635638a3b6c05f06c4dc87584", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.2", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/mime": "^7.2|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/messenger": "<6.4", + "symfony/mime": "<6.4", + "symfony/twig-bridge": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/twig-bridge": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v7.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T17:40:08+00:00" + }, + { + "name": "symfony/messenger", + "version": "v7.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/messenger.git", + "reference": "3976018381c403e369d3027592e911b979eaeed9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/messenger/zipball/3976018381c403e369d3027592e911b979eaeed9", + "reference": "3976018381c403e369d3027592e911b979eaeed9", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/console": "<7.2", + "symfony/event-dispatcher": "<6.4", + "symfony/event-dispatcher-contracts": "<2.5", + "symfony/framework-bundle": "<6.4", + "symfony/http-kernel": "<7.3", + "symfony/lock": "<7.4", + "symfony/serializer": "<6.4.32|>=7.3,<7.3.10|>=7.4,<7.4.4|>=8.0,<8.0.4" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/console": "^7.2|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^7.3|^8.0", + "symfony/lock": "^7.4|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.32|~7.3.10|^7.4.4|^8.0.4", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Messenger\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Samuel Roze", + "email": "samuel.roze@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps applications send and receive messages to/from other applications or via message queues", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/messenger/tree/v7.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T17:40:08+00:00" + }, + { + "name": "symfony/mime", + "version": "v7.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "bf328d82105831db3e409195db0540ff57f27c80" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/bf328d82105831db3e409195db0540ff57f27c80", + "reference": "bf328d82105831db3e409195db0540ff57f27c80", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/mailer": "<6.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.44|^7.4.17|^8.1.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v7.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-22T09:04:42+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "2888fcdc4dc2fd5f7c7397be78631e8af12e02b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/2888fcdc4dc2fd5f7c7397be78631e8af12e02b4", + "reference": "2888fcdc4dc2fd5f7c7397be78631e8af12e02b4", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.41.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/bb899c1db0aa8127dc3afe8cda4a67eb24915f8d", + "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.41.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-28T08:25:59+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "dc21118016c039a66235cf93d96b435ffb282412" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/dc21118016c039a66235cf93d96b435ffb282412", + "reference": "dc21118016c039a66235cf93d96b435ffb282412", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.38.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-25T15:22:23+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.38.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-25T13:48:31+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.38.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-27T06:59:30+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.41.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "5ea99087fb99c273a9b9236ed4c31e78b16103c6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/5ea99087fb99c273a9b9236ed4c31e78b16103c6", + "reference": "5ea99087fb99c273a9b9236ed4c31e78b16103c6", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.41.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-01T12:47:55+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.38.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-26T12:51:13+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.41.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/255fab485aaa1006ed411040c42aecd7b5302d7a", + "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.41.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-01T12:47:55+00:00" + }, + { + "name": "symfony/polyfill-uuid", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/26dfec253c4cf3e51b541b52ddf7e42cb0908e94", + "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-uuid": "*" + }, + "suggest": { + "ext-uuid": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for uuid functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/process", + "version": "v7.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "058d17fc284cce14efb2385783b55014a461b176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/058d17fc284cce14efb2385783b55014a461b176", + "reference": "058d17fc284cce14efb2385783b55014a461b176", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T17:40:08+00:00" + }, + { + "name": "symfony/property-access", + "version": "v7.4.16", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-access.git", + "reference": "c3dce76a6697c6428f7d80d5cd1cf0aa5d9679f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-access/zipball/c3dce76a6697c6428f7d80d5cd1cf0aa5d9679f3", + "reference": "c3dce76a6697c6428f7d80d5cd1cf0aa5d9679f3", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/property-info": "^6.4.32|~7.3.10|^7.4.4|^8.0.4" + }, + "require-dev": { + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4.1|^7.0.1|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyAccess\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides functions to read and write from/to an object or array using a simple string notation", + "homepage": "https://symfony.com", + "keywords": [ + "access", + "array", + "extraction", + "index", + "injection", + "object", + "property", + "property-path", + "reflection" + ], + "support": { + "source": "https://github.com/symfony/property-access/tree/v7.4.16" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-30T12:37:26+00:00" + }, + { + "name": "symfony/property-info", + "version": "v7.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-info.git", + "reference": "a37837b52ed5dd7474c251d15694007344c6462d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-info/zipball/a37837b52ed5dd7474c251d15694007344c6462d", + "reference": "a37837b52ed5dd7474c251d15694007344c6462d", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/string": "^6.4|^7.0|^8.0", + "symfony/type-info": "^7.4.7|^8.0.7" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/cache": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/serializer": "<6.4" + }, + "require-dev": { + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts information about PHP class' properties using metadata of popular sources", + "homepage": "https://symfony.com", + "keywords": [ + "doctrine", + "phpdoc", + "property", + "symfony", + "type", + "validator" + ], + "support": { + "source": "https://github.com/symfony/property-info/tree/v7.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T17:40:08+00:00" + }, + { + "name": "symfony/rate-limiter", + "version": "v7.4.16", + "source": { + "type": "git", + "url": "https://github.com/symfony/rate-limiter.git", + "reference": "6703d040ab83401b27f3c7b29ff4c8c8106fedba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/rate-limiter/zipball/6703d040ab83401b27f3c7b29ff4c8c8106fedba", + "reference": "6703d040ab83401b27f3c7b29ff4c8c8106fedba", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/options-resolver": "^7.3|^8.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/lock": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\RateLimiter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Wouter de Jong", + "email": "wouter@wouterj.nl" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a Token Bucket implementation to rate limit input and output in your application", + "homepage": "https://symfony.com", + "keywords": [ + "limiter", + "rate-limiter" + ], + "support": { + "source": "https://github.com/symfony/rate-limiter/tree/v7.4.16" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-07T15:34:54+00:00" + }, + { + "name": "symfony/routing", + "version": "v7.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "ddd558991e98f693ae6bf5063cc1b0362c6bbec3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/ddd558991e98f693ae6bf5063cc1b0362c6bbec3", + "reference": "ddd558991e98f693ae6bf5063cc1b0362c6bbec3", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/config": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v7.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-17T13:12:36+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/c0a284bab1ed8aa0417e3d69250ab437739563a0", + "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-16T09:55:08+00:00" + }, + { + "name": "symfony/string", + "version": "v7.4.15", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "e394af32256bf9e7bf80849d95e589167c10097b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/e394af32256bf9e7bf80849d95e589167c10097b", + "reference": "e394af32256bf9e7bf80849d95e589167c10097b", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.33", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.4.15" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-28T07:33:02+00:00" + }, + { + "name": "symfony/translation", + "version": "v7.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "2ee1e4a3b32a528a642babe041ff7c440b213b4b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/2ee1e4a3b32a528a642babe041ff7c440b213b4b", + "reference": "2ee1e4a3b32a528a642babe041ff7c440b213b4b", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.5.3|^3.3" + }, + "conflict": { + "nikic/php-parser": "<5.0", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<6.4", + "symfony/yaml": "<6.4" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v7.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T17:40:08+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/ccb206b98faccc511ebae8e5fad50f2dc0b30621", + "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:23:12+00:00" + }, + { + "name": "symfony/type-info", + "version": "v7.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/type-info.git", + "reference": "62d0ad6995630a4f7ea8fd270aadb3e2545c8e31" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/type-info/zipball/62d0ad6995630a4f7ea8fd270aadb3e2545c8e31", + "reference": "62d0ad6995630a4f7ea8fd270aadb3e2545c8e31", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "phpstan/phpdoc-parser": "<1.30" + }, + "require-dev": { + "phpstan/phpdoc-parser": "^1.30|^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\TypeInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mathias Arlaud", + "email": "mathias.arlaud@gmail.com" + }, + { + "name": "Baptiste LEDUC", + "email": "baptiste.leduc@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts PHP types information.", + "homepage": "https://symfony.com", + "keywords": [ + "PHPStan", + "phpdoc", + "symfony", + "type" + ], + "support": { + "source": "https://github.com/symfony/type-info/tree/v7.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T15:52:48+00:00" + }, + { + "name": "symfony/uid", + "version": "v7.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "69d732355a139c6f8881337d28515aa01f12b8be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/69d732355a139c6f8881337d28515aa01f12b8be", + "reference": "69d732355a139c6f8881337d28515aa01f12b8be", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/uid/tree/v7.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-11T07:38:58+00:00" + }, + { + "name": "symfony/validator", + "version": "v7.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/validator.git", + "reference": "b1cbb758c005fbe0d7b2b8d1561869d318628a10" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/validator/zipball/b1cbb758c005fbe0d7b2b8d1561869d318628a10", + "reference": "b1cbb758c005fbe0d7b2b8d1561869d318628a10", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php83": "^1.27", + "symfony/translation-contracts": "^2.5|^3" + }, + "conflict": { + "doctrine/lexer": "<1.1", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<7.0", + "symfony/expression-language": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/intl": "<6.4", + "symfony/property-info": "<6.4", + "symfony/translation": "<6.4.3|>=7.0,<7.0.3", + "symfony/var-exporter": "<6.4.25|>=7.0,<7.3.3", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3|^4", + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/string": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4.3|^7.0.3|^8.0", + "symfony/type-info": "^7.1.8", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Validator\\": "" + }, + "exclude-from-classmap": [ + "/Tests/", + "/Resources/bin/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to validate values", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/validator/tree/v7.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-22T09:04:42+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v7.4.16", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "ca31404415670aa3834809005b529df1b84f0790" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/ca31404415670aa3834809005b529df1b84f0790", + "reference": "ca31404415670aa3834809005b529df1b84f0790", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "require-dev": { + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "lazy-loading", + "proxy", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v7.4.16" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-30T12:37:26+00:00" + }, + { + "name": "symfony/yaml", + "version": "v7.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "0b040d7b66ceb10b7bb24c8e6656257932693a12" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/0b040d7b66ceb10b7bb24c8e6656257932693a12", + "reference": "0b040d7b66ceb10b7bb24c8e6656257932693a12", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v7.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T12:09:28+00:00" + }, + { + "name": "typo3/class-alias-loader", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/TYPO3/class-alias-loader.git", + "reference": "c44de30ec91e134e28d4e886bc642bacaf4f407f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TYPO3/class-alias-loader/zipball/c44de30ec91e134e28d4e886bc642bacaf4f407f", + "reference": "c44de30ec91e134e28d4e886bc642bacaf4f407f", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0", + "php": ">=8.2" + }, + "replace": { + "helhum/class-alias-loader": "*" + }, + "require-dev": { + "composer/composer": "^2.0@dev", + "friendsofphp/php-cs-fixer": "^3.95", + "mikey179/vfsstream": "~1.4.0@dev", + "phpunit/phpunit": "^11" + }, + "type": "composer-plugin", + "extra": { + "class": "TYPO3\\ClassAliasLoader\\Plugin", + "branch-alias": { + "dev-main": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "TYPO3\\ClassAliasLoader\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Helmut Hummel", + "email": "info@helhum.io" + } + ], + "description": "Amends the composer class loader to support class aliases to provide backwards compatibility for packages", + "homepage": "http://github.com/TYPO3/class-alias-loader", + "keywords": [ + "alias", + "autoloader", + "classloader", + "composer" + ], + "support": { + "issues": "https://github.com/TYPO3/class-alias-loader/issues", + "source": "https://github.com/TYPO3/class-alias-loader/tree/v2.0.1" + }, + "time": "2026-04-17T13:11:31+00:00" + }, + { + "name": "typo3/cms-backend", + "version": "v14.3.6", + "source": { + "type": "git", + "url": "https://github.com/TYPO3-CMS/backend.git", + "reference": "a19fb339ad606185754b569f810d77edefb256d8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TYPO3-CMS/backend/zipball/a19fb339ad606185754b569f810d77edefb256d8", + "reference": "a19fb339ad606185754b569f810d77edefb256d8", + "shasum": "" + }, + "require": { + "ext-intl": "*", + "ext-libxml": "*", + "psr/event-dispatcher": "^1.0", + "typo3/cms-core": "14.3.6" + }, + "conflict": { + "typo3/cms": "*" + }, + "replace": { + "typo3/cms-about": "self.version", + "typo3/cms-context-help": "self.version", + "typo3/cms-cshmanual": "self.version", + "typo3/cms-func-wizards": "self.version", + "typo3/cms-recordlist": "self.version", + "typo3/cms-setup": "self.version", + "typo3/cms-t3editor": "self.version", + "typo3/cms-wizard-crpages": "self.version", + "typo3/cms-wizard-sortpages": "self.version" + }, + "suggest": { + "typo3/cms-install": "Displays a link to the Environment module in the System Information toolbar." + }, + "type": "typo3-cms-framework", + "extra": { + "typo3/cms": { + "Package": { + "protected": true, + "serviceProvider": "TYPO3\\CMS\\Backend\\ServiceProvider", + "partOfFactoryDefault": true, + "partOfMinimalUsableSystem": true + }, + "extension-key": "backend" + }, + "branch-alias": { + "dev-main": "14.3.x-dev" + }, + "typo3/class-alias-loader": { + "class-alias-maps": [ + "Migrations/Code/ClassAliasMap.php" + ] + } + }, + "autoload": { + "psr-4": { + "TYPO3\\CMS\\Backend\\": "Classes/", + "TYPO3\\CMS\\Setup\\Event\\": "DeprecatedClasses/Setup/Event/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "TYPO3 Core Team", + "email": "typo3cms@typo3.org", + "role": "Developer" + } + ], + "description": "TYPO3 CMS Backend", + "homepage": "https://typo3.community/", + "support": { + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" + }, + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-08-11T07:21:33+00:00" + }, + { + "name": "typo3/cms-belog", + "version": "v14.3.6", + "source": { + "type": "git", + "url": "https://github.com/TYPO3-CMS/belog.git", + "reference": "668ccbbea2bb4bc51c66e9ec54b325538ecf96bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TYPO3-CMS/belog/zipball/668ccbbea2bb4bc51c66e9ec54b325538ecf96bd", + "reference": "668ccbbea2bb4bc51c66e9ec54b325538ecf96bd", + "shasum": "" + }, + "require": { + "typo3/cms-core": "14.3.6" + }, + "conflict": { + "typo3/cms": "*" + }, + "type": "typo3-cms-framework", + "extra": { + "typo3/cms": { + "Package": { + "partOfFactoryDefault": true + }, + "extension-key": "belog" + }, + "branch-alias": { + "dev-main": "14.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "TYPO3\\CMS\\Belog\\": "Classes/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "TYPO3 Core Team", + "email": "typo3cms@typo3.org", + "role": "Developer" + } + ], + "description": "TYPO3 CMS Log - View logs from the sys_log table in the TYPO3 backend modules System>Log", + "homepage": "https://typo3.community/", + "support": { + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" + }, + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-08-11T07:21:33+00:00" + }, + { + "name": "typo3/cms-beuser", + "version": "v14.3.6", + "source": { + "type": "git", + "url": "https://github.com/TYPO3-CMS/beuser.git", + "reference": "43afb0acfe144d5b322ae504bb307f56b3fbf072" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TYPO3-CMS/beuser/zipball/43afb0acfe144d5b322ae504bb307f56b3fbf072", + "reference": "43afb0acfe144d5b322ae504bb307f56b3fbf072", + "shasum": "" + }, + "require": { + "typo3/cms-core": "14.3.6" + }, + "conflict": { + "typo3/cms": "*" + }, + "type": "typo3-cms-framework", + "extra": { + "typo3/cms": { + "Package": { + "partOfFactoryDefault": true + }, + "extension-key": "beuser" + }, + "branch-alias": { + "dev-main": "14.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "TYPO3\\CMS\\Beuser\\": "Classes/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "TYPO3 Core Team", + "email": "typo3cms@typo3.org", + "role": "Developer" + } + ], + "description": "TYPO3 CMS Backend User - TYPO3 backend module Administration > Users for managing backend users and groups.", + "homepage": "https://typo3.community/", + "support": { + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" + }, + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-08-11T07:21:33+00:00" + }, + { + "name": "typo3/cms-cli", + "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/TYPO3/cms-cli.git", + "reference": "7e26bf51bd8dbd74f86f7c68cb14965c678930f8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TYPO3/cms-cli/zipball/7e26bf51bd8dbd74f86f7c68cb14965c678930f8", + "reference": "7e26bf51bd8dbd74f86f7c68cb14965c678930f8", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "bin": [ + "typo3" + ], + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "TYPO3 command line binary", + "homepage": "https://typo3.org", + "support": { + "issues": "https://github.com/TYPO3/cms-cli/issues", + "source": "https://github.com/TYPO3/cms-cli/tree/v3.1.3" + }, + "time": "2025-12-31T13:41:20+00:00" + }, + { + "name": "typo3/cms-composer-installers", + "version": "v5.0.2", + "source": { + "type": "git", + "url": "https://github.com/TYPO3/CmsComposerInstallers.git", + "reference": "becd622c869eb5cba6b8ea13965a73ae84e37346" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TYPO3/CmsComposerInstallers/zipball/becd622c869eb5cba6b8ea13965a73ae84e37346", + "reference": "becd622c869eb5cba6b8ea13965a73ae84e37346", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.1.0", + "php": "^8.1" + }, + "replace": { + "lw/typo3cms-installers": "*", + "netresearch/composer-installers": "*" + }, + "require-dev": { + "composer/composer": "^2.1", + "friendsofphp/php-cs-fixer": "^3.62.0", + "overtrue/phplint": "^9.4.1", + "phpunit/phpunit": "^10.5.30" + }, + "type": "composer-plugin", + "extra": { + "class": "TYPO3\\CMS\\Composer\\Installer\\Plugin", + "branch-alias": { + "dev-main": "5.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "TYPO3\\CMS\\Composer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "TYPO3 CMS Core Team", + "homepage": "https://forge.typo3.org/projects/typo3cms-core", + "role": "Developer" + }, + { + "name": "The TYPO3 Community", + "homepage": "https://typo3.org/community/", + "role": "Contributor" + } + ], + "description": "TYPO3 CMS Installers", + "homepage": "https://github.com/TYPO3/CmsComposerInstallers", + "keywords": [ + "cms", + "core", + "extension", + "installer", + "typo3" + ], + "support": { + "general": "https://typo3.org/support/", + "issues": "https://github.com/TYPO3/CmsComposerInstallers/issues", + "source": "https://github.com/TYPO3/CmsComposerInstallers/tree/v5.0.2" + }, + "time": "2025-11-25T18:08:15+00:00" + }, + { + "name": "typo3/cms-core", + "version": "v14.3.6", + "source": { + "type": "git", + "url": "https://github.com/TYPO3-CMS/core.git", + "reference": "3615717fdcf4e069ac2da346a533d97a86a3d2a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TYPO3-CMS/core/zipball/3615717fdcf4e069ac2da346a533d97a86a3d2a9", + "reference": "3615717fdcf4e069ac2da346a533d97a86a3d2a9", + "shasum": "" + }, + "require": { + "bacon/bacon-qr-code": "^3.1.1", + "christian-riesen/base32": "^2.0", + "composer-runtime-api": "^2.1", + "composer/class-map-generator": "^1.7.2", + "composer/semver": "^3.4", + "doctrine/dbal": "~4.4.3", + "doctrine/lexer": "^3.0.1", + "egulias/email-validator": "^4.0", + "enshrined/svg-sanitize": "~0.22", + "ext-dom": "*", + "ext-intl": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-pcre": "*", + "ext-pdo": "*", + "ext-session": "*", + "ext-sodium": "*", + "ext-tokenizer": "*", + "ext-xml": "*", + "firebase/php-jwt": "^6.10.2 || ^7.0.5", + "guzzlehttp/guzzle": "^7.15.2", + "guzzlehttp/psr7": "^2.10.2", + "lolli42/finediff": "^1.1.2", + "masterminds/html5": "^2.10.0", + "php": "^8.2", + "psr/clock": "^1.0", + "psr/container": "^2.0", + "psr/event-dispatcher": "^1.0", + "psr/http-client": "^1.0.3", + "psr/http-factory": "^1.1.0", + "psr/http-message": "^1.1 || ^2.0", + "psr/http-server-handler": "^1.0", + "psr/http-server-middleware": "^1.0", + "psr/log": "^3.0.1", + "symfony/config": "^7.4.8", + "symfony/console": "^7.4.8", + "symfony/dependency-injection": "^7.4.8", + "symfony/doctrine-messenger": "^7.4.6", + "symfony/event-dispatcher-contracts": "^3.6.0", + "symfony/expression-language": "^7.4.8", + "symfony/filesystem": "^7.4.8", + "symfony/finder": "^7.4.8", + "symfony/http-foundation": "^7.4.13", + "symfony/mailer": "^7.4.12", + "symfony/messenger": "^7.4.8", + "symfony/mime": "^7.4.12", + "symfony/options-resolver": "^7.4.8", + "symfony/polyfill-php83": "^1.36.0", + "symfony/polyfill-php84": "^1.38", + "symfony/polyfill-php85": "^1.38", + "symfony/process": "^7.4.8", + "symfony/rate-limiter": "^7.4.8", + "symfony/routing": "^7.4.13", + "symfony/translation": "^7.4.8", + "symfony/uid": "^7.4.8", + "symfony/yaml": "^7.4.12", + "typo3/class-alias-loader": "^2.0.1", + "typo3/cms-cli": "^3.1.3", + "typo3/cms-composer-installers": "^5.0.2", + "typo3/html-sanitizer": "^2.3.2", + "typo3fluid/fluid": "^5.3.1" + }, + "conflict": { + "hoa/core": "*", + "typo3/cms": "*" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "replace": { + "typo3/cms-lang": "self.version", + "typo3/cms-saltedpasswords": "self.version", + "typo3/cms-sv": "self.version" + }, + "suggest": { + "ext-apcu": "Needed when non-default APCU based cache backends are used", + "ext-exif": "Used to extract exif metadata 'Orientation' of uploaded images", + "ext-fileinfo": "Used for proper file type detection in the file abstraction layer", + "ext-gd": "GDlib/Freetype is required for building images with text (GIFBUILDER) and can also be used to scale images", + "ext-mysqli": "", + "ext-openssl": "OpenSSL is required for sending SMTP mails over an encrypted channel endpoint", + "ext-zip": "", + "ext-zlib": "TYPO3 uses zlib for resource compression and un/packing t3x extension files" + }, + "type": "typo3-cms-framework", + "extra": { + "typo3/cms": { + "Package": { + "protected": true, + "serviceProvider": "TYPO3\\CMS\\Core\\ServiceProvider", + "partOfFactoryDefault": true, + "partOfMinimalUsableSystem": true + }, + "extension-key": "core" + }, + "branch-alias": { + "dev-main": "14.3.x-dev" + }, + "typo3/class-alias-loader": { + "class-alias-maps": [ + "Migrations/Code/ClassAliasMap.php" + ] + } + }, + "autoload": { + "files": [ + "Resources/PHP/GlobalDebugFunctions.php" + ], + "psr-4": { + "TYPO3\\CMS\\Core\\": "Classes/" + }, + "classmap": [ + "DeprecatedClasses/ext-install/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "TYPO3 Core Team", + "email": "typo3cms@typo3.org", + "role": "Developer" + } + ], + "description": "TYPO3 CMS Core", + "homepage": "https://typo3.community/", + "support": { + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" + }, + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-08-11T07:21:33+00:00" + }, + { + "name": "typo3/cms-dashboard", + "version": "v14.3.6", + "source": { + "type": "git", + "url": "https://github.com/TYPO3-CMS/dashboard.git", + "reference": "9a26e3a9ef375d1107bedead7bd6531625fd799f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TYPO3-CMS/dashboard/zipball/9a26e3a9ef375d1107bedead7bd6531625fd799f", + "reference": "9a26e3a9ef375d1107bedead7bd6531625fd799f", + "shasum": "" + }, + "require": { + "typo3/cms-backend": "14.3.6", + "typo3/cms-core": "14.3.6", + "typo3/cms-extbase": "14.3.6", + "typo3/cms-fluid": "14.3.6", + "typo3/cms-frontend": "14.3.6" + }, + "conflict": { + "typo3/cms": "*" + }, + "type": "typo3-cms-framework", + "extra": { + "typo3/cms": { + "Package": { + "serviceProvider": "TYPO3\\CMS\\Dashboard\\ServiceProvider", + "partOfFactoryDefault": true + }, + "extension-key": "dashboard" + }, + "branch-alias": { + "dev-main": "14.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "TYPO3\\CMS\\Dashboard\\": "Classes/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "TYPO3 Core Team", + "email": "typo3cms@typo3.org", + "role": "Developer" + } + ], + "description": "TYPO3 CMS Dashboard - TYPO3 backend module used to configure and create backend widgets.", + "homepage": "https://typo3.community/", + "support": { + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/c/typo3/cms-dashboard/main/en-us/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" + }, + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-08-11T07:21:33+00:00" + }, + { + "name": "typo3/cms-extbase", + "version": "v14.3.6", + "source": { + "type": "git", + "url": "https://github.com/TYPO3-CMS/extbase.git", + "reference": "866a40995d9d30687268dd73f16bd56eb1c30548" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TYPO3-CMS/extbase/zipball/866a40995d9d30687268dd73f16bd56eb1c30548", + "reference": "866a40995d9d30687268dd73f16bd56eb1c30548", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.5 || ^2.0", + "phpdocumentor/reflection-docblock": "^6.0.3", + "phpdocumentor/type-resolver": "^2.0.0", + "phpstan/phpdoc-parser": "^2.1", + "symfony/dependency-injection": "^7.4.8", + "symfony/property-access": "^7.4.8", + "symfony/property-info": "^7.4.15", + "symfony/validator": "^7.4.8", + "typo3/cms-core": "14.3.6" + }, + "conflict": { + "typo3/cms": "*" + }, + "suggest": { + "typo3/cms-scheduler": "Additional scheduler tasks" + }, + "type": "typo3-cms-framework", + "extra": { + "typo3/cms": { + "Package": { + "protected": true, + "serviceProvider": "TYPO3\\CMS\\Extbase\\ServiceProvider", + "partOfFactoryDefault": true, + "partOfMinimalUsableSystem": true + }, + "extension-key": "extbase" + }, + "branch-alias": { + "dev-main": "14.3.x-dev" + }, + "typo3/class-alias-loader": { + "class-alias-maps": [ + "Migrations/Code/ClassAliasMap.php" + ] + } + }, + "autoload": { + "psr-4": { + "TYPO3\\CMS\\Extbase\\": "Classes/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "TYPO3 Core Team", + "email": "typo3cms@typo3.org", + "role": "Developer" + } + ], + "description": "TYPO3 CMS Extbase - Extension framework to create TYPO3 frontend plugins and TYPO3 backend modules.", + "homepage": "https://typo3.community/", + "support": { + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" + }, + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-08-11T07:21:33+00:00" + }, + { + "name": "typo3/cms-felogin", + "version": "v14.3.6", + "source": { + "type": "git", + "url": "https://github.com/TYPO3-CMS/felogin.git", + "reference": "ed0e21f43f04151276da0c61a715ea68978a8665" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TYPO3-CMS/felogin/zipball/ed0e21f43f04151276da0c61a715ea68978a8665", + "reference": "ed0e21f43f04151276da0c61a715ea68978a8665", + "shasum": "" + }, + "require": { + "typo3/cms-core": "14.3.6" + }, + "conflict": { + "typo3/cms": "*" + }, + "type": "typo3-cms-framework", + "extra": { + "typo3/cms": { + "Package": { + "partOfFactoryDefault": true + }, + "extension-key": "felogin" + }, + "branch-alias": { + "dev-main": "14.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "TYPO3\\CMS\\FrontendLogin\\": "Classes/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "TYPO3 Core Team", + "email": "typo3cms@typo3.org", + "role": "Developer" + } + ], + "description": "TYPO3 CMS Frontend Login - A template-based plugin to log in website users in the TYPO3 frontend.", + "homepage": "https://typo3.community/", + "support": { + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/c/typo3/cms-felogin/main/en-us/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" + }, + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-08-11T07:21:33+00:00" + }, + { + "name": "typo3/cms-filelist", + "version": "v14.3.6", + "source": { + "type": "git", + "url": "https://github.com/TYPO3-CMS/filelist.git", + "reference": "0b4a61275e3a7b377a047abaec10bd09ecd28709" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TYPO3-CMS/filelist/zipball/0b4a61275e3a7b377a047abaec10bd09ecd28709", + "reference": "0b4a61275e3a7b377a047abaec10bd09ecd28709", + "shasum": "" + }, + "require": { + "typo3/cms-core": "14.3.6" + }, + "conflict": { + "typo3/cms": "*" + }, + "type": "typo3-cms-framework", + "extra": { + "typo3/cms": { + "Package": { + "protected": true, + "partOfFactoryDefault": true, + "partOfMinimalUsableSystem": true + }, + "extension-key": "filelist" + }, + "branch-alias": { + "dev-main": "14.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "TYPO3\\CMS\\Filelist\\": "Classes/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "TYPO3 Core Team", + "email": "typo3cms@typo3.org", + "role": "Developer" + } + ], + "description": "TYPO3 CMS Filelist - TYPO3 backend module 'Media' used for managing files.", + "homepage": "https://typo3.community/", + "support": { + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" + }, + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-08-11T07:21:33+00:00" + }, + { + "name": "typo3/cms-fluid", + "version": "v14.3.6", + "source": { + "type": "git", + "url": "https://github.com/TYPO3-CMS/fluid.git", + "reference": "34adb02bd7eb87417ef2804da19c649f343df8af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TYPO3-CMS/fluid/zipball/34adb02bd7eb87417ef2804da19c649f343df8af", + "reference": "34adb02bd7eb87417ef2804da19c649f343df8af", + "shasum": "" + }, + "require": { + "symfony/dependency-injection": "^7.4.8", + "symfony/finder": "^7.4.8", + "typo3/cms-core": "14.3.6", + "typo3/cms-extbase": "14.3.6", + "typo3fluid/fluid": "^5.3.1" + }, + "conflict": { + "typo3/cms": "*" + }, + "type": "typo3-cms-framework", + "extra": { + "typo3/cms": { + "Package": { + "protected": true, + "serviceProvider": "TYPO3\\CMS\\Fluid\\ServiceProvider", + "partOfFactoryDefault": true, + "partOfMinimalUsableSystem": true + }, + "extension-key": "fluid" + }, + "branch-alias": { + "dev-main": "14.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "TYPO3\\CMS\\Fluid\\": "Classes/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "TYPO3 Core Team", + "email": "typo3cms@typo3.org", + "role": "Developer" + } + ], + "description": "TYPO3 CMS Fluid Integration - Integration of the Fluid templating engine into TYPO3.", + "homepage": "https://typo3.community/", + "support": { + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/other/typo3/view-helper-reference/main/en-us/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" + }, + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-08-11T07:21:33+00:00" + }, + { + "name": "typo3/cms-fluid-styled-content", + "version": "v14.3.6", + "source": { + "type": "git", + "url": "https://github.com/TYPO3-CMS/fluid_styled_content.git", + "reference": "44b71d3be1e32b40ae4b67ea370faeb5d0a3b53f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TYPO3-CMS/fluid_styled_content/zipball/44b71d3be1e32b40ae4b67ea370faeb5d0a3b53f", + "reference": "44b71d3be1e32b40ae4b67ea370faeb5d0a3b53f", + "shasum": "" + }, + "require": { + "typo3/cms-core": "14.3.6", + "typo3/cms-fluid": "14.3.6", + "typo3/cms-frontend": "14.3.6" + }, + "conflict": { + "typo3/cms": "*" + }, + "type": "typo3-cms-framework", + "extra": { + "typo3/cms": { + "Package": { + "partOfFactoryDefault": true + }, + "extension-key": "fluid_styled_content" + }, + "branch-alias": { + "dev-main": "14.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "TYPO3\\CMS\\FluidStyledContent\\": "Classes/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "TYPO3 Core Team", + "email": "typo3cms@typo3.org", + "role": "Developer" + } + ], + "description": "TYPO3 CMS Fluid Styled Content - Fluid templates for TYPO3 content elements.", + "homepage": "https://typo3.community/", + "support": { + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/c/typo3/cms-fluid-styled-content/main/en-us/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" + }, + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-08-11T07:21:33+00:00" + }, + { + "name": "typo3/cms-form", + "version": "v14.3.6", + "source": { + "type": "git", + "url": "https://github.com/TYPO3-CMS/form.git", + "reference": "b465f891ab33c715c9ad2dd45675660dd17672a7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TYPO3-CMS/form/zipball/b465f891ab33c715c9ad2dd45675660dd17672a7", + "reference": "b465f891ab33c715c9ad2dd45675660dd17672a7", + "shasum": "" + }, + "require": { + "psr/http-message": "^1.1 || ^2.0", + "symfony/expression-language": "^7.4.8", + "typo3/cms-core": "14.3.6", + "typo3/cms-frontend": "14.3.6" + }, + "conflict": { + "typo3/cms": "*" + }, + "suggest": { + "typo3/cms-filelist": "Listing of files in the directory", + "typo3/cms-impexp": "Import and Export of records from TYPO3 in a custom serialized format (.T3D) for data exchange with other TYPO3 systems.", + "typo3/cms-lowlevel": "To display the YAML configuration in the configuration module" + }, + "type": "typo3-cms-framework", + "extra": { + "typo3/cms": { + "Package": { + "partOfFactoryDefault": true + }, + "extension-key": "form" + }, + "branch-alias": { + "dev-main": "14.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "TYPO3\\CMS\\Form\\": "Classes/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "TYPO3 Core Team", + "email": "typo3cms@typo3.org", + "role": "Developer" + } + ], + "description": "TYPO3 CMS Form - Flexible TYPO3 frontend form framework that comes with a backend editor interface.", + "homepage": "https://typo3.community/", + "support": { + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/c/typo3/cms-form/main/en-us/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" + }, + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-08-11T07:21:33+00:00" + }, + { + "name": "typo3/cms-frontend", + "version": "v14.3.6", + "source": { + "type": "git", + "url": "https://github.com/TYPO3-CMS/frontend.git", + "reference": "5794317270a8b7560755b226127f745dc33a8753" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TYPO3-CMS/frontend/zipball/5794317270a8b7560755b226127f745dc33a8753", + "reference": "5794317270a8b7560755b226127f745dc33a8753", + "shasum": "" + }, + "require": { + "ext-libxml": "*", + "typo3/cms-core": "14.3.6" + }, + "conflict": { + "typo3/cms": "*" + }, + "suggest": { + "typo3/cms-adminpanel": "Provides additional information and functionality for backend users in the frontend." + }, + "type": "typo3-cms-framework", + "extra": { + "typo3/cms": { + "Package": { + "protected": true, + "partOfFactoryDefault": true, + "partOfMinimalUsableSystem": true + }, + "extension-key": "frontend" + }, + "branch-alias": { + "dev-main": "14.3.x-dev" + }, + "typo3/class-alias-loader": { + "class-alias-maps": [ + "Migrations/Code/ClassAliasMap.php" + ] + } + }, + "autoload": { + "psr-4": { + "TYPO3\\CMS\\Frontend\\": "Classes/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "TYPO3 Core Team", + "email": "typo3cms@typo3.org", + "role": "Developer" + } + ], + "description": "TYPO3 CMS Frontend", + "homepage": "https://typo3.community/", + "support": { + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" + }, + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-08-11T07:21:33+00:00" + }, + { + "name": "typo3/cms-impexp", + "version": "v14.3.6", + "source": { + "type": "git", + "url": "https://github.com/TYPO3-CMS/impexp.git", + "reference": "c5eaabd9fd6456f26d8420699b8284cc9f9433fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TYPO3-CMS/impexp/zipball/c5eaabd9fd6456f26d8420699b8284cc9f9433fb", + "reference": "c5eaabd9fd6456f26d8420699b8284cc9f9433fb", + "shasum": "" + }, + "require": { + "typo3/cms-core": "14.3.6" + }, + "conflict": { + "typo3/cms": "*" + }, + "type": "typo3-cms-framework", + "extra": { + "typo3/cms": { + "Package": { + "partOfFactoryDefault": true + }, + "extension-key": "impexp" + }, + "branch-alias": { + "dev-main": "14.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "TYPO3\\CMS\\Impexp\\": "Classes/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "TYPO3 Core Team", + "email": "typo3cms@typo3.org", + "role": "Developer" + } + ], + "description": "TYPO3 CMS Import/Export - Tool for importing and exporting records using XML or the custom T3D format.", + "homepage": "https://typo3.community/", + "support": { + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/c/typo3/cms-impexp/main/en-us/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" + }, + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-08-11T07:21:33+00:00" + }, + { + "name": "typo3/cms-info", + "version": "v14.3.6", + "source": { + "type": "git", + "url": "https://github.com/TYPO3-CMS/info.git", + "reference": "bfc112bc518167ef88da7e7b453cd545e7c901a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TYPO3-CMS/info/zipball/bfc112bc518167ef88da7e7b453cd545e7c901a5", + "reference": "bfc112bc518167ef88da7e7b453cd545e7c901a5", + "shasum": "" + }, + "require": { + "typo3/cms-core": "14.3.6" + }, + "conflict": { + "typo3/cms": "*" + }, + "replace": { + "typo3/cms-info-pagetsconfig": "self.version" + }, + "type": "typo3-cms-framework", + "extra": { + "typo3/cms": { + "Package": { + "partOfFactoryDefault": true + }, + "extension-key": "info" + }, + "branch-alias": { + "dev-main": "14.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "TYPO3\\CMS\\Info\\": "Classes/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "TYPO3 Core Team", + "email": "typo3cms@typo3.org", + "role": "Developer" + } + ], + "description": "TYPO3 CMS Info - TYPO3 backend module for displaying information, such as a pagetree overview and localization information.", + "homepage": "https://typo3.community/", + "support": { + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" + }, + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-08-11T07:21:33+00:00" + }, + { + "name": "typo3/cms-install", + "version": "v14.3.6", + "source": { + "type": "git", + "url": "https://github.com/TYPO3-CMS/install.git", + "reference": "445d83327c8e4de51e80da151d2e70714bc30930" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TYPO3-CMS/install/zipball/445d83327c8e4de51e80da151d2e70714bc30930", + "reference": "445d83327c8e4de51e80da151d2e70714bc30930", + "shasum": "" + }, + "require": { + "doctrine/dbal": "~4.4.3", + "guzzlehttp/promises": "^2.0.3", + "nikic/php-parser": "^5.4.0", + "symfony/finder": "^7.4.8", + "symfony/http-foundation": "^7.4.13", + "typo3/cms-core": "14.3.6", + "typo3/cms-extbase": "14.3.6", + "typo3/cms-fluid": "14.3.6" + }, + "conflict": { + "typo3/cms": "*" + }, + "type": "typo3-cms-framework", + "extra": { + "typo3/cms": { + "Package": { + "protected": true, + "serviceProvider": "TYPO3\\CMS\\Install\\ServiceProvider", + "partOfFactoryDefault": true, + "partOfMinimalUsableSystem": true + }, + "extension-key": "install" + }, + "branch-alias": { + "dev-main": "14.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "TYPO3\\CMS\\Install\\": "Classes/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "TYPO3 Core Team", + "email": "typo3cms@typo3.org", + "role": "Developer" + } + ], + "description": "TYPO3 CMS Install Tool - The Install Tool is used for installation, upgrade, system administration and setup tasks.", + "homepage": "https://typo3.community/", + "support": { + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" + }, + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-08-11T07:21:33+00:00" + }, + { + "name": "typo3/cms-reactions", + "version": "v14.3.6", + "source": { + "type": "git", + "url": "https://github.com/TYPO3-CMS/reactions.git", + "reference": "48384bfd14f2c63b2f5010cb7904122caf93a5d1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TYPO3-CMS/reactions/zipball/48384bfd14f2c63b2f5010cb7904122caf93a5d1", + "reference": "48384bfd14f2c63b2f5010cb7904122caf93a5d1", + "shasum": "" + }, + "require": { + "typo3/cms-core": "14.3.6" + }, + "conflict": { + "typo3/cms": "*" + }, + "suggest": { + "typo3/cms-lowlevel": "To display registered reactions in the configuration module" + }, + "type": "typo3-cms-framework", + "extra": { + "typo3/cms": { + "extension-key": "reactions" + }, + "branch-alias": { + "dev-main": "14.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "TYPO3\\CMS\\Reactions\\": "Classes/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "TYPO3 Core Team", + "email": "typo3cms@typo3.org", + "role": "Developer" + } + ], + "description": "TYPO3 CMS Reactions - Handle incoming Webhooks for TYPO3", + "homepage": "https://typo3.community/", + "support": { + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/c/typo3/cms-reactions/main/en-us/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" + }, + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-08-11T07:21:33+00:00" + }, + { + "name": "typo3/cms-recycler", + "version": "v14.3.6", + "source": { + "type": "git", + "url": "https://github.com/TYPO3-CMS/recycler.git", + "reference": "72fa6a7c18b044d487726dd73c23544974e16393" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TYPO3-CMS/recycler/zipball/72fa6a7c18b044d487726dd73c23544974e16393", + "reference": "72fa6a7c18b044d487726dd73c23544974e16393", + "shasum": "" + }, + "require": { + "typo3/cms-core": "14.3.6" + }, + "conflict": { + "typo3/cms": "*" + }, + "suggest": { + "typo3/cms-scheduler": "Remove deleted records after given time" + }, + "type": "typo3-cms-framework", + "extra": { + "typo3/cms": { + "Package": { + "partOfFactoryDefault": true + }, + "extension-key": "recycler" + }, + "branch-alias": { + "dev-main": "14.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "TYPO3\\CMS\\Recycler\\": "Classes/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "TYPO3 Core Team", + "email": "typo3cms@typo3.org", + "role": "Developer" + } + ], + "description": "TYPO3 CMS Recycler - Restore deleted records or remove them from the database permanently.", + "homepage": "https://typo3.community/", + "support": { + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/c/typo3/cms-recycler/main/en-us/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" + }, + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-08-11T07:21:33+00:00" + }, + { + "name": "typo3/cms-rte-ckeditor", + "version": "v14.3.6", + "source": { + "type": "git", + "url": "https://github.com/TYPO3-CMS/rte_ckeditor.git", + "reference": "01facd6eae020faa648d1bb335220fd5a1117120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TYPO3-CMS/rte_ckeditor/zipball/01facd6eae020faa648d1bb335220fd5a1117120", + "reference": "01facd6eae020faa648d1bb335220fd5a1117120", + "shasum": "" + }, + "require": { + "typo3/cms-core": "14.3.6" + }, + "conflict": { + "typo3/cms": "*" + }, + "type": "typo3-cms-framework", + "extra": { + "typo3/cms": { + "Package": { + "partOfFactoryDefault": true + }, + "extension-key": "rte_ckeditor" + }, + "branch-alias": { + "dev-main": "14.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "TYPO3\\CMS\\RteCKEditor\\": "Classes/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "TYPO3 Core Team", + "email": "typo3cms@typo3.org", + "role": "Developer" + } + ], + "description": "TYPO3 CMS RTE CKEditor - Integration of CKEditor as a Rich Text Editor for the TYPO3 backend.", + "homepage": "https://typo3.community/", + "support": { + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/c/typo3/cms-rte-ckeditor/main/en-us/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" + }, + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-08-11T07:21:33+00:00" + }, + { + "name": "typo3/cms-seo", + "version": "v14.3.6", + "source": { + "type": "git", + "url": "https://github.com/TYPO3-CMS/seo.git", + "reference": "0e7a5b0ab289ea8032e05300bb8d6a6929f86b97" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TYPO3-CMS/seo/zipball/0e7a5b0ab289ea8032e05300bb8d6a6929f86b97", + "reference": "0e7a5b0ab289ea8032e05300bb8d6a6929f86b97", + "shasum": "" + }, + "require": { + "typo3/cms-core": "14.3.6", + "typo3/cms-extbase": "14.3.6", + "typo3/cms-frontend": "14.3.6" + }, + "conflict": { + "typo3/cms": "*" + }, + "suggest": { + "typo3/cms-dashboard": "TYPO3 users can add widgets that can help to optimise their website for search engines" + }, + "type": "typo3-cms-framework", + "extra": { + "typo3/cms": { + "Package": { + "partOfFactoryDefault": true + }, + "extension-key": "seo" + }, + "branch-alias": { + "dev-main": "14.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "TYPO3\\CMS\\Seo\\": "Classes/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "TYPO3 Core Team", + "email": "typo3cms@typo3.org", + "role": "Developer" + } + ], + "description": "TYPO3 CMS SEO - SEO features including specific fields for SEO purposes, rendering of HTML meta tags and sitemaps.", + "homepage": "https://typo3.community/", + "support": { + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/c/typo3/cms-seo/main/en-us/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" + }, + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-08-11T07:21:33+00:00" + }, + { + "name": "typo3/cms-sys-note", + "version": "v14.3.6", + "source": { + "type": "git", + "url": "https://github.com/TYPO3-CMS/sys_note.git", + "reference": "5fb786d976ebbdff039a726860a5b660e152bd89" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TYPO3-CMS/sys_note/zipball/5fb786d976ebbdff039a726860a5b660e152bd89", + "reference": "5fb786d976ebbdff039a726860a5b660e152bd89", + "shasum": "" + }, + "require": { + "typo3/cms-core": "14.3.6" + }, + "conflict": { + "typo3/cms": "*" + }, + "suggest": { + "typo3/cms-dashboard": "TYPO3 users can add widgets that can help to quickly see existing sys_note records" + }, + "type": "typo3-cms-framework", + "extra": { + "typo3/cms": { + "Package": { + "partOfFactoryDefault": true + }, + "extension-key": "sys_note" + }, + "branch-alias": { + "dev-main": "14.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "TYPO3\\CMS\\SysNote\\": "Classes/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "TYPO3 Core Team", + "email": "typo3cms@typo3.org", + "role": "Developer" + } + ], + "description": "TYPO3 CMS System Notes - Records with messages which can be placed on any page and contain instructions or other information related to a page or section.", + "homepage": "https://typo3.community/", + "support": { + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" + }, + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-08-11T07:21:33+00:00" + }, + { + "name": "typo3/cms-tstemplate", + "version": "v14.3.6", + "source": { + "type": "git", + "url": "https://github.com/TYPO3-CMS/tstemplate.git", + "reference": "4b8e78335bc6792e71879b4e3bb52e56072e76d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TYPO3-CMS/tstemplate/zipball/4b8e78335bc6792e71879b4e3bb52e56072e76d0", + "reference": "4b8e78335bc6792e71879b4e3bb52e56072e76d0", + "shasum": "" + }, + "require": { + "typo3/cms-core": "14.3.6" + }, + "conflict": { + "typo3/cms": "*" + }, + "type": "typo3-cms-framework", + "extra": { + "typo3/cms": { + "Package": { + "partOfFactoryDefault": true + }, + "extension-key": "tstemplate" + }, + "branch-alias": { + "dev-main": "14.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "TYPO3\\CMS\\Tstemplate\\": "Classes/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "TYPO3 Core Team", + "email": "typo3cms@typo3.org", + "role": "Developer" + } + ], + "description": "TYPO3 CMS TypoScript - TYPO3 backend module for the management of TypoScript records for the CMS frontend.", + "homepage": "https://typo3.community/", + "support": { + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" + }, + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-08-11T07:21:33+00:00" + }, + { + "name": "typo3/cms-viewpage", + "version": "v14.3.6", + "source": { + "type": "git", + "url": "https://github.com/TYPO3-CMS/viewpage.git", + "reference": "839c8442f56cbde023defe750a1f1c37c592c785" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TYPO3-CMS/viewpage/zipball/839c8442f56cbde023defe750a1f1c37c592c785", + "reference": "839c8442f56cbde023defe750a1f1c37c592c785", + "shasum": "" + }, + "require": { + "typo3/cms-core": "14.3.6" + }, + "conflict": { + "typo3/cms": "*" + }, + "type": "typo3-cms-framework", + "extra": { + "typo3/cms": { + "Package": { + "partOfFactoryDefault": true + }, + "extension-key": "viewpage" + }, + "branch-alias": { + "dev-main": "14.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "TYPO3\\CMS\\Viewpage\\": "Classes/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "TYPO3 Core Team", + "email": "typo3cms@typo3.org", + "role": "Developer" + } + ], + "description": "TYPO3 CMS Viewpage - Use the (Web>View) backend module to view a frontend page inside the TYPO3 backend.", + "homepage": "https://typo3.community/", + "support": { + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" + }, + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-08-11T07:21:33+00:00" + }, + { + "name": "typo3/cms-webhooks", + "version": "v14.3.6", + "source": { + "type": "git", + "url": "https://github.com/TYPO3-CMS/webhooks.git", + "reference": "d7b094a4f5e6aed485ba981a62aaf2f9980c3383" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TYPO3-CMS/webhooks/zipball/d7b094a4f5e6aed485ba981a62aaf2f9980c3383", + "reference": "d7b094a4f5e6aed485ba981a62aaf2f9980c3383", + "shasum": "" + }, + "require": { + "symfony/uid": "^7.4.8", + "typo3/cms-core": "14.3.6" + }, + "conflict": { + "typo3/cms": "*" + }, + "suggest": { + "typo3/cms-lowlevel": "To display registered webhooks in the configuration module" + }, + "type": "typo3-cms-framework", + "extra": { + "typo3/cms": { + "extension-key": "webhooks" + }, + "branch-alias": { + "dev-main": "14.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "TYPO3\\CMS\\Webhooks\\": "Classes/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "TYPO3 Core Team", + "email": "typo3cms@typo3.org", + "role": "Developer" + } + ], + "description": "TYPO3 CMS Webhooks - Handle outgoing Webhooks for TYPO3", + "homepage": "https://typo3.community/", + "support": { + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/c/typo3/cms-webhooks/main/en-us/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" + }, + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-08-11T07:21:33+00:00" + }, + { + "name": "typo3/html-sanitizer", + "version": "v2.3.2", + "source": { + "type": "git", + "url": "https://github.com/TYPO3/html-sanitizer.git", + "reference": "8b5d0be44ded457ca993ec9ca93d859941c63764" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TYPO3/html-sanitizer/zipball/8b5d0be44ded457ca993ec9ca93d859941c63764", + "reference": "8b5d0be44ded457ca993ec9ca93d859941c63764", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "masterminds/html5": "^2.7.6", + "php": "^7.2 || ^8.0", + "psr/log": "^1.0 || ^2.0 || ^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "TYPO3\\HtmlSanitizer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oliver Hader", + "email": "oliver@typo3.org" + } + ], + "description": "HTML sanitizer aiming to provide XSS-safe markup based on explicitly allowed tags, attributes and values.", + "support": { + "issues": "https://github.com/TYPO3/html-sanitizer/issues", + "source": "https://github.com/TYPO3/html-sanitizer/tree/v2.3.2" + }, + "time": "2026-06-08T15:19:36+00:00" + }, + { + "name": "typo3/theme-camino", + "version": "v14.3.6", + "source": { + "type": "git", + "url": "https://github.com/TYPO3-CMS/theme_camino.git", + "reference": "8edcc21c40632beaa5c4c29c6dc1016cb105841d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TYPO3-CMS/theme_camino/zipball/8edcc21c40632beaa5c4c29c6dc1016cb105841d", + "reference": "8edcc21c40632beaa5c4c29c6dc1016cb105841d", + "shasum": "" + }, + "require": { + "typo3/cms-core": "14.3.6" + }, + "conflict": { + "typo3/cms": "*" + }, + "type": "typo3-cms-framework", + "extra": { + "typo3/cms": { + "extension-key": "theme_camino" + }, + "branch-alias": { + "dev-main": "14.3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "TYPO3 Core Team", + "email": "typo3cms@typo3.org", + "role": "Developer" + } + ], + "description": "TYPO3 Theme: \"Camino\" - The default theme for TYPO3 sites in the TYPO3 v14 series.", + "homepage": "https://typo3.community/", + "support": { + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" + }, + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-08-11T07:21:33+00:00" + }, + { + "name": "typo3fluid/fluid", + "version": "5.3.1", + "source": { + "type": "git", + "url": "https://github.com/TYPO3/Fluid.git", + "reference": "28c42c75b171aef196ddbe151b0d1146c290249d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TYPO3/Fluid/zipball/28c42c75b171aef196ddbe151b0d1146c290249d", + "reference": "28c42c75b171aef196ddbe151b0d1146c290249d", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^8.2" + }, + "require-dev": { + "ext-json": "*", + "ext-simplexml": "*", + "friendsofphp/php-cs-fixer": "^3.94.2", + "infection/infection": "^0.32.6", + "phpstan/phpstan": "^2.1.40", + "phpstan/phpstan-phpunit": "^2.0.16", + "phpunit/phpunit": "^11.5.55 || ^12.5.14 || ^13.0.5", + "psr/container": "^2.0.2", + "t3docs/fluid-documentation-generator": "^4.4.2" + }, + "suggest": { + "ext-json": "PHP JSON is needed when using JSONVariableProvider: A relatively rare use case", + "ext-simplexml": "SimpleXML is required for the XSD schema generator" + }, + "bin": [ + "bin/fluid" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "TYPO3Fluid\\Fluid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "description": "The TYPO3 Fluid template rendering engine", + "homepage": "https://github.com/TYPO3/Fluid", + "support": { + "docs": "https://docs.typo3.org/other/typo3fluid/fluid/main/en-us/", + "issues": "https://github.com/TYPO3/Fluid/issues", + "source": "https://github.com/TYPO3/Fluid" + }, + "time": "2026-04-16T17:09:54+00:00" + }, + { + "name": "webmozart/assert", + "version": "2.4.1", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/2ccb7c2e821038c03a3e6e1700c570c158c55f70", + "reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-date": "*", + "ext-filter": "*", + "php": "^8.2" + }, + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" + }, + "type": "library", + "extra": { + "psalm": { + "pluginClass": "Webmozart\\Assert\\PsalmPlugin" + }, + "branch-alias": { + "dev-master": "2.0-dev", + "dev-feature/2-0": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + }, + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/2.4.1" + }, + "time": "2026-06-15T15:31:57+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "platform-overrides": { + "php": "8.2.0" + }, + "plugin-api-version": "2.9.0" +} diff --git a/tests/e2e/fixtures/camino/site/config/sites/camino/config.yaml b/tests/e2e/fixtures/camino/site/config/sites/camino/config.yaml new file mode 100644 index 0000000..f5ae35f --- /dev/null +++ b/tests/e2e/fixtures/camino/site/config/sites/camino/config.yaml @@ -0,0 +1,16 @@ +base: /camino/ +dependencies: + - typo3/theme-camino +languages: + - + title: English + enabled: '1' + locale: en-US + hreflang: en-US + base: / + websiteTitle: Camino + navigationTitle: English + flag: global + languageId: '0' +rootPageId: 1 +websiteTitle: Camino diff --git a/tests/e2e/fixtures/camino/site/config/system/settings.php b/tests/e2e/fixtures/camino/site/config/system/settings.php new file mode 100644 index 0000000..be06ad4 --- /dev/null +++ b/tests/e2e/fixtures/camino/site/config/system/settings.php @@ -0,0 +1,114 @@ + [ + 'debug' => true, + 'installToolPassword' => '$argon2i$v=19$m=65536,t=16,p=1$NjRzZ1hoOEFRWmFiZXZsVw$wQYO4aXLFrel/R/L7+JZCwHNVhljOgOmPEknwOnUEYM', + 'passwordHashing' => [ + 'className' => 'TYPO3\\CMS\\Core\\Crypto\\PasswordHashing\\Argon2iPasswordHash', + 'options' => [], + ], + ], + 'DB' => [ + 'Connections' => [ + 'Default' => [ + 'charset' => 'utf8mb4', + 'dbname' => 'db', + 'defaultTableOptions' => [ + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + ], + 'driver' => 'mysqli', + 'host' => 'db', + 'password' => 'db', + 'port' => 3306, + 'user' => 'db', + ], + ], + ], + 'EXTENSIONS' => [ + 'backend' => [ + 'backendFavicon' => '', + 'backendLogo' => '', + 'loginBackgroundImage' => '', + 'loginFootnote' => '', + 'loginHighlightColor' => '', + 'loginLogo' => '', + 'loginLogoAlt' => '', + ], + ], + 'FE' => [ + 'cacheHash' => [ + 'enforceValidation' => true, + ], + 'debug' => true, + 'disableNoCacheParameter' => true, + 'passwordHashing' => [ + 'className' => 'TYPO3\\CMS\\Core\\Crypto\\PasswordHashing\\Argon2iPasswordHash', + 'options' => [], + ], + ], + 'GFX' => [ + 'processor' => 'GraphicsMagick', + 'processor_effects' => false, + 'processor_enabled' => true, + 'processor_path' => '/usr/bin/', + ], + 'LOG' => [ + 'TYPO3' => [ + 'CMS' => [ + 'deprecations' => [ + 'writerConfiguration' => [ + 'notice' => [ + 'TYPO3\CMS\Core\Log\Writer\FileWriter' => [ + 'disabled' => false, + ], + ], + ], + ], + ], + ], + ], + 'MAIL' => [ + 'transport' => 'sendmail', + 'transport_sendmail_command' => '/usr/local/bin/mailpit sendmail -t --smtp-addr 127.0.0.1:1025', + 'transport_smtp_encrypt' => '', + 'transport_smtp_password' => '', + 'transport_smtp_server' => '', + 'transport_smtp_username' => '', + ], + 'SYS' => [ + 'UTF8filesystem' => true, + 'caching' => [ + 'cacheConfigurations' => [ + 'hash' => [ + 'backend' => 'TYPO3\\CMS\\Core\\Cache\\Backend\\Typo3DatabaseBackend', + ], + 'pages' => [ + 'backend' => 'TYPO3\\CMS\\Core\\Cache\\Backend\\Typo3DatabaseBackend', + 'options' => [ + 'compression' => true, + ], + ], + 'rootline' => [ + 'backend' => 'TYPO3\\CMS\\Core\\Cache\\Backend\\Typo3DatabaseBackend', + 'options' => [ + 'compression' => true, + ], + ], + ], + ], + 'devIPmask' => '*', + 'displayErrors' => 1, + 'encryptionKey' => 'b3bbde2273e5779e0132e14362f91cbd98d0f00d3bf333d747f135809124f3d0dea8bd21493a52a69b668ce3e542e00b', + 'exceptionalErrors' => 12290, + 'features' => [ + 'frontend.cache.autoTagging' => true, + 'security.system.enforceAllowedFileExtensions' => true, + ], + 'sitename' => 'Permission Kit Test', + 'systemMaintainers' => [ + 2, + ], + 'trustedHostsPattern' => '.*', + ], +]; diff --git a/tests/e2e/fixtures/camino/site/packages/.gitkeep b/tests/e2e/fixtures/camino/site/packages/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/e2e/fixtures/camino/site/public/.htaccess b/tests/e2e/fixtures/camino/site/public/.htaccess new file mode 100644 index 0000000..582c59e --- /dev/null +++ b/tests/e2e/fixtures/camino/site/public/.htaccess @@ -0,0 +1,382 @@ +##### +# +# Example .htaccess file for TYPO3 CMS - for use with Apache Webserver +# +# This file includes settings for the following configuration options: +# +# - Compression +# - Caching +# - MIME types +# - Cross Origin requests +# - Rewriting and Access +# - Miscellaneous +# - PHP optimisation +# +# If you want to use it, you have to copy it to the root folder of your TYPO3 installation (if its +# not there already) and rename it to '.htaccess'. To make .htaccess files work, you might need to +# adjust the 'AllowOverride' directive in your Apache configuration file. +# +# IMPORTANT: You may need to change this file depending on your TYPO3 installation! +# Consider adding this file's content to your webserver's configuration directly for speed improvement +# +# Lots of the options are taken from https://github.com/h5bp/html5-boilerplate/blob/master/dist/.htaccess +# +#### + + +### Begin: Compression ### + + + # Force compression for mangled `Accept-Encoding` request headers + + + SetEnvIfNoCase ^(Accept-EncodXng|X-cept-Encoding|X{15}|~{15}|-{15})$ ^((gzip|deflate)\s*,?\s*)+|[X~-]{4,13}$ HAVE_Accept-Encoding + RequestHeader append Accept-Encoding "gzip,deflate" env=HAVE_Accept-Encoding + + + + # Compress all output labeled with one of the following media types. + # + # (!) For Apache versions below version 2.3.7 you don't need to + # enable `mod_filter` and can remove the `` + # and `` lines as `AddOutputFilterByType` is still in + # the core directives. + # + # https://httpd.apache.org/docs/current/mod/mod_filter.html#addoutputfilterbytype + + + AddOutputFilterByType DEFLATE application/atom+xml \ + application/javascript \ + application/json \ + application/ld+json \ + application/manifest+json \ + application/rdf+xml \ + application/rss+xml \ + application/schema+json \ + application/vnd.geo+json \ + application/geo+json \ + application/vnd.ms-fontobject \ + application/x-font-ttf \ + application/x-javascript \ + application/x-web-app-manifest+json \ + application/xhtml+xml \ + application/xml \ + font/eot \ + font/opentype \ + font/otf \ + font/ttf \ + image/bmp \ + image/svg+xml \ + image/vnd.microsoft.icon \ + image/x-icon \ + text/cache-manifest \ + text/css \ + text/html \ + text/javascript \ + text/plain \ + text/vcard \ + text/vnd.rim.location.xloc \ + text/vtt \ + text/x-component \ + text/x-cross-domain-policy \ + text/xml + + + + AddEncoding gzip svgz + + + +### End: Compression ### + + + +### Begin: Browser caching of resource files ### + +# This affects Frontend and Backend and increases performance. + + + ExpiresActive On + ExpiresDefault "access plus 1 month" + + ExpiresByType text/css "access plus 1 year" + + ExpiresByType application/json "access plus 0 seconds" + ExpiresByType application/ld+json "access plus 0 seconds" + ExpiresByType application/schema+json "access plus 0 seconds" + ExpiresByType application/vnd.geo+json "access plus 0 seconds" + ExpiresByType application/geo+json "access plus 0 seconds" + ExpiresByType application/xml "access plus 0 seconds" + ExpiresByType text/xml "access plus 0 seconds" + + ExpiresByType image/vnd.microsoft.icon "access plus 1 week" + ExpiresByType image/x-icon "access plus 1 week" + + ExpiresByType text/x-component "access plus 1 month" + + ExpiresByType text/html "access plus 0 seconds" + + ExpiresByType application/javascript "access plus 1 year" + ExpiresByType application/x-javascript "access plus 1 year" + ExpiresByType text/javascript "access plus 1 year" + + ExpiresByType application/manifest+json "access plus 1 week" + ExpiresByType application/x-web-app-manifest+json "access plus 0 seconds" + ExpiresByType text/cache-manifest "access plus 0 seconds" + + ExpiresByType audio/ogg "access plus 1 month" + ExpiresByType image/apng "access plus 1 month" + ExpiresByType image/avif "access plus 1 month" + ExpiresByType image/avif-sequence "access plus 1 month" + ExpiresByType image/bmp "access plus 1 month" + ExpiresByType image/gif "access plus 1 month" + ExpiresByType image/jpeg "access plus 1 month" + ExpiresByType image/jxl "access plus 1 month" + ExpiresByType image/png "access plus 1 month" + ExpiresByType image/svg+xml "access plus 1 month" + ExpiresByType image/webp "access plus 1 month" + ExpiresByType video/mp4 "access plus 1 month" + ExpiresByType video/ogg "access plus 1 month" + ExpiresByType video/webm "access plus 1 month" + + ExpiresByType application/atom+xml "access plus 1 hour" + ExpiresByType application/rdf+xml "access plus 1 hour" + ExpiresByType application/rss+xml "access plus 1 hour" + + ExpiresByType font/collection "access plus 1 month" + ExpiresByType application/vnd.ms-fontobject "access plus 1 month" + ExpiresByType font/eot "access plus 1 month" + ExpiresByType font/opentype "access plus 1 month" + ExpiresByType font/otf "access plus 1 month" + ExpiresByType application/x-font-ttf "access plus 1 month" + ExpiresByType font/ttf "access plus 1 month" + ExpiresByType application/font-woff "access plus 1 month" + ExpiresByType application/x-font-woff "access plus 1 month" + ExpiresByType font/woff "access plus 1 month" + ExpiresByType application/font-woff2 "access plus 1 month" + ExpiresByType font/woff2 "access plus 1 month" + + ExpiresByType text/x-cross-domain-policy "access plus 1 week" + + + ExpiresActive Off + + + + +### End: Browser caching of resource files ### + + +### Begin: MIME types ### + +# Proper MIME types for all files + + # Security configuration + RemoveType .html .htm + + AddType text/html .html .htm + + + RemoveType .svg .svgz + + AddType image/svg+xml .svg .svgz + + + # Data interchange + AddType application/atom+xml atom + AddType application/json json map topojson + AddType application/ld+json jsonld + AddType application/rss+xml rss + AddType application/vnd.geo+json geojson + AddType application/xml rdf xml + + # JavaScript + AddType application/javascript js mjs + + # Manifest files + AddType application/manifest+json webmanifest + AddType application/x-web-app-manifest+json webapp + AddType text/cache-manifest appcache + + # Media files + + AddType audio/mp4 f4a f4b m4a + AddType audio/ogg oga ogg opus + AddType image/avif avif + AddType image/avif-sequence avifs + AddType image/bmp bmp + AddType image/jxl jxl + AddType image/webp webp + AddType video/mp4 f4v f4p m4v mp4 + AddType video/ogg ogv + AddType video/webm webm + AddType video/x-flv flv + AddType image/x-icon cur ico + + # Web fonts + AddType font/woff woff + AddType font/woff2 woff2 + AddType application/vnd.ms-fontobject eot + AddType font/ttf ttc ttf + AddType font/otf otf + + # Other + AddType application/octet-stream safariextz + AddType application/x-bb-appworld bbaw + AddType application/x-chrome-extension crx + AddType application/x-opera-extension oex + AddType application/x-xpinstall xpi + AddType text/vcard vcard vcf + AddType text/vnd.rim.location.xloc xloc + AddType text/vtt vtt + AddType text/x-component htc + + + +# UTF-8 encoding +AddDefaultCharset utf-8 + + AddCharset utf-8 .atom .css .js .json .manifest .rdf .rss .vtt .webapp .webmanifest .xml + + +### End: MIME types ### + + + +### Begin: Cross Origin ### + +# Send the CORS header for images when browsers request it. + + + + SetEnvIf Origin ":" IS_CORS + Header set Access-Control-Allow-Origin "*" env=IS_CORS + + + + +# Allow cross-origin access to web fonts. + + + Header set Access-Control-Allow-Origin "*" + + + +### End: Cross Origin ### + + + +### Begin: Rewriting and Access ### + + + + # Enable URL rewriting + RewriteEngine On + + # Store the current location in an environment variable CWD to use + # mod_rewrite in .htaccess files without knowing the RewriteBase + RewriteCond $0#%{REQUEST_URI} ([^#]*)#(.*)\1$ + RewriteRule ^.*$ - [E=CWD:%2] + + # Rules to set ApplicationContext based on hostname + #RewriteCond %{HTTP_HOST} ^dev\.example\.com$ + #RewriteRule .? - [E=TYPO3_CONTEXT:Development] + #RewriteCond %{HTTP_HOST} ^staging\.example\.com$ + #RewriteRule .? - [E=TYPO3_CONTEXT:Production/Staging] + #RewriteCond %{HTTP_HOST} ^www\.example\.com$ + #RewriteRule .? - [E=TYPO3_CONTEXT:Production] + + # Rule for versioned static files, configured through: + # - $GLOBALS['TYPO3_CONF_VARS']['BE']['versionNumberInFilename'] + # - $GLOBALS['TYPO3_CONF_VARS']['FE']['versionNumberInFilename'] + # IMPORTANT: This rule has to be the very first RewriteCond in order to work! + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteRule ^(.+)\.(\d+)\.(php|js|mjs|css|png|jpg|gif|svg|avif|webp|gz|xsl|ico)$ %{ENV:CWD}$1.$3 [L] + + # Access block for folders + RewriteRule _(?:recycler|temp)_/ - [F] + RewriteRule fileadmin/templates/.*\.(?:txt|ts)$ - [F] + RewriteRule ^(?:vendor|typo3_src|typo3temp/var) - [F] + RewriteRule (?:typo3conf/ext|typo3/sysext|typo3/ext)/[^/]+/(?:Configuration|Resources/Private|Tests?|Documentation|docs?)/ - [F] + + # Block access to all hidden files and directories with the exception of + # the visible content from within the `/.well-known/` hidden directory (RFC 5785). + RewriteCond %{REQUEST_URI} "!(^|/)\.well-known/([^./]+./?)+$" [NC] + RewriteCond %{SCRIPT_FILENAME} -d [OR] + RewriteCond %{SCRIPT_FILENAME} -f + RewriteRule (?:^|/)\. - [F] + + # Stop rewrite processing, if we are in any known directory + # NOTE: Add your additional local storages here + RewriteRule ^(?:fileadmin/|typo3conf/|typo3temp/|_assets/) - [L] + + # If the file/symlink/directory does not exist but is below /typo3/, redirect to the main TYPO3 entry point. + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^typo3/(.*)$ %{ENV:CWD}index.php [QSA,L] + + # If the file/symlink/directory does not exist => Redirect to index.php. + # For httpd.conf, you need to prefix each '%{REQUEST_FILENAME}' with '%{DOCUMENT_ROOT}'. + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-l + RewriteRule ^.*$ %{ENV:CWD}index.php [QSA,L] + + + +# Access block for files +# Apache < 2.3 + + + Order allow,deny + Deny from all + Satisfy All + + +# Apache ≥ 2.3 + + + Require all denied + + + +# Block access to vcs directories + + RedirectMatch 404 /\.(?:git|svn|hg)/ + + +### End: Rewriting and Access ### + + + +### Begin: Miscellaneous ### + +# 404 error prevention for non-existing redirected folders +Options -MultiViews + +# Make sure that directory listings are disabled. + + Options -Indexes + + + + # Force IE to render pages in the highest available mode + Header set X-UA-Compatible "IE=edge" + + Header unset X-UA-Compatible + + + # Reducing MIME type security risks + Header set X-Content-Type-Options "nosniff" + + +# ETag removal + + Header unset ETag + +FileETag None + +### End: Miscellaneous ### + + +# Add your own rules here. diff --git a/tests/e2e/fixtures/camino/site/public/index.php b/tests/e2e/fixtures/camino/site/public/index.php new file mode 100644 index 0000000..ab264d7 --- /dev/null +++ b/tests/e2e/fixtures/camino/site/public/index.php @@ -0,0 +1,33 @@ +has(\TYPO3\CMS\Core\Http\Application::class)) { + $container->get(\TYPO3\CMS\Core\Http\Application::class)->run(); + return; + } + + $container->get(\TYPO3\CMS\Install\Http\Application::class)->run(); +}); diff --git a/tests/e2e/fixtures/make-bare-origin.sh b/tests/e2e/fixtures/make-bare-origin.sh new file mode 100644 index 0000000..64aaa6b --- /dev/null +++ b/tests/e2e/fixtures/make-bare-origin.sh @@ -0,0 +1,81 @@ +#!/bin/sh +# tests/e2e/fixtures/make-bare-origin.sh — build the per-run bare git origin +# for the e2e-ddev git-flow tier (docs/design/ddev-e2e-test.md §7.2, DD12). +# Run via `sh` (like fake-ddev, it carries no exec bit and is not in the CI +# chmod lists). Reconciled against the real burn-in bare repo +# (local/TEST-PROJECT-INSTALL.txt, 2026-08-22): the tracked set there is +# .ddev/config.yaml + the composer/TYPO3 tree — no AGENTS.md, so the +# top-level branch works on README.md/LICENSE instead. +# +# Usage: sh make-bare-origin.sh +# created (must not already be a git repo) +# the camino fixture master (tests/e2e/fixtures/camino/site) +# +# Produces a bare repo with: +# main site master + a tracked custom host command +# (.ddev/commands/host/hello) — the realistic baseline +# feature/top-level README.md replaced (unlink+recreate on switch) and +# LICENSE deleted (unlink/recreate both switch directions) +# feature/settings config/system/settings.php + config/sites/camino/ +# config.yaml modified (tracked-file replacement under +# handed-over settings dirs) +# feature/ddev-tree .ddev/commands/host/hello deleted + .ddev/config.yaml +# modified (git unlink/rename inside opencode-owned .ddev/) +set -eu + +usage() { sed -n 's/^# \{0,1\}//p' "$0" | sed -n '/^Usage:/,$p'; exit 2; } + +[ "$#" -eq 2 ] || usage +BARE_TARGET=$1 +SITE_DIR=$2 + +[ -f "$SITE_DIR/composer.json" ] || { echo "error: $SITE_DIR is not the camino site fixture" >&2; exit 1; } +[ ! -e "$BARE_TARGET/.git" ] && [ ! -e "$BARE_TARGET/config" ] || { + echo "error: $BARE_TARGET already looks like a git repo" >&2 + exit 1 +} + +WORK=$(mktemp -d) +trap 'rm -rf "$WORK"' EXIT INT TERM + +cp -a "$SITE_DIR/." "$WORK/" +mkdir -p "$WORK/.ddev/commands/host" +printf '#!/bin/sh\necho "hello from a custom ddev host command"\n' \ + > "$WORK/.ddev/commands/host/hello" +chmod +x "$WORK/.ddev/commands/host/hello" + +git -C "$WORK" init -q -b main +git -C "$WORK" config user.email e2e@opencode-permissions-kit.invalid +git -C "$WORK" config user.name "e2e fixture" +git -C "$WORK" add -A +git -C "$WORK" commit -q -m "site master (camino fixture, DD12 baseline)" + +git -C "$WORK" checkout -q -b feature/top-level +printf "# camino e2e — top-level file replaced on feature/top-level\n" > "$WORK/README.md" +rm "$WORK/LICENSE" +git -C "$WORK" add -A +git -C "$WORK" commit -q -m "replace README.md, drop LICENSE (top-level unlink/recreate)" + +git -C "$WORK" checkout -q -b feature/settings main +printf '\n// feature/settings: appended marker line\n' >> "$WORK/config/system/settings.php" +sed -i 's/websiteTitle: Camino/websiteTitle: Camino (feature-settings)/' \ + "$WORK/config/sites/camino/config.yaml" +git -C "$WORK" add -A +git -C "$WORK" commit -q -m "modify tracked settings/site config" + +git -C "$WORK" checkout -q -b feature/ddev-tree main +rm "$WORK/.ddev/commands/host/hello" +sed -i 's/^nodejs_version: .*/nodejs_version: 20/' "$WORK/.ddev/config.yaml" +git -C "$WORK" add -A +git -C "$WORK" commit -q -m "drop custom host command, tweak ddev config" + +git -C "$WORK" checkout -q main +mkdir -p "$BARE_TARGET" +git -C "$WORK" init -q --bare "$BARE_TARGET" +# fresh bare repos default HEAD to refs/heads/master — point it at main so +# plain `git clone` checks out the fixture baseline. +git --git-dir="$BARE_TARGET" symbolic-ref HEAD refs/heads/main +git -C "$WORK" push -q "$BARE_TARGET" main feature/top-level feature/settings feature/ddev-tree + +echo "bare origin ready: $BARE_TARGET" +git -C "$BARE_TARGET" for-each-ref --format=' %(refname:short)' refs/heads diff --git a/tests/e2e/lib.sh b/tests/e2e/lib.sh index f86d415..e78a3a3 100755 --- a/tests/e2e/lib.sh +++ b/tests/e2e/lib.sh @@ -13,7 +13,14 @@ # E2E_SYSTEMD 1 = systemd-as-PID1 container (rootless suite). e2e_start_container # then auto-detects the OUTER docker layout (rootful vs # rootless) and builds the cgroup args accordingly. +# E2E_SKIP_BUILD 1 = e2e_start_container runs the EXISTING E2E_IMAGE +# without building (golden-image cache path of the +# run-ddev.sh suite; the image was setuid-guarded at +# build time — DD0 re-asserts it on every boot). # E2E_DEBUG 1 = keep the container on failure for inspection +# E2E_KEEP 1 = keep the container after ANY run (fast iteration: +# exec manual retries inside it; the next run removes the +# leftover itself, see e2e_start_container) # E2E_OLD_VERSION pinned old opencode for the upgrade test # (default: 1.18.15; only fetched if e2e_fetch_old is called) # @@ -45,7 +52,9 @@ E2E_DOCKERFILE="${E2E_DOCKERFILE:-Dockerfile}" E2E_RUN_ARGS="${E2E_RUN_ARGS:-}" E2E_CMD="${E2E_CMD:-sleep infinity}" E2E_SYSTEMD="${E2E_SYSTEMD:-0}" +E2E_SKIP_BUILD="${E2E_SKIP_BUILD:-0}" E2E_DEBUG="${E2E_DEBUG:-0}" +E2E_KEEP="${E2E_KEEP:-0}" E2E_OLD_VERSION="${E2E_OLD_VERSION:-1.18.15}" E2E_HOST_LAYOUT="unknown" @@ -240,27 +249,29 @@ e2e_detect_host_layout() { } e2e_start_container() { - echo "" - echo "--- Building Docker image ($E2E_DOCKERFILE) ---" - docker build -t "$E2E_IMAGE" -f "$SCRIPT_DIR/$E2E_DOCKERFILE" "$SCRIPT_DIR" + if [ "$E2E_SKIP_BUILD" != "1" ]; then + echo "" + echo "--- Building Docker image ($E2E_DOCKERFILE) ---" + docker build -t "$E2E_IMAGE" -f "$SCRIPT_DIR/$E2E_DOCKERFILE" "$SCRIPT_DIR" - # Setuid guard: daemons that unpack images through the containerd - # snapshotter (the rootless docker the kit provisions does this by - # default) can strip setuid bits from image layers. sudo inside the - # e2e container then dies with "must be owned by uid 0 and have the - # setuid bit set" and EVERY sudo-based check fails with a misleading - # error. Detect it (sudo must keep its setuid bit) and rebuild with - # the classic builder, which preserves file modes — that is also why - # an agent session on the kit's own rootless daemon CAN run these - # suites: `DOCKER_BUILDKIT=0 make e2e` works, and this guard makes - # even a plain `make e2e` self-heal. - if ! docker run --rm --entrypoint sh "$E2E_IMAGE" -c 'test -u /usr/bin/sudo' >/dev/null 2>&1; then - echo " ${YELLOW}NOTE${NC} this daemon's BuildKit unpack strips setuid bits (containerd snapshotter) —" - echo " rebuilding with the classic builder (DOCKER_BUILDKIT=0) so sudo works in the container..." - if ! DOCKER_BUILDKIT=0 docker build -t "$E2E_IMAGE" -f "$SCRIPT_DIR/$E2E_DOCKERFILE" "$SCRIPT_DIR"; then - echo " ${RED}FAIL${NC} classic-builder rebuild failed (removed in this Docker version?)." - echo " Run the suite on a daemon whose images keep setuid bits (e.g. the rootful system docker)." - exit 1 + # Setuid guard: daemons that unpack images through the containerd + # snapshotter (the rootless docker the kit provisions does this by + # default) can strip setuid bits from image layers. sudo inside + # the e2e container then dies with "must be owned by uid 0 and have + # the setuid bit set" and EVERY sudo-based check fails with a + # misleading error. Detect it (sudo must keep its setuid bit) and + # rebuild with the classic builder, which preserves file modes — + # that is also why an agent session on the kit's own rootless + # daemon CAN run these suites: `DOCKER_BUILDKIT=0 make e2e` works, + # and this guard makes even a plain `make e2e` self-heal. + if ! docker run --rm --entrypoint sh "$E2E_IMAGE" -c 'test -u /usr/bin/sudo' >/dev/null 2>&1; then + echo " ${YELLOW}NOTE${NC} this daemon's BuildKit unpack strips setuid bits (containerd snapshotter) —" + echo " rebuilding with the classic builder (DOCKER_BUILDKIT=0) so sudo works in the container..." + if ! DOCKER_BUILDKIT=0 docker build -t "$E2E_IMAGE" -f "$SCRIPT_DIR/$E2E_DOCKERFILE" "$SCRIPT_DIR"; then + echo " ${RED}FAIL${NC} classic-builder rebuild failed (removed in this Docker version?)." + echo " Run the suite on a daemon whose images keep setuid bits (e.g. the rootful system docker)." + exit 1 + fi fi fi @@ -284,6 +295,8 @@ e2e_start_container() { echo "" echo "--- Running E2E container ---" + # A leftover container from an aborted/debug run must not block the name. + docker rm -f "$E2E_CONTAINER" >/dev/null 2>&1 || true # shellcheck disable=SC2086 # E2E_RUN_ARGS / E2E_CMD are intentional word splits docker run -d --name "$E2E_CONTAINER" \ -v "$REPO_DIR:/home/dev/repo" \ @@ -296,6 +309,12 @@ e2e_start_container() { E() { docker exec -u dev "$E2E_CONTAINER" sh -c "$@"; } e2e_cleanup() { + if [ "${E2E_KEEP:-0}" = "1" ]; then + echo "" + echo " ${YELLOW}E2E_KEEP=1: keeping container '$E2E_CONTAINER' (fast iteration)${NC}" + echo " docker exec -it $E2E_CONTAINER bash (removed automatically by the next run)" + return + fi if [ "$E2E_DEBUG" = "1" ] && [ "$failures" -gt 0 ]; then echo "" echo " ${YELLOW}--debug: keeping container '$E2E_CONTAINER' for inspection${NC}" diff --git a/tests/e2e/run-ddev.sh b/tests/e2e/run-ddev.sh new file mode 100755 index 0000000..f4c6799 --- /dev/null +++ b/tests/e2e/run-ddev.sh @@ -0,0 +1,732 @@ +#!/bin/sh +# e2e/run-ddev.sh — real-ddev e2e suite with a cached golden container +# Run from repo root: ./tests/e2e/run-ddev.sh (or: make e2e-ddev) +# +# Implements docs/design/ddev-e2e-test.md. Unlike the other two suites this +# one runs the REAL ddev against the REAL inner docker-rootless daemon +# inside the systemd test container (Dockerfile.rootless, like +# run-docker-rootless.sh) — the burn-in class (issues #18 #20 #21 #25, the +# 2026-08-22 findings §7.3) that fake-ddev cannot catch. +# +# The expensive environment (rootless daemon + ddev + pulled images + the +# camino fixture master) is baked ONCE into a golden image via `docker +# commit` (§4) and reused: cached runs skip the multi-minute download and +# install phase. The kit itself is NEVER cached — every run installs it +# fresh from the repo bind mount (§4.1). +# +# Cache key = image labels: kit.e2e.ddev.version (pinned via $DDEV_VERSION, +# else latest release), kit.e2e.format (warm-up recipe revision), kit.e2e.site +# (fixture tier), kit.e2e.built (TTL, $E2E_DDEV_TTL days, 0 = off). +# `make e2e-ddev-fresh` (or --fresh) forces a rebuild. +# +# Tiers: default = skeleton checks (DD0-DD9, DD13/DD14 on generated +# projects). E2E_DDEV_SITE=camino adds the real-site tier (DD10/DD11) and +# the git-flow tier (DD12) against tests/e2e/fixtures/camino. +# +# Usage: run-ddev.sh [--debug] [--fresh] +# --debug on failure: keep the container alive for inspection +# --fresh rebuild the golden image (new ddev version, recipe bump, TTL) +set -e + +E2E_DEBUG="${E2E_DEBUG:-0}" +FRESH=0 +for _arg in "$@"; do + case "$_arg" in + --debug) E2E_DEBUG=1 ;; + --fresh) FRESH=1 ;; + --help|-h) + sed -n 's/^# \{0,1\}//p' "$0" | sed -n '/^Usage:/,$p' + exit 0 + ;; + *) echo "unknown argument: $_arg (see --help)" >&2; exit 1 ;; + esac +done + +GOLDEN_IMAGE="${E2E_DDEV_IMAGE:-opencode-e2e-ddev:cached}" +BASE_IMAGE="opencode-e2e-ddev-base" +E2E_IMAGE="$BASE_IMAGE" # the build path starts from the base image +E2E_CONTAINER="opencode-e2e-ddev-test" +E2E_DOCKERFILE="Dockerfile.rootless" +E2E_SYSTEMD=1 +E2E_CMD="/sbin/init" +. "$(dirname "$(readlink -f "$0")")/lib.sh" + +# Golden-image cache key (§5). Bump GOLDEN_FORMAT when the warm-up recipe +# changes so cached images rebuild. +GOLDEN_FORMAT=1 +GOLDEN_TTL_DAYS="${E2E_DDEV_TTL:-14}" +SITE_TIER="${E2E_DDEV_SITE:-}" # camino | (empty) + +echo "" +echo "${CYAN}========================================================${NC}" +echo "${CYAN} opencode permissions kit — real-ddev E2E (golden)${NC}" +echo "${CYAN}========================================================${NC}" +echo "" + +# --- resolve the ddev version + fetch the binary (host-side cache) ----------- +# Mirrors e2e_resolve_cache: pin via DDEV_VERSION, else latest release, else +# the golden image's label (offline fallback). The tarball lands in +# tests/e2e/cache/ddev-/ (gitignored, mounted read-only into the container). +DD_WANT="${DDEV_VERSION:-}" +if [ -z "$DD_WANT" ]; then + DD_WANT=$(curl -fsSL --retry 5 --retry-delay 10 --retry-all-errors --max-time 30 \ + https://api.github.com/repos/ddev/ddev/releases/latest 2>/dev/null \ + | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p' || true) +fi +if [ -z "$DD_WANT" ]; then + DD_WANT=$(docker image inspect -f '{{ index .Config.Labels "kit.e2e.ddev.version" }}' "$GOLDEN_IMAGE" 2>/dev/null || true) + [ -n "$DD_WANT" ] && echo " ${YELLOW}WARNING: ddev version endpoint unreachable - using cached ddev $DD_WANT${NC}" +fi +[ -n "$DD_WANT" ] || { echo " ${RED}FAIL${NC} cannot resolve a ddev version"; exit 1; } +case "$DD_WANT" in v*) DD_VER="${DD_WANT#v}" ;; *) DD_VER="$DD_WANT"; DD_WANT="v$DD_WANT" ;; esac + +DD_ARCH=$(uname -m) +case "$DD_ARCH" in + x86_64) DD_ARCH="amd64" ;; + aarch64|arm64) DD_ARCH="arm64" ;; + *) echo " ${RED}FAIL${NC} unsupported arch: $DD_ARCH"; exit 1 ;; +esac +DD_CACHE="$SCRIPT_DIR/cache/ddev-$DD_VER" +DD_BIN="$DD_CACHE/ddev" +if [ ! -x "$DD_BIN" ]; then + echo " Downloading ddev $DD_WANT (linux-$DD_ARCH) into cache..." + mkdir -p "$DD_CACHE" + curl -fsSL --retry 5 --retry-delay 10 --retry-all-errors --max-time 300 \ + "https://github.com/ddev/ddev/releases/download/$DD_WANT/ddev_linux-$DD_ARCH.$DD_WANT.tar.gz" \ + -o "$DD_CACHE/ddev.tar.gz" \ + || { echo " ${RED}FAIL${NC} ddev $DD_WANT download failed"; exit 1; } + tar -xzf "$DD_CACHE/ddev.tar.gz" -C "$DD_CACHE" \ + || { echo " ${RED}FAIL${NC} cannot extract ddev tarball"; exit 1; } + rm -f "$DD_CACHE/ddev.tar.gz" + chmod +x "$DD_BIN" 2>/dev/null || true +fi +[ -s "$DD_BIN" ] || { echo " ${RED}FAIL${NC} cached ddev binary is empty"; exit 1; } +echo " Using ddev $DD_WANT (cache: $DD_BIN)" + +# Golden-image v2 (2026-08-22 pivot): the golden image contains NO inner +# docker store. Baking the store into a `docker commit` corrupted every +# cached image — commit is not a faithful copy for overlay whiteouts +# ("deleted" files like /etc/mysql/mariadb.cnf reappeared; every project db +# died on an inaccessible unix socket), and a named-volume store corrupted +# the same way through the nested storage stack. Instead the IMAGES are +# cached host-side as a docker-save tarball (ext4, gitignored, next to the +# opencode binaries) and `docker load`-ed into a FRESH inner store on every +# warm start — the exact write path that produced working images in the +# cold build. System state (packages, ddev, composer/fixture data, +# /home/opencode/.ddev) stays in the golden commit (regular files only). +DD_IMG_TAR="$SCRIPT_DIR/cache/ddev-images-$DD_VER-f$GOLDEN_FORMAT.tar" + +# Fixtures at a neutral mount: opencode-side ddev commands (import-db) must +# read db.sql.gz, but the repo bind mount sits under /home/dev (750, +# dev-owned — opencode cannot traverse it). /e2e-fixtures is world-readable. +E2E_RUN_ARGS="-v $SCRIPT_DIR/fixtures:/e2e-fixtures:ro" + +dd_label() { + docker image inspect -f "{{ index .Config.Labels \"$1\" }}" "$GOLDEN_IMAGE" 2>/dev/null || true +} + +dd_golden_current() { + [ "$FRESH" = "0" ] || return 1 + docker image inspect "$GOLDEN_IMAGE" >/dev/null 2>&1 || return 1 + [ -f "$DD_IMG_TAR" ] || return 1 + [ "$(dd_label kit.e2e.ddev.version)" = "$DD_VER" ] || return 1 + [ "$(dd_label kit.e2e.format)" = "$GOLDEN_FORMAT" ] || return 1 + [ "$(dd_label kit.e2e.site)" = "${SITE_TIER:-none}" ] || return 1 + if [ "$GOLDEN_TTL_DAYS" != "0" ]; then + _built=$(dd_label kit.e2e.built) + _now=$(date +%s) + [ -n "$_built" ] && [ "$((_now - _built))" -le "$((GOLDEN_TTL_DAYS * 86400))" ] || return 1 + fi + return 0 +} + +echo "--- opencode binary cache (version-keyed) ---" +e2e_resolve_cache +e2e_prepare_project + +# Helpers used by the cold-build path below (the kit is not installed yet, +# so there is no ddev() hook — env is set explicitly, like the wrapper does). +# Note: $1/$2 are expanded at construction time on purpose — the dev shell +# must receive the FULL inner command (a literal $2 would expand empty there +# and break the inner sh -c). +dd_oc_uid() { E 'id -u opencode'; } +dd_build_oc() { + _ocu=$(dd_oc_uid) + E 'sudo -u opencode env HOME=/home/opencode XDG_RUNTIME_DIR=/run/user/'"$_ocu"' DOCKER_HOST=unix:///run/user/'"$_ocu"'/docker.sock DDEV_NO_INSTRUMENTATION=true sh -c "cd '"$1"' && '"$2"'"' +} + +if dd_golden_current; then + echo "" + echo " ${GREEN}golden image current${NC} (ddev $DD_VER, format $GOLDEN_FORMAT, site: ${SITE_TIER:-none}) — warm start" +else + _why="label mismatch" + [ "$FRESH" = "1" ] && _why="--fresh" + echo "" + echo " ${YELLOW}golden image rebuild${NC} ($_why): provisioning daemon + ddev + images once (§4.2)..." + E2E_IMAGE="$BASE_IMAGE" + E2E_SKIP_BUILD=0 + e2e_start_container + + # RL0: systemd must be up (same gate as run-docker-rootless.sh). + _boot_ok=false + for _i in $(seq 1 15); do + E 'test -e /run/systemd/private' 2>/dev/null && { _boot_ok=true; break; } + sleep 1 + done + if [ "$_boot_ok" != true ]; then + echo " ${YELLOW}SKIP${NC} systemd did not boot in the container (runner limitation)" + skip "systemd not available" + e2e_finish + exit $? + fi + + # Provision the inner environment (kit code path, §4.2). The kit itself + # is NOT installed into the golden image (§4.1) — only its provisioning + # helper runs, so every cached run installs the kit fresh (DD1). + E 'id opencode >/dev/null 2>&1 || sudo useradd -m -s /bin/bash opencode' + # Deterministic subuid seed (nested-userns constraint): the e2e + # container's own uid_map covers only uids 1..65536, so the kit default + # (100000+) would be unmappable when the OUTER daemon is rootless; the + # seeded window fits. It cannot be enlarged to cover uid 65534 (nobody): + # 65535 consecutive slots under 65536 inevitably collide with the daemon + # user's own uid in the map. nginx-fpm chowns to exactly that uid, so + # EVERY project in this suite must use apache-fpm (www-data = 33, well + # inside the window) — like the camino fixture. The provisioning helper + # KEEPS an existing entry, so this pins the range on every layout. + E 'sudo sh -c "printf \"opencode:4096:60000\n\" > /etc/subuid; printf \"opencode:4096:60000\n\" > /etc/subgid"' + # fuse-overlayfs pin — kernel overlay2 cannot stack on the container's + # own overlay rootfs (nested docker-in-docker; as in run-docker-rootless.sh). + E 'sudo -u opencode sh -c "mkdir -p /home/opencode/.config/docker"' + E 'printf "{\"storage-driver\":\"fuse-overlayfs\"}\n" | sudo -u opencode tee /home/opencode/.config/docker/daemon.json >/dev/null' + + if ! E 'sudo bash /home/dev/repo/files/opencode-permissions-kit-lib/setup-container-backend.sh docker-rootless --yes >/tmp/dd-setup.log 2>&1'; then + echo " ${RED}FAIL${NC} backend provisioning failed — /tmp/dd-setup.log:"; failures=$((failures + 1)) + E 'sed "s/\x1b\[[0-9;]*m//g" /tmp/dd-setup.log | tail -30' || true + exit 1 + fi + _sock_ok=false + for _i in $(seq 1 30); do + E "sudo test -S /run/user/$(dd_oc_uid)/docker.sock" 2>/dev/null && { _sock_ok=true; break; } + sleep 1 + done + [ "$_sock_ok" = true ] || { echo " ${RED}FAIL${NC} inner rootless daemon socket never appeared"; failures=$((failures + 1)); exit 1; } + + # Real ddev from the host-side cache (bind-mounted read-only), then the + # rootless router ports (§6 R7 — the documented rootless answer instead + # of the host-wide sysctl). ddev instrumentation is opt-in: stays off. + E 'sudo install -m 755 /opencode-cache/ddev-'"$DD_VER"'/ddev /usr/local/bin/ddev' + dd_build_oc /tmp 'ddev config global --router-http-port 8080 --router-https-port 8443' \ + || { echo " ${RED}FAIL${NC} ddev global config failed"; failures=$((failures + 1)); exit 1; } + + # Warm-up (§4.2.6): pull images + mutagen (+ camino master, §7.1). + if [ "$SITE_TIER" = "camino" ] && [ -f "$SCRIPT_DIR/fixtures/camino/site/composer.json" ]; then + echo "" + echo " warm-up: camino site (start + composer install + db import + verify) ..." + E 'sudo cp -a /home/dev/repo/tests/e2e/fixtures/camino/site /var/www/vhosts/dd-warmup' + # Fully opencode-owned (what the kit handover arranges): ddev's + # bootstrap-phase root chmod AND the post-install settings writes + # both run unimpeded. + E 'sudo chown -R opencode:opencode /var/www/vhosts/dd-warmup' + dd_build_oc /var/www/vhosts/dd-warmup 'ddev start >/tmp/dd-warm.log 2>&1' \ + || { E 'tail -20 /tmp/dd-warm.log' || true; echo " ${RED}FAIL${NC} warm-up ddev start failed"; failures=$((failures + 1)); exit 1; } + dd_build_oc /var/www/vhosts/dd-warmup 'ddev composer install >/tmp/dd-warm.log 2>&1' \ + || { E 'tail -20 /tmp/dd-warm.log' || true; echo " ${RED}FAIL${NC} warm-up composer install failed"; failures=$((failures + 1)); exit 1; } + dd_build_oc /var/www/vhosts/dd-warmup 'ddev import-db --file=/e2e-fixtures/camino/db.sql.gz >/tmp/dd-warm.log 2>&1' \ + || { E 'tail -20 /tmp/dd-warm.log' || true; echo " ${RED}FAIL${NC} warm-up db import failed"; failures=$((failures + 1)); exit 1; } + E 'curl --resolve dd-warmup.local:8080:127.0.0.1 -fsS http://dd-warmup.local:8080/camino/ -o /tmp/dd-warm-front.html' \ + || { echo " ${RED}FAIL${NC} warm-up frontend not reachable"; failures=$((failures + 1)); E 'head -5 /tmp/dd-warm-front.html 2>/dev/null' || true; exit 1; } + E 'grep -qiE "camino|typo3|permission kit" /tmp/dd-warm-front.html' \ + || { echo " ${RED}FAIL${NC} warm-up frontend has no marker content"; failures=$((failures + 1)); exit 1; } + # Pristine master WITH vendor/ (§7.1): per-run copies are page-cached + # cp -a, no per-run composer install needed. + E 'sudo mkdir -p /opt/e2e/fixtures && sudo cp -a /var/www/vhosts/dd-warmup /opt/e2e/fixtures/camino' + dd_build_oc /var/www/vhosts/dd-warmup 'ddev delete -Oy >/dev/null 2>&1 || true' + else + echo "" + echo " warm-up: skeleton php project (image pulls + mutagen) ..." + E 'sudo mkdir -p /var/www/vhosts/dd-warmup && sudo chown opencode:opencode /var/www/vhosts/dd-warmup' + dd_build_oc /var/www/vhosts/dd-warmup 'ddev config --project-type=php --webserver-type=apache-fpm --docroot=public --auto >/dev/null 2>&1 && ddev start >/tmp/dd-warm.log 2>&1' \ + || { E 'tail -20 /tmp/dd-warm.log' || true; echo " ${RED}FAIL${NC} warm-up ddev start failed"; failures=$((failures + 1)); exit 1; } + dd_build_oc /var/www/vhosts/dd-warmup 'ddev delete -Oy >/dev/null 2>&1 || true' + fi + + # Quiesce + export + commit (§4.2.8-9): prune leftover containers + # (images stay), export the images to the host-side tarball cache WHILE + # THE DAEMON STILL RUNS, stop the daemon, DELETE the store from the + # container filesystem (golden-image v2: committing the store corrupts + # it — see DD_IMG_TAR rationale), then snapshot the stopped container. + dd_build_oc /tmp 'docker system prune -f >/dev/null 2>&1 || true' + echo "" + echo " exporting inner images to $DD_IMG_TAR ..." + _ocu=$(dd_oc_uid) + _imgs=$(E 'sudo -u opencode env HOME=/home/opencode XDG_RUNTIME_DIR=/run/user/'"$_ocu"' DOCKER_HOST=unix:///run/user/'"$_ocu"'/docker.sock docker images --format "{{.Repository}}:{{.Tag}}" | grep -v ""') + [ -n "$_imgs" ] || { echo " ${RED}FAIL${NC} no inner images to export"; failures=$((failures + 1)); exit 1; } + # shellcheck disable=SC2086 # word splitting is the image list + docker exec -u dev "$E2E_CONTAINER" \ + sudo -u opencode env HOME=/home/opencode XDG_RUNTIME_DIR="/run/user/$_ocu" \ + DOCKER_HOST="unix:///run/user/$_ocu/docker.sock" \ + docker save $_imgs > "$DD_IMG_TAR" \ + || { echo " ${RED}FAIL${NC} docker save failed"; failures=$((failures + 1)); exit 1; } + dd_build_oc /tmp 'systemctl --user stop docker.service' || true + E 'sudo rm -rf /home/opencode/.local/share/docker' + echo " committing golden image $GOLDEN_IMAGE (ddev $DD_VER, format $GOLDEN_FORMAT, site: ${SITE_TIER:-none}) ..." + docker stop -t 30 "$E2E_CONTAINER" >/dev/null + docker commit \ + --change 'LABEL kit.e2e.ddev.version='"$DD_VER" \ + --change 'LABEL kit.e2e.format='"$GOLDEN_FORMAT" \ + --change 'LABEL kit.e2e.built='"$(date +%s)" \ + --change 'LABEL kit.e2e.site='"${SITE_TIER:-none}" \ + "$E2E_CONTAINER" "$GOLDEN_IMAGE" >/dev/null + docker rm "$E2E_CONTAINER" >/dev/null + echo " golden size: $(docker image inspect -f '{{ .Size }}' "$GOLDEN_IMAGE" | awk '{printf "%.1f GB", $1/1024/1024/1024}')" +fi + +# --- warm start (§4.3): boot the golden image fresh --------------------------- +E2E_IMAGE="$GOLDEN_IMAGE" +E2E_SKIP_BUILD=1 +e2e_start_container + +echo "" +echo "--- DD0. cache-integrity gates ---" +_boot_ok=false +for _i in $(seq 1 20); do + E 'test -e /run/systemd/private' 2>/dev/null && { _boot_ok=true; break; } + sleep 1 +done +if [ "$_boot_ok" != true ]; then + echo " ${YELLOW}SKIP${NC} systemd did not boot (golden image damaged? try --fresh)" + skip "systemd not available" + e2e_finish + exit $? +fi +check "DD0: systemd is PID 1" \ + E 'test "$(cat /proc/1/comm)" = "systemd"' + +OC_UID=$(E 'id -u opencode') +SOCK="unix:///run/user/$OC_UID/docker.sock" +SOCKPATH="/run/user/$OC_UID/docker.sock" +echo " opencode uid: $OC_UID (socket: $SOCK)" + +_sock_ok=false +for _i in $(seq 1 30); do + E "sudo test -S $SOCKPATH" 2>/dev/null && { _sock_ok=true; break; } + sleep 1 +done +if [ "$_sock_ok" != true ]; then + echo " ${YELLOW}SKIP${NC} DD0: inner daemon socket never appeared (image damaged? try --fresh)" + skip "inner daemon not available" + e2e_finish + exit $? +fi + +# Agent-context runner: ddev as the opencode user with the session env the +# wrapper exports. Dev-context runs go through DEVSH (interactive bash +# sources the kit's ddev() hook from ~/.bashrc). +OC() { + docker exec -u dev "$E2E_CONTAINER" sudo -u opencode \ + env HOME=/home/opencode XDG_RUNTIME_DIR="/run/user/$OC_UID" \ + DOCKER_HOST="$SOCK" DDEV_NO_INSTRUMENTATION=true sh -c "cd ${OC_CWD:-/tmp} && $1" +} +# Project-scoped wrappers: `OC_CWD=x OC ...` is only parsed as an env +# assignment when written LITERALLY — passed through check()'s "$@" the +# word becomes a command ("OC_CWD=...: not found" in dash). The wrappers +# put the assignment in their own body, safe everywhere. +OC_DD2() { OC_CWD=/var/www/vhosts/dd2 OC "$1"; } +OC_DD13() { OC_CWD=/var/www/vhosts/dd13-proj OC "$1"; } +OC_CAM() { OC_CWD=/var/www/vhosts/camino-e2e OC "$1"; } +OC_DD12() { OC_CWD=/var/www/vhosts/dd12-proj OC "$1"; } +DEVSH() { docker exec -u dev "$E2E_CONTAINER" bash -ic "$1" 2>&1; } + +_act_ok=false +for _i in $(seq 1 30); do + E 'sudo -u opencode sh -c "XDG_RUNTIME_DIR=/run/user/'"$OC_UID"' systemctl --user is-active docker" 2>/dev/null | grep -qx active' \ + && { _act_ok=true; break; } + sleep 1 +done +check "DD0: inner rootless daemon is active (linger auto-start)" \ + test "$_act_ok" = true + +# Warm start (§4.3): the golden image ships NO image store — restore the +# cached images from the host-side docker-save tarball into the fresh inner +# store (same unpack path the cold build used; ~1-3 min from local disk). +if [ ! -f "$DD_IMG_TAR" ]; then + echo " ${RED}FAIL${NC} image tarball missing: $DD_IMG_TAR (rebuild with --fresh)" + failures=$((failures + 1)) + e2e_finish + exit $? +fi +echo " loading cached ddev images ($(du -h "$DD_IMG_TAR" | cut -f1)) ..." +if ! docker exec -i -u dev "$E2E_CONTAINER" \ + sudo -u opencode env HOME=/home/opencode \ + XDG_RUNTIME_DIR="/run/user/$OC_UID" DOCKER_HOST="$SOCK" \ + docker load < "$DD_IMG_TAR" >/tmp/dd-load.log 2>&1; then + echo " ${RED}FAIL${NC} docker load failed:"; failures=$((failures + 1)) + docker exec -u dev "$E2E_CONTAINER" sh -c 'sed "s/\x1b\[[0-9;]*m//g" /tmp/dd-load.log | tail -10' || true + exit 1 +fi +check "DD0: cached images restored (R3 canary)" \ + OC 'test -n "$(docker images -q)"' +# Image-integrity canary (2026-08-22 corruption class): the ddev db image +# DELETES /etc/mysql/mariadb.cnf in an upper layer; if that deletion is +# void, mariadbd reads the Debian socket config (/run/mysqld, mysql:999) +# and every project db dies with "Bind on unix socket: Permission denied". +check "DD0: db base image intact (mariadb.cnf ghost absent)" \ + OC 'docker run --rm --entrypoint sh ddev/ddev-dbserver-mariadb-11.8:v'"$DD_VER"' -c "test ! -e /etc/mysql/mariadb.cnf"' +check "DD0: ddev $DD_WANT present and working" \ + OC "ddev version 2>/dev/null | grep -q '$DD_WANT'" +if ! E 'test -u /usr/bin/sudo' >/dev/null 2>&1; then + echo " ${YELLOW}NOTE${NC} sudo lost its setuid bit (R2) — self-healing with chmod u+s" + E 'sudo chmod u+s /usr/bin/sudo' +fi +check "DD0: sudo setuid bit intact" \ + E 'test -u /usr/bin/sudo' + +echo "" +echo "--- DD1. kit install on the warm image (upgrade-style) ---" +E 'bash /opencode-cache/install.sh --binary /opencode-cache/opencode-'"$OC_VERSION"'/opencode' || { + E 'test -x /home/dev/.opencode/bin/opencode' || { echo " ${RED}E2E aborted — cannot install opencode.${NC}"; failures=$((failures + 1)); exit 1; } +} +if ! E 'sudo bash /home/dev/repo/files/install.sh --yes --container-backend docker-rootless --projects /var/www/vhosts --skip-ddev-migration >/tmp/dd-install.log 2>&1'; then + echo " ${RED}FAIL${NC} kit install failed:"; failures=$((failures + 1)) + E 'sed "s/\x1b\[[0-9;]*m//g" /tmp/dd-install.log | tail -30' || true + exit 1 +fi +echo " ${GREEN}OK${NC} kit install completed" +check "DD1: install.conf records docker-rootless" \ + E 'grep -q "^CONTAINER_BACKEND=docker-rootless" /etc/opencode-permissions-kit/install.conf' +check "DD1: dev-owned mode is the installer default" \ + E 'grep -q "^DDEV_DEV_OWNED=true" /etc/opencode-permissions-kit/install.conf' +check "DD1: wrapper at /usr/local/bin/opencode" \ + E 'test -x /usr/local/bin/opencode' +check "DD1: sudoers carries the ddev-as-opencode helper rule" \ + E 'sudo grep -q "bin/ddev-as-opencode" /etc/sudoers.d/opencode-permissions-kit' +check "DD1: ddev() hook wired into dev's bashrc" \ + E 'grep -q "ddev-as-opencode.sh" /home/dev/.bashrc' +check "DD1: global ddev home provisioned for opencode" \ + E 'sudo test -d /home/opencode/.ddev' + +_daemon_ok=true + +echo "" +echo "--- DD2/DD3/DD4. bootstrap, chmod modes, dev-owned (project dd2) ---" +# Handover-mode section first (ddev-settings off = the old model), then the +# dev-owned default — both against one fresh typo3 bootstrap project. +E 'sudo bash /home/dev/repo/files/config.sh --yes ddev-settings off >/dev/null 2>&1' +E 'sudo -u dev mkdir -p /var/www/vhosts/dd2' +DEVSH 'cd /var/www/vhosts/dd2 && ddev config --project-type=typo3 --webserver-type=apache-fpm --docroot=public --project-tld local >/tmp/dd2-config.log 2>&1' +# Deterministic handover-mode state: strip the dev-owned flag ddev's own +# config template may carry over (its commented default is part of the +# generated yaml) so the bootstrap-path checks below are not mode-dependent. +E 'sudo sed -i "/^disable_settings_management:/d" /var/www/vhosts/dd2/.ddev/config.yaml' +check "DD2: ddev config on empty dir completes (burn-in flags)" \ + E 'grep -q "Configuration complete" /tmp/dd2-config.log' +check "DD2: .ddev is opencode-owned after config" \ + E 'test "$(stat -c %U /var/www/vhosts/dd2/.ddev)" = opencode' +E 'sudo bash /home/dev/repo/files/config.sh --yes handover /var/www/vhosts/dd2 >/tmp/dd2-handover.log 2>&1' +check "DD2: handover mode: bootstrap root inode handed to opencode 2755" \ + E 'test "$(stat -c %U:%a /var/www/vhosts/dd2)" = "opencode:2755"' +if OC_DD2 'ddev start >/tmp/dd2-start.log 2>&1'; then + check "DD2: agent-side start completes (real ddev, no EPERM)" \ + E 'grep -q "Successfully started" /tmp/dd2-start.log' + # ddev v1.25.3 pinned behavior (typo3.go:24-27): with undetected TYPO3 + # the settings writer EARLY-RETURNS (paths "skipable") — the start + # neither chmods the bootstrap root to 0755 nor writes a root + # AdditionalConfiguration.php. Tripwire: if ddev changes this bootstrap + # behavior again, these checks flip and tell us (issue #25 mechanics + # are still covered by DD13 + the camino tier). + check "DD3: v1.25.3: start leaves undetected bootstrap root at 2755 (typo3.go early-return)" \ + E 'test "$(stat -c %a /var/www/vhosts/dd2)" = "2755"' + check "DD3: v1.25.3: no root AdditionalConfiguration.php while TYPO3 undetected" \ + E 'test ! -e /var/www/vhosts/dd2/AdditionalConfiguration.php' + E 'sudo bash /home/dev/repo/files/config.sh --yes refresh >/dev/null 2>&1' + check "DD3: config.sh refresh restores g+w on the root (2755)" \ + E 'test "$(stat -c %a /var/www/vhosts/dd2)" = "2755"' +else + echo " ${YELLOW}SKIP${NC} DD2: ddev start failed — output:" + E 'sed "s/\x1b\[[0-9;]*m//g" /tmp/dd2-start.log 2>/dev/null | tail -15' || true + skip "dd2 start failed" + _daemon_ok=false +fi + +if [ "$_daemon_ok" = true ]; then + # Dev-owned mode: the committed-flag model (ddev-dev-owned-projects.md). + E 'sudo bash /home/dev/repo/files/config.sh --yes ddev-settings on >/dev/null 2>&1' + E 'sudo bash /home/dev/repo/files/config.sh --yes handover /var/www/vhosts/dd2 >/tmp/dd2-handback.log 2>&1' + check "DD4: dev-owned handover: flag written into .ddev/config.yaml" \ + E 'grep -q "^disable_settings_management: true" /var/www/vhosts/dd2/.ddev/config.yaml' + check "DD4: dev-owned handover: bootstrap root handed back to dev (2775)" \ + E 'test "$(stat -c %U:%a /var/www/vhosts/dd2)" = "dev:2775"' + check "DD4: .ddev stays opencode-owned" \ + E 'test "$(stat -c %U /var/www/vhosts/dd2/.ddev)" = opencode' + if OC_DD2 'ddev restart >/tmp/dd2-restart.log 2>&1'; then + check "DD4: dev-owned restart succeeds" \ + E 'grep -qE "Successfully started|Restarted" /tmp/dd2-restart.log' + else + echo " ${RED}FAIL${NC} DD4: dev-owned restart failed:"; failures=$((failures + 1)) + E 'sed "s/\x1b\[[0-9;]*m//g" /tmp/dd2-restart.log 2>/dev/null | tail -10' || true + failures=$((failures + 1)) + fi + check "DD4: root still dev-owned after dev-owned restart" \ + E 'test "$(stat -c %U /var/www/vhosts/dd2)" = dev' + + echo "" + echo "--- DD5. developer-side transports (ddev() function + BASH_ENV) ---" + check "DD5: ddev() function active in dev's interactive shell" \ + DEVSH 'type ddev | grep -q "function"' + check "DD5: export -f lands the function in nested bash -c (issue #18)" \ + DEVSH 'bash -c "type ddev | grep -q function"' + check "DD5: ddev via dev function runs the REAL ddev (version)" \ + DEVSH 'cd /var/www/vhosts/dd2 && ddev version 2>/dev/null | grep -q ddev' + check "DD5: ddev processes run as opencode (pgrep uid — issue #18 root cause)" \ + DEVSH 'cd /var/www/vhosts/dd2 && (ddev exec sleep 6 >/dev/null 2>&1 &) && sleep 2 && pgrep -x ddev -u opencode >/dev/null' + + echo "" + echo "--- DD6. container identity with real ddev containers ---" + check "DD6: ddev exec works (agent side)" \ + OC_DD2 'ddev exec true' + _dd6_uidmap() { + OC_DD2 'ddev exec cat /proc/self/uid_map' 2>/dev/null \ + | grep -qE "0[[:space:]]+${OC_UID}[[:space:]]" + } + check "DD6: web container userns maps root to the opencode host UID (§9.1)" \ + _dd6_uidmap + check "DD6: container reads project files via bind mount (soft-only goal)" \ + OC_DD2 'ddev exec cat /mnt/ddev_config/config.yaml | grep -q ddev' + + echo "" + echo "--- DD8. describe parity: agent vs developer context (§10a) ---" + OC_DD2 'ddev describe --json-output >/tmp/dd8-oc.json 2>/dev/null' || true + DEVSH 'cd /var/www/vhosts/dd2 && ddev describe --json-output >/tmp/dd8-dev.json 2>/dev/null' || true + check "DD8: agent-side describe reports the project running" \ + E 'grep -Eq "\"status\": ?\"running\"" /tmp/dd8-oc.json' + check "DD8: dev-side describe reports the SAME state (one driver, one daemon)" \ + E 'grep -Eq "\"status\": ?\"running\"" /tmp/dd8-dev.json' + + echo "" + echo "--- DD9. db round-trip (import/export) ---" + E 'printf "CREATE TABLE dd9_mark (id INT);\n" | gzip > /tmp/dd9.sql.gz' + if OC_DD2 'ddev import-db --file=/tmp/dd9.sql.gz >/tmp/dd9.log 2>&1'; then + check "DD9: export-db returns the imported mark" \ + OC_DD2 'ddev export-db -f=/tmp/dd9-out.sql.gz >/dev/null 2>&1 && zcat /tmp/dd9-out.sql.gz | grep -q dd9_mark' + else + echo " ${YELLOW}SKIP${NC} DD9: import-db failed" + skip "dd9 import failed" + fi + + echo "" + echo "--- DD7. lifecycle + suite-wide EPERM sweep ---" + check "DD7: ddev restart" \ + OC_DD2 'ddev restart >/tmp/dd7.log 2>&1' + check "DD7: ddev stop" \ + OC_DD2 'ddev stop >/tmp/dd7.log 2>&1' + check "DD7: ddev delete -Oy" \ + OC_DD2 'ddev delete -Oy >/tmp/dd7.log 2>&1' + # EXCLUDED logs hold EXPECTED EPERM text: dd13-start1 (the DD13 + # tripwire asserts that exact bootstrap EPERM), dd2-config (the burn-in + # Finding-1 chmod warning from `ddev config`). + check "DD7: zero 'operation not permitted' across the suite log" \ + E '_s=$(grep -il "operation not permitted" /tmp/dd2-start.log /tmp/dd2-restart.log /tmp/dd2-handover.log /tmp/dd7.log /tmp/dd9.log /tmp/dd10-*.log /tmp/dd11-*.log /tmp/dd12-*.log 2>/dev/null); test -z "$_s"' +fi + +if [ "$SITE_TIER" = "camino" ] && E 'test -d /opt/e2e/fixtures/camino' 2>/dev/null; then + echo "" + echo "--- DD10/DD11. real-site tier (camino) ---" + if E 'test -f /opt/e2e/fixtures/camino/vendor/bin/typo3'; then + E 'sudo cp -a /opt/e2e/fixtures/camino /var/www/vhosts/camino-e2e' + E 'sudo chown -R dev:dev /var/www/vhosts/camino-e2e' + E 'sudo bash /home/dev/repo/files/config.sh --yes handover /var/www/vhosts/camino-e2e >/tmp/dd10-handover.log 2>&1' + check "DD10: handover leaves settings dev-owned (flag committed)" \ + E 'test "$(stat -c %U /var/www/vhosts/camino-e2e/config/system)" = dev' + check "DD10: .ddev opencode-owned after handover" \ + E 'test "$(stat -c %U /var/www/vhosts/camino-e2e/.ddev)" = opencode' + if OC_CAM 'ddev start >/tmp/dd10-start.log 2>&1'; then + check "DD10: real site starts" \ + E 'grep -q "Successfully started" /tmp/dd10-start.log' + check "DD10: settings dir keeps g+w through start (dev-owned mode, #25)" \ + E 'test "$(stat -c %a /var/www/vhosts/camino-e2e/config/system)" = "2775"' + OC_CAM 'ddev import-db --file=/e2e-fixtures/camino/db.sql.gz >/tmp/dd10-imp.log 2>&1' \ + || echo " ${YELLOW}NOTE${NC} DD10: db import failed — site may show the install tool" + E 'curl --resolve camino-e2e.local:8080:127.0.0.1 -fsS http://camino-e2e.local:8080/camino/ -o /tmp/dd10-front.html' + check "DD10: frontend answers 200 (first real 'site works' assert)" \ + E 'test -s /tmp/dd10-front.html' + check "DD10: frontend renders CMS content" \ + E 'grep -qiE "camino|typo3|permission kit" /tmp/dd10-front.html' + check "DD10: settings.php exists and stays user-managed" \ + E 'test -f /var/www/vhosts/camino-e2e/config/system/settings.php && ! grep -q "ddev-generated" /var/www/vhosts/camino-e2e/config/system/settings.php' + + echo "" + echo "--- DD11. two-owner reality on the live site ---" + if DEVSH 'cd /var/www/vhosts/camino-e2e && ddev restart >/tmp/dd11-restart.log 2>&1'; then + check "DD11: developer-side restart through ddev() works while running" \ + E 'grep -qE "Successfully started|Restarted" /tmp/dd11-restart.log' + else + echo " ${RED}FAIL${NC} DD11: developer-side restart failed:"; failures=$((failures + 1)) + E 'sed "s/\x1b\[[0-9;]*m//g" /tmp/dd11-restart.log 2>/dev/null | tail -10' || true + failures=$((failures + 1)) + fi + E 'curl --resolve camino-e2e.local:8080:127.0.0.1 -fsS http://camino-e2e.local:8080/camino/ -o /tmp/dd11-front.html' + check "DD11: site still answers 200 after the dev restart" \ + E 'test -s /tmp/dd11-front.html' + check "DD11: dev can edit the settings file (#25 regression)" \ + DEVSH 'cd /var/www/vhosts/camino-e2e && printf "\n// dd11 edit\n" >> config/system/settings.php' + OC_CAM 'ddev delete -Oy >/dev/null 2>&1' || true + else + echo " ${YELLOW}SKIP${NC} DD10: site start failed — output:" + E 'sed "s/\x1b\[[0-9;]*m//g" /tmp/dd10-start.log 2>/dev/null | tail -15' || true + skip "camino start failed" + fi + else + echo " ${YELLOW}SKIP${NC} DD10/DD11: golden image lacks the camino master (built without site tier?)" + skip "camino master missing" + fi +elif [ "$SITE_TIER" = "camino" ]; then + echo "" + echo " ${YELLOW}SKIP${NC} site tier requested but master absent — rebuild with --fresh" + skip "camino master missing" +fi + +if [ "$SITE_TIER" = "camino" ]; then + echo "" + echo "--- DD12. git flow against the bare origin ---" + E 'sh /home/dev/repo/tests/e2e/fixtures/make-bare-origin.sh /tmp/dd12-origin /home/dev/repo/tests/e2e/fixtures/camino/site >/tmp/dd12-gen.log 2>&1' + if E 'test -f /tmp/dd12-origin/HEAD'; then + # (a) dev-owned default: clone as dev, handover, all branch switches free. + E 'git clone -q /tmp/dd12-origin /var/www/vhosts/dd12-proj' + E 'sudo bash /home/dev/repo/files/config.sh --yes handover /var/www/vhosts/dd12-proj >/dev/null 2>&1' + check "DD12: dev-owned: top-level branch switch is free (the promise)" \ + DEVSH 'cd /var/www/vhosts/dd12-proj && git checkout -q feature/top-level && test ! -f LICENSE && grep -q feature/top-level README.md' + check "DD12: dev-owned: settings-tree switch is free" \ + DEVSH 'cd /var/www/vhosts/dd12-proj && git checkout -q feature/settings && grep -q marker config/system/settings.php' + check "DD12: dev-owned: .ddev-tree switch is free (group-writable .ddev)" \ + DEVSH 'cd /var/www/vhosts/dd12-proj && git checkout -q feature/ddev-tree && test ! -f .ddev/commands/host/hello && git checkout -q main && test -z "$(git status --porcelain)"' + + # (b) handover-mode tripwire (§7.2 12.2): bootstrap root opencode-owned + # -> a top-level switch must hit the documented unlink EPERM. + # sed runs as root: dev cannot edit .ddev/config.yaml yet (Finding 1). + E 'sudo bash /home/dev/repo/files/config.sh --yes ddev-settings off >/dev/null 2>&1' + E 'sudo sed -i "/^disable_settings_management:/d" /var/www/vhosts/dd12-proj/.ddev/config.yaml' + E 'sudo bash /home/dev/repo/files/config.sh --yes handover /var/www/vhosts/dd12-proj >/dev/null 2>&1' + check "DD12: handover mode: bootstrap root is opencode-owned (tripwire setup)" \ + E 'test "$(stat -c %U /var/www/vhosts/dd12-proj)" = opencode' + check_fail "DD12: handover mode: top-level switch hits unlink EPERM (documented pain)" \ + DEVSH 'cd /var/www/vhosts/dd12-proj && git checkout -q main 2>/dev/null; git checkout -q feature/top-level' + # Recover the possibly half-switched tree before the next section. + DEVSH 'cd /var/www/vhosts/dd12-proj && git checkout -q -f main && git clean -qfd' || true + + # (c) back to dev-owned: the same switch now succeeds. The tripwire's + # sed-delete + the handover's re-append leave the TRACKED + # .ddev/config.yaml dirty (position differs from the committed flag + # line) — restore the committed state (the fixture commits the flag, + # i.e. exactly what dev-owned mode wants) so switch assertions and + # config.yaml-touching branches stay clean. + E 'sudo bash /home/dev/repo/files/config.sh --yes ddev-settings on >/dev/null 2>&1' + E 'sudo bash /home/dev/repo/files/config.sh --yes handover /var/www/vhosts/dd12-proj >/dev/null 2>&1' + E 'git -C /var/www/vhosts/dd12-proj checkout -q -- .' + check "DD12: after dev-owned handover the same switch succeeds" \ + DEVSH 'cd /var/www/vhosts/dd12-proj && git checkout -q feature/top-level && test ! -f LICENSE && git checkout -q main && test -z "$(git status --porcelain)"' + + # (d) agent-side git on a dev-owned clone (#17: safe.directory). + check "DD12: agent git log works on the dev-owned tree (#17)" \ + OC 'git -C /var/www/vhosts/dd12-proj log --oneline -1' + + # (e) pull-while-running: git replaces tracked .ddev content, ddev + # must survive it (restart, not a broken tree). + if OC_DD12 'ddev start >/tmp/dd12-start.log 2>&1'; then + DEVSH 'cd /var/www/vhosts/dd12-proj && git checkout -q feature/ddev-tree && git checkout -q main' \ + && _dd12_sw=0 || _dd12_sw=1 + check "DD12: checkout inside the running project's .ddev works" \ + test "$_dd12_sw" = 0 + if DEVSH 'cd /var/www/vhosts/dd12-proj && ddev restart >/tmp/dd12-restart.log 2>&1'; then + check "DD12: ddev survives git's .ddev file replacement (restart)" \ + E 'grep -qE "Successfully started|Restarted" /tmp/dd12-restart.log' + else + echo " ${RED}FAIL${NC} DD12: restart after .ddev checkout failed:"; failures=$((failures + 1)) + E 'sed "s/\x1b\[[0-9;]*m//g" /tmp/dd12-restart.log 2>/dev/null | tail -10' || true + failures=$((failures + 1)) + fi + OC_DD12 'ddev delete -Oy >/dev/null 2>&1' || true + else + echo " ${YELLOW}SKIP${NC} DD12: project start failed" + E 'sed "s/\x1b\[[0-9;]*m//g" /tmp/dd12-start.log 2>/dev/null | tail -10' || true + skip "dd12 start failed" + fi + else + echo " ${YELLOW}SKIP${NC} DD12: bare-origin generator failed" + E 'cat /tmp/dd12-gen.log 2>/dev/null' || true + skip "bare origin missing" + fi +fi + +echo "" +echo "--- DD13. ddev config on a fresh empty dir (2026-08-22 burn-in) ---" +E 'sudo -u dev mkdir -p /var/www/vhosts/dd13-proj' +DEVSH 'cd /var/www/vhosts/dd13-proj && ddev config --php-version 8.3 --project-type=typo3 --docroot=public --webserver-type=apache-fpm --project-tld local >/tmp/dd13-config.log 2>&1' +check "DD13: config completes despite the settings chmod warning" \ + E 'grep -q "Configuration complete" /tmp/dd13-config.log' +check "DD13: .ddev opencode-owned (ddev-created)" \ + E 'test "$(stat -c %U /var/www/vhosts/dd13-proj/.ddev)" = opencode' +check "DD13: TRIPWIRE .ddev/config.yaml NOT group-writable (Finding 1 — flips when fixed)" \ + E 'test $(( $(stat -c %a /var/www/vhosts/dd13-proj/.ddev/config.yaml) & 0020 )) -eq 0' +check_fail "DD13: TRIPWIRE dev cannot edit .ddev/config.yaml (Finding 1 — flips when fixed)" \ + DEVSH 'cd /var/www/vhosts/dd13-proj && printf "\n" >> .ddev/config.yaml' +DEVSH 'cd /var/www/vhosts/dd13-proj && ddev start >/tmp/dd13-start1.log 2>&1' && _dd13_first=0 || _dd13_first=1 +check "DD13: first start prints the bootstrap hint (hook promise)" \ + E 'grep -q "hint: fresh typo3 clone" /tmp/dd13-start1.log' +check "DD13: TRIPWIRE first start fails EPERM until handover (burn-in flow)" \ + test "$_dd13_first" = 1 +E 'sudo bash /home/dev/repo/files/config.sh --yes handover /var/www/vhosts/dd13-proj >/tmp/dd13-handover.log 2>&1' +check "DD13: handover writes the dev-owned flag (durable fix)" \ + E 'grep -q "^disable_settings_management: true" /var/www/vhosts/dd13-proj/.ddev/config.yaml' +check "DD13: dev can edit .ddev/config.yaml after handover" \ + DEVSH 'cd /var/www/vhosts/dd13-proj && printf "# dd13 edit\n" >> .ddev/config.yaml' +check "DD13: root stays dev-owned (dev-owned mode)" \ + E 'test "$(stat -c %U /var/www/vhosts/dd13-proj)" = dev' +if OC_DD13 'ddev start >/tmp/dd13-start2.log 2>&1'; then + check "DD13: start succeeds after handover (burn-in end state)" \ + E 'grep -q "Successfully started" /tmp/dd13-start2.log' + + echo "" + echo "--- DD14. ddev composer create-project (exit-23 burn-in finding) ---" + if [ "${E2E_DDEV_SKIP_CREATE:-0}" != "1" ]; then + # Root cause (diagnosed 2026-08-22): create-project moves the created + # project with `rsync -rltgopD` — `-o/-g` chowns the composer root, + # which only works when the ddev user OWNS it. In dev-owned mode the + # root belongs to dev, so as opencode rsync dies with + # "chown \"/var/www/html/.\" failed: Operation not permitted" (23). + # Handover mode (root opencode-owned) works. Both sides asserted. + check_fail "DD14: TRIPWIRE create-project fails in dev-owned mode (rsync chown EPERM — Finding 2)" \ + OC_DD13 'ddev composer create-project "typo3/cms-base-distribution:^14" >/tmp/dd14-devowned.log 2>&1' + OC_DD13 'ddev exec rm -rf /tmp/cp-* >/dev/null 2>&1' || true + E 'sudo bash /home/dev/repo/files/config.sh --yes ddev-settings off >/dev/null 2>&1' + E 'sudo sed -i "/^disable_settings_management:/d" /var/www/vhosts/dd13-proj/.ddev/config.yaml' + E 'sudo bash /home/dev/repo/files/config.sh --yes handover /var/www/vhosts/dd13-proj >/dev/null 2>&1' + check "DD14: handover mode: root handed to opencode (create-project precondition)" \ + E 'test "$(stat -c %U /var/www/vhosts/dd13-proj)" = opencode' + # The failed dev-owned attempt left partial files behind; ddev only + # allows [AdditionalConfiguration.php LocalConfiguration.php public] + # beside .ddev for create-project (checkForComposerCreateAllowedPaths) + # — the exact recovery situation from the burn-in. Clean up. + E 'sudo sh -c "cd /var/www/vhosts/dd13-proj && find . -mindepth 1 -maxdepth 1 ! -name .ddev -exec rm -rf {} +"' + if OC_DD13 'ddev composer create-project "typo3/cms-base-distribution:^14" >/tmp/dd14.log 2>&1'; then + check "DD14: handover mode: create-project exits 0" true + check "DD14: composer.lock + vendor + public/index.php landed completely" \ + E 'test -f /var/www/vhosts/dd13-proj/composer.lock && test -f /var/www/vhosts/dd13-proj/public/index.php' + else + echo " ${RED}FAIL${NC} DD14: create-project failed even with opencode-owned root — full tail:" + E 'sed "s/\x1b\[[0-9;]*m//g" /tmp/dd14.log | tail -25' || true + failures=$((failures + 1)) + fi + else + skip "DD14 skipped (E2E_DDEV_SKIP_CREATE=1)" + fi + OC_DD13 'ddev delete -Oy >/dev/null 2>&1' || true +else + echo " ${YELLOW}SKIP${NC} DD13: second start failed — output:" + E 'sed "s/\x1b\[[0-9;]*m//g" /tmp/dd13-start2.log 2>/dev/null | tail -15' || true + skip "dd13 second start failed" +fi + +# Leave the kit in its default state for subsequent runs. +E 'sudo bash /home/dev/repo/files/config.sh --yes ddev-settings on >/dev/null 2>&1' || true + +e2e_finish diff --git a/tests/test-workflows.sh b/tests/test-workflows.sh index a9337d9..b789725 100755 --- a/tests/test-workflows.sh +++ b/tests/test-workflows.sh @@ -1,6 +1,6 @@ #!/bin/sh # Unit tests for CI workflow consistency (AGENTS.md rule: every executable -# goes into the chmod +x list of BOTH workflow files): +# goes into the chmod +x list of ALL workflow files that run it): # 1. every ./path a workflow chmods must exist in the repo (renames and # typos otherwise fail silently — CI chmods a ghost and loses the bit) # 2. every executable CI needs (unit tests, e2e scripts, check-host, @@ -19,6 +19,7 @@ NC='\033[0m' REPO="$(cd "$(dirname "$0")/.." && pwd)" WF_TEST="$REPO/.github/workflows/test.yml" WF_E2E="$REPO/.github/workflows/e2e.yml" +WF_DDEV_E2E="$REPO/.github/workflows/e2e-ddev.yml" failures=0 passed=0 @@ -41,7 +42,7 @@ trap 'rm -f "$TMP_TOKENS"' EXIT INT TERM # --- 1. every chmodded path exists ------------------------------------------ -for wf in "$WF_TEST" "$WF_E2E"; do +for wf in "$WF_TEST" "$WF_E2E" "$WF_DDEV_E2E"; do name="${wf##*/}" chmod_tokens "$wf" > "$TMP_TOKENS" ghosts="" @@ -70,6 +71,7 @@ for f in "$REPO"/tests/test-*.sh \ "$REPO"/tests/check-host.sh \ "$REPO"/tests/e2e/run.sh \ "$REPO"/tests/e2e/run-docker-rootless.sh \ + "$REPO"/tests/e2e/run-ddev.sh \ "$REPO"/tests/e2e/lib.sh; do required="$required ./${f#"$REPO"/}" done @@ -81,7 +83,7 @@ for f in $(find "$REPO/files" -type f | sort); do required="$required ./${f#"$REPO"/}" done -for wf in "$WF_TEST" "$WF_E2E"; do +for wf in "$WF_TEST" "$WF_E2E" "$WF_DDEV_E2E"; do name="${wf##*/}" chmod_tokens "$wf" > "$TMP_TOKENS" missing=""