Skip to content

Commit 94855e2

Browse files
committed
Pre-live technical hardening
1 parent b1b766a commit 94855e2

73 files changed

Lines changed: 8622 additions & 7066 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,23 @@ jobs:
6565

6666
- name: Security gate
6767
run: ./scripts/security-gate.sh
68+
69+
architecture:
70+
runs-on: ubuntu-latest
71+
steps:
72+
- name: Checkout
73+
uses: actions/checkout@v4
74+
75+
- name: Install ripgrep
76+
run: sudo apt-get update && sudo apt-get install -y ripgrep
77+
78+
- name: Architecture guard
79+
run: ./scripts/architecture-guard.sh
80+
81+
- name: Go formatting guard
82+
run: |
83+
unformatted="$(gofmt -l backend/cmd/server/*.go)"
84+
if [ -n "$unformatted" ]; then
85+
echo "$unformatted"
86+
exit 1
87+
fi

backend/Dockerfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags='-s -w' -o
2424
FROM gcr.io/distroless/static-debian12
2525
WORKDIR /app
2626

27+
ARG RSM_VERSION=dev
28+
ARG RSM_COMMIT=
29+
2730
COPY --from=backend-build /out/retrosave-api /usr/local/bin/retrosave-api
2831
COPY --from=backend-build /out/retrosave-healthcheck /usr/local/bin/retrosave-healthcheck
2932
COPY --from=frontend-build /src/frontend/dist /app/frontend-dist
@@ -35,6 +38,8 @@ ENV SAVE_ROOT=/saves
3538
ENV STATE_ROOT=/config
3639
ENV FRONTEND_DIST_DIR=/app/frontend-dist
3740
ENV CHEAT_PACK_ROOT=/app/contracts/cheats/packs
41+
ENV RSM_VERSION=${RSM_VERSION}
42+
ENV RSM_COMMIT=${RSM_COMMIT}
3843

3944
EXPOSE 80
4045

backend/cmd/server/agent_api_handlers.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ func registerAgentRoutes(r chi.Router, app *app, prefix string) {
2323
}
2424

2525
func mountAgentRoutes(r chi.Router, app *app) {
26+
r.Get("/runtime-config", app.handleRuntimeConfig)
2627
r.Get("/overview", app.handleAgentOverview)
2728
r.Get("/sync/status", app.handleAgentSyncStatus)
2829
r.Get("/systems", app.handleAgentSystems)

backend/cmd/server/app_state.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ type app struct {
5858
playStationStore *playStationStore
5959
n64ControllerPakStoreRef *n64ControllerPakStore
6060
saveRecords []saveRecord
61+
saveIndex saveRecordIndex
6162
enricher *gameEnricher
6263
conflicts map[string]conflictRecord
6364
syncLogs []syncLogRecord
@@ -315,6 +316,7 @@ func (a *app) createSave(input saveCreateInput) (saveRecord, error) {
315316
a.mu.Lock()
316317
a.saveRecords = append([]saveRecord{record}, a.saveRecords...)
317318
a.saves = append([]saveSummary{record.Summary}, a.saves...)
319+
a.saveIndex = buildSaveRecordIndex(a.saveRecords)
318320
a.mu.Unlock()
319321
return record, nil
320322
}
@@ -342,6 +344,7 @@ func (a *app) reloadSavesFromDisk() error {
342344
a.mu.Lock()
343345
a.saveRecords = records
344346
a.saves = summaries
347+
a.saveIndex = buildSaveRecordIndex(records)
345348
a.mu.Unlock()
346349
return nil
347350
}
@@ -361,6 +364,9 @@ func conflictKey(romSHA1, slotName string) string {
361364
func (a *app) latestSaveRecord(romSHA1, slotName string) (saveRecord, bool) {
362365
a.mu.Lock()
363366
defer a.mu.Unlock()
367+
if record, ok := a.saveIndex.latestByROMSlot(romSHA1, slotName); ok {
368+
return record, true
369+
}
364370
return latestSaveRecordLocked(a.saveRecords, romSHA1, slotName)
365371
}
366372

backend/cmd/server/contract_test.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/base64"
66
"fmt"
77
"net/http"
8+
"net/url"
89
"os"
910
"strings"
1011
"testing"
@@ -113,6 +114,106 @@ func TestContractSaveLatestMissingAndSuccessShape(t *testing.T) {
113114
}
114115
}
115116

117+
func TestContractSaveLatestFallsBackToCanonicalTrackContext(t *testing.T) {
118+
h := newContractHarness(t)
119+
120+
upload := uploadSave(t, h, "/saves", map[string]string{
121+
"rom_sha1": "pokemon-cloud-rom",
122+
"slotName": "default",
123+
"system": "gameboy",
124+
}, "Pokemon Noise Loop.srm", buildNonBlankPayload(8192, 0x23))
125+
saveObj := mustObject(t, upload["save"], "save")
126+
wantID := mustString(t, saveObj["id"], "save.id")
127+
wantSHA := mustString(t, saveObj["sha256"], "save.sha256")
128+
129+
withoutContext := h.request(http.MethodGet, "/save/latest?romSha1=pokemon-local-rom&slotName=default", nil)
130+
assertStatus(t, withoutContext, http.StatusOK)
131+
withoutBody := decodeJSONMap(t, withoutContext.Body)
132+
if mustBool(t, withoutBody["exists"], "exists") {
133+
t.Fatalf("expected exact latest miss without track context: %s", prettyJSON(withoutBody))
134+
}
135+
136+
query := url.Values{}
137+
query.Set("romSha1", "pokemon-local-rom")
138+
query.Set("slotName", "default")
139+
query.Set("filename", "Pokemon Noise Loop.srm")
140+
query.Set("system", "gameboy")
141+
withContext := h.request(http.MethodGet, "/save/latest?"+query.Encode(), nil)
142+
assertStatus(t, withContext, http.StatusOK)
143+
withBody := decodeJSONMap(t, withContext.Body)
144+
if !mustBool(t, withBody["exists"], "exists") {
145+
t.Fatalf("expected latest fallback hit with track context: %s", prettyJSON(withBody))
146+
}
147+
if got := mustString(t, withBody["id"], "id"); got != wantID {
148+
t.Fatalf("expected fallback id %q, got %q", wantID, got)
149+
}
150+
if got := mustString(t, withBody["sha256"], "sha256"); got != wantSHA {
151+
t.Fatalf("expected fallback sha %q, got %q", wantSHA, got)
152+
}
153+
if indexed, ok := h.app.findSaveRecordByID(wantID); !ok || indexed.Summary.SHA256 != wantSHA {
154+
t.Fatalf("expected indexed lookup to match uploaded save, got ok=%v record=%+v", ok, indexed.Summary)
155+
}
156+
}
157+
158+
func TestContractRuntimeConfigShapeAndAuthMode(t *testing.T) {
159+
t.Setenv("AUTH_MODE", "disabled")
160+
t.Setenv("RSM_VERSION", "test-version")
161+
t.Setenv("RSM_COMMIT", "test-commit")
162+
h := newContractHarness(t)
163+
164+
rr := h.request(http.MethodGet, "/api/runtime-config", nil)
165+
assertStatus(t, rr, http.StatusOK)
166+
assertJSONContentType(t, rr)
167+
168+
body := decodeJSONMap(t, rr.Body)
169+
if !mustBool(t, body["success"], "success") {
170+
t.Fatalf("expected success=true")
171+
}
172+
runtime := mustObject(t, body["runtime"], "runtime")
173+
if mustString(t, runtime["appName"], "runtime.appName") != "RetroSaveManager" {
174+
t.Fatalf("unexpected app name: %s", prettyJSON(runtime))
175+
}
176+
if got := mustString(t, runtime["authMode"], "runtime.authMode"); got != "disabled" {
177+
t.Fatalf("unexpected auth mode %q", got)
178+
}
179+
if mustBool(t, runtime["authEnabled"], "runtime.authEnabled") {
180+
t.Fatalf("expected authEnabled=false when AUTH_MODE=disabled")
181+
}
182+
if got := mustString(t, runtime["version"], "runtime.version"); got != "test-version" {
183+
t.Fatalf("unexpected version %q", got)
184+
}
185+
if got := mustString(t, runtime["commit"], "runtime.commit"); got != "test-commit" {
186+
t.Fatalf("unexpected commit %q", got)
187+
}
188+
features := mustObject(t, runtime["features"], "runtime.features")
189+
if !mustBool(t, features["selfHosted"], "features.selfHosted") {
190+
t.Fatalf("expected selfHosted feature")
191+
}
192+
if mustBool(t, features["publicSignup"], "features.publicSignup") {
193+
t.Fatalf("public signup should be disabled for self-host release")
194+
}
195+
warnings := mustArray(t, runtime["warnings"], "runtime.warnings")
196+
if len(warnings) == 0 {
197+
t.Fatalf("expected LAN-only warning when auth is disabled")
198+
}
199+
}
200+
201+
func TestContractRuntimeConfigAuthEnabled(t *testing.T) {
202+
t.Setenv("AUTH_MODE", "enabled")
203+
h := newContractHarness(t)
204+
205+
rr := h.request(http.MethodGet, "/runtime-config", nil)
206+
assertStatus(t, rr, http.StatusOK)
207+
body := decodeJSONMap(t, rr.Body)
208+
runtime := mustObject(t, body["runtime"], "runtime")
209+
if got := mustString(t, runtime["authMode"], "runtime.authMode"); got != "enabled" {
210+
t.Fatalf("unexpected auth mode %q", got)
211+
}
212+
if !mustBool(t, runtime["authEnabled"], "runtime.authEnabled") {
213+
t.Fatalf("expected authEnabled=true when AUTH_MODE=enabled")
214+
}
215+
}
216+
116217
func TestContractSaveLatestSkipsBrokenPayloadRecords(t *testing.T) {
117218
h := newContractHarness(t)
118219

backend/cmd/server/dreamcast_vmu_test.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -172,12 +172,12 @@ func TestContractSavesMultipartAcceptsDreamcastVMUImage(t *testing.T) {
172172
helperKey := createHelperAppPassword(t, h, "", "dreamcast-helper")
173173

174174
uploadSave(t, h, "/saves", map[string]string{
175-
"app_password": helperKey,
176-
"rom_sha1": "dc-line:dreamcast:mister:a1",
177-
"slotName": "A1",
178-
"system": "dreamcast",
179-
"device_type": "mister",
180-
"fingerprint": "dreamcast-device",
175+
"app_password": helperKey,
176+
"rom_sha1": "dc-line:dreamcast:mister:a1",
177+
"slotName": "A1",
178+
"system": "dreamcast",
179+
"device_type": "mister",
180+
"fingerprint": "dreamcast-device",
181181
"runtimeProfile": "dreamcast/mister",
182182
}, "Sonic Adventure 2.A1.bin", buildDreamcastVMUWithSingleSave())
183183

@@ -210,12 +210,12 @@ func TestContractSavesMultipartRejectsEmptyDreamcastVMUImage(t *testing.T) {
210210
helperKey := createHelperAppPassword(t, h, "", "dreamcast-helper")
211211

212212
rr := h.multipart("/saves", map[string]string{
213-
"app_password": helperKey,
214-
"rom_sha1": "dc-line:dreamcast:mister:a1",
215-
"slotName": "A1",
216-
"system": "dreamcast",
217-
"device_type": "mister",
218-
"fingerprint": "dreamcast-device",
213+
"app_password": helperKey,
214+
"rom_sha1": "dc-line:dreamcast:mister:a1",
215+
"slotName": "A1",
216+
"system": "dreamcast",
217+
"device_type": "mister",
218+
"fingerprint": "dreamcast-device",
219219
"runtimeProfile": "dreamcast/mister",
220220
}, "file", "Sonic Adventure 2.A1.bin", buildDreamcastEmptyVMU())
221221
assertStatus(t, rr, http.StatusUnprocessableEntity)

backend/cmd/server/dto.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,39 @@ type apiError struct {
2525
StatusCode int `json:"statusCode"`
2626
}
2727

28+
type runtimeConfigResponse struct {
29+
Success bool `json:"success"`
30+
Runtime runtimeConfig `json:"runtime"`
31+
}
32+
33+
type runtimeConfig struct {
34+
AppName string `json:"appName"`
35+
AuthMode string `json:"authMode"`
36+
AuthEnabled bool `json:"authEnabled"`
37+
BaseURL string `json:"baseUrl"`
38+
Version string `json:"version"`
39+
Commit string `json:"commit"`
40+
Features runtimeConfigFeatures `json:"features"`
41+
Warnings []string `json:"warnings"`
42+
}
43+
44+
type runtimeConfigFeatures struct {
45+
SelfHosted bool `json:"selfHosted"`
46+
PublicSignup bool `json:"publicSignup"`
47+
HelperPairing bool `json:"helperPairing"`
48+
SaveValidation bool `json:"saveValidation"`
49+
RuntimeModules bool `json:"runtimeModules"`
50+
CloudMultiTenant bool `json:"cloudMultiTenant"`
51+
}
52+
53+
type saveListResponse struct {
54+
Success bool `json:"success"`
55+
Saves []saveSummary `json:"saves"`
56+
Total int `json:"total"`
57+
Limit int `json:"limit"`
58+
Offset int `json:"offset"`
59+
}
60+
2861
type lookupQuery struct {
2962
Type string `json:"type"`
3063
Value json.RawMessage `json:"value"`

backend/cmd/server/routes.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ func registerCompatRoutes(r chi.Router, app *app, prefix string) {
5959

6060
func mountCompatRoutes(r chi.Router, app *app) {
6161
r.Get("/stripe/status", handleUnsupportedBillingAlias)
62+
r.Get("/runtime-config", app.handleRuntimeConfig)
6263

6364
r.Post("/auth/login", app.handleAuthLogin)
6465
r.Post("/auth/signup", app.handleAuthSignup)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package main
2+
3+
import (
4+
"net/http"
5+
"os"
6+
"strings"
7+
)
8+
9+
func (a *app) handleRuntimeConfig(w http.ResponseWriter, r *http.Request) {
10+
_ = requestPrincipal(r)
11+
mode := authMode()
12+
writeJSON(w, http.StatusOK, runtimeConfigResponse{
13+
Success: true,
14+
Runtime: runtimeConfig{
15+
AppName: "RetroSaveManager",
16+
AuthMode: mode,
17+
AuthEnabled: mode != "disabled",
18+
BaseURL: baseURLForRequest(r),
19+
Version: runtimeConfigValue("RSM_VERSION", "dev"),
20+
Commit: runtimeConfigValue("RSM_COMMIT", ""),
21+
Features: runtimeConfigFeatures{
22+
SelfHosted: true,
23+
PublicSignup: false,
24+
HelperPairing: true,
25+
SaveValidation: true,
26+
RuntimeModules: true,
27+
CloudMultiTenant: false,
28+
},
29+
Warnings: runtimeConfigWarnings(mode),
30+
},
31+
})
32+
}
33+
34+
func runtimeConfigValue(key, fallback string) string {
35+
value := strings.TrimSpace(os.Getenv(key))
36+
if value == "" {
37+
return fallback
38+
}
39+
return value
40+
}
41+
42+
func runtimeConfigWarnings(mode string) []string {
43+
if mode == "disabled" {
44+
return []string{"Authentication is disabled. Keep this instance on a trusted LAN or protect it behind your own reverse proxy."}
45+
}
46+
return []string{}
47+
}

0 commit comments

Comments
 (0)