feat(api): add account workspaces and signed content events

This commit is contained in:
justme committed 2026-10-14 04:05:30 +08:00
1 parent 937b774a93
commit 7b8d61d1ba
63 files changed
+5407 -370

No files matched your search

+5 -4
View File
@@ -3,14 +3,15 @@ import { supportsApiContract } from './contract';
describe('API contract compatibility', () => {
it('accepts compatible SemVer releases in the supported major', () => {
expect(supportsApiContract('2.0.0')).toBe(true);
expect(supportsApiContract('2.8.2')).toBe(true);
expect(supportsApiContract('2.2.0-rc.1')).toBe(true);
expect(supportsApiContract('6.0.0')).toBe(true);
expect(supportsApiContract('6.8.2')).toBe(true);
expect(supportsApiContract('6.2.0-rc.1')).toBe(true);
});
it('rejects a breaking major and malformed versions', () => {
expect(supportsApiContract('1.0.0')).toBe(false);
expect(supportsApiContract('3.0.0')).toBe(false);
expect(supportsApiContract('4.0.0')).toBe(false);
expect(supportsApiContract('5.0.0')).toBe(false);
expect(supportsApiContract('01.0.0')).toBe(false);
expect(supportsApiContract(undefined)).toBe(false);
});
+1 -1
View File
@@ -1,4 +1,4 @@
export const SUPPORTED_API_CONTRACT_MAJOR = 2;
export const SUPPORTED_API_CONTRACT_MAJOR = 6;
const semverPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
+422
View File
@@ -0,0 +1,422 @@
// Generated from backend DTOs; do not edit.
// backend-contract-sha256: 654ec761e3481b423296b402497a97f4092ce167e7a2ca51a47d25f0fca4466e
export interface AccountChannelBody {
channel_id: string;
created_at: number;
payload: string;
role: string;
}
export interface AccountDeviceBody {
created_at: number;
current: boolean;
id: string;
last_seen: number;
name: string;
}
export interface AccountDeviceNameRequest {
name: string;
}
export interface AccountDeviceSigningRequest {
device_id: string;
public_key: string;
signature: string;
}
export interface AccountProfile {
avatar?: string | null;
displayName: string;
modifiedAt: number;
username: string;
}
export interface AccountProfileRequest {
avatar?: string | null;
displayName: string;
}
export interface AccountRegisterRequest {
public_key: string;
salt: string;
signing_public_key: string;
username: string;
vault: string;
verifier: string;
}
export interface AccountSaltBody {
salt: string;
}
export interface AccountSaltRequest {
username: string;
}
export interface AccountSessionBody {
token: string;
username: string;
}
export interface AccountSessionRequest {
device_id?: string | null;
device_name?: string | null;
username: string;
verifier: string;
}
export interface AccountVaultBody {
revision: number;
vault: string;
}
export interface AccountVaultRequest {
revision: number;
vault: string;
}
export interface AppendResult {
seq: number;
}
export interface ChannelHistoryRequest {
chatHistory: boolean;
filesHistory: boolean;
}
export interface ChannelInfo {
can_manage_structure: boolean;
can_upload: boolean;
created_at: number;
creator_account?: string | null;
expires_at: number;
id: string;
last_seq: number;
mode: ChannelMode;
oldest_seq: number;
password_required: boolean;
seats_used: number;
settings: ChannelSettings;
}
export type ChannelMode = 'enc' | 'raw';
export interface ChannelSettings {
allow_guests: boolean;
delete_policy: DeletePolicy;
file_max_downloads: number;
join_policy: JoinPolicy;
max_items: number;
members_can_upload: boolean;
name?: string | null;
seats: number;
ttl_seconds: number;
ui_restrict_copy_download: boolean;
}
export interface ChannelSettingsPatch {
allow_guests?: boolean | null;
delete_policy?: DeletePolicy | null;
file_max_downloads?: number | null;
join_password?: string | null;
join_policy?: JoinPolicy | null;
max_items?: number | null;
members_can_upload?: boolean | null;
name?: string | null;
seats?: number | null;
ttl_seconds?: number | null;
ui_restrict_copy_download?: boolean | null;
}
export interface CreateChannelRequest {
id?: string | null;
join_password?: string | null;
key_package?: string | null;
mode?: ChannelMode | null;
owner_hash?: string | null;
read_hash?: string | null;
settings?: ChannelSettings | null;
write_hash?: string | null;
}
export interface CreateShareRequest {
id?: string | null;
owner_hash?: string | null;
read_hash?: string | null;
settings?: ShareSettings | null;
}
export interface CryptoState {
access: ChannelAccess[];
envelopes: DomainEnvelope[];
objects: ObjectEnvelope[];
revision: number;
}
export interface ChannelAccess {
channelId: string;
chatFromRevision: number;
chatRead: boolean;
chatWrite: boolean;
filesFromRevision: number;
filesRead: boolean;
filesUpload: boolean;
username: string;
}
export type DeletePolicy = 'owner_only' | 'own' | 'anyone';
export interface DomainEnvelope {
channelId: string;
domain: CryptoDomain;
epoch: number;
payload: string;
username: string;
}
export type CryptoDomain = 'chat' | 'files';
export interface DomainPublishRequest {
channelId: string;
domain: CryptoDomain;
envelopes: DomainEnvelope[];
epoch: number;
}
export interface ErrorBody {
error: string;
}
export interface FinishShareRequest {
manifest_item_id?: string | null;
}
export interface GoneLogBody {
error: string;
oldest_seq: number;
}
export interface InvitationBody {
channel_id: string;
created_at: number;
payload: string;
username: string;
}
export interface InvitationCryptoPlan {
batches: CryptoGrantBatch[];
channelId: string;
username: string;
}
export interface CryptoGrantBatch {
domain: CryptoDomain;
epoch: number;
recipients: string[];
rotate: boolean;
}
export interface InvitationRequest {
domain_envelopes?: DomainEnvelope[] | null;
payload: string;
username: string;
}
export type JoinPolicy = 'open' | 'invite';
export interface JoinRequest {
password?: string | null;
}
export interface KeyPackageRequest {
payload: string;
}
export interface MemberBody {
joined_at: number;
role: string;
username: string;
}
export interface ObjectEnvelope {
authorAccount?: string | null;
channelId: string;
createdRevision: number;
deleteEvent?: SignedContentEventRecord | null;
domain: CryptoDomain;
epoch: number;
event?: SignedContentEventRecord | null;
itemId: string;
keyEnvelope: string;
}
export interface PasskeyBeginRequest {
username: string;
verifier?: string | null;
}
export interface PasskeyEnvelopeRequest {
credential: string;
id: string;
key_envelope: string;
username: string;
}
export interface PasskeyFinishRequest {
credential: string;
device_id?: string | null;
device_name?: string | null;
id: string;
key_envelope?: string | null;
name?: string | null;
username: string;
}
export interface PasskeyInfoBody {
can_recover: boolean;
created_at: number;
id: string;
name: string;
prf_salt: string;
}
export interface PasskeyOptionsBody {
credentials: PasskeyInfoBody[];
id: string;
options: string;
prf_salt?: string | null;
}
export interface PasskeySessionBody {
credential_id: string;
key_envelope?: string | null;
prf_salt: string;
token: string;
username: string;
}
export interface PublicKeyBody {
public_key: string;
signing_public_key?: string | null;
}
export interface SeatInfo {
account_name?: string | null;
endpoint_hash: string;
joined_at: number;
label?: string | null;
}
export interface SeatJoined {
endpoint_hash: string;
joined_at: number;
seats_max: number;
seats_used: number;
}
export interface ServerInfoBody {
captcha: CaptchaBody;
choices: SettingChoicesBody;
contract_version: string;
defaults: DefaultsBody;
limits: LimitsBody;
protocol: number;
transports: TransportsBody;
}
export interface CaptchaBody {
api_endpoint?: string | null;
enabled: boolean;
provider: string;
}
export interface DefaultsBody {
channel: ChannelSettings;
channel_mode: ChannelMode;
share: ShareSettings;
}
export interface LimitsBody {
max_channel_bytes: number;
max_frames_per_item: number;
max_items: number;
max_payload_bytes: number;
max_seats: number;
max_ttl_seconds: number;
max_visitors: number;
min_ttl_seconds: number;
}
export interface SettingChoicesBody {
file_max_downloads: number[];
max_visitors: number[];
seats: number[];
ttl_seconds: number[];
}
export interface ShareInfo {
created_at: number;
expires_at: number;
id: string;
items: ShareItemInfo[];
manifest_item_id?: string | null;
sealed_at?: number | null;
settings: ShareSettings;
state: string;
}
export interface ShareItemInfo {
bytes: number;
complete: boolean;
downloads_left: number | null;
frames: number;
item_id: string;
max_downloads: number;
}
export interface ShareSettings {
file_max_downloads: number;
max_visitors: number;
ttl_seconds: number;
}
export interface ShareSettingsPatch {
file_max_downloads?: number | null;
max_visitors?: number | null;
ttl_seconds?: number | null;
}
export interface SignedContentEventRecord {
deviceCertificateSignature: string;
eventId: string;
frameCount: number;
kind: string;
manifestHash: string;
signature: string;
signerDeviceId: string;
signerDevicePublicKey: string;
signerRootPublicKey: string;
targetItemId?: string | null;
}
export interface TransportsBody {
http3: boolean;
sse: boolean;
websocket: boolean;
}
export interface WorkspaceBody {
people: AccountProfile[];
structure: WorkspaceState;
}
export interface WorkspaceChannelCreatedBody {
channel: WorkspaceChannel;
workspace: WorkspaceBody;
}
export interface WorkspaceChannel {
chatEpoch: number;
chatHistory: boolean;
createdAt: number;
createdBy: string;
filesEpoch: number;
filesHistory: boolean;
groupId?: string | null;
id: string;
name: string;
}
export interface WorkspaceChannelRequest {
groupId?: string | null;
name: string;
}
export interface WorkspaceFolderRequest {
name: string;
parentId?: string | null;
threadId: string;
}
export interface WorkspaceGroupCreatedBody {
group: WorkspaceGroup;
workspace: WorkspaceBody;
}
export interface WorkspaceGroup {
createdAt: number;
id: string;
name: string;
}
export interface WorkspaceGroupRequest {
groupId?: string | null;
}
export interface WorkspaceMoveRequest {
folderId?: string | null;
itemIds: string[];
threadId: string;
}
export interface WorkspaceNameRequest {
name: string;
}
export interface WorkspaceState {
channels: WorkspaceChannel[];
defaultChannelId?: string | null;
folders: WorkspaceFolder[];
groups: WorkspaceGroup[];
locations: WorkspaceLocation[];
revision: number;
}
export interface WorkspaceFolder {
id: string;
name: string;
parentId?: string | null;
threadId: string;
}
export interface WorkspaceLocation {
authorAccount?: string | null;
folderId?: string | null;
itemId: string;
threadId: string;
}
+2 -2
View File
@@ -8,7 +8,7 @@ describe('API contract preflight', () => {
});
it('blocks API requests when the server has a newer breaking major', async () => {
const fetchMock = vi.fn(async () => new Response(JSON.stringify({ contract_version: '3.0.0' }), { status: 200 }));
const fetchMock = vi.fn(async () => new Response(JSON.stringify({ contract_version: '7.0.0' }), { status: 200 }));
vi.stubGlobal('fetch', fetchMock);
setApiBase('http://api.test/api/v1');
@@ -19,7 +19,7 @@ describe('API contract preflight', () => {
it('checks the contract once and then sends the API request', async () => {
const fetchMock = vi.fn()
.mockResolvedValueOnce(new Response(JSON.stringify({ contract_version: '2.2.0' }), { status: 200 }))
.mockResolvedValueOnce(new Response(JSON.stringify({ contract_version: '6.0.0' }), { status: 200 }))
.mockResolvedValueOnce(new Response('{}', { status: 200 }))
.mockResolvedValueOnce(new Response('{}', { status: 200 }));
vi.stubGlobal('fetch', fetchMock);