import { parseChannelLink, type ParsedChannelLink } from '@/lib/channelLink'; import { makeLink } from './links'; import { decryptInvitation, encryptInvitation } from './accountCrypto'; interface ChannelGrant { v: 1; channelId: string; mode: 'enc' | 'raw'; role: 'owner' | 'member'; keys: ParsedChannelLink['keys']; /** 仅供被邀请方在收件箱显示,在加密 payload 内,服务端不可见 */ name?: string; invitedBy?: string; } export interface InvitationMeta { name?: string; invitedBy?: string } export async function sealChannelGrant(link: string, recipient: string, publicKey: JsonWebKey, meta: InvitationMeta = {}): Promise { const parsed = parseChannelLink(link); if (!parsed.ok) throw new Error('空间凭据格式无效'); const grant: ChannelGrant = { v: 1, channelId: parsed.link.id, mode: parsed.link.mode, role: parsed.link.isOwner ? 'owner' : 'member', keys: parsed.link.keys, ...(meta.name ? { name: meta.name } : {}), ...(meta.invitedBy ? { invitedBy: meta.invitedBy } : {}) }; return encryptInvitation(JSON.stringify(grant), recipient, grant.channelId, publicKey); } /** 只读取显示用的元信息;解不开或没有这些字段时返回空,调用方回退到空间 ID */ export async function peekChannelGrant(payload: string, recipient: string, channelId: string, privateKey: JsonWebKey): Promise { try { const grant = JSON.parse(await decryptInvitation(payload, recipient, channelId, privateKey)) as Partial; return { ...(typeof grant.name === 'string' ? { name: grant.name } : {}), ...(typeof grant.invitedBy === 'string' ? { invitedBy: grant.invitedBy } : {}) }; } catch { return {}; } } export async function openChannelGrant(payload: string, recipient: string, channelId: string, privateKey: JsonWebKey, role: 'owner' | 'member', origin: string): Promise { const plain = await decryptInvitation(payload, recipient, channelId, privateKey); let grant: ChannelGrant; try { grant = JSON.parse(plain) as ChannelGrant; } catch { throw new Error('空间授权包格式无效'); } if (!grant || grant.v !== 1 || grant.channelId !== channelId || grant.role !== role || !['enc', 'raw'].includes(grant.mode) || !grant.keys || typeof grant.keys !== 'object') throw new Error('空间授权包校验失败'); const parsed = parseChannelLink(makeLink(origin, channelId, grant.keys)); if (!parsed.ok || parsed.link.mode !== grant.mode) throw new Error('空间授权包模式不匹配'); if (!parsed.ok || parsed.link.id !== channelId || parsed.link.isOwner !== (role === 'owner')) throw new Error('空间授权包权限不匹配'); return parsed.link; }