feat: Z34b restore-by-host + harden web nginx DNS race

Add HMAC restore-by-host for phone-less rebind; resolve gateway at request time and recreate web after stack up.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
whm
2026-08-07 00:16:08 +08:00
parent f0fcc1fbda
commit e571e98387
12 changed files with 492 additions and 30 deletions

View File

@@ -35,6 +35,43 @@ func TestVerifyBadSig(t *testing.T) {
}
}
func TestSignVerifyRestoreNoPhone(t *testing.T) {
secret := "test-secret"
now := time.Unix(1_700_000_000, 0).UTC()
c := Claims{
Iss: "yuheng", Aud: "aijianzhan", HostKey: "6655aabbccddeeff00112233",
Exp: now.Add(90 * time.Second).Unix(), JTI: "jti-restore", Scope: "sync_restore",
YuhengUserID: "6655aabbccddeeff00112233",
}
tok, err := Sign(secret, c)
if err != nil {
t.Fatal(err)
}
got, err := Verify(tok, VerifyOpts{
Secret: secret, Now: now, AllowEmptyPhone: true,
AllowedScopes: []string{"sync_bind", "sync_restore"},
})
if err != nil {
t.Fatal(err)
}
if got.HostKey != c.HostKey || got.Phone != "" {
t.Fatalf("claims mismatch: %+v", got)
}
// 默认 Verify须 phone应拒绝无 phone 的 sync_bindsync_restore 在默认 scopes 外也应拒绝
if _, err := Verify(tok, VerifyOpts{Secret: secret, Now: now}); err == nil {
t.Fatal("expected scope reject without AllowedScopes")
}
}
func TestSignRequiresPhoneForSyncBind(t *testing.T) {
_, err := Sign("s", Claims{
HostKey: "h", Exp: time.Now().Add(time.Minute).Unix(), JTI: "j", Scope: "sync_bind",
})
if err == nil {
t.Fatal("expected phone required for sync_bind")
}
}
func TestJTIConsume(t *testing.T) {
dir := t.TempDir()
st, err := NewJTIStore(dir)