feat!: genesis-pinned operator set, server identity with an encrypted control link, key-less join codes; release key continuity
ci/woodpecker/tag/gitea-release Pipeline is pending
ci/woodpecker/tag/linux-release Pipeline failed
ci/woodpecker/tag/windows-release Pipeline was canceled

Join codes need no passkey touch and carry only public values (genesis
fingerprint, owner credentials, installer hashes). Nodes pin the genesis and
owners at enrollment and only follow passkey-signed operator set versions.
The panel's server key is listed in the set; pinned nodes require its
signature and run the control link under AES-256-GCM. Password sessions carry
no capabilities. The updater adopts a next_key from a signed manifest.

Bump version to 0.2.0. [CI SKIP]
This commit is contained in:
justme committed 2026-10-08 03:21:44 +08:00
1 parent 9bb6841dce
commit a2f88a344b
89 files changed
+2145 -1195

No files matched your search

@@ -61,8 +61,11 @@ public sealed class ApiBasicsTests
[Fact]
public async Task Login_WithReturnSession_GivesABearerSession_WithoutACookie_AndNoCsrfNeeded()
{
// 密码登录的会话没有任何能力(只能注册 passkey);有能力的会话来自 passkey 登录
(string username, SoftwarePasskey passkey) = await _backend.CreateUserWithPasskeyAsync();
using SoftwarePasskey passkeyScope = passkey;
using HttpClient client = _backend.NewClient();
using HttpResponseMessage login = await client.PostAsync("/api/admin/auth/login", BackendFixture.Json(new JsonObject { ["username"] = BackendFixture.AdminUser, ["password"] = BackendFixture.AdminPassword, ["return_session"] = true }));
using HttpResponseMessage login = await _backend.PasskeyLoginAsync(client, passkey);
Assert.Equal(HttpStatusCode.OK, login.StatusCode);
Assert.False(login.Headers.Contains("Set-Cookie"));
string session = JsonNode.Parse(await login.Content.ReadAsStringAsync())!["session_token"]!.GetValue<string>();
@@ -73,7 +76,7 @@ public sealed class ApiBasicsTests
me.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", session);
using HttpResponseMessage meResponse = await client.SendAsync(me);
Assert.Equal(HttpStatusCode.OK, meResponse.StatusCode);
Assert.Equal(BackendFixture.AdminUser, JsonNode.Parse(await meResponse.Content.ReadAsStringAsync())!["username"]!.GetValue<string>());
Assert.Equal(username, JsonNode.Parse(await meResponse.Content.ReadAsStringAsync())!["username"]!.GetValue<string>());
using var create = new HttpRequestMessage(HttpMethod.Post, "/api/admin/api-tokens") { Content = BackendFixture.Json(new JsonObject { ["name"] = "session-write", ["capabilities"] = new JsonArray("fleet.read") }) };
create.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", session);
@@ -94,8 +97,10 @@ public sealed class ApiBasicsTests
[Fact]
public async Task SessionCookie_RequiresCsrfHeaderOnUnsafeMethods()
{
(_, SoftwarePasskey passkey) = await _backend.CreateUserWithPasskeyAsync();
using SoftwarePasskey passkeyScope = passkey;
using HttpClient client = _backend.NewClient();
using HttpResponseMessage login = await client.PostAsync("/api/admin/auth/login", BackendFixture.Json(new JsonObject { ["username"] = BackendFixture.AdminUser, ["password"] = BackendFixture.AdminPassword }));
using HttpResponseMessage login = await _backend.PasskeyLoginAsync(client, passkey, returnSession: false);
string csrf = JsonNode.Parse(await login.Content.ReadAsStringAsync())!["csrf_token"]!.GetValue<string>();
string cookie = login.Headers.GetValues("Set-Cookie").Single().Split(';')[0];
@@ -220,15 +225,6 @@ public sealed class ApiBasicsTests
Assert.Contains("moshiro_agents_online", text);
Assert.Contains("moshiro_analysis_queue_length", text);
}
[Fact]
public async Task InstallScripts_RequireAValidCode()
{
// 一行安装脚本凭一次性代码获取;没有代码或代码不能用是 404(完整行为见 InstallTests)
using HttpResponseMessage response = await _backend.Anonymous.GetAsync("/install.sh");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
Assert.Equal("not_found", JsonNode.Parse(await response.Content.ReadAsStringAsync())!["code"]!.GetValue<string>());
}
}
[CollectionDefinition("backend")]