Piattaforma ITI TECHNOLOGY: monorepo, Medusa, API, sito, area riservata e deploy
Deploy stack su Proxmox / deploy (push) Failing after 3s

Monorepo npm con apps/public (Angular SSR), apps/private (Angular SPA),
apps/api (Fastify), packages/shared e packages/styles; Medusa 2.21 in medusa/;
infrastruttura Docker (Postgres PostGIS+pgvector, Redis, Caddy, backup) e
workflow Gitea Actions per il deploy via SSH a ogni push su master.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018jQyT6dR9czzGwqb7FkG6u
This commit is contained in:
Lorenzo Sanesi
2026-10-20 09:30:48 +02:00
co-authored by Claude Opus 5.5
commit b83ce039fb
505 changed files with 84544 additions and 0 deletions
@@ -0,0 +1,102 @@
import { createHash, createHmac } from "crypto"
import type { Logger } from "@medusajs/framework/types"
export const SIGNATURE_HEADER = "x-signature"
export const WEBHOOK_PATH = "/webhooks/medusa"
const MAX_ATTEMPTS = 5
const REQUEST_TIMEOUT_MS = 5_000
const DEFAULT_RETRY_BASE_MS = 2_000
/** Namespace (UUID) per gli id evento derivati: costante, non cambiarlo o gli id cambiano. */
const EVENT_ID_NAMESPACE = "6f1d2c3a-8b7e-4f5a-9c0d-1e2f3a4b5c6d"
export type WebhookPayload = {
id: string
event: string
occurred_at: string
data: Record<string, unknown>
}
/**
* UUID v5 deterministico da nome evento + dati dell'evento.
* Lo stesso evento (es. order.placed dello stesso ordine) ha sempre lo stesso id, anche se il
* subscriber viene rieseguito: apps/api lo usa per l'idempotenza (tabella webhook_events).
*/
export function deriveEventId(event: string, eventData: unknown): string {
const ns = Buffer.from(EVENT_ID_NAMESPACE.replace(/-/g, ""), "hex")
const hash = createHash("sha1")
.update(ns)
.update(`${event}:${JSON.stringify(eventData)}`)
.digest()
hash[6] = (hash[6] & 0x0f) | 0x50 // versione 5
hash[8] = (hash[8] & 0x3f) | 0x80 // variante RFC 4122
const hex = hash.subarray(0, 16).toString("hex")
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`
}
/** Firma: HMAC-SHA256 del body grezzo (stringa JSON inviata) con WEBHOOK_SECRET, in esadecimale. */
export function signBody(body: string, secret: string): string {
return createHmac("sha256", secret).update(body).digest("hex")
}
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
/**
* Invia il payload a {FASTIFY_WEBHOOK_URL}/webhooks/medusa firmato in x-signature.
* Fino a 5 tentativi con backoff esponenziale (base WEBHOOK_RETRY_BASE_MS: 2s, 4s, 8s, 16s)
* se la risposta non è 2xx o la richiesta fallisce. Non lancia mai: gli errori finiscono nei log
* (l'ultimo con il payload completo, per un eventuale reinvio manuale).
*/
export async function deliverWebhook(
payload: WebhookPayload,
logger: Logger
): Promise<boolean> {
const baseUrl = process.env.FASTIFY_WEBHOOK_URL
const secret = process.env.WEBHOOK_SECRET
const tag = `[webhook ${payload.event} ${payload.id}]`
if (!baseUrl || !secret) {
logger.warn(`${tag} FASTIFY_WEBHOOK_URL o WEBHOOK_SECRET non impostati: invio saltato`)
return false
}
const url = `${baseUrl.replace(/\/+$/, "")}${WEBHOOK_PATH}`
const body = JSON.stringify(payload)
const signature = signBody(body, secret)
const retryBaseMs = Number(process.env.WEBHOOK_RETRY_BASE_MS) || DEFAULT_RETRY_BASE_MS
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
let outcome: string
try {
const res = await fetch(url, {
method: "POST",
headers: { "content-type": "application/json", [SIGNATURE_HEADER]: signature },
body,
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
})
if (res.ok) {
logger.info(`${tag} consegnato (tentativo ${attempt}/${MAX_ATTEMPTS}, HTTP ${res.status})`)
return true
}
outcome = `HTTP ${res.status}`
} catch (e) {
const err = e as Error & { cause?: { code?: string } }
outcome = err.cause?.code ?? err.name ?? err.message
}
if (attempt < MAX_ATTEMPTS) {
const delay = retryBaseMs * 2 ** (attempt - 1)
logger.warn(
`${tag} tentativo ${attempt}/${MAX_ATTEMPTS} fallito (${outcome}), nuovo tentativo tra ${delay} ms`
)
await sleep(delay)
} else {
logger.error(
`${tag} tentativo ${attempt}/${MAX_ATTEMPTS} fallito (${outcome}): consegna abbandonata. Payload: ${body}`
)
}
}
return false
}
@@ -0,0 +1,37 @@
import { createHash, timingSafeEqual } from "crypto"
import type {
MedusaNextFunction,
MedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
export const INTERNAL_SECRET_HEADER = "x-internal-secret"
const digest = (value: string) => createHash("sha256").update(value).digest()
/**
* Protegge le route /internal/*: chiamabili solo da apps/api (Fastify) con il segreto condiviso
* INTERNAL_API_SECRET nell'header x-internal-secret.
* Il confronto avviene sui digest SHA-256 (stessa lunghezza) con timingSafeEqual,
* così né il contenuto né la lunghezza del segreto trapelano dai tempi di risposta.
* Se INTERNAL_API_SECRET non è configurato, tutte le richieste vengono rifiutate.
*/
export function requireInternalSecret(
req: MedusaRequest,
res: MedusaResponse,
next: MedusaNextFunction
) {
const expected = process.env.INTERNAL_API_SECRET
const provided = req.headers[INTERNAL_SECRET_HEADER]
if (
!expected ||
typeof provided !== "string" ||
!timingSafeEqual(digest(provided), digest(expected))
) {
res.status(401).json({ type: "unauthorized", message: "Unauthorized" })
return
}
next()
}