nis2-agile/docs/STANDARD_MARKETING_TENANT_PROVISIONING.md
DevEnv nis2-agile c0bf7b6c15 [DOCS] Standard cross-suite AgileHub + governance CLAUDE.md + registri agent
- CLAUDE.md: TZ, SSO, vault-steward, versioning, persona v2.0, multitenant, KB RAG
- docs/standards: persona-conversational-rules v2.0
- docs/STANDARD_*: installer-integration, email-relay, AI-prodotto, marketing-tenant, multitenant
- AGENT_CHANGES.md + OPEN_TICKETS.md (registri agent automatico)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 15:41:54 +02:00

238 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
```