- 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>
238 lines
16 KiB
Markdown
238 lines
16 KiB
Markdown
# 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
|
||
```
|