Files
OwnCord/Server/scripts/genprotocol/main.go
T
Claude 2e7a80171b feat(server,client): protocol codegen + audit quick-wins batch
Protocol codegen (decision D4, audit A-2026-07-08):
- Add docs/protocol-schema.json as the real single source of truth for
  WS message-type constants, making the long-standing 'generated from'
  comment in both constant files true.
- Add Server/scripts/genprotocol, a generator emitting both
  Server/ws/message_types.go and Client .../lib/protocolTypes.ts
  (constants byte-for-byte value-identical to before; only headers,
  ordering alignment, and provenance comments changed).
- Add make protocol-generate / protocol-verify and wire protocol-verify
  into CI next to sqlc-verify.

Quick wins (decision D8):
- admin: log LogAudit write failures in the backup handlers instead of
  discarding them (prior audit #10).
- api: fix self-contradictory upload Cache-Control to 'private,
  no-cache' per remediation plan W3-4; drop the now-unused
  fileCacheMaxAgeSeconds constant; update test.
- ws: route the hub settings cache through db.GetSetting instead of
  inline SQL.
- ws: fix a latent data race — main.go wires SetEventPersister and
  SetEventStore after NewRouter has already started the hub Run loop,
  which reads those fields on the broadcast/replay paths. They (and
  pluginSink, which one test sets post-Run) are now atomic pointers;
  the remaining pre-Run-only setters reject late calls with an error
  log instead of racing silently.

Update the audit closure table and decisions doc statuses accordingly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 13:32:58 +00:00

165 lines
5.6 KiB
Go

// genprotocol generates the WebSocket protocol message-type constant files
// for both the Go server and the TypeScript client from the single source of
// truth at docs/protocol-schema.json.
//
// Usage (from the Server/ directory):
//
// go run ./scripts/genprotocol
//
// or via make:
//
// make protocol-generate # regenerate both outputs
// make protocol-verify # fail if committed outputs are stale (CI)
package main
import (
"encoding/json"
"flag"
"fmt"
"go/format"
"log"
"os"
"strings"
)
type message struct {
Wire string `json:"wire"`
Go string `json:"go"`
TS string `json:"ts"`
Note string `json:"note,omitempty"`
GoTrailingComment string `json:"go_trailing_comment,omitempty"`
}
type schema struct {
Comment string `json:"$comment"`
Version int `json:"version"`
ClientToServer []message `json:"client_to_server"`
ServerToClient []message `json:"server_to_client"`
}
func main() {
schemaPath := flag.String("schema", "../docs/protocol-schema.json", "path to protocol-schema.json")
goOut := flag.String("go-out", "ws/message_types.go", "path to the generated Go file")
tsOut := flag.String("ts-out", "../Client/tauri-client/src/lib/protocolTypes.ts", "path to the generated TypeScript file")
flag.Parse()
raw, err := os.ReadFile(*schemaPath)
if err != nil {
log.Fatalf("read schema: %v", err)
}
var s schema
dec := json.NewDecoder(strings.NewReader(string(raw)))
dec.DisallowUnknownFields()
if err := dec.Decode(&s); err != nil {
log.Fatalf("parse schema: %v", err)
}
if err := validate(s); err != nil {
log.Fatalf("invalid schema: %v", err)
}
goSrc, err := renderGo(s)
if err != nil {
log.Fatalf("render Go: %v", err)
}
if err := os.WriteFile(*goOut, goSrc, 0o644); err != nil {
log.Fatalf("write %s: %v", *goOut, err)
}
if err := os.WriteFile(*tsOut, []byte(renderTS(s)), 0o644); err != nil {
log.Fatalf("write %s: %v", *tsOut, err)
}
fmt.Printf("generated %s (%d client→server, %d server→client) and %s\n",
*goOut, len(s.ClientToServer), len(s.ServerToClient), *tsOut)
}
// validate rejects duplicate identifiers and empty fields early so a bad
// schema edit fails the generator instead of producing broken output.
func validate(s schema) error {
goNames := map[string]bool{}
for _, list := range [][]message{s.ClientToServer, s.ServerToClient} {
tsNames := map[string]bool{}
for _, m := range list {
if m.Wire == "" || m.Go == "" || m.TS == "" {
return fmt.Errorf("entry %+v: wire, go, and ts are all required", m)
}
if goNames[m.Go] {
return fmt.Errorf("duplicate Go constant %q", m.Go)
}
goNames[m.Go] = true
if tsNames[m.TS] {
return fmt.Errorf("duplicate TS key %q within one direction", m.TS)
}
tsNames[m.TS] = true
}
}
return nil
}
func header(comment string) string {
return "// Code generated by scripts/genprotocol from docs/protocol-schema.json; DO NOT EDIT.\n" +
comment + "\n"
}
func renderGo(s schema) ([]byte, error) {
var b strings.Builder
b.WriteString(header("//\n// WebSocket protocol message type constants — single source of truth for\n" +
"// both Server (Go) and Client (TypeScript). Edit docs/protocol-schema.json\n" +
"// and run `make protocol-generate` (see Server/Makefile)."))
b.WriteString("\npackage ws\n\n")
writeBlock := func(title string, msgs []message) {
b.WriteString("// " + title + "\nconst (\n")
for _, m := range msgs {
b.WriteString("\t" + m.Go + " = " + fmt.Sprintf("%q", m.Wire))
switch {
case m.GoTrailingComment != "":
b.WriteString(" " + m.GoTrailingComment)
case m.Note != "":
b.WriteString(" // " + m.Note)
}
b.WriteString("\n")
}
b.WriteString(")\n")
}
writeBlock("Client → Server message types (received by handlers).", s.ClientToServer)
b.WriteString("\n")
writeBlock("Server → Client message types (sent in broadcasts/responses).", s.ServerToClient)
return format.Source([]byte(b.String()))
}
func renderTS(s schema) string {
var b strings.Builder
b.WriteString(header("//\n// Shared WebSocket protocol message type constants — single source of truth\n" +
"// for both Server (Go) and Client (TypeScript). Edit docs/protocol-schema.json\n" +
"// and run `make protocol-generate` in Server/.\n//\n" +
"// Usage: import { MessageType } from \"@lib/protocolTypes\";\n" +
"// ws.send({ type: MessageType.CHAT_SEND, payload: { ... } });"))
writeBlock := func(title, name string, msgs []message) {
b.WriteString("\n// " + strings.Repeat("-", 75) + "\n")
b.WriteString("// " + title + "\n")
b.WriteString("// " + strings.Repeat("-", 75) + "\n\n")
b.WriteString("export const " + name + " = {\n")
for _, m := range msgs {
b.WriteString(" " + m.TS + ": " + fmt.Sprintf("%q", m.Wire) + ",")
if m.Note != "" {
b.WriteString(" // " + m.Note)
}
b.WriteString("\n")
}
b.WriteString("} as const;\n\n")
b.WriteString("export type " + name + "Value = (typeof " + name + ")[keyof typeof " + name + "];\n")
}
writeBlock("Server → Client message types", "ServerMessageType", s.ServerToClient)
writeBlock("Client → Server message types", "ClientMessageType", s.ClientToServer)
b.WriteString("\n// " + strings.Repeat("-", 75) + "\n")
b.WriteString("// Unified MessageType — all message types in one object for convenience\n")
b.WriteString("// " + strings.Repeat("-", 75) + "\n\n")
b.WriteString("export const MessageType = {\n ...ServerMessageType,\n ...ClientMessageType,\n} as const;\n\n")
b.WriteString("export type MessageTypeValue = (typeof MessageType)[keyof typeof MessageType];\n")
return b.String()
}