feat: own the JetStream subject contract, and ingest jupiter rider telemetry

Half of this binary's js.Publish calls were bound to no stream at all.
The streams were declared only by an external Python script on another
machine (Birock/doormile-bookings/setup_jetstream.py) and had drifted
from the code: booking.cancelled, booking.outcome and
booking.assignment_failed had no stream, and CHAT declared the literal
"chat.room.closed" while chat.go publishes "chat.room.closed.<id>",
which it does not match. Every publish site is best-effort
(`if db.Js != nil` + warn-log), so those events were failing and being
dropped silently — every cancellation, delivery outcome and assignment
failure since the streams were created.

db.EnsureStreams now declares the streams at startup from a map that
sits next to the code that publishes, so the contract cannot drift
again. It only ever adds: existing streams keep their storage type,
retention, limits and every subject they already have. Nothing is
deleted. Losing the create race against a sibling replica is expected
and reconciles rather than erroring.

Alongside that, /internal/miler/* ingests rider telemetry still arriving
over the jupiter NATS chain. The forwarding worker holds no rider JWT —
the rider app is still jupiter-shaped — so LegacyMilerIdentity resolves
an identity from a header into c.Locals("userid") behind the existing
X-Internal-Key guard. That lets the routes reuse the miler handlers
unchanged instead of growing a parallel set that would drift.

Identity comes from a header, never the body: the telemetry handlers
overwrite a body-supplied userid precisely so one rider cannot write
another's GPS trail, and reading it from the body here would reopen that
from behind the internal key. MilerProfile.Legacyuserid (nullable,
indexed) maps a jupiter userid to a Doormile one.

Only fire-and-forget telemetry is exposed. Transactional actions stay
synchronous — a rider needs a real answer from pickup-complete, which a
queue in front of it cannot give.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Suriya
2026-08-10 12:56:30 +05:30
parent 90fa4fbb74
commit 6738d37d7b
5 changed files with 251 additions and 8 deletions

View File

@@ -0,0 +1,76 @@
package middlewares
import (
"strconv"
"doormile/db"
"doormile/models"
"doormile/utils"
"github.com/gofiber/fiber/v2"
)
// LegacyMilerIdentity resolves a rider identity for machine-to-machine ingest
// and puts it in c.Locals("userid"), which is exactly where the normal miler
// handlers read it from. That is the whole point: the ingest routes reuse the
// existing handlers unchanged rather than growing a parallel set with their own
// (inevitably drifting) validation.
//
// It must be mounted *behind* InternalKeyAuth. On its own it is not
// authentication — it names a rider, it does not prove anything about the
// caller. The X-Internal-Key check is what makes that safe, and it is the reason
// this cannot be reached from the public internet.
//
// Two headers, checked in order:
//
// X-Miler-Userid a Doormile userid, used directly
// X-Legacy-Userid a jupiter/Nearle userid, resolved via MilerProfile.Legacyuserid
//
// The identity deliberately does NOT come from the request body. The miler
// telemetry handlers overwrite any body-supplied userid with the token's, which
// is what stops one rider writing another's GPS trail; taking it from the body
// here would reopen that hole from behind the internal key. A header keeps the
// rule intact and works for POST /consignments/logs, whose body is a bare JSON
// array with nowhere to put an id anyway.
func LegacyMilerIdentity(c *fiber.Ctx) error {
if raw := c.Get("X-Miler-Userid"); raw != "" {
userID, err := strconv.Atoi(raw)
if err != nil || userID <= 0 {
return utils.BadRequest(c, "invalid X-Miler-Userid")
}
var count int64
if err := db.DB.Model(&models.MilerProfile{}).
Where("userid = ?", userID).Count(&count).Error; err != nil {
return utils.Internal(c, "failed to resolve miler")
}
if count == 0 {
return utils.NotFound(c, "no miler with that userid")
}
c.Locals("userid", userID)
return c.Next()
}
raw := c.Get("X-Legacy-Userid")
if raw == "" {
return utils.BadRequest(c, "X-Miler-Userid or X-Legacy-Userid header is required")
}
legacyID, err := strconv.Atoi(raw)
if err != nil || legacyID <= 0 {
return utils.BadRequest(c, "invalid X-Legacy-Userid")
}
var profile models.MilerProfile
if err := db.DB.Where("legacyuserid = ?", legacyID).First(&profile).Error; err != nil {
// Unmapped is the expected case for every rider who was never migrated
// from jupiter, so this is a routine 404 rather than an error condition.
// The forwarder treats it as "drop, do not retry" — retrying cannot
// invent a mapping, and NAK-looping on it would wedge the consumer.
return utils.NotFound(c, "no Doormile miler mapped to that legacy userid")
}
c.Locals("userid", profile.Userid)
return c.Next()
}