CDN edge in front of S3/MinIO via origin-pull. Backend signs URLs
with Bunny.net token-auth (SHA-256 over security_key + path + expires)
so edges verify before serving cached objects ; origin is never hit
on a valid token. Cloudflare CDN / R2 / CloudFront stubs kept.
- internal/services/cdn_service.go : new providers CDNProviderBunny +
CDNProviderCloudflareR2. SecurityKey added to CDNConfig.
generateBunnySignedURL implements the documented Bunny scheme
(url-safe base64, no padding, expires query). HLSSegmentCacheHeaders
+ HLSPlaylistCacheHeaders helpers exported for handlers.
- internal/services/cdn_service_test.go : pin Bunny URL shape +
base64-url charset ; assert empty SecurityKey fails fast (no
silent fallback to unsigned URLs).
- internal/core/track/service.go : new CDNURLSigner interface +
SetCDNService(cdn). GetStorageURL prefers CDN signed URL when
cdnService.IsEnabled, falls back to direct S3 presign on signing
error so a CDN partial outage doesn't block playback.
- internal/api/routes_tracks.go + routes_core.go : wire SetCDNService
on the two TrackService construction sites that serve stream/download.
- internal/config/config.go : 4 new env vars (CDN_ENABLED, CDN_PROVIDER,
CDN_BASE_URL, CDN_SECURITY_KEY). config.CDNService always non-nil
after init ; IsEnabled gates the actual usage.
- internal/handlers/hls_handler.go : segments now return
Cache-Control: public, max-age=86400, immutable (content-addressed
filenames make this safe). Playlists at max-age=60.
- veza-backend-api/.env.template : 4 placeholder env vars.
- docs/ENV_VARIABLES.md §12 : provider matrix + Bunny vs Cloudflare
vs R2 trade-offs.
Bug fix collateral : v1.0.9 Day 11 introduced veza_cache_hits_total
which collided in name with monitoring.CacheHitsTotal (different
label set ⇒ promauto MustRegister panic at process init). Day 13
deletes the monitoring duplicate and restores the metrics-package
counter as the single source of truth (label: subsystem). All 8
affected packages green : services, core/track, handlers, middleware,
websocket/chat, metrics, monitoring, config.
Acceptance (Day 13) : code path is wired ; verifying via real Bunny
edge requires a Pull Zone provisioned by the user (EX-? in roadmap).
On the user side : create Pull Zone w/ origin = MinIO, copy token
auth key into CDN_SECURITY_KEY, set CDN_ENABLED=true.
W3 progress : Redis Sentinel ✓ · MinIO distribué ✓ · CDN ✓ ·
DMCA ⏳ Day 14 · embed ⏳ Day 15.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes FUNCTIONAL_AUDIT.md §4 #1: WebRTC 1:1 calls had working
signaling but no NAT traversal, so calls between two peers behind
symmetric NAT (corporate firewalls, mobile carrier CGNAT, Incus
container default networking) failed silently after the SDP exchange.
Backend:
- GET /api/v1/config/webrtc (public) returns {iceServers: [...]}
built from WEBRTC_STUN_URLS / WEBRTC_TURN_URLS / *_USERNAME /
*_CREDENTIAL env vars. Half-config (URLs without creds, or vice
versa) deliberately omits the TURN block — a half-configured TURN
surfaces auth errors at call time instead of falling back cleanly
to STUN-only.
- 4 handler tests cover the matrix.
Frontend:
- services/api/webrtcConfig.ts caches the config for the page
lifetime and falls back to the historical hardcoded Google STUN
if the fetch fails.
- useWebRTC fetches at mount, hands iceServers synchronously to
every RTCPeerConnection, exposes a {hasTurn, loaded} hint.
- CallButton tooltip warns up-front when TURN isn't configured
instead of letting calls time out silently.
Ops:
- infra/coturn/turnserver.conf — annotated template with the SSRF-
safe denied-peer-ip ranges, prometheus exporter, TLS for TURNS,
static lt-cred-mech (REST-secret rotation deferred to v1.1).
- infra/coturn/README.md — Incus deploy walkthrough, smoke test
via turnutils_uclient, capacity rules of thumb.
- docs/ENV_VARIABLES.md gains a 13bis. WebRTC ICE servers section.
Coturn deployment itself is a separate ops action — this commit lands
the plumbing so the deploy can light up the path with zero code
changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both files were dated v1.0.4 (2026-04-15) — three releases out of
date. Surgical updates rather than a rewrite, since the underlying
feature inventory is mostly unchanged.
PROJECT_STATE.md
- §1 "Version actuelle" : tag v1.0.4 → v1.0.8 (2026-04-26). Phase
description + next-version hint refreshed (v1.0.9 with item G +
WebRTC TURN as cibles).
- §2 "Ce qui est livré" : prepended v1.0.8, v1.0.7, v1.0.5–v1.0.6.2
consolidated entries (with batch labels A/B/B9/C and the
money-movement plan items A–F). The v0.x sections kept verbatim
for archive — they document phases that pre-date the launch.
- §3 "Prochaines étapes" : replaced the v0.701 retry/dashboard plan
(long since shipped) with the v1.0.9 candidate list, ordered by
effort × impact. Item G subscription pending_payment + WebRTC TURN
are the two cibles. C6 flake stab + wrappers consolidation +
multipart S3 + register UX + email tokens header migration listed
alongside.
FEATURE_STATUS.md
- Header date refreshed to 2026-04-26 / v1.0.8 with the chantier
summary.
- "Upload de tracks" row : added the v1.0.8 MinIO/S3 wiring detail
(TRACK_STORAGE_BACKEND flag, chunked upload assembly, signed-URL
redirect 302).
- "HLS Streaming" feature-flag row : flipped default from `true`
(v0.101 era) to `false` (v1.0.7 default) — referencing the
fallback /tracks/:id/stream Range cache bypass landed in
v1.0.7-rc1 commit `b875efcff`.
- "Appels WebRTC" limitation row : note refreshed — signaling OK,
NAT traversal still HS without STUN/TURN per FUNCTIONAL_AUDIT 🟡#1,
cible bumped from v1.1 to v1.0.9 (matches the v1.0.9 plan above).
The v0.x section in PROJECT_STATE.md (Phases 1–5) intentionally left
as-is — it serves as historical record of what shipped before
launch. Future agents reading the file should focus on §1, §2 v1.0.x,
and §3 for current state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the second-to-last item of Batch C (after C3 reuseExistingServer
and C4 seed --ci flag landed earlier). Wires the existing Playwright
suite (60+ spec files in tests/e2e/) into Forgejo Actions.
Workflow shape (.github/workflows/e2e.yml):
- pull_request → @critical only (5-7min target, 20min timeout)
- push to main → full suite (~25min target, 45min timeout)
- nightly cron 03:00 UTC → full suite, catches infra drift
- workflow_dispatch → full suite, manual trigger
Single job structure with conditional steps based on github.event_name.
The job:
1. Boots Postgres / Redis / RabbitMQ via docker compose.
2. Runs Go migrations.
3. `go run ./cmd/tools/seed --ci` — the lean seed landed in C4
(5 test accounts + 10 tracks + 3 playlists, ~5s).
4. Builds + starts the backend with APP_ENV=test plus
DISABLE_RATE_LIMIT_FOR_TESTS=true and the lockout-exempt
emails matching the auth fixture.
5. `playwright install --with-deps chromium`.
6. `npm run e2e:critical` (PR) or `npm run e2e` (push/cron).
7. Uploads the Playwright HTML report + backend log on failure
(7-day retention, sufficient for triage).
The `CI: "true"` env var is set workflow-wide so playwright.config.ts
(line 141, 155) sees `process.env.CI` and flips reuseExistingServer
to false, guaranteeing a fresh backend + Vite per job.
Secrets fall back to dev defaults (devpassword / 38-char dev JWT /
guest:guest@localhost:5672) so a fresh repo runs without configuring
secrets first; production-style runs should set `E2E_DB_PASSWORD`,
`E2E_JWT_SECRET`, `E2E_RABBITMQ_URL` in Forgejo Actions secrets.
Runbook (docs/CI_E2E.md):
- Trigger / scope / target time table.
- Step-by-step explanation of what a CI run does.
- Required secrets + their fallbacks.
- "Reproducing a CI failure locally" — exact mirror of the workflow
invocation so a dev can rerun without pushing.
- "Debugging a red run" — where to look in the Forgejo UI, what the
artifacts contain, when to check SKIPPED_TESTS.md.
- "Adding a new E2E test" — fixture usage, when to tag @critical.
Action pin SHAs match the rest of the workflows (consistent supply-
chain hygiene). Go 1.25 (matches ci.yml backend job, NOT the older
1.24 used in the disabled accessibility.yml template).
Remaining Batch C item: C6 — flake stabilisation (~3-5 of the 22
SKIPPED_TESTS.md entries that look fixable). Defer to a follow-up
session — wiring the workflow first means the next push-to-main run
will tell us empirically which @critical tests are flaky in CI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 0 of the MinIO upload migration (FUNCTIONAL_AUDIT §4 item 2).
Schema + config only — Phase 1 will wire TrackService.UploadTrack()
to actually route writes to S3 when the flag is flipped.
Schema (migration 985):
- tracks.storage_backend VARCHAR(16) NOT NULL DEFAULT 'local'
CHECK in ('local', 's3')
- tracks.storage_key VARCHAR(512) NULL (S3 object key when backend=s3)
- Partial index on storage_backend = 's3' (migration progress queries)
- Rollback drops both columns + index; safe only while all rows are
still 'local' (guard query in the rollback comment)
Go model (internal/models/track.go):
- StorageBackend string (default 'local', not null)
- StorageKey *string (nullable)
- Both tagged json:"-" — internal plumbing, never exposed publicly
Config (internal/config/config.go):
- New field Config.TrackStorageBackend
- Read from TRACK_STORAGE_BACKEND env var (default 'local')
- Production validation rule #11 (ValidateForEnvironment):
- Must be 'local' or 's3' (reject typos like 'S3' or 'minio')
- If 's3', requires AWS_S3_ENABLED=true (fail fast, do not boot with
TrackStorageBackend=s3 while S3StorageService is nil)
- Dev/staging warns and falls back to 'local' instead of fail — keeps
iteration fast while still flagging misconfig.
Docs:
- docs/ENV_VARIABLES.md §13 restructured as "HLS + track storage backend"
with a migration playbook (local → s3 → migrate-storage CLI)
- docs/ENV_VARIABLES.md §28 validation rules: +2 entries for new rules
- docs/ENV_VARIABLES.md §29 drift findings: TRACK_STORAGE_BACKEND added
to "missing from template" list before it was fixed
- veza-backend-api/.env.template: TRACK_STORAGE_BACKEND=local with
comment pointing at Phase 1/2/3 plans
No behavior change yet — TrackService.UploadTrack() still hardcodes the
local path via copyFileAsync(). Phase 1 wires it.
Refs:
- AUDIT_REPORT.md §9 item (deferrals v1.0.8)
- FUNCTIONAL_AUDIT.md §4 item 2 "Stockage local disque only"
- /home/senke/.claude/plans/audit-fonctionnel-wild-hickey.md Item 3
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move ASVS_CHECKLIST_v0.12.6.md, PENTEST_REPORT_VEZA_v0.12.6.md, and
REMEDIATION_MATRIX_v0.12.6.md to docs/archive/ — all reference a
pentest conducted on v0.12.6 (2026-03), stale relative to the current
v1.0.7 codebase (different security middleware, different payment
flow, different config validation).
Update CLAUDE.md tree listing and AUDIT_REPORT.md §9.1 to reflect the
archive location. Keep docs/SECURITY_SCAN_RC1.md (still current).
Closes AUDIT_REPORT §9.1 obsolete-doc item.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Triple cleanup, landed together because they share the same cleanup
branch intent and touch non-overlapping trees.
1. 38× tracked .playwright-mcp/*.yml stage-deleted
MCP session recordings that had been inadvertently committed.
.gitignore already covers .playwright-mcp/ (post-audit J2 block
added in d12b901de). Working tree copies removed separately.
2. 19× disabled CI workflows moved to docs/archive/workflows/
Legacy .yml.disabled files in .github/workflows/ were 1676 LOC of
dead config (backend-ci, cd, staging-validation, accessibility,
chromatic, visual-regression, storybook-audit, contract-testing,
zap-dast, container-scan, semgrep, sast, mutation-testing,
rust-mutation, load-test-nightly, flaky-report, openapi-lint,
commitlint, performance). Preserved in docs/archive/workflows/
for historical reference; `.github/workflows/` now only lists the
5 actually-running pipelines.
3. Orphan code removed (0 consumers confirmed via grep)
- veza-backend-api/internal/repository/user_repository.go
In-memory UserRepository mock, never imported anywhere.
- proto/chat/chat.proto
Chat server Rust deleted 2026-02-22 (commit 279a10d31); proto
file was orphan spec. Chat lives 100% in Go backend now.
- veza-common/src/types/chat.rs (Conversation, Message, MessageType,
Attachment, Reaction)
- veza-common/src/types/websocket.rs (WebSocketMessage,
PresenceStatus, CallType — depended on chat::MessageType)
- veza-common/src/types/mod.rs updated: removed `pub mod chat;`,
`pub mod websocket;`, and their re-exports.
Only `veza_common::logging` is consumed by veza-stream-server
(verified with `grep -r "veza_common::"`). `cargo check` on
veza-common passes post-removal.
Refs: AUDIT_REPORT.md §8.2 "Code mort / orphelin" + §9.1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every POST /webhooks/hyperswitch delivery now writes a row to
`hyperswitch_webhook_log` regardless of signature-valid or
processing outcome. Captures both legitimate deliveries and attack
probes — a forensics query now has the actual bytes to read, not
just a "webhook rejected" log line. Disputes (axis-1 P1.6) ride
along: the log captures dispute.* events alongside payment and
refund events, ready for when disputes get a handler.
Table shape (migration 984):
* payload TEXT — readable in psql, invalid UTF-8 replaced with
empty (forensics value is in headers + ip + timing for those
attacks, not the binary body).
* signature_valid BOOLEAN + partial index for "show me attack
attempts" being instantaneous.
* processing_result TEXT — 'ok' / 'error: <msg>' /
'signature_invalid' / 'skipped'. Matches the P1.5 action
semantic exactly.
* source_ip, user_agent, request_id — forensics essentials.
request_id is captured from Hyperswitch's X-Request-Id header
when present, else a server-side UUID so every row correlates
to VEZA's structured logs.
* event_type — best-effort extract from the JSON payload, NULL
on malformed input.
Hardening:
* 64KB body cap via io.LimitReader rejects oversize with 413
before any INSERT — prevents log-spam DoS.
* Single INSERT per delivery with final state; no two-phase
update race on signature-failure path. signature_invalid and
processing-error rows both land.
* DB persistence failures are logged but swallowed — the
endpoint's contract is to ack Hyperswitch, not perfect audit.
Retention sweep:
* CleanupHyperswitchWebhookLog in internal/jobs, daily tick,
batched DELETE (10k rows + 100ms pause) so a large backlog
doesn't lock the table.
* HYPERSWITCH_WEBHOOK_LOG_RETENTION_DAYS (default 90).
* Same goroutine-ticker pattern as ScheduleOrphanTracksCleanup.
* Wired in cmd/api/main.go alongside the existing cleanup jobs.
Tests: 5 in webhook_log_test.go (persistence, request_id auto-gen,
invalid-JSON leaves event_type empty, invalid-signature capture,
extractEventType 5 sub-cases) + 4 in cleanup_hyperswitch_webhook_
log_test.go (deletes-older-than, noop, default-on-zero,
context-cancel). Migration 984 applied cleanly to local Postgres;
all indexes present.
Also (v107-plan.md):
* Item G acceptance gains an explicit Idempotency-Key threading
requirement with an empty-key loud-fail test — "literally
copy-paste D's 4-line test skeleton". Closes the risk that
item G silently reopens the HTTP-retry duplicate-charge
exposure D closed.
Out of scope for E (noted in CHANGELOG):
* Rate limit on the endpoint — pre-existing middleware covers
it at the router level; adding a per-endpoint limit is
separate scope.
* Readable-payload SQL view — deferred, the TEXT column is
already human-readable; a convenience view is a nice-to-have
not a ship-blocker.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every outbound POST /payments and POST /refunds from the Hyperswitch
client now carries an Idempotency-Key HTTP header. Key values are
explicit parameters at every call site — no context-carrier magic,
no auto-generation. An empty key is a loud error from the client
(not silent header omission) so a future new call site that forgets
to supply one fails immediately, not months later under an obscure
replay scenario.
Key choices, both stable across HTTP retries of the same logical
call:
* CreatePayment → order.ID.String() (GORM BeforeCreate populates
order.ID before the PSP call in ConfirmOrder).
* CreateRefund → pendingRefund.ID.String() (populated by the
Phase 1 tx.Create in RefundOrder, available for the Phase 2 PSP
call).
Scope note (reproduced here for the next reader who grep-s the
commit log for "Idempotency-Key"):
Idempotency-Key covers HTTP-transport retry (TLS reconnect,
proxy retry, DNS flap) within a single CreatePayment /
CreateRefund invocation. It does NOT cover application-level
replay (user double-click, form double-submit, retry after crash
before DB write). That class of bug requires state-machine
preconditions on VEZA side — already addressed by the order
state machine + the handler-level guards on POST
/api/v1/payments (for payments) and the partial UNIQUE on
`refunds.hyperswitch_refund_id` landed in v1.0.6.1 (for refunds).
Hyperswitch TTL on Idempotency-Key: typically 24h-7d server-side
(verify against current PSP docs). Beyond TTL, a retry with the
same key is treated as a new request. Not a concern at current
volumes; document if retry logic ever extends beyond 1 hour.
Explicitly out of scope: item D does NOT add application-level
retry logic. The current "try once, fail loudly" behavior on PSP
errors is preserved. Adding retries is a separate design exercise
(backoff, max attempts, circuit breaker) not part of this commit.
Interfaces changed:
* hyperswitch.Client.CreatePayment(ctx, idempotencyKey, ...)
* hyperswitch.Client.CreatePaymentSimple(...) convenience wrapper
* hyperswitch.Client.CreateRefund(ctx, idempotencyKey, ...)
* hyperswitch.Provider.CreatePayment threads through
* hyperswitch.Provider.CreateRefund threads through
* marketplace.PaymentProvider interface — first param after ctx
* marketplace.refundProvider interface — first param after ctx
Removed:
* hyperswitch.Provider.Refund (zero callers, superseded by
CreateRefund which returns (refund_id, status, err) and is the
only method marketplace's refundProvider cares about).
Tests:
* Two new httptest.Server-backed tests (client_test.go) pin the
Idempotency-Key header value for CreatePayment and CreateRefund.
* Two new empty-key tests confirm the client errors rather than
silently sending no header.
* TestRefundOrder_OpensPendingRefund gains an assertion that
f.provider.lastIdempotencyKey == refund.ID.String() — if a
future refactor threads the key from somewhere else (paymentID,
uuid.New() per call, etc.) the test fails loudly.
* Four pre-existing test mocks updated for the new signature
(mockRefundPaymentProvider in marketplace, mockPaymentProvider
in tests/integration and tests/contract, mockRefundPayment
Provider in tests/integration/refund_flow).
Subscription's CreateSubscriptionPayment interface declares its own
shape and has no live Hyperswitch-backed implementation today —
v1.0.6.2 noted this as the payment-gate bypass surface, v1.0.7
item G will ship the real provider. When that lands, item G's
implementation threads the idempotency key through in the same
pattern (documented in v107-plan.md item G acceptance).
CHANGELOG v1.0.7-rc1 entry updated with the full item D scope note
and the "out of scope: retries" caveat.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
TransferService.CreateTransfer signature changes from (...) error to
(...) (string, error) — the caller now captures the Stripe transfer
identifier and persists it on the SellerTransfer row. Pre-v1.0.7 the
stripe_transfer_id column was declared on the model and table but
never written to, which blocked the reversal worker (v1.0.7 item B)
from identifying which transfer to reverse on refund.
Changes:
* `TransferService` interface and `StripeConnectService.CreateTransfer`
both return the Stripe transfer id alongside the error.
* `processSellerTransfers` (marketplace service) persists the id on
success before `tx.Create(&st)` so a crash between Stripe ACK and
DB commit leaves no inconsistency.
* `TransferRetryWorker.retryOne` persists on retry success — a row
that failed on first attempt and succeeded via the worker is
reversal-ready all the same.
* `admin_transfer_handler.RetryTransfer` (manual retry) persists too.
* `SellerPayout.ExternalPayoutID` is populated by the Connect payout
flow (`payout.go`) — the field existed but was never written.
* Four test mocks updated; two tests assert the id is persisted on
the happy path, one on the failure path confirms we don't write a
fake id when the provider errors.
Migration `981_seller_transfers_stripe_reversal_id.sql`:
* Adds nullable `stripe_reversal_id` column for item B.
* Partial UNIQUE indexes on both stripe_transfer_id and
stripe_reversal_id (WHERE IS NOT NULL AND <> ''), mirroring the
v1.0.6.1 pattern for refunds.hyperswitch_refund_id.
* Logs a count of historical completed transfers that lack an id —
these are candidates for the backfill CLI follow-up task.
Backfill for historical rows is a separate follow-up (cmd/tools/
backfill_stripe_transfer_ids, calling Stripe's transfers.List with
Destination + Metadata[order_id]). Pre-v1.0.7 transfers without a
backfilled id cannot be auto-reversed on refund — document in P2.9
admin-recovery when it lands. Acceptable scope per v107-plan.
Migration number bumped 980 → 981 because v1.0.6.2 used 980 for the
unpaid-subscription cleanup; v107-plan updated with the note.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CHANGELOG v1.0.6.2 block now documents the distribution-handler
propagate fix as part of the release (applied in commit 26cb52333
before re-tagging). v1.0.7 item G acceptance gains a recovery
endpoint requirement so the "complete payment" error message has a
real target rather than leaving users stuck.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Self-review of the v1.0.6.2 hotfix surfaced that
distribution.checkEligibility silently swallowed
subscription.ErrSubscriptionNoPayment as "ineligible, no extra info",
so a user with a fantôme subscription trying to submit a distribution
got "Distribution requires Creator or Premium plan" — misleading, the
user has a plan but no payment. checkEligibility now propagates the
error so the handler can surface "Your subscription is not linked to
a payment. Complete payment to enable distribution."
Security is unchanged — the gate still refuses. This is a UX clarity
fix for honest-path users who landed in the fantôme state via a
broken payment flow.
Also:
- Closure timestamp added to axis-1 P0.12 ("closed 2026-04-17 in
v1.0.6.2 (commit 9a8d2a4e7)") so future readers know the finding's
lifecycle without re-grepping the CHANGELOG.
- Item G in v107-plan.md gains an explicit E2E Playwright @critical
acceptance — the shell probe + Go unit tests validate the fix
today but don't run on every commit, so a refactor of Subscribe or
checkEligibility could silently re-open the bypass. The E2E test
makes regression coverage automatic.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 Q2 probe confirmed the subscription money-movement finding
wasn't a "needs confirmation from ops" P1 — it was a live P0 bypass.
An authenticated user could POST /api/v1/subscriptions/subscribe,
receive 201 active without payment, and satisfy the distribution
eligibility gate. v1.0.6.2 (commit 9a8d2a4e7) closed the bypass at
the consumption site via GetUserSubscription filter + migration 980
cleanup.
axis-1-correctness.md:
* P1.7 renamed to P0.12 with the bypass chain, probe evidence, and
v1.0.6.2 closure cross-reference.
* Residual subscription-refund / webhook completeness work split out
as P1.7' (original scope, still v1.0.8).
v107-plan.md:
* Item G added (M effort) — replaces the v1.0.6.2 filter with a
mandatory pending_payment state + webhook-driven activation,
closing the creation path rather than compensating at the gate.
* Dependency graph gains a third track (independent of A/B/C/D/E/F).
* Effort total revised from 9-10d to 12-13d single-dev, 5d to 7d
two-dev parallel.
* Item D acceptance gains a TTL caveat section — Hyperswitch
Idempotency-Key has a 24h-7d server-side TTL; app-level
idempotency (order.id / partial UNIQUE) remains the load-bearing
guard beyond that window.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Axis 1 of the 5-axis VEZA audit, scoped to money-movement correctness
and ledger↔PSP reconciliation. Layout: one file per axis under
docs/audit-2026-04/, README index, v107-plan.md derived.
P0 findings (block v1.0.7 "ready-to-show" gate):
* P0.1 — SellerTransfer.StripeTransferID declared but never populated.
stripe_connect_service.CreateTransfer discards the *stripe.Transfer
return value (`_, err := transfer.New(params)`), so the column in
models.go:237 is dead. Structural blocker for the CHANGELOG-parked
v1.0.7 "Stripe Connect reversal" item.
* P0.2 — No Stripe Connect reversal on refund.succeeded. Every refund
today creates a permanent VEZA↔Stripe ledger gap. Action reworked
to decouple via a new `seller_transfers.status = 'reversal_pending'`
state + async worker, so Stripe flaps never block buyer-facing
refund UX.
* P0.3 — No reconciliation sweep for stuck orders / refunds / refund
rows with empty hyperswitch_refund_id. Hourly worker recommended,
same pattern as v1.0.5 Fix 6 orphan-tracks cleaner.
* P0.4 — No Idempotency-Key on outbound Hyperswitch POST /payments and
POST /refunds. Action includes an explicit scope note: the header
covers HTTP-transport retry only, NOT application-level replay (for
which the fix is a state-machine precondition).
P1 findings:
* P1.5 — Webhook raw payloads not persisted (blocks dispute forensics)
* P1.6 — Disputes / chargebacks silently dropped (new, surfaced during
review; dispute.* webhooks fall through the default case)
* P1.7 — Subscription money-movement not covered by v1.0.6 hardening
* P1.8 — No ledger-health Prometheus metrics
P2 findings:
* P2.9 — No admin API for manual override
* P2.10 — Partial refund latent compromise (amount *int64 always nil)
wontfix:
* wontfix.11 — Per-seller retry interval (re-evaluate at 10× load)
Derived deliverable: v107-plan.md sequences the 6 de-duplicated items
(4 P0 + 2 P1) with a dependency graph, two parallel tracks, per-commit
effort estimates (D→A→B; E→C→F), release gating and open questions
(volume magnitude, Connect backfill %).
Info needed from ops (tracked in axis-1 doc, not determinable from
code): last manual reconciliation date, whether subscriptions are
currently sold, current order/refund volume.
Axes 2-5 deferred: README.md marks axis 2 (state machines) as gated
on v1.0.7 landing first, otherwise the transition matrix captures a
v1.0.6.1 snapshot that's immediately stale.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7-day cleanup sprint (J1–J7) done. The codebase is unchanged
functionally but the working tree, docs, k8s runbooks, CI, and
Go dependency graph are all realigned with reality for the first
time since the v1.0.0 release.
VERSION 1.0.2 → 1.0.4 (skips v1.0.3 — that tag already
exists upstream, unused on this branch)
CHANGELOG.md full v1.0.4 entry with per-day (J1–J7) breakdown
and the govulncheck + CI fix trail
docs/PROJECT_STATE.md header month + version table refreshed,
pointer to AUDIT_REPORT.md added
docs/FEATURE_STATUS.md header updated — no feature matrix
changes (no feature work in this sprint)
Key deliverables of the sprint:
J1 0e7097ed1 purge 220 MB of debris (binaries, reports,
session docs, stale MVP scripts)
J2 2aea1af36 rewrite CLAUDE.md, fix README, purge chat-server
refs from k8s runbooks and env examples
J3 67f18892a remove 3 deprecated unused handlers
J3+ 7fa314866 2FA handler duplicate removal (bundled by parallel
ci-cache commit)
J4 9cdfc6d89 GDPR-compliant hard delete with Redis SCAN cursor
and ES DeleteByQuery — closes TODO(HIGH-007)
J5 0589ec9fc defer GeoIP, rename v2-v3-types.ts to domain.ts,
document Storybook kill
J5+ 7f89bebe1 fix lint-staged eslint rule (was linting the
whole project — root cause of earlier --no-verify)
J6 113210734 mark 3 dormant docker-compose files deprecated
fix 3d1f127ad bump x/image, quic-go, testcontainers-go — drops
containerd + docker/docker from dep graph,
resolving 5 govulncheck findings without allowlist
fix b33227a57 bump go.work to 1.25 to match veza-backend-api
fix 73fc6e128 bump x/net v0.51.0 for GO-2026-4559
fix 376d9adc4 retire legacy backend-ci.yml, centralize Docker
probe in SkipIfNoIntegration
CI status on the consolidated ci.yml workflow for 376d9adc4:
Veza CI / Backend (Go) OK 6m36s
Veza CI / Frontend (Web) OK 20m57s
Veza CI / Rust (Stream) OK 6m25s
Security Scan / gitleaks OK 4m13s
Veza CI / Notify skipped (fires only on failure)
First fully green CI run of the sprint and the first in a long
time overall. The tag v1.0.4 is cut on this state.
Refs: AUDIT_REPORT.md, all commits 0e7097ed1..376d9adc4
First-attempt commit 3a5c6e184 only captured the .gitignore change; the
pre-commit hook silently dropped the 343 staged moves/deletes during
lint-staged's "no matching task" path. This commit re-applies the intended
J1 content on top of bec75f143 (which was pushed in parallel).
Uses --no-verify because:
- J1 only touches .md/.json/.log/.png/binaries — zero code that would
benefit from lint-staged, typecheck, or vitest
- The hook demonstrated it corrupts pure-rename commits in this repo
- Explicitly authorized by user for this one commit
Changes (343 total: 169 deletions + 174 renames):
Binaries purged (~167 MB):
- veza-backend-api/{server,modern-server,encrypt_oauth_tokens,seed,seed-v2}
Generated reports purged:
- 9 apps/web/lint_report*.json (~32 MB)
- 8 apps/web/tsc_*.{log,txt} + ts_*.log (TS error snapshots)
- 3 apps/web/storybook_*.json (1375+ stored errors)
- apps/web/{build_errors*,build_output,final_errors}.txt
- 70 veza-backend-api/coverage*.out + coverage_groups/ (~4 MB)
- 3 veza-backend-api/internal/handlers/*.bak
Root cleanup:
- 54 audit-*.png (visual regression baselines, ~11 MB)
- 9 stale MVP-era scripts (Jan 27, hardcoded v0.101):
start_{iteration,mvp,recovery}.sh,
test_{mvp_endpoints,protected_endpoints,user_journey}.sh,
validate_v0101.sh, verify_logs_setup.sh, gen_hash.py
Session docs archived (not deleted — preserved under docs/archive/):
- 78 apps/web/*.md → docs/archive/frontend-sessions-2026/
- 43 veza-backend-api/*.md → docs/archive/backend-sessions-2026/
- 53 docs/{RETROSPECTIVE_V,SMOKE_TEST_V,PLAN_V0_,V0_*_RELEASE_SCOPE,
AUDIT_,PLAN_ACTION_AUDIT,REMEDIATION_PROGRESS}*.md
→ docs/archive/v0-history/
README.md and CONTRIBUTING.md preserved in apps/web/ and veza-backend-api/.
Note: The .gitignore rules preventing recurrence were already pushed in
3a5c6e184 and remain in place — this commit does not modify .gitignore.
Refs: AUDIT_REPORT.md §11