refactor(core): drop per-terminal device labels

Remove sealLabel/openLabel and their label constants; devices are no
longer named per channel. Rename the create setting seats to
max_members and point the e2e join helper at POST /join.
This commit is contained in:
justme committed 2026-10-14 22:19:24 +08:00
1 parent d946fc5e8a
commit 5fd4ddd411
6 files changed
+12 -171

No files matched your search

+2 -34
View File
@@ -114,7 +114,7 @@ async function createChannel(opts: Parameters<typeof ChannelSession.create>[0] =
}
async function joinChannel(session: ChannelSession, role: AuthRole = 'read') {
const res = await fetch(api(`/channels/${session.id}/seats`), { method: 'POST', headers: headersFor(session, role) });
const res = await fetch(api(`/channels/${session.id}/join`), { method: 'POST', headers: headersFor(session, role) });
expect(res.status, await res.clone().text()).toBe(200);
}
@@ -390,38 +390,6 @@ suite('端到端(真实服务端)', () => {
expect(Buffer.compare(concatAll([part1.bytes, part2]), content)).toBe(0);
}, 60_000);
it('设备名:成员加密上传,所有者在席位列表里解开', async () => {
const owner = await createChannel();
const member = await ChannelSession.open(owner.link(ORIGIN));
await joinChannel(member, 'read');
const blob = await member.sealLabel('我的 iPhone');
const put = await fetch(api(`/channels/${owner.id}/seats/me`), {
method: 'PUT',
headers: { ...headersFor(member, 'read'), 'Content-Type': 'application/octet-stream' },
body: blob,
});
expect(put.status).toBe(204);
const res = await fetch(api(`/channels/${owner.id}/seats`), { headers: headersFor(owner, 'owner') });
const seats = (await res.json()) as Array<{ endpoint_hash: string; label?: string }>;
const mine = seats.find((s) => s.endpoint_hash === toBase64Url(member.endpointHash))!;
expect(await owner.openLabel(mine.label!, mine.endpoint_hash)).toBe('我的 iPhone');
// 服务端存的是密文
expect(Buffer.from(mine.label!, 'base64url').toString('utf8')).not.toContain('iPhone');
});
it('最长的设备名(228 字节)也被服务端接受', async () => {
const owner = await createChannel();
const blob = await owner.sealLabel('x'.repeat(228));
const put = await fetch(api(`/channels/${owner.id}/seats/me`), {
method: 'PUT',
headers: { ...headersFor(owner, 'owner'), 'Content-Type': 'application/octet-stream' },
body: blob,
});
expect(put.status).toBe(204);
});
it('raw 频道:匿名成员无需令牌即可读取,服务端看得到明文', async () => {
const owner = await createChannel({ mode: 'raw' });
const member = await ChannelSession.open(owner.link(ORIGIN));
@@ -448,7 +416,7 @@ suite('端到端(真实服务端)', () => {
const member = await ChannelSession.open(owner.link(ORIGIN));
await joinChannel(member, 'read');
const res = await fetch(api(`/channels/${owner.id}/seats`), { headers: { ...headersFor(member, 'read') } });
const res = await fetch(api(`/channels/${owner.id}/guests`), { headers: { ...headersFor(member, 'read') } });
expect(res.status).toBe(403);
expect(() => headersFor(member, 'owner')).toThrow();
});
+2 -36
View File
@@ -3,7 +3,6 @@ import { fromBase64Url, toBase64Url } from '../src/bytes.js';
import {
CapabilityError,
ChannelSession,
IntegrityError,
LimitError,
LinkError,
ModeMismatchError,
@@ -61,8 +60,8 @@ describe('创建 enc 频道', () => {
});
it('设置原样放进请求体', async () => {
const { request } = await ChannelSession.create({ settings: { seats: 3, delete_policy: 'anyone' } });
expect(request.body.settings).toEqual({ seats: 3, delete_policy: 'anyone' });
const { request } = await ChannelSession.create({ settings: { max_members: 3, delete_policy: 'anyone' } });
expect(request.body.settings).toEqual({ max_members: 3, delete_policy: 'anyone' });
});
});
@@ -281,39 +280,6 @@ describe('发送和接收(enc)', () => {
});
});
describe('设备名', () => {
it('所有者能读出成员的设备名,其他人换个终端哈希读不出', async () => {
const { session: owner } = await ChannelSession.create();
const member = await ChannelSession.open(owner.link(ORIGIN));
const blob = await member.sealLabel('办公室电脑');
expect(await owner.openLabel(blob, member.endpointHash)).toBe('办公室电脑');
expect(await owner.openLabel(toBase64Url(blob), toBase64Url(member.endpointHash))).toBe('办公室电脑');
await expect(owner.openLabel(blob, owner.endpointHash)).rejects.toThrowError(IntegrityError);
});
it('每次加密使用新的随机 IV', async () => {
const { session } = await ChannelSession.create();
const a = await session.sealLabel('x');
const b = await session.sealLabel('x');
expect(toHex(a.slice(0, 12))).not.toBe(toHex(b.slice(0, 12)));
});
it('篡改后解不开', async () => {
const { session } = await ChannelSession.create();
const blob = await session.sealLabel('abc');
blob[blob.length - 1]! ^= 1;
await expect(session.openLabel(blob, session.endpointHash)).rejects.toThrowError(IntegrityError);
});
it('raw 模式下设备名是明文', async () => {
const { session } = await ChannelSession.create({ mode: 'raw' });
const blob = await session.sealLabel('plain');
expect(new TextDecoder().decode(blob)).toBe('plain');
expect(await session.openLabel(blob, session.endpointHash)).toBe('plain');
});
});
describe('raw 模式往返', () => {
it('单帧:保留统一元数据,没有加密标志', async () => {
const { session: owner } = await ChannelSession.create({ mode: 'raw' });
-24
View File
@@ -182,30 +182,6 @@ describe('raw 帧', () => {
});
});
describe('设备名', () => {
it('固定 IV 下的密文一致,并能解回', async () => {
const session = await openVectorSession();
for (const l of vectors.labels) {
const sealed = await session.sealLabel(l.name, { iv: h(l.iv) });
expect(toHex(sealed)).toBe(l.blob);
expect(sealed.length).toBeLessThanOrEqual(256);
expect(await session.openLabel(sealed, session.endpointHash)).toBe(l.name);
}
});
it('换一个终端的哈希就解不开(AAD 绑定终端)', async () => {
const session = await openVectorSession();
const sealed = await session.sealLabel('我的 iPhone');
await expect(session.openLabel(sealed, new Uint8Array(32))).rejects.toThrowError(IntegrityError);
});
it('超过 228 字节 UTF-8 时拒绝', async () => {
const session = await openVectorSession();
await expect(session.sealLabel('x'.repeat(229))).rejects.toThrow();
await expect(session.sealLabel('x'.repeat(228))).resolves.toHaveLength(256);
});
});
describe('链接', () => {
it('合法链接解析结果与向量一致', () => {
for (const v of vectors.links.valid) {