@@ -6,6 +6,7 @@ package app
66import (
77 "archive/zip"
88 "database/sql"
9+ "encoding/json"
910 "errors"
1011 "fmt"
1112 "io/fs"
@@ -288,30 +289,33 @@ func (a *App) serveFoundryModuleManifest(c echo.Context) error {
288289 return echo .NewHTTPError (http .StatusNotFound , "module.json not found" )
289290 }
290291
291- baseURL := strings .TrimRight (a .Config .BaseURL , "/" )
292- content := string (data )
292+ var manifest map [string ]any
293+ if err := json .Unmarshal (data , & manifest ); err != nil {
294+ return echo .NewHTTPError (http .StatusInternalServerError , "invalid module.json" )
295+ }
293296
294- // Replace manifest and download URLs with this Chronicle instance's URLs.
295- content = strings .Replace (content ,
296- `"manifest": "https://raw.githubusercontent.com/keyxmakerx/Chronicle/main/foundry-module/module.json"` ,
297- fmt .Sprintf (`"manifest": "%s/foundry-module/module.json"` , baseURL ), 1 )
298- content = strings .Replace (content ,
299- `"download": "https://github.com/keyxmakerx/Chronicle/releases/download/foundry-v0.1.0/chronicle-sync.zip"` ,
300- fmt .Sprintf (`"download": "%s/foundry-module/chronicle-sync.zip"` , baseURL ), 1 )
297+ // Always rewrite manifest and download URLs to point at this instance.
298+ baseURL := strings .TrimRight (a .Config .BaseURL , "/" )
299+ manifest ["manifest" ] = baseURL + "/foundry-module/module.json"
300+ manifest ["download" ] = baseURL + "/foundry-module/chronicle-sync.zip"
301301
302- return c .JSONBlob (http .StatusOK , [] byte ( content ) )
302+ return c .JSON (http .StatusOK , manifest )
303303}
304304
305305// serveFoundryModuleZip dynamically zips the foundry-module/ directory and
306306// serves it as chronicle-sync.zip. Foundry VTT downloads this during module
307307// installation. The zip contains all files under a chronicle-sync/ root
308308// directory, which is the expected structure for Foundry module archives.
309+ // The module.json inside the zip gets its manifest/download URLs rewritten
310+ // to point at this Chronicle instance.
309311func (a * App ) serveFoundryModuleZip (c echo.Context ) error {
310312 moduleDir := "foundry-module"
311313 if _ , err := os .Stat (moduleDir ); err != nil {
312314 return echo .NewHTTPError (http .StatusNotFound , "foundry module directory not found" )
313315 }
314316
317+ baseURL := strings .TrimRight (a .Config .BaseURL , "/" )
318+
315319 c .Response ().Header ().Set ("Content-Type" , "application/zip" )
316320 c .Response ().Header ().Set ("Content-Disposition" , "attachment; filename=chronicle-sync.zip" )
317321 c .Response ().WriteHeader (http .StatusOK )
@@ -343,6 +347,16 @@ func (a *App) serveFoundryModuleZip(c echo.Context) error {
343347 if err != nil {
344348 return err
345349 }
350+
351+ // Rewrite module.json URLs inside the zip so the installed module
352+ // points back at this Chronicle instance for updates.
353+ if name == "module.json" {
354+ data , err = a .rewriteModuleManifest (data , baseURL )
355+ if err != nil {
356+ return err
357+ }
358+ }
359+
346360 _ , err = w .Write (data )
347361 return err
348362 })
@@ -352,6 +366,18 @@ func (a *App) serveFoundryModuleZip(c echo.Context) error {
352366 return zw .Close ()
353367}
354368
369+ // rewriteModuleManifest parses module.json and rewrites the manifest and
370+ // download URLs to point at the given baseURL.
371+ func (a * App ) rewriteModuleManifest (data []byte , baseURL string ) ([]byte , error ) {
372+ var manifest map [string ]any
373+ if err := json .Unmarshal (data , & manifest ); err != nil {
374+ return data , err
375+ }
376+ manifest ["manifest" ] = baseURL + "/foundry-module/module.json"
377+ manifest ["download" ] = baseURL + "/foundry-module/chronicle-sync.zip"
378+ return json .MarshalIndent (manifest , "" , " " )
379+ }
380+
355381// Start begins listening for HTTP requests on the configured port.
356382func (a * App ) Start () error {
357383 addr := fmt .Sprintf (":%d" , a .Config .Port )
0 commit comments