# 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: ` - 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_` (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_" } ``` 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=` | **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 --firm-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 --firm-slug --selector agile2027 # T+30gg: revoca vecchia node scripts/dkim-provision.js --revoke --tenant-id --firm-slug --selector agile2026 ```