# Standard `marketing-tenant-provisioning` v1.4

**Owner**: AgileHub MARKETER agent (governance) + TITAN (esecuzione DKIM) + MAESTRO (orchestratore atomico AC30)
**Stato**: adopted (LIVE 2026-04-26 mattina v1.3 — DKIM per-tenant; v1.4 mattina — AC30 pronto in branch, deploy pending trigger)
**Applies to**: `*` (tutti i prodotti suite — TRPG, SUSTAINAI, NIS2, LG231, TAXAI, DFM, MKTG, ALLRISK, WMS, MADEBYCLOUD, AGILEHUB)
**Versione**: 1.4
**MS implementatore**: `nexus-marketing-ms` (porta 4221) + `email-automation-ms` (porta 4004) + OpenDKIM milter Postfix Helsinki + `agilehub-workflow-engine` (porta 4230, blocco AWE `AC30_MarketingTenantProvision`)
**Endpoint base**: `/api/marketing/*`

---

## 1. Scopo

Lo standard definisce il contratto cross-suite tra prodotti consumer e modulo Marketing AgileHub centralizzato. Ogni prodotto (TRPG come pilot) consuma 28 endpoint stabili `/api/marketing/*` per fornire ai propri clienti (consulting firm) un servizio email marketing white-label multi-tenant.

## 2. Multi-tenancy

| Concetto | Identificativo | Source of truth |
|---|---|---|
| Tenant marketing | `tenant_id` (es. `tnt_agile-technology_b4f9a3141333`) | `nexus_marketing_db.tenants` |
| Consulting firm consumer | `consulting_firm_id` (FK logico) | DB del prodotto consumer (es. `trpg.consulting_firms`) |
| `firm_slug` | derivato da `slugify(consulting_firm.name)` | calcolato server-side al provisioning |
| API key per-tenant | `ah_live_*` (prod) / `ah_test_*` (sandbox) | `nexus_marketing_db.api_keys` |

Vincolo: 1:1 tra `tenant_id` e `consulting_firm_id` nel prodotto consumer (nessuna 1:N).

**Naming convention `firm_slug`** (formalizzato v1.1):
- Algoritmo: `slugify(consulting_firm.name)` — lowercase ASCII, separatore `-`, rimozione caratteri non alfanumerici, max 64 char
- Esempi:
  - `"Tremolada Consulting S.r.l."` → `tremolada-consulting`
  - `"Agile Technology s.r.l."` → `agile-technology`
  - `"O'Brien & Co."` → `obrien-co`
- `tenant_id` auto-generato server-side: `tnt_${firm_slug}_${randomId(6)}` (esempio: `tnt_agile-technology_b4f9a3141333`)
- Sottodominio derivato: `{firm_slug}.agile.software` (es. `agile-technology.agile.software`)
- Reply-to default: `noreply@{firm_slug}.agile.software`
- Il consumer NON costruisce mai il `firm_slug` autonomamente: lo riceve in risposta a `POST /admin/tenants` e lo persiste come dato derivato

## 3. Autenticazione

- Header obbligatorio: `X-AgileHub-Key: <api_key_plaintext>`
- Header obbligatorio: `X-AgileHub-API-Version: 1.0`
- Hash SHA-256 della key in DB (`api_keys.key_hash`); plaintext mai persistito leggibile (cifrato AES-256-GCM in `api_keys.key_encrypted`)
- Provisioning admin via endpoint interno `/api/marketing/admin/tenants/:id/api-keys` (header `X-Internal-Key` solo Esperto Agile/installer)

## 4. API versioning policy

- Header request: `X-AgileHub-API-Version: 1.0` (obbligatorio)
- Header response: `X-AgileHub-API-Version: 1.0` (echo)
- Header response: `X-Request-Id: req_<uuid>` (cross-system tracing)
- Deprecation policy: 6 mesi notice (`X-Deprecation-Date`) + 6 mesi sunset (`X-Sunset-Date`) prima rimozione versione precedente
- Canary opt-in: `X-API-Canary: true` su feature flag tenant

## 5. Rate limiting

| Endpoint family | Default | Override |
|---|---|---|
| Read endpoints | 60 req/min | – |
| `/contacts/import` | 10 req/min | `RATE_LIMIT_IMPORT` env |
| `/segments/preview` | 20 req/min | `RATE_LIMIT_PREVIEW` env |
| Tracking pixel/click | nessuno (pubblico) | – |

Header response: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`.
Risposta 429 con `retry_after_sec`.

## 6. Error envelope

```json
{
  "error": {
    "code": "STRING_CODE",
    "message": "Human readable",
    "details": { ... opzionale ... }
  },
  "request_id": "req_<uuid>"
}
```

Codici stabili documentati: `MISSING_API_KEY`, `INVALID_API_KEY`, `API_KEY_REVOKED`, `API_KEY_EXPIRED`, `TENANT_NOT_FOUND`, `TENANT_DELETED`, `TENANT_SUSPENDED`, `UNSUPPORTED_API_VERSION`, `RATE_LIMITED`, `DSL_INVALID`, `DSL_TOO_DEEP`, `DSL_FIELD_NOT_ALLOWED`, `DSL_OP_NOT_ALLOWED`, `CONTACT_SUPPRESSED`, `CONTACT_NOT_FOUND`, `INVALID_EMAIL`, `CAMPAIGN_NOT_FOUND`, `CAMPAIGN_LOCKED`, `CAMPAIGN_FINAL`, `CAMPAIGN_NOT_SCHEDULABLE`, `NO_RECIPIENTS`, `TEMPLATE_NOT_FOUND`, `TEMPLATE_INVALID`, `TEMPLATE_REQUIRED`, `EXPORT_JOB_NOT_FOUND`, `IMPORT_JOB_NOT_FOUND`, `INTERNAL_ERROR`.

## 7. SLA

| Indicatore | Target | Note |
|---|---|---|
| Uptime | 99.5% rolling 30gg | Allineato altri MS suite |
| p95 GET | <500ms | Cache Redis 30s su preview |
| p95 POST | <2000ms | DB persist + audit + idempotency |
| `/segments/preview` 50k contatti | <2s | Cache 30s + indici DB |
| RTO | 30 min | mysqldump nightly + restore documentato |
| RPO | 1h | Replication MySQL slave (futuro) |

## 8. GDPR compliance

- Double opt-in obbligatorio (POST `/contacts` con `consent_pre_confirmed=false` invia email confirm)
- Footer unsubscribe RFC 8058 auto in tutti i template (one-click `List-Unsubscribe` + `List-Unsubscribe-Post`)
- Suppression list permanente per-tenant (`unsubscribe`, `complaint`, `hard_bounce`, `gdpr_request`)
- Hard delete contact (`POST /contacts/:id/hard-delete`) ritorna `audit_trail_id`
- Export ZIP portabilità (`POST /exports`) — contacts.csv + campaigns.json + suppression.csv
- Offboarding tenant: 90gg grace → hard delete (zero retention post-offboard)
- Retention metrics_events: 90gg rolling, cron pulizia notturna

## 9. Branding tenant

- Sottodominio `{firm-slug}.agile.software` per envelope From
- 4 placeholder firm: `firm.name`, `firm.logo_url`, `firm.primary_color`, `firm.accent_color`
- 3 personalization tag contact: `contact.first_name`, `contact.last_name`, `contact.email`
- Reply-to configurabile per tenant
- DKIM selector dedicato per tenant (deliverability isolation)

## 10. 28 endpoint

Vedi `/var/www/agile-services/nexus-marketing-ms/src/routes/*` o ticket `TICKET_AGILEHUB_MARKETING_API.md` per spec completa. Riassunto:

- Sprint 2 (5 read-only): `/tenants/me`, `/contacts` GET, `/campaigns` GET/{id}, `/templates` GET, `/metrics/overview`
- Sprint 3 (12 write): contacts POST, segments POST/GET/preview, campaigns POST/PATCH/schedule/cancel, templates render, test-send, metrics per-campaign
- Sprint 4 (10 GDPR): contacts PATCH/DELETE/hard-delete/import, exports POST/{id}, quota, admin tenants suspend/resume/delete

## 11. Sandbox

- URL primary: `https://agilehub-staging.agile.software/api/marketing` (LIVE 2026-04-25)
- URL secondary (legacy): `https://staging.agilehub.agile.software/api/marketing`
- Tenant test corrente: `tnt_agile-technology_b4f9a3141333` (`plan='enterprise_test'`, `quota_default=5000`)
- Tenant precedente: `tnt_test_tremolada_001` (suspended 2026-04-25 sera, swap-out per testing su entità Agile Technology s.r.l.)
- Key dedicata: `ah_test_*` (mai mescolare con `ah_live_*`)
- DB: `nexus_marketing_db` (clone notturno cron)
- Subdomain tenant: `{firm_slug}.agile.software` (es. `agile-technology.agile.software`) — zone Cloudflare-managed, automazione full
- DNS deliverability sandbox: SPF + DMARC + **DKIM** tutti `active` (LIVE 2026-04-26 mattina)
- **DKIM signing**: OpenDKIM milter su Postfix Helsinki, selector `agile2026`, RSA 2048, rotation annuale Q1

## 12. Deroghe

I prodotti consumer non possono opporsi unilateralmente. Per esenzioni: aprire ticket AgileHub con tag `standard-exception` + motivazione tecnica + termine. MARKETER + REGENT decidono entro 5gg lavorativi.

## 13. Distribuzione

| Prodotto | docs_file | claude_md_section | claude_memory | adoption_status |
|---|---|---|---|---|
| trpg | ✅ TODO | ✅ TODO | ✅ TODO | pending → acknowledged after deploy |
| sustainai | TBD M5 | – | – | pending |
| nis2 | TBD M5 | – | – | pending |
| altri 8 | TBD post-Tremolada | – | – | pending |

## 14. Riferimenti

- Spec produttore TRPG: `/var/www/trpg-agile/docs/PROMPT_MARKETING_FEATURE.md` v1.3
- Spec contratto: `/var/www/trpg-agile/docs/TICKET_AGILEHUB_MARKETING_API.md`
- Doc agente MARKETER: `docs/AGENT_MARKETER.md`
- Reply formale TRPG: `docs/REPLY_TO_TRPG_MARKETING_REQUEST_DRAFT.md`
- Audit gap: `docs/HANDOVER_FROM_TRPG_MARKETING_FEATURE_REQUEST.md`

## 15. Changelog

### v1.4 (2026-04-26 mattina, AC30 blocco AWE pronto in branch — MAESTRO impl + 11/11 test)
- **Nuovo blocco AWE `AC30_MarketingTenantProvision`** implementato + testato (619 righe blocco + 397 righe test, file untracked nel branch `feature/awe-m1-m2-foundation`)
- **Orchestrazione atomica** end-to-end provisioning tenant marketing in 1 nodo workflow: tenant create + 3 DNS Cloudflare (A + SPF + DMARC) + DKIM SSH exec `dkim-provision.js` + DPA opzionale + API key generation
- **Compensation pattern**: hook `compensate(ctx, result)` engine-level + rollback inline su partial-state failure (cancella DNS records, revoca DKIM via `--revoke`, soft-delete tenant 90gg grace)
- **Idempotency H7**: `sha256(runId + nodeId + {firmSlug, firmLegalName})` TTL 7gg
- **Dry-run mode** completo (mock conforme `outputSchema`, no side-effect)
- **Vault integration**: `tier1__nexus-hub-ms__cloudflare/api_token` per CF token, `INTERNAL_SERVICE_KEY` env per X-Internal-Key marketing-ms admin endpoints
- **SSH credential**: pattern `loadAndDecrypt(sshCredentialId)` riusato da AC23
- **Auto-loaded** dal catalog registry (`src/blocks/index.js`) come 13° blocco `action`
- **Test verdi**: 11/11 (6 scenari §9 handover MAESTRO + 3 schema/shape + 2 dryRun) — happy path + rollback DNS + rollback DKIM + skipDkim + firm_slug duplicato + idempotency
- **Trigger originale Q3 2026** (≥3 tenant prod), implementazione **anticipata** disponibile on-demand
- **Decisione GO/NO-GO deploy**: pending — richiede `pm2 reload agilehub-workflow-engine` + bump health.js block count + smoke E2E contro marketing-ms reale
- **Spec autoritativa**: [`HANDOVER_AC30_MARKETING_TENANT_PROVISION_FOR_MAESTRO.md`](HANDOVER_AC30_MARKETING_TENANT_PROVISION_FOR_MAESTRO.md)
- **Coordinamento agenti** (al deploy): MAESTRO (impl) + TITAN (SSH exec coord) + MARKETER (acceptance + bump v1.5 con esempio workflow JSON pubblicato) + PRISMA (form auto-render verify) + VIGILE (security inventory blocco side-effect)

### v1.3 (2026-04-26 mattina, DKIM per-tenant LIVE — TITAN Phase 1-4)
- **DKIM signing OPERATIVO**: OpenDKIM 2.11.0 installato su Postfix Helsinki, milter `inet:localhost:8891`, `milter_default_action=accept` (fail-open per zero downtime)
- **Schema scope chiave**: 1 keypair RSA 2048 per-tenant, selector `agile2026._domainkey.{firm_slug}.agile.software`. Pattern conferma standard §16 (deliverability isolation per-tenant).
- **Tenant Agile Technology**: keypair generata + DNS TXT pubblicato Cloudflare + KeyTable + SigningTable + DB UPDATE → `dkim_status='active'`
- **Automation script**: `scripts/dkim-provision.js` self-contained Node.js (no npm install, mysql CLI + https native + execSync). Idempotente. Eseguito on-host Hetzner.
- **§16 ggiornato**: `Strategia DKIM sandbox vs prod` rimossa "skip sandbox" (DKIM ora attivo anche in sandbox per testing realistico). Mantengono `dkim_status='not_required'` come stato simbolico per tenant esplicitamente esentati.
- **Rotation policy**: annuale Q1 (Q1 2027 prossimo turno), dual-key 30gg overlap durante transizione, coordinata con VIGILE Pillar 1
- **Backward compat**: 99.994% del traffico Postfix Helsinki (cron, www-data, noreply@agile.software) NON matcha SigningTable → continua identica a oggi (15.466 email/7gg invariate, verificato empiricamente)

### v1.2 (2026-04-25 sera, post-allineamento Cristiano TRPG — stesso giorno di v1.1)
- **Convention dominio cambiata**: `*.agilehub.it` → `*.agile.software` (correzione architetturale: Agile ha un solo dominio commerciale `agile.software`, già zone Cloudflare-managed per altri sottodomini come `trpg.agile.software`, `agilehub.agile.software`)
- **Schema `tenant_branding.dkim_status`**: ALTER additive enum, aggiunto valore `not_required` per sandbox/non-prod (oltre a `pending`/`active`/`failed`)
- **Tenant `tnt_agile-technology_b4f9a3141333`**: UPDATE branding subdomain → `agile-technology.agile.software`, reply_to → `noreply@agile-technology.agile.software`, dkim_status=not_required, spf_status=active, dmarc_status=active
- **§16 riscritto**: sostituito vecchio scenario register.it con scenario reale Cloudflare-managed
- **§11 Sandbox**: aggiornato per nuovo subdomain pattern + DNS deliverability LIVE
- **DKIM strategia**: skipped per sandbox, **mandatory per produzione** (go-live Tremolada 23/6 — vedi §16)

### v1.1 (2026-04-25 sera, superseded by v1.2 stesso giorno)
- **§2 Multi-tenancy**: formalizzato `firm_slug = slugify(consulting_firm.name)` come algoritmo derivato server-side. Aggiunti 3 esempi concreti, regola `tenant_id = tnt_${firm_slug}_${randomId(6)}`, derivazione automatica sottodominio + reply-to.
- **§11 Sandbox**: aggiornato tenant corrente a `tnt_agile-technology_b4f9a3141333` (sostituisce `tnt_test_tremolada_001` suspended)
- v1.1 documentava convention `*.agilehub.it` (corretta in v1.2 stesso giorno)

## 16. Gestione zone DNS `*.agile.software` + deliverability

**Provider DNS authoritative**: Cloudflare (zone id `e3d677355677a6397d2caa77264cbfa2`).
**MX inbound**: separato (Microsoft 365 su zone `agilehub.it`); modulo Marketing fa SOLO outbound, no impatto MX.
**Automazione**: full via Cloudflare API (token in vault `tier1__nexus-hub-ms__cloudflare/api_token`).

**Pattern record per tenant** (provisioning automatico con MARKETER+TITAN):
| Record | Esempio Agile Technology | Scope |
|---|---|---|
| `A` | `agile-technology.agile.software → 135.181.149.254` | Helsinki Postfix outbound |
| `TXT` SPF | `v=spf1 ip4:135.181.149.254 -all` | autorizzazione mittente |
| `TXT` DMARC | `_dmarc.agile-technology.agile.software TXT v=DMARC1; p=none; rua=mailto:dmarc-reports@agile.software; pct=100` | report-only modalità monitoring |
| `TXT` DKIM | `agile2026._domainkey.agile-technology.agile.software TXT v=DKIM1; k=rsa; p=<pubkey>` | **solo PROD**, skip sandbox |

**Strategia DKIM sandbox vs prod (v1.3 LIVE 2026-04-26)**:
- **DKIM mandatory per tutti i tenant produttivi** (no skip). DMARC inizia `p=none` (monitoring) e bumppa `p=quarantine` poi `p=reject` quando deliverability stabile post-3 settimane di metriche pulite.
- **`dkim_status='not_required'`**: stato simbolico per tenant esplicitamente esentati (es. tenant disabilitati o test temporanei senza traffico real-world). Widget consumer lo mappa come "n/a".
- **Sandbox attiva DKIM by default** dal v1.3: `agile-technology.agile.software` ha DKIM `active` per testing realistico pre-prod.

**Effort provisioning per nuovo tenant prod (con script automation v1.3)**:
1. POST `/admin/tenants` (auto-genera firm_slug + subdomain) — 1s
2. POST 3 record Cloudflare base (A + SPF + DMARC) — manuale via curl o futuro workflow AWE — 10s
3. **`node scripts/dkim-provision.js --tenant-id <id> --firm-slug <slug>`** ON HOST HETZNER (root):
   - genera keypair RSA 2048 (idempotente)
   - append KeyTable + SigningTable
   - publish DNS DKIM TXT su Cloudflare (idempotente, PATCH se esiste)
   - UPDATE `tenant_branding.dkim_*` + `dkim_status='active'`
   - `systemctl reload opendkim` hot
   - **~3-5 secondi end-to-end**

**Totale**: ~15s tenant pronto. Future workflow AWE blocco `AC30_MarketingTenantProvision` Q3 2026 può chiamare script via webhook host.

**Rotation policy DKIM** (annuale Q1, coord VIGILE):
1. Q1 ogni anno: VIGILE notifica scadenza
2. TITAN: nuovo selector (es. `agile2027` per Q1 2027), genera keypair nuovo
3. Pubblica DNS DKIM nuovo (entrambi vecchio+nuovo coesistono per 30gg overlap)
4. Aggiorna `KeyTable` + `SigningTable` per usare nuovo selector
5. `systemctl reload opendkim` → email firmate con nuova chiave
6. T+30gg: revoca vecchio selector (DELETE DNS vecchio + cleanup KeyTable/SigningTable)
7. Audit log TITAN+VIGILE entry coordinated

**Comandi rapidi rotation** (futuro Phase 4 enhancement):
```bash
# T0: nuova chiave
node scripts/dkim-provision.js --tenant-id <id> --firm-slug <slug> --selector agile2027

# T+30gg: revoca vecchia
node scripts/dkim-provision.js --revoke --tenant-id <id> --firm-slug <slug> --selector agile2026
```
