mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
* feat(b2-2): declare protocol_epoch in the schema and generate both constants protocol/schema.json gains protocol_epoch (1). genprotocol emits ws.ProtocolEpoch and PROTOCOL_EPOCH from it; the contract test pins the Go constant to the schema so a stale regeneration fails the required check. * feat(b2-2): check the client's protocol epoch in the auth handshake The auth payload gains epoch (absent = 0). Outside [minClientEpoch, ProtocolEpoch] the server answers one auth_error with code protocol_epoch_unsupported, the client/server/min epochs, and a message naming which side to update, then closes 1008 like every other handshake failure. minClientEpoch is 0 for epoch 1 only so alpha.4 clients keep connecting; the epoch-1 fixtures are unchanged. * feat(b2-2): send the protocol epoch and offer the update on a refused connect ws.ts sends epoch: PROTOCOL_EPOCH in the auth frame (contract test extended on purpose). On auth_error code protocol_epoch_unsupported with a newer server the dispatcher records the host in ui.store.updateRequiredHost and main.ts mounts the UpdateNotifier on the connect page, so a refused client gets the same Update Now banner it would have had on the main page. * feat(b2-2): withhold client releases newer than the server's protocol epoch The signed server-update manifest gains protocol_epoch (release.yml reads it from protocol/schema.json). Updater.ReleaseProtocolEpoch verifies the manifest and reads it; the client-update endpoint answers 204 when the release's epoch is newer than ws.ProtocolEpoch or the manifest does not verify. Releases without a manifest are epoch 0 and advertised as before. Docs: protocol.md Compatibility section, api.md, deployment.md, protocol README, CHANGELOG Unreleased. * docs(b2-2): record the slim B2-2 decision and evidence; fold B2-3/B2-4 into it * ci: prove the protocol_epoch manifest read on every PR, not only at tag time * fix(b2-2): offer the update on an already-mounted connect page and keep the credential on a protocol refusal Codex P1: on a first login or startup auto-login no overlay exists before auth_ok, so a refusal never re-rendered the connect page and the one-time read of updateRequiredHost missed it. The connect page now subscribes to it, and a later refusal replaces the banner. Codex P2: a refusal on reconnect went through the generic logout and deleted the stored credential although the token is still valid. clearAuth gets a protocol_epoch reason; main.ts keeps the credential on it (the skip-auto-login flag is still set and, being sessionStorage, does not survive the relaunch the update triggers).
175 lines
6.1 KiB
Go
175 lines
6.1 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 protocol/schema.json.
|
|
//
|
|
// Usage (from the Server/ directory):
|
|
//
|
|
// go run ./cmd/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"`
|
|
ProtocolEpoch int `json:"protocol_epoch"`
|
|
ClientToServer []message `json:"client_to_server"`
|
|
ServerToClient []message `json:"server_to_client"`
|
|
}
|
|
|
|
func main() {
|
|
schemaPath := flag.String("schema", "../protocol/schema.json", "path to the protocol schema")
|
|
goOut := flag.String("go-out", "ws/message_types.go", "path to the generated Go file")
|
|
tsOut := flag.String("ts-out", "../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 {
|
|
if s.ProtocolEpoch < 1 {
|
|
return fmt.Errorf("protocol_epoch must be >= 1, got %d", s.ProtocolEpoch)
|
|
}
|
|
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 cmd/genprotocol from 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 protocol/schema.json\n" +
|
|
"// and run `make protocol-generate` (see Server/Makefile)."))
|
|
b.WriteString("\npackage ws\n\n")
|
|
b.WriteString("// ProtocolEpoch is the wire epoch this server speaks. The auth handshake\n")
|
|
b.WriteString("// negotiates on it (serve_auth.go); see docs/protocol.md, Compatibility.\n")
|
|
fmt.Fprintf(&b, "const ProtocolEpoch = %d\n\n", s.ProtocolEpoch)
|
|
|
|
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 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")
|
|
}
|
|
|
|
b.WriteString("\n// The wire epoch this client speaks; sent in the auth frame and checked by\n")
|
|
b.WriteString("// the server. See docs/protocol.md, Compatibility.\n")
|
|
fmt.Fprintf(&b, "export const PROTOCOL_EPOCH = %d;\n", s.ProtocolEpoch)
|
|
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()
|
|
}
|