fix(node): clear consumed invitations after confirmed pairing

This commit is contained in:
DCCONSTRUCTIONS
2026-09-05 21:37:56 +03:00
parent e82d012907
commit 3616acc648
6 changed files with 41 additions and 3 deletions
+9
View File
@@ -81,6 +81,13 @@ func OpenPairing(store *Store, dir, version string, inventory func() Inventory)
if (p.state.Phase == "inviting" || p.state.Phase == "pending") && p.state.Invitation == nil {
return nil, errors.New("incomplete invitation")
}
if (p.state.Phase == "paired" || p.state.Phase == "revoked") && p.state.Invitation != nil {
next := p.state
next.Invitation = nil
if e := p.save(next); e != nil {
return nil, e
}
}
return p, nil
}
func savePrivateJSON(path string, value any) error {
@@ -186,6 +193,8 @@ func (p *Pairing) invite(address string) (map[string]any, error) {
if e := p.save(PairState{Schema: PairSchema, Phase: "inviting", Invitation: i, Revocations: p.state.Revocations}); e != nil {
return nil, e
}
p.connection = "offline"
p.lastSeen = 0
id, _ := p.store.Public()
code, _ := json.Marshal(map[string]any{"schema": PairSchema, "node_id": id, "id": i.ID, "endpoint": i.Endpoint, "expires_at": i.ExpiresAt, "secret": secret})
return map[string]any{"code": "MCN1." + base64.RawURLEncoding.EncodeToString(code), "expires_at": i.ExpiresAt}, nil
@@ -103,6 +103,9 @@ func TestPairingDurableCommitConflictAndReplay(t *testing.T) {
if _, e = restored.invite("192.168.10.4"); e == nil {
t.Fatal("paired Node offered another invitation")
}
if restored.state.Invitation != nil {
t.Fatal("consumed invitation retained")
}
raw, _ := json.Marshal(restored.status())
if strings.Contains(string(raw), i["secret"].(string)) || strings.Contains(string(raw), "client_pem") {
t.Fatal("status leaked trust")
@@ -138,6 +138,7 @@ func (p *Pairing) remoteHandler() http.Handler {
}
next := p.state
next.Phase = "paired"
next.Invitation = nil
if p.save(next) != nil {
http.Error(w, "State unavailable", 503)
return
+1 -1
View File
@@ -13,7 +13,7 @@ import tarfile
ROOT = Path(__file__).resolve().parents[1]
VERSION = "0.5.0"
VERSION = "0.5.1"
BRAND_SHA256 = "8bfee8ca9f98e0db48d98aae3af4b32493b8593e18b064a0239d513d824182af"
@@ -57,7 +57,7 @@ export function CoreConnectionView({ failure }: { failure: (error: unknown) => v
<Select label="Адрес БК для подключения" value={address} options={value.addresses.map(item => ({ value: item, label: item }))} onChange={setAddress} disabled={pending || !available} />
{value.addresses.length === 0 && <p className="node-note">Подключите БК к частной сети. Доступные адреса появятся автоматически.</p>}
<Button disabled={pending || !available || !address} onClick={() => void create()}>{pending ? "Создаём…" : value.phase === "inviting" ? "Создать новое приглашение" : "Создать приглашение"}</Button>
{value.invitation && <p className="node-note">Действует до {new Date(value.invitation.expires_at * 1000).toLocaleTimeString("ru-RU")}. Приглашение позволяет одному Core получить доверие этого БК. Передавайте его только нужному оператору.</p>}
{value.phase === "inviting" && value.invitation && <p className="node-note">Действует до {new Date(value.invitation.expires_at * 1000).toLocaleTimeString("ru-RU")}. Приглашение позволяет одному Core получить доверие этого БК. Передавайте его только нужному оператору.</p>}
{code && <><TextAreaField label="Код приглашения" value={code} readOnly rows={5} spellCheck={false} onFocus={event => event.target.select()} /><Button onClick={async () => { try { await navigator.clipboard.writeText(code); setNotice("Код скопирован"); } catch { setNotice("Выделите код в поле и скопируйте его сочетанием Ctrl+C."); } }}>Скопировать код</Button></>}
{value.phase === "inviting" && !code && <p className="node-note">Код показывается только при создании. Создайте новое приглашение, если он не сохранился у вас.</p>}
</div>}