feat(web): add account workspaces and signed content flows
This commit is contained in:
1 parent
7b8d61d1ba
commit
b5cf3d5958
117 files changed
+2789
-1674
No files matched your search
@@ -1,12 +1,16 @@
|
||||
import { parseLogRecords, toBase64Url, type ChannelSession, type LogRecord, type ReceivedItem } from '@webdrop/core';
|
||||
import { getResource, saveResource, type SavedResource } from '../database';
|
||||
import { parseLogRecords, parseEnvelope, toBase64Url, type ChannelSession, type LogRecord, type ReceivedItem } from '@webdrop/core';
|
||||
import { canDecryptRecord, objectEventProjection, readDataKeys, verifyObjectDeletion, verifyObjectEvent } from './channelDataKeys';
|
||||
import { getResource, cacheResource, type SavedResource } from '../database';
|
||||
import { channelCache } from './resourceMetadata';
|
||||
import { loadWorkspace } from './workspaceStructure';
|
||||
import type { SeatInfo } from '../generated/api';
|
||||
import type { FileItem, Item, Snapshot } from '../types';
|
||||
import { previewKindOf } from '@/lib/fileKind';
|
||||
import { blobs, readers } from './sessions';
|
||||
import { showRejected, deviceIdentity, deviceInfo, publishWorkspace } from './presentation';
|
||||
import { type ChannelWire, toChannelInfo } from './wire';
|
||||
import { apiFetch, readJson } from './http';
|
||||
import { applyControl } from './channelThreads';
|
||||
import { readResourceRecord } from './workspaceRecords';
|
||||
import { clearFileCatalog, fileCatalogs, visibleFiles } from './fileCatalog';
|
||||
|
||||
export function mapReceived(item: ReceivedItem): Item {
|
||||
@@ -30,6 +34,10 @@ export function mapReceived(item: ReceivedItem): Item {
|
||||
}
|
||||
|
||||
export async function applyChannelRecord(resource: SavedResource, session: ChannelSession, input: Uint8Array | LogRecord): Promise<void> {
|
||||
if (!(input instanceof Uint8Array)) {
|
||||
const itemId = input.kind === 'frame' ? toBase64Url(parseEnvelope(input.envelope).itemId) : input.itemId;
|
||||
if (!(await canDecryptRecord(resource, session, itemId))) return;
|
||||
}
|
||||
let reader = readers.get(resource.id);
|
||||
if (!reader) {
|
||||
reader = session.createItemReader();
|
||||
@@ -39,7 +47,15 @@ export async function applyChannelRecord(resource: SavedResource, session: Chann
|
||||
for (const event of events) {
|
||||
switch (event.type) {
|
||||
case 'item': {
|
||||
const control = applyControl(event.item);
|
||||
if (!event.item.complete || !event.item.ciphertextManifestHash
|
||||
|| !await verifyObjectEvent(resource, session, event.item.itemId, event.item.ciphertextManifestHash)) {
|
||||
showRejected(resource.id, { type: 'rejected', itemId: event.item.itemId, seq: event.item.seq, reason: 'auth_failed' });
|
||||
break;
|
||||
}
|
||||
const signedEvent = await objectEventProjection(resource, session, event.item.itemId);
|
||||
if (signedEvent?.kind === 'chat.edit' && signedEvent.targetItemId)
|
||||
publishWorkspace(resource.id, { t: 'removed', id: signedEvent.targetItemId });
|
||||
const control = readResourceRecord(event.item);
|
||||
if (control !== false) {
|
||||
if (control && control.t === 'file-catalog') {
|
||||
const key = `${resource.id}:${control.item.id}`;
|
||||
@@ -84,6 +100,10 @@ export async function applyChannelRecord(resource: SavedResource, session: Chann
|
||||
break;
|
||||
}
|
||||
case 'removed':
|
||||
if (event.reason === 'deleted' && !await verifyObjectDeletion(resource, session, event.itemId)) {
|
||||
showRejected(resource.id, { type: 'rejected', itemId: event.itemId, seq: event.seq, reason: 'auth_failed' });
|
||||
break;
|
||||
}
|
||||
visibleFiles.delete(`${resource.id}:${event.itemId}`);
|
||||
fileCatalogs.delete(`${resource.id}:${event.itemId}`);
|
||||
blobs.delete(`${resource.id}:${event.itemId}`);
|
||||
@@ -101,19 +121,16 @@ export async function applyChannelRecord(resource: SavedResource, session: Chann
|
||||
}
|
||||
|
||||
export async function channelMetadata(resource: SavedResource, session: ChannelSession): Promise<void> {
|
||||
const info = await readJson<ChannelWire>(`/channels/${resource.id}`, session.authHeaders('read'));
|
||||
const info = await readJson<ChannelWire>(`/channels/${resource.id}`, session.authHeaders(session.can('owner') ? 'owner' : session.can('write') ? 'write' : 'read'));
|
||||
session.verifyChannelInfo(info);
|
||||
resource.name = info.settings.name;
|
||||
resource.membersCanUpload = info.settings.members_can_upload;
|
||||
resource.expiresAt = info.expires_at;
|
||||
const latest = await getResource(resource.id);
|
||||
await saveResource({ ...latest, ...resource, localItems: latest?.localItems });
|
||||
Object.assign(resource, await cacheResource(resource, channelCache(info)));
|
||||
publishWorkspace(resource.id, { t: 'channel', info: toChannelInfo(info, resource) });
|
||||
await loadWorkspace(resource.id);
|
||||
const ownId = toBase64Url(session.endpointHash);
|
||||
const ownName = resource.deviceLabel ?? (await deviceIdentity(session.endpointSecret)).name;
|
||||
let devices = [deviceInfo(ownId, Date.now(), ownName, ownId)];
|
||||
if (session.can('owner')) {
|
||||
const seats = await readJson<Array<{ endpoint_hash: string; joined_at: number; label?: string }>>(
|
||||
const seats = await readJson<SeatInfo[]>(
|
||||
`/channels/${resource.id}/seats`, session.authHeaders('owner'));
|
||||
devices = await Promise.all(seats.map(async (seat) => {
|
||||
let name: string | undefined;
|
||||
@@ -128,6 +145,7 @@ export async function channelMetadata(resource: SavedResource, session: ChannelS
|
||||
}
|
||||
|
||||
export async function syncChannel(resource: SavedResource, session: ChannelSession, signal: AbortSignal): Promise<number> {
|
||||
if (resource.mode === 'enc') await readDataKeys(resource, session);
|
||||
publishWorkspace(resource.id, { t: 'reset' });
|
||||
clearFileCatalog(resource.id);
|
||||
readers.set(resource.id, session.createItemReader());
|
||||
|
||||
Reference in new issue
Block a user