feat(cloudflare): add DNS pointing integration

Implement Cloudflare connection management, pointing groups and members, asynchronous A-record reconciliation, node IP triggers, admin APIs, management pages, migrations, tests, and documentation.
This commit is contained in:
ryan
2026-08-04 12:32:37 +08:00
parent 99f0f63b99
commit 3edcdb9e9f
47 changed files with 7019 additions and 49 deletions
@@ -0,0 +1,60 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
// Package credential seals and opens OpenFlare integration credentials.
package credential
import (
"crypto/sha256"
"encoding/hex"
"errors"
"strings"
"github.com/Rain-kl/Wavelet/internal/infra/config"
"github.com/Rain-kl/Wavelet/pkg/util"
)
// Prefix identifies values encrypted with the current credential format.
const Prefix = "enc:v1:"
func encryptionKey() string {
if config.Config == nil || strings.TrimSpace(config.Config.App.SessionSecret) == "" {
return ""
}
sum := sha256.Sum256([]byte(config.Config.App.SessionSecret))
return hex.EncodeToString(sum[:])
}
// Seal trims and encrypts plaintext when a session secret is configured.
// Plaintext storage is preserved for installations without a session secret.
func Seal(plaintext string) (string, error) {
plaintext = strings.TrimSpace(plaintext)
if plaintext == "" {
return "", nil
}
key := encryptionKey()
if key == "" {
return plaintext, nil
}
encrypted, err := util.Encrypt(key, plaintext)
if err != nil {
return "", err
}
return Prefix + encrypted, nil
}
// Open decrypts a sealed value and accepts legacy plaintext values.
func Open(stored string) (string, error) {
stored = strings.TrimSpace(stored)
if stored == "" {
return "", nil
}
if !strings.HasPrefix(stored, Prefix) {
return stored, nil
}
key := encryptionKey()
if key == "" {
return "", errors.New("cannot decrypt sensitive field without session secret")
}
return util.Decrypt(key, strings.TrimPrefix(stored, Prefix))
}
@@ -0,0 +1,73 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package credential
import (
"strings"
"testing"
"github.com/Rain-kl/Wavelet/internal/infra/config"
)
func TestSealAndOpenSensitiveValue(t *testing.T) {
previous := config.Config.App.SessionSecret
config.Config.App.SessionSecret = "cloudflare-pointing-test-secret"
t.Cleanup(func() { config.Config.App.SessionSecret = previous })
sealed, err := Seal(`{"api_token":"secret-token"}`)
if err != nil {
t.Fatalf("Seal() error = %v", err)
}
if !strings.HasPrefix(sealed, Prefix) {
t.Fatalf("Seal() = %q, want prefix %q", sealed, Prefix)
}
if strings.Contains(sealed, "secret-token") {
t.Fatalf("Seal() = %q, want token redacted", sealed)
}
opened, err := Open(sealed)
if err != nil {
t.Fatalf("Open() error = %v", err)
}
if want := `{"api_token":"secret-token"}`; opened != want {
t.Errorf("Open(Seal(value)) = %q, want %q", opened, want)
}
}
func TestSealWithoutSessionSecretKeepsPlaintextCompatibility(t *testing.T) {
previous := config.Config.App.SessionSecret
config.Config.App.SessionSecret = ""
t.Cleanup(func() { config.Config.App.SessionSecret = previous })
sealed, err := Seal(" legacy-value ")
if err != nil {
t.Fatalf("Seal() error = %v", err)
}
if sealed != "legacy-value" {
t.Errorf("Seal() = %q, want %q", sealed, "legacy-value")
}
opened, err := Open(sealed)
if err != nil {
t.Fatalf("Open(plaintext) error = %v", err)
}
if opened != "legacy-value" {
t.Errorf("Open(plaintext) = %q, want %q", opened, "legacy-value")
}
}
func TestOpenEncryptedValueRequiresSessionSecret(t *testing.T) {
previous := config.Config.App.SessionSecret
config.Config.App.SessionSecret = "cloudflare-pointing-test-secret"
sealed, err := Seal("secret")
if err != nil {
t.Fatalf("Seal() error = %v", err)
}
config.Config.App.SessionSecret = ""
t.Cleanup(func() { config.Config.App.SessionSecret = previous })
if _, err := Open(sealed); err == nil {
t.Fatal("Open(encrypted) error = nil, want missing session secret error")
}
}