refactor(host): single-source ChannelPolicy, space-signed file grants, whole-file registration

- all authorization rules (owner/creator, per-channel read/write grants, who may
  remove whom, delete rules, structure management) live in ChannelPolicy; a test
  fails if they are reimplemented elsewhere
- ChannelInfo carries what the caller may do (can_*), member and invitation lists
  say who can be removed; clients display it and never derive it
- the host no longer stores or calls the file service: it only signs short-lived
  tickets from space permissions (create/put/get/delete/domain), and keeps an opaque
  fileDomain reference on the channel
- a whole file is registered in one request (last frame header + per-frame length and
  digest) instead of one request per 1 MiB frame; the signed manifest still has to
  match every digest
- drop file quota/threshold, receipts, reconcile and delete-queue code from the host
- contract generator emits constants for enums, error codes, token roles, permissions
  and share states
This commit is contained in:
justme committed 2026-10-19 13:38:40 +08:00
1 parent ccb2c074d6
commit 17638b6e20
36 files changed
+926 -589

No files matched your search

@@ -4,6 +4,13 @@ namespace WebDrop.Core.Tests;
public sealed class ChannelCryptoTests
{
/// <summary>文件域的对象主后端只登记头部和摘要,字节不在这里:一个单帧文件</summary>
private static Task Register(ChannelService svc, Id16 id, Credentials creds, byte[] frame)
{
svc.RegisterFileItem(id, creds, frame.AsMemory(0, Envelope.HeaderSize), [new ChannelService.FrameEntry(frame.Length - Envelope.HeaderSize, System.Security.Cryptography.SHA256.HashData(frame))], null);
return Task.CompletedTask;
}
[Fact]
public void ClientMayBootstrapButCannotChooseAChannelEpochOrGrantAccess()
{
@@ -81,7 +88,7 @@ public sealed class ChannelCryptoTests
var channelId = env.Service.GetWorkspace(space.Id, owner).Channels.Single().Id;
var oldFile = TestEnv.NewId();
env.Service.DeclareObject(space.Id, owner, new ObjectEnvelope { ItemId = oldFile.ToBase64Url(), ChannelId = channelId, Domain = CryptoDomain.Files, Epoch = 0, KeyEnvelope = "" });
await env.Service.AppendFrameAsync(space.Id, owner, TestEnv.Frame(oldFile, 0, FrameFlags.Final, 16), null);
await Register(env.Service, space.Id, owner, TestEnv.Frame(oldFile, 0, FrameFlags.Final, 16));
env.Service.MoveWorkspaceItems(space.Id, owner, [oldFile.ToBase64Url()], channelId, null);
env.Service.SetChannelHistory(space.Id, owner, channelId, chat: false, files: false);
@@ -98,22 +105,6 @@ public sealed class ChannelCryptoTests
Assert.Single((await env.Service.ReadLogAsync(space.Id, member, 0, 100)).Records);
}
[Fact]
public async Task AnUnfinishedUploadCanBeDeletedWithoutASignedEvent()
{
using var env = new TestEnv(); var ownerEndpoint = TestEnv.Endpoint(); var memberEndpoint = TestEnv.Endpoint();
var space = env.Create(ownerEndpoint, ChannelMode.Raw); var owner = space.Owner(ownerEndpoint) with { RequireSignedContent = true };
var channelId = env.Service.GetWorkspace(space.Id, owner).Channels.Single().Id;
var member = space.Member(memberEndpoint) with { AccountName = "member", RequireSignedContent = true }; env.Service.Join(space.Id, member);
var file = TestEnv.NewId();
env.Service.DeclareObject(space.Id, member, new ObjectEnvelope { ItemId = file.ToBase64Url(), ChannelId = channelId, Domain = CryptoDomain.Files, Epoch = 0, KeyEnvelope = "" });
// 只传了第 0 帧,没有末帧:上传被打断
await env.Service.AppendFrameAsync(space.Id, member, TestEnv.Frame(file, 0, FrameFlags.Gated, 16), null);
Assert.Throws<WebDropException>(() => env.Service.DeleteItem(space.Id, space.Member(TestEnv.Endpoint()) with { AccountName = "stranger", RequireSignedContent = true }, file));
env.Service.DeleteItem(space.Id, owner, file);
Assert.Null(env.Service.ReadCrypto(space.Id, owner).Objects.Find(o => o.ItemId == file.ToBase64Url())?.Event);
}
[Fact]
public void MemberOnlySeesChannelsItWasGrantedAndNeverTheirKeysObjectsOrWrites()
{