diff --git a/README.en.md b/README.en.md
new file mode 100644
index 0000000..d652e74
--- /dev/null
+++ b/README.en.md
@@ -0,0 +1,117 @@
+# spip-serverless
+
+[Français](README.md) · **English**
+
+Run [SPIP](https://www.spip.net/) — the French free-software CMS — **serverless on AWS**:
+PHP on AWS Lambda (via [Bref](https://bref.sh/)), an Aurora DSQL database, sessions in
+DynamoDB, static assets on S3, all fronted by CloudFront. No servers to patch, scale to
+zero, pay per request.
+
+This repo is a **generic, reusable platform** — bring your own content, plugins and
+skeletons. Default region: **eu-west-3** (Paris) 🇫🇷.
+
+## Architecture
+
+```
+ ┌────────────┐
+ visitor ──────▶│ CloudFront │
+ └─────┬──────┘
+ /IMG,/plugins-dist,… everything else
+ │ │
+ ┌─────▼─────┐ ┌─────▼───────────┐
+ │ S3 assets │ │ API Gateway │
+ └───────────┘ │ (proxy, X-Ray) │
+ └─────┬───────────┘
+ │ AWS_PROXY
+ ┌─────▼───────────┐ IAM-auth token
+ │ Lambda (SPIP) │──────────────▶ Aurora DSQL
+ │ Bref, PHP 8.5 │──────────────▶ DynamoDB (sessions)
+ └─────┬───────────┘──────────────▶ S3 (media, read/write)
+ │
+ SSM (spip keys), CloudWatch, X-Ray
+```
+
+- **`spip/`** — the SPIP runtime image. SPIP core is **fetched at build time** (pinned in
+ `spip/SPIP_VERSION`), not vendored. Overlays adapt core to Lambda/DSQL; two custom
+ plugins (`s3upload`, `sessions_dynamodb`) and `logs_stderr` are baked in.
+- **`iac/spip/static/`** — Terraform: DSQL cluster, S3 bucket, ECR repo, DynamoDB table,
+ SSM key parameter.
+- **`iac/spip/app/`** — Terraform: Lambda + alias, API Gateway (transparent proxy),
+ CloudFront (+ optional custom domain via ACM/Route53).
+- **`docs/`** — how everything fits together.
+
+## Prerequisites
+
+- An AWS account with permission to create the resources above (Aurora DSQL is available
+ in eu-west-3 among others).
+- **Docker** installed and **running** (buildx; the image is `linux/arm64`).
+- **Terraform ≥ 1.14** (an older 1.5.x on your PATH will fail the `required_version` check).
+- **AWS CLI v2** with an **active session** for your profile — `aws sso login --profile
`
+ or exported credentials. The principal must be able to create DSQL, S3, ECR, DynamoDB,
+ SSM, Lambda, API Gateway, CloudFront and IAM roles.
+- **make**, `git`, `curl`, `unzip`.
+- Aurora DSQL, Bedrock etc. beyond the above are **not** used by the bare platform.
+
+## Quick start
+
+```bash
+# 0. pick an environment name + AWS profile
+export ENV=test AWS_PROFILE=your-profile
+
+# 1. create the S3 bucket that will hold Terraform state (once per account)
+aws s3 mb s3://your-tfstate-bucket --region eu-west-3
+
+# 2. create per-env config from the template
+for s in static app; do cp -r iac/spip/$s/var/example iac/spip/$s/var/$ENV; done
+# then edit iac/spip/*/var/$ENV/{values.tfvars,backend.tfbackend}:
+# set the state bucket (both backend.tfbackend + app's static_state_bucket),
+# region, and optionally a custom domain.
+
+# 3. base infra (DSQL, S3, ECR, DynamoDB, SSM)
+make deploy-static ENV=$ENV
+
+# 4. fill the SPIP key material in SSM → see docs/db-bootstrap.md
+
+# 5. build + push image, sync assets, deploy the app stack
+make deploy ENV=$ENV
+
+# 6. create the schema + admin author → see docs/db-bootstrap.md
+```
+
+`make deploy` prints the CloudFront URL; the admin is at `/ecrire`. Verified end-to-end
+on a fresh AWS account (eu-west-3): CloudFront serves the public site and `/spip.php?page=login`.
+
+## Working with SPIP core locally
+
+Core is git-ignored. To index it in your IDE or run it with Apache locally:
+
+```bash
+make fetch-spip # downloads the pinned version into spip/src/
+make run-local # SPIP on http://localhost:8080 (Apache)
+```
+
+## Common tasks
+
+| Task | Doc |
+|---|---|
+| Add a plugin (third-party or custom) | [docs/plugins.md](docs/plugins.md) |
+| Upgrade SPIP core | [docs/spip-upgrade.md](docs/spip-upgrade.md) |
+| Add / configure an environment | [docs/environments.md](docs/environments.md) |
+| Initialise the database + admin | [docs/db-bootstrap.md](docs/db-bootstrap.md) |
+| Reset an admin password | [docs/spip-passwords.md](docs/spip-passwords.md) |
+| DSQL, sessions, S3, secrets, tracing, logging, cron | `docs/*.md` |
+
+## Security notes
+
+- API Gateway is a **transparent proxy** — it does not authenticate. **SPIP** enforces
+ auth for the back-office (`/ecrire`). If you add your own API routes, add an authorizer
+ in `iac/spip/app/` yourself.
+- SPIP secret keys live in SSM (SecureString) and are written back by the Lambda on first
+ boot; the S3 bucket is private (CloudFront OAC only); DSQL uses short-lived IAM auth
+ tokens (no stored DB password).
+- You can turn the public site off (admin/login only) with `spip_public_disabled = true`.
+
+## License
+
+The SPIP core fetched at build time is GPL-3.0 (© the SPIP community). This repo's own
+glue code is provided under the terms in [LICENSE](LICENSE).
diff --git a/README.md b/README.md
index c51e336..2e71f71 100644
--- a/README.md
+++ b/README.md
@@ -1,106 +1,119 @@
# spip-serverless
-Run [SPIP](https://www.spip.net/) — the French free-software CMS — **serverless on AWS**:
-PHP on AWS Lambda (via [Bref](https://bref.sh/)), an Aurora DSQL database, sessions in
-DynamoDB, static assets on S3, all fronted by CloudFront. No servers to patch, scale to
-zero, pay per request.
+**Français** · [English](README.en.md)
-This repo is a **generic, reusable platform** — bring your own content, plugins and
-skeletons. Default region: **eu-west-3** (Paris) 🇫🇷.
+Faire tourner [SPIP](https://www.spip.net/) — le CMS libre français — **serverless sur AWS** :
+PHP sur AWS Lambda (via [Bref](https://bref.sh/)), une base Aurora DSQL, les sessions dans
+DynamoDB, les assets statiques sur S3, le tout derrière CloudFront. Aucun serveur à patcher,
+scale-to-zero, facturation à l'usage.
+
+Ce dépôt est une **plateforme générique et réutilisable** — apportez votre contenu, vos
+plugins et vos squelettes. Région par défaut : **eu-west-3** (Paris) 🇫🇷.
## Architecture
```
┌────────────┐
- visitor ──────▶│ CloudFront │
+ visiteur ─────▶│ CloudFront │
└─────┬──────┘
- /IMG,/plugins-dist,… everything else
- │ │
- ┌─────▼─────┐ ┌─────▼───────────┐
- │ S3 assets │ │ API Gateway │
+ /IMG,/plugins-dist,… tout le reste
+ │ │
+ ┌─────▼─────┐ ┌─────────▼───────┐
+ │ assets S3 │ │ API Gateway │
└───────────┘ │ (proxy, X-Ray) │
└─────┬───────────┘
│ AWS_PROXY
- ┌─────▼───────────┐ IAM-auth token
+ ┌─────▼───────────┐ jeton IAM
│ Lambda (SPIP) │──────────────▶ Aurora DSQL
│ Bref, PHP 8.5 │──────────────▶ DynamoDB (sessions)
- └─────┬───────────┘──────────────▶ S3 (media, read/write)
+ └─────┬───────────┘──────────────▶ S3 (média, lecture/écriture)
│
- SSM (spip keys), CloudWatch, X-Ray
+ SSM (clés spip), CloudWatch, X-Ray
```
-- **`spip/`** — the SPIP runtime image. SPIP core is **fetched at build time** (pinned in
- `spip/SPIP_VERSION`), not vendored. Overlays adapt core to Lambda/DSQL; two custom
- plugins (`s3upload`, `sessions_dynamodb`) and `logs_stderr` are baked in.
-- **`iac/spip/static/`** — Terraform: DSQL cluster, S3 bucket, ECR repo, DynamoDB table,
- SSM key parameter.
-- **`iac/spip/app/`** — Terraform: Lambda + alias, API Gateway (transparent proxy),
- CloudFront (+ optional custom domain via ACM/Route53).
-- **`docs/`** — how everything fits together.
-
-## Prerequisites
-
-- An AWS account with permission to create the resources above (Aurora DSQL is available
- in eu-west-3 among others).
-- Docker (buildx, ARM64), Terraform ≥ 1.14, the AWS CLI, and `make`.
-- Aurora DSQL, Bedrock etc. are **not** used by the bare platform — only DSQL, S3,
- DynamoDB, Lambda, API Gateway, CloudFront, ECR, SSM.
-
-## Quick start
+- **`spip/`** — l'image runtime SPIP. Le cœur SPIP est **récupéré au build** (épinglé dans
+ `spip/SPIP_VERSION`), non vendorisé. Des overlays adaptent le cœur à Lambda/DSQL ; deux
+ plugins maison (`s3upload`, `sessions_dynamodb`) et `logs_stderr` sont embarqués.
+- **`iac/spip/static/`** — Terraform : cluster DSQL, bucket S3, dépôt ECR, table DynamoDB,
+ paramètre SSM (clés).
+- **`iac/spip/app/`** — Terraform : Lambda + alias, API Gateway (proxy transparent),
+ CloudFront (+ domaine custom optionnel via ACM/Route53).
+- **`docs/`** — comment tout s'articule.
+
+## Prérequis
+
+- Un compte AWS autorisé à créer les ressources ci-dessus (Aurora DSQL est disponible en
+ eu-west-3, entre autres).
+- **Docker** installé et **démarré** (buildx ; l'image est `linux/arm64`).
+- **Terraform ≥ 1.14** (un vieux 1.5.x dans le PATH échoue sur le contrôle `required_version`).
+- **AWS CLI v2** avec une **session active** pour votre profil — `aws sso login --profile
`
+ ou des credentials exportés. Le principal doit pouvoir créer DSQL, S3, ECR, DynamoDB, SSM,
+ Lambda, API Gateway, CloudFront et des rôles IAM.
+- **make**, `git`, `curl`, `unzip`.
+- Au-delà, Aurora DSQL, Bedrock, etc. ne sont **pas** utilisés par la plateforme nue.
+
+## Démarrage rapide
```bash
-# 0. pick an environment name + AWS profile
-export ENV=test AWS_PROFILE=your-profile
+# 0. choisir un nom d'environnement + un profil AWS
+export ENV=test AWS_PROFILE=votre-profil
+
+# 1. créer le bucket S3 qui contiendra l'état Terraform (une fois par compte)
+aws s3 mb s3://votre-bucket-tfstate --region eu-west-3
-# 1. create per-env config from the template
+# 2. créer la config par env depuis le modèle
for s in static app; do cp -r iac/spip/$s/var/example iac/spip/$s/var/$ENV; done
-# then edit iac/spip/*/var/$ENV/{values.tfvars,backend.tfbackend}
-# (state bucket, region, optional domain)
+# puis éditer iac/spip/*/var/$ENV/{values.tfvars,backend.tfbackend} :
+# renseigner le bucket d'état (les deux backend.tfbackend + static_state_bucket de app),
+# la région, et éventuellement un domaine custom.
-# 2. base infra (DSQL, S3, ECR, DynamoDB, SSM)
+# 3. infra de base (DSQL, S3, ECR, DynamoDB, SSM)
make deploy-static ENV=$ENV
-# 3. fill the SPIP key material in SSM → see docs/db-bootstrap.md
+# 4. remplir les clés SPIP dans SSM → voir docs/db-bootstrap.md
-# 4. build + push image, sync assets, deploy the app stack
+# 5. build + push de l'image, sync des assets, déploiement de la stack app
make deploy ENV=$ENV
-# 5. create the schema + admin author → see docs/db-bootstrap.md
+# 6. créer le schéma + l'auteur admin → voir docs/db-bootstrap.md
```
-`make deploy` prints the CloudFront URL; the admin is at `/ecrire`.
+`make deploy` affiche l'URL CloudFront ; l'admin est sur `/ecrire`. Vérifié de bout en bout
+sur un compte AWS vierge (eu-west-3) : CloudFront sert le site public et `/spip.php?page=login`.
-## Working with SPIP core locally
+## Travailler avec le cœur SPIP en local
-Core is git-ignored. To index it in your IDE or run it with Apache locally:
+Le cœur est git-ignoré. Pour l'indexer dans votre IDE ou le lancer avec Apache en local :
```bash
-make fetch-spip # downloads the pinned version into spip/src/
-make run-local # SPIP on http://localhost:8080 (Apache)
+make fetch-spip # télécharge la version épinglée dans spip/src/
+make run-local # SPIP sur http://localhost:8080 (Apache)
```
-## Common tasks
+## Tâches courantes
-| Task | Doc |
+| Tâche | Doc |
|---|---|
-| Add a plugin (third-party or custom) | [docs/plugins.md](docs/plugins.md) |
-| Upgrade SPIP core | [docs/spip-upgrade.md](docs/spip-upgrade.md) |
-| Add / configure an environment | [docs/environments.md](docs/environments.md) |
-| Initialise the database + admin | [docs/db-bootstrap.md](docs/db-bootstrap.md) |
-| Reset an admin password | [docs/spip-passwords.md](docs/spip-passwords.md) |
-| DSQL, sessions, S3, secrets, tracing, logging, cron | `docs/*.md` |
-
-## Security notes
-
-- API Gateway is a **transparent proxy** — it does not authenticate. **SPIP** enforces
- auth for the back-office (`/ecrire`). If you add your own API routes, add an authorizer
- in `iac/spip/app/` yourself.
-- SPIP secret keys live in SSM (SecureString) and are written back by the Lambda on first
- boot; the S3 bucket is private (CloudFront OAC only); DSQL uses short-lived IAM auth
- tokens (no stored DB password).
-- You can turn the public site off (admin/login only) with `spip_public_disabled = true`.
-
-## License
-
-The SPIP core fetched at build time is GPL-3.0 (© the SPIP community). This repo's own
-glue code is provided under the terms in [LICENSE](LICENSE).
+| Ajouter un plugin (tiers ou maison) | [docs/plugins.md](docs/plugins.md) |
+| Mettre à jour le cœur SPIP | [docs/spip-upgrade.md](docs/spip-upgrade.md) |
+| Ajouter / configurer un environnement | [docs/environments.md](docs/environments.md) |
+| Initialiser la base + l'admin | [docs/db-bootstrap.md](docs/db-bootstrap.md) |
+| Réinitialiser un mot de passe admin | [docs/spip-passwords.md](docs/spip-passwords.md) |
+| DSQL, sessions, S3, secrets, tracing, logs, cron | `docs/*.md` |
+
+> Les docs détaillées sous `docs/*.md` sont en anglais pour l'instant.
+
+## Notes de sécurité
+
+- L'API Gateway est un **proxy transparent** — elle n'authentifie pas. C'est **SPIP** qui
+ gère l'auth du back-office (`/ecrire`). Si vous ajoutez vos propres routes API, ajoutez
+ vous-même un authorizer dans `iac/spip/app/`.
+- Les clés secrètes SPIP vivent dans SSM (SecureString) et sont réécrites par le Lambda au
+ premier boot ; le bucket S3 est privé (OAC CloudFront uniquement) ; DSQL utilise des jetons
+ IAM à courte durée (aucun mot de passe DB stocké).
+- Vous pouvez couper le site public (admin/login seulement) avec `spip_public_disabled = true`.
+
+## Licence
+
+Le cœur SPIP récupéré au build est en GPL-3.0 (© la communauté SPIP). Le code de glue propre
+à ce dépôt est fourni selon les termes du fichier [LICENSE](LICENSE).
diff --git a/docs/db-bootstrap.md b/docs/db-bootstrap.md
index e729b8a..6af969f 100644
--- a/docs/db-bootstrap.md
+++ b/docs/db-bootstrap.md
@@ -43,12 +43,13 @@ eval "$(AWS_PROFILE=$PROFILE aws configure export-credentials --format env)"
docker run --rm \
-e SPIP_DSQL_CLUSTER=$CLUSTER \
-e SPIP_TABLE_PREFIX=spip \
+ -e SPIP_PUBLIC_URL=https:// \
-e SPIP_CLES="$SPIP_CLES" \
-e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_SESSION_TOKEN \
-e AWS_REGION=$REGION \
--entrypoint php \
.dkr.ecr.$REGION.amazonaws.com/spip-serverless: \
- php -d auto_prepend_file= /var/task/scripts/bootstrap-db.php \
+ -d auto_prepend_file= /var/task/scripts/bootstrap-db.php \
--admin-login=admin --admin-email=you@example.org --admin-pass=''
```
diff --git a/docs/environments.md b/docs/environments.md
index c148a7c..ea9c531 100644
--- a/docs/environments.md
+++ b/docs/environments.md
@@ -34,8 +34,17 @@ Fill in:
- the state S3 bucket in both `backend.tfbackend` files (create it once per account)
- `static_state_bucket` / `static_state_region` in the app stack (point at the static
stack's state)
-- optionally `domain_name` + `hosted_zone_name` for a custom domain (Route53 zone must
- exist in the same account)
+- optionally `domain_name` + `hosted_zone_name` for a custom domain. Setting
+ `domain_name` (via Terraform, not the CloudFront console) drives everything from one
+ variable: the CloudFront alias, a us-east-1 ACM certificate (DNS-validated against the
+ `hosted_zone_name` Route53 zone, which must exist **in the same account**), the Route53
+ A-record, **and** the Lambda's `SPIP_PUBLIC_URL` (so SPIP builds its absolute links on
+ that domain). Leave both empty to use the default `*.cloudfront.net` domain.
+
+ > If you change `domain_name` on an already-bootstrapped environment, the runtime host
+ > (prepend.php / `SPIP_PUBLIC_URL`) follows automatically, but the stored `adresse_site`
+ > meta does not — re-run the bootstrap (or `UPDATE spip_meta SET valeur='https://'
+ > WHERE nom='adresse_site'`). See `docs/db-bootstrap.md`.
## First bring-up (per environment)
diff --git a/docs/plugins.md b/docs/plugins.md
index 819aabe..61ac7ae 100644
--- a/docs/plugins.md
+++ b/docs/plugins.md
@@ -34,6 +34,25 @@ Everything placed under `plugins-dist/` is scanned and activated by SPIP on cold
SVP registers it in the DB (`spip_paquets` with `actif='oui'`) and wires the
`paquet.xml` pipeline declarations automatically.
+### `plugins/` vs `plugins-dist/` — why the distinction disappears at runtime
+
+In a **normal** SPIP install the two directories mean different things:
+
+- `plugins-dist/` — plugins **shipped with the SPIP core**, always active, no activation step.
+- `plugins/` — plugins **you added**, which must be **activated** (registered in the DB via
+ SVP, normally by clicking "activate" in the private area).
+
+That activation step is interactive and writes to disk — neither is possible on an
+**immutable, read-only Lambda** that scales to zero. So this build deliberately **copies our
+own plugins into `plugins-dist/`** (see the Dockerfile), where they are always active from
+the first cold start, with no manual step.
+
+Consequently, at **runtime** the `plugins/` vs `plugins-dist/` distinction no longer exists —
+everything lives in `/var/task/plugins-dist/`. In the **repo**, the separate source folders
+(`spip/plugins/` = ours, `spip/plugins-vendor/` = third-party) are kept only for
+organisation and provenance; there is no `spip/plugins-dist/` (that name belongs to the
+fetched SPIP core).
+
## Add a THIRD-PARTY plugin
1. Download the plugin into `spip/plugins-vendor//` (it must have a valid
diff --git a/docs/spip-upgrade.md b/docs/spip-upgrade.md
index 1de202a..1f8e62a 100644
--- a/docs/spip-upgrade.md
+++ b/docs/spip-upgrade.md
@@ -4,6 +4,20 @@ SPIP core is **not vendored** in this repo. It is fetched at build time from the
archive (`files.spip.net`) at a single pinned version. Upgrading SPIP is therefore just:
**bump the pinned version, rebuild, test on a non-prod environment, promote.**
+## Repo side vs online side (the mental model)
+
+The image is **immutable and stateless**. Two clean halves:
+
+| You change it in the REPO (→ build → deploy) | It happens ONLINE, automatically, in DSQL |
+|---|---|
+| SPIP version (`spip/SPIP_VERSION`), plugins, overlays, config | The SPIP/plugin **DB schema migration** replays on the **first authenticated `/ecrire` visit** after deploy (if `spip_version_base` bumped) — no manual DB step |
+| Rebuild the Docker image, `make deploy` / CI | Your **content** (articles, media, users) stays in DSQL, untouched by a deploy |
+
+So: **all code/version changes are repo → build → deploy; all data/migration changes are
+runtime**, triggered by the first admin request (same cold-start mechanism as the initial
+plugin migrations — see `docs/db-bootstrap.md`). You never run a DB migration by hand for
+an upgrade; you deploy the image and open the admin once.
+
## Where the version is pinned
One source of truth: **`spip/SPIP_VERSION`** (e.g. `4.4.22`).
@@ -35,7 +49,7 @@ It is consumed by:
Our customisations live **outside** SPIP core, so a core upgrade never touches them:
- `spip/overlay/**` — files that override core at build time (connect.php, mes_options*,
- install.php, dsql.php, SpipCles.php, prepend.php, router.php)
+ install.php, dsql.php, prepend.php, router.php)
- `spip/plugins/**`, `spip/plugins-vendor/**` — our + third-party plugins
- `spip/scripts/patch-documents.php` — the S3 patch applied to `ecrire/inc/documents.php`
diff --git a/iac/spip/app/cloudfront.tf b/iac/spip/app/cloudfront.tf
index 21d1e48..4d6d82e 100644
--- a/iac/spip/app/cloudfront.tf
+++ b/iac/spip/app/cloudfront.tf
@@ -77,7 +77,7 @@ resource "aws_cloudfront_distribution" "spip" {
cached_methods = ["GET", "HEAD"]
target_origin_id = "apigw"
cache_policy_id = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad" # Managed-CachingDisabled
- origin_request_policy_id = "216adef6-5c7f-47e4-b989-5492eafa07d3" # Managed-AllViewer
+ origin_request_policy_id = "b689b0a8-53d0-40ab-baf2-68738e2966ac" # Managed-AllViewerExceptHostHeader
viewer_protocol_policy = "redirect-to-https"
}
diff --git a/iac/spip/app/cron.tf b/iac/spip/app/cron.tf
new file mode 100644
index 0000000..88e3737
--- /dev/null
+++ b/iac/spip/app/cron.tf
@@ -0,0 +1,39 @@
+# EventBridge rule that runs SPIP's cron/queue every 5 minutes.
+#
+# On Lambda, SPIP's queue is disabled on normal web requests (see prepend.php,
+# _DEBUG_BLOCK_QUEUE) so page loads stay fast. This rule invokes the Lambda with a
+# synthetic API Gateway event for `/spip.php?action=cron`, which is the one path that
+# runs the queue — so scheduled jobs fire deterministically, independent of traffic.
+resource "aws_cloudwatch_event_rule" "spip_cron" {
+ name = "${local.app_name}-cron"
+ schedule_expression = "rate(5 minutes)"
+ state = "ENABLED"
+}
+
+resource "aws_cloudwatch_event_target" "spip_cron" {
+ rule = aws_cloudwatch_event_rule.spip_cron.name
+ target_id = "spip-cron"
+ arn = aws_lambda_alias.spip_live.arn
+
+ input = jsonencode({
+ version = "2.0"
+ routeKey = "GET /spip.php"
+ rawPath = "/spip.php"
+ rawQueryString = "action=cron"
+ headers = { "x-spip-cron" = "1" }
+ queryStringParameters = { action = "cron" }
+ requestContext = {
+ http = { method = "GET", path = "/spip.php" }
+ }
+ isBase64Encoded = false
+ })
+}
+
+resource "aws_lambda_permission" "eventbridge_cron" {
+ statement_id = "AllowEventBridgeCron"
+ action = "lambda:InvokeFunction"
+ function_name = aws_lambda_function.spip.function_name
+ qualifier = aws_lambda_alias.spip_live.name
+ principal = "events.amazonaws.com"
+ source_arn = aws_cloudwatch_event_rule.spip_cron.arn
+}
diff --git a/iac/spip/app/locals.tf b/iac/spip/app/locals.tf
index fac8d63..63e4028 100644
--- a/iac/spip/app/locals.tf
+++ b/iac/spip/app/locals.tf
@@ -56,6 +56,10 @@ locals {
CF_DISTRIBUTION_ID = aws_cloudfront_distribution.spip.id
APP_NAME = local.app_name
+ # Public base URL SPIP builds its absolute links from (see prepend.php). Uses the
+ # custom domain when set, else the CloudFront default domain.
+ SPIP_PUBLIC_URL = "https://${var.domain_name != "" ? var.domain_name : aws_cloudfront_distribution.spip.domain_name}"
+
# When "1", the public SPIP site (skeletons) returns a 404 "off" page.
# API/admin paths stay available.
SPIP_PUBLIC_DISABLED = var.spip_public_disabled ? "1" : "0"
diff --git a/spip/Dockerfile b/spip/Dockerfile
index 88a0013..17b28da 100644
--- a/spip/Dockerfile
+++ b/spip/Dockerfile
@@ -128,7 +128,6 @@ COPY --from=vendors /app/ /var/task/
# Overlays that replace core files (DSQL connection, install wizard, options, router).
COPY spip/overlay/ecrire/exec/install.php /var/task/ecrire/exec/install.php
COPY spip/overlay/ecrire/req/dsql.php /var/task/ecrire/req/dsql.php
-COPY spip/overlay/ecrire/src/Chiffrer/SpipCles.php /var/task/ecrire/src/Chiffrer/SpipCles.php
COPY spip/overlay/config/connect.php /var/task/config/connect.php
COPY spip/overlay/config/mes_options_lambda.php /var/task/config/mes_options.php
COPY spip/overlay/php/prepend.php /var/task/php/prepend.php
diff --git a/spip/overlay/ecrire/src/Chiffrer/SpipCles.php b/spip/overlay/ecrire/src/Chiffrer/SpipCles.php
deleted file mode 100644
index f45b9b8..0000000
--- a/spip/overlay/ecrire/src/Chiffrer/SpipCles.php
+++ /dev/null
@@ -1,155 +0,0 @@
-getSecretSite();
- }
-
- private function __construct(string $file = '') {
- if ($file) {
- $this->file = $file;
- }
- $this->cles = new Cles($this->read());
- }
-
- public function getSecretSite(bool $autoInit = true): ?string {
- $key = $this->getKey('secret_du_site', $autoInit);
- $meta = $this->getMetaKey('secret_du_site', $autoInit);
- return $key ^ $meta;
- }
-
- public function getSecretAuth(bool $autoInit = false): ?string {
- return $this->getKey('secret_des_auth', $autoInit);
- }
-
- public function save(): bool {
- $ok = ecrire_fichier_securise($this->file, $this->cles->toJson());
- // Persist to SSM so keys survive cold starts
- if ($ok) {
- $this->saveToSsm();
- }
- return $ok;
- }
-
- private function saveToSsm(): void {
- $param = getenv('SPIP_CLES_SSM_NAME');
- if (!$param) {
- $param = '/spip-serverless/' . (getenv('SPIP_ENV') ?: 'test') . '/spip/cles';
- }
- try {
- require_once '/var/task/vendor/autoload.php';
- $ssm = new \Aws\Ssm\SsmClient(['region' => getenv('AWS_REGION') ?: 'us-east-1', 'version' => 'latest']);
- $ssm->putParameter([
- 'Name' => $param,
- 'Value' => $this->cles->toJson(),
- 'Type' => 'SecureString',
- 'Overwrite' => true,
- ]);
- } catch (\Throwable $e) {
- spip_log('SSM save failed: ' . $e->getMessage(), 'chiffrer' . _LOG_ERREUR);
- }
- }
-
- public function backup(
- #[\SensitiveParameter]
- string $withKey
- ): string {
- if (count($this->cles)) {
- return Chiffrement::chiffrer($this->cles->toJson(), $withKey);
- }
- return '';
- }
-
- public function restore(
- string $backup,
- #[\SensitiveParameter]
- string $password_clair,
- #[\SensitiveParameter]
- string $password_hash,
- int $id_auteur
- ): bool {
- if (empty($backup)) {
- return false;
- }
- $sauvegarde = Chiffrement::dechiffrer($backup, $password_clair);
- $json = json_decode($sauvegarde, true);
- if (!$json) {
- return false;
- }
- $cles_potentielles = array_map('base64_decode', $json);
- if (!empty($cles_potentielles['secret_des_auth'])) {
- if (!Password::verifier($password_clair, $password_hash, $cles_potentielles['secret_des_auth'])) {
- spip_log("Restauration de la cle `secret_des_auth` par id_auteur $id_auteur erronnee, on ignore", 'chiffrer' . _LOG_INFO_IMPORTANTE);
- unset($cles_potentielles['secret_des_auth']);
- }
- }
- $restauration = false;
- foreach ($cles_potentielles as $name => $key) {
- if (!$this->cles->has($name)) {
- $this->cles->set($name, $key);
- spip_log("Restauration de la cle $name par id_auteur $id_auteur", 'chiffrer' . _LOG_INFO_IMPORTANTE);
- $restauration = true;
- }
- }
- return $restauration;
- }
-
- private function getKey(string $name, bool $autoInit): ?string {
- if ($this->cles->has($name)) {
- return $this->cles->get($name);
- }
- if ($autoInit) {
- $this->cles->generate($name);
- if ($this->save()) {
- return $this->cles->get($name);
- }
- spip_log('Echec ecriture du fichier cle ' . $this->file . " ; impossible de generer une cle $name", 'chiffrer' . _LOG_ERREUR);
- $this->cles->delete($name);
- }
- return null;
- }
-
- private function getMetaKey(string $name, bool $autoInit = true): ?string {
- if (!isset($GLOBALS['meta'][$name])) {
- include_spip('base/abstract_sql');
- $GLOBALS['meta'][$name] = sql_getfetsel('valeur', 'spip_meta', 'nom = ' . sql_quote($name, '', 'string'));
- }
- $key = base64_decode($GLOBALS['meta'][$name] ?? '');
- if (strlen($key) === \SODIUM_CRYPTO_SECRETBOX_KEYBYTES) {
- return $key;
- }
- if (!$autoInit) {
- return null;
- }
- $key = Chiffrement::keygen();
- ecrire_meta($name, base64_encode($key), 'non');
- lire_metas();
- return $key;
- }
-
- private function read(): array {
- lire_fichier_securise($this->file, $json);
- if (
- $json
- and $json = \json_decode($json, true)
- and is_array($json)
- ) {
- return array_map('base64_decode', $json);
- }
- return [];
- }
-}
diff --git a/spip/overlay/php/prepend.php b/spip/overlay/php/prepend.php
index 0b3ef77..47d853f 100644
--- a/spip/overlay/php/prepend.php
+++ b/spip/overlay/php/prepend.php
@@ -14,6 +14,25 @@
$_SERVER['DOCUMENT_ROOT'] = '/var/task';
}
+// ── 0b. Pin SPIP's public base URL (host + scheme) ─────────────────────────
+// Behind CloudFront + API Gateway the origin sees the execute-api host (CloudFront
+// strips the viewer Host so API Gateway accepts the request), so SPIP's url_de_base()
+// would build absolute URLs (login "converser", redirects, canonical, emails) on the
+// wrong host, without the stage path. Force the real public host/scheme here so every
+// absolute URL SPIP emits points at the CloudFront (or custom) domain.
+$publicUrl = getenv('SPIP_PUBLIC_URL');
+if ($publicUrl && ($p = parse_url($publicUrl)) && !empty($p['host'])) {
+ $_SERVER['HTTP_HOST'] = $p['host'] . (isset($p['port']) ? ':' . $p['port'] : '');
+ $_SERVER['SERVER_NAME'] = $p['host'];
+ if (($p['scheme'] ?? 'https') === 'https') {
+ $_SERVER['HTTPS'] = 'on';
+ $_SERVER['SERVER_PORT'] = '443';
+ }
+ unset($p);
+}
+unset($publicUrl);
+
+
// ── 1. Create writable dirs ────────────────────────────────────────────────
if (!is_dir('/tmp/spip/cache')) {
@mkdir('/tmp/spip/cache/skel', 0777, true);
diff --git a/spip/scripts/bootstrap-db.php b/spip/scripts/bootstrap-db.php
index 42a392c..f47a212 100644
--- a/spip/scripts/bootstrap-db.php
+++ b/spip/scripts/bootstrap-db.php
@@ -154,6 +154,20 @@ function _out(string $k, string $v): void { echo str_pad($k, 22) . $v . "\n"; }
_out('aleas', $has_alea ? 'already present' : 'renouvelle_alea unavailable');
}
+// ── 2b. Site public URL (adresse_site meta) ──────────────────────────────────
+// The CLI install cannot infer the site's public URL (no HTTP host), so creer_base
+// leaves adresse_site as "http://" → SPIP emits "http:///" for logo/home/canonical
+// links and transactional emails. Take it from SPIP_PUBLIC_URL (same value the Lambda
+// forces as HTTP_HOST at runtime; see prepend.php).
+$publicUrl = rtrim((string) getenv('SPIP_PUBLIC_URL'), '/');
+if ($publicUrl !== '' && function_exists('ecrire_meta')) {
+ ecrire_meta('adresse_site', $publicUrl, 'non');
+ _out('adresse_site', $publicUrl);
+} else {
+ _out('adresse_site', 'skipped (SPIP_PUBLIC_URL unset)');
+}
+unset($publicUrl);
+
// ── 3. Plugin install/upgrade (plugin tables + your plugin migrations) ────────────
// Two steps, mirroring what the first authenticated visit to the private area does:
// a) actualise_plugins_actifs(): refresh the active-plugins list (meta 'plugin')
@@ -173,6 +187,10 @@ function _out(string $k, string $v): void { echo str_pad($k, 22) . $v . "\n"; }
_out('plugin_installes', 'seeded empty (SVP guard)');
}
if (function_exists('plugin_installes_meta')) {
+ // plugin_installes_meta() runs plugin install boxes that call template helpers
+ // like typo()/propre(); load them (not auto-loaded in this CLI context).
+ include_spip('inc/texte');
+ include_spip('inc/filtres');
// plugin_installes_meta() may echo install boxes; capture and drop that output.
ob_start();
plugin_installes_meta();