feat: customer-level credentials shared across all nodes (item 2)

One credential set per customer (UUID + 32-byte root relay key + shortId)
lives on the customer record; node bindings no longer store credentials.
SS2022 user keys derive deterministically from the root key by method
length (16/24/32-byte prefix), so a single password serves every protocol,
matching 3x-ui's per-client credential model. Reality public keys stay a
node property and are shown per binding in the credentials pane.

- store: load-time convergence — first binding's set wins; a converged
  password that is not a 32-byte root key is regenerated (uuid/shortId
  preserved); bindings are stripped; idempotent, covered by tests
- deploy: the orchestrator stamps the customer's set onto a build copy;
  subscriptions resolve the same set; the derivation applies in both the
  inbound builder and the URI builder (verified live: Alice's N1 link is
  byte-identical to the pre-change seed expectation, her N2 user key is
  exactly the root key's 16-byte prefix)
- API: GET /api/customers/:id/credentials returns {credentials, bindings};
  regenerate is now POST /api/customers/:id/credentials/regenerate and
  rotates the whole set, rebuilding every deployed node of the customer
- UI: credentials pane shows the single shared set plus per-binding
  node/protocol/Reality-public-key rows
- golden subscriptions fixture regenerated under one shared set (RF and
  SS entries byte-identical to before; five entries audited by hand)
- golden baseline re-captured for the new shapes; difftest normalizer now
  treats a base64 root key's prefix-truncation encodings and the Surge
  '='-mangled variants as the same secret
This commit is contained in:
aaaamoooo
2026-10-20 21:21:13 +00:00
parent bb8a0cb0c4
commit ad18e16ac6
16 changed files with 673 additions and 272 deletions
+7 -3
View File
@@ -108,13 +108,17 @@ func NodeClientURI(d *Data, nc *NodeClient) string {
if networkMode == "" {
networkMode = "ipv4"
}
creds := Credentials{}
if customer := findCustomer(d, nc.CustomerID); customer != nil && customer.Credentials != nil {
creds = *customer.Credentials
}
uri, ok := BuildClientURI(BuildClientURIParams{
Protocol: node.Protocol,
RelayServer: server,
RelayPort: node.Port,
ClientID: nc.Credentials.ClientID,
RelayPassword: nc.Credentials.RelayPassword,
ShortID: nc.Credentials.ShortID,
ClientID: creds.ClientID,
RelayPassword: creds.RelayPassword,
ShortID: creds.ShortID,
WSPath: xbDeref(node.WSPath),
SocksUser: NodeEmail(nc.CustomerID),
Reality: node.Reality,