refactor: connection module and update identity handling
Some checks failed
Build & Test / build (20.x) (push) Has been cancelled
Build & Test / build (22.x) (push) Has been cancelled

This commit is contained in:
Shibo Lyu 2025-05-22 01:22:29 +08:00
parent b05c47037c
commit fbfcc34102
5 changed files with 97 additions and 42 deletions

View file

@ -1,8 +1,16 @@
import { version } from '$app/environment';
import type { BlahRichText } from '@blah-im/core/richText';
import type { BlahKeyPair, BlahSignedPayload } from '@blah-im/core/crypto';
import type { BlahAuth, BlahMessage, BlahRoomInfo, BlahUserJoinMessage } from '../structures';
import type { BlahKeyPair, BlahSignedPayload, SignOptions } from '@blah-im/core/crypto';
import {
blahUserUnregisteredResponseSchema,
type BlahAuth,
type BlahMessage,
type BlahRoomInfo,
type BlahUserJoinMessage,
type BlahUserRegisterRequest
} from '../structures';
import { BlahError } from './error';
import type { BlahIdentity } from '@blah-im/core/identity';
const RECONNECT_TIMEOUT = 1500;
const RECONNECT_MAX_TRIES = 5;
@ -19,14 +27,14 @@ export class BlahChatServerConnection {
private static commonHeaders = { 'x-blah-client': `Weblah/${version}` };
private endpoint_: string;
private keypair_: BlahKeyPair | null;
private identity_: BlahIdentity | null;
get endpoint() {
return this.endpoint_;
}
get keypair() {
return this.keypair_;
get identity() {
return this.identity_;
}
private webSocket: WebSocket | null = null;
@ -34,15 +42,15 @@ export class BlahChatServerConnection {
private serverListeners: Set<MessageListener> = new Set();
private webSocketRetryTimeout: number | null = null;
constructor(endpoint: string, keypair: BlahKeyPair | null = null) {
this.endpoint_ = endpoint;
this.keypair_ = keypair;
constructor(endpoint: string, identity: BlahIdentity | null = null) {
this.endpoint_ = new URL(endpoint).href.replace(/\/$/, '');
this.identity_ = identity;
}
private async generateAuthHeader(): Promise<{ Authorization: string }> {
if (!this.keypair) throw new Error('Must generate auth header with a keypair');
if (!this.identity) throw new Error('Must generate auth header with an identity');
const authPayload: BlahAuth = { typ: 'auth' };
const signedAuthPayload = await this.keypair.signPayload(authPayload);
const signedAuthPayload = await this.identity.signPayload(authPayload);
return { Authorization: JSON.stringify(signedAuthPayload) };
}
@ -57,11 +65,12 @@ export class BlahChatServerConnection {
private async fetchWithSignedPayload<P>(
url: string,
payload: P,
init?: RequestInit
init?: RequestInit,
signOptions?: Omit<SignOptions, 'identityKeyID'>
): Promise<Response> {
if (!this.keypair) throw new Error('Must fetch with a keypair');
if (!this.identity) throw new Error('Must fetch with an identity');
const signedPayload = await this.keypair.signPayload(payload);
const signedPayload = await this.identity.signPayload(payload, signOptions);
return fetch(url, {
...init,
headers: {
@ -73,12 +82,18 @@ export class BlahChatServerConnection {
});
}
public async apiCall<P, R>(method: 'POST' | 'GET', path: `/${string}`, payload?: P): Promise<R> {
if (payload && !this.keypair) throw new Error('Must make authorized API call with a keypair');
public async apiCall<P, R>(
method: 'POST' | 'GET',
path: `/${string}`,
payload?: P,
signOptions?: Omit<SignOptions, 'identityKeyID'>
): Promise<R> {
if (payload && !this.identity)
throw new Error('Must make authorized API call with an identity');
let response: Response;
if (method === 'GET') {
if (this.keypair) {
if (this.identity) {
response = await this.fetchWithAuthHeader(`${this.endpoint_}${path}`);
} else {
response = await fetch(`${this.endpoint_}${path}`, {
@ -86,7 +101,12 @@ export class BlahChatServerConnection {
});
}
} else {
response = await this.fetchWithSignedPayload(`${this.endpoint_}${path}`, payload, { method });
response = await this.fetchWithSignedPayload(
`${this.endpoint_}${path}`,
payload,
{ method },
signOptions
);
}
if (!response.ok) throw await BlahError.fromResponse(response);
@ -94,13 +114,28 @@ export class BlahChatServerConnection {
}
async tryRegisterIfNoyYet(): Promise<void> {
if (!this.keypair) throw new Error('Must register with a keypair');
if (!this.identity) throw new Error('Must register with an identity');
try {
await this.apiCall('GET', '/user/me');
} catch (e) {
if (e instanceof BlahError && e.statusCode === 404) {
// TODO: Register user
const { data } = blahUserUnregisteredResponseSchema.safeParse(e.raw);
if (!data) throw e;
const request: BlahUserRegisterRequest = {
typ: 'user_register',
server_url: this.endpoint_,
id_url: this.identity.profile.id_urls[0],
id_key: this.identity.idPublicKey.id,
challenge: {
pow: { nonce: data.register_challenge.pow.nonce }
}
};
const response = await this.apiCall('POST', '/user/register', request, {
powDifficulty: data.register_challenge.pow.difficulty
});
} else {
throw e;
}
@ -122,7 +157,7 @@ export class BlahChatServerConnection {
socket.addEventListener('close', onSocketClose);
socket.addEventListener('open', async () => {
if (this.keypair) {
if (this.identity) {
const { Authorization } = await this.generateAuthHeader();
socket.send(Authorization);
}
@ -146,7 +181,7 @@ export class BlahChatServerConnection {
}
connect() {
if (!this.webSocket && this.keypair) this.webSocket = this.createWebSocket();
if (!this.webSocket && this.identity) this.webSocket = this.createWebSocket();
}
disconnect() {
@ -155,8 +190,8 @@ export class BlahChatServerConnection {
this.webSocket = null;
}
changeKeyPair(keypair: BlahKeyPair | null) {
this.keypair_ = keypair;
changeIdentity(newIdentity: BlahIdentity | null) {
this.identity_ = newIdentity;
if (this.webSocket) {
this.disconnect();
this.connect();

View file

@ -0,0 +1,4 @@
export * from './connection';
export * from './error';
export * from './messageManager.svelte';
export * from './roomManager.svelte';

View file

@ -1,10 +1,10 @@
import type { BlahRichText } from '@blah-im/core/richText';
import type { BlahSignedPayload } from '@blah-im/core/crypto';
import type { BlahChatServerConnection } from './chatServer';
import type { BlahChatServerConnection } from './connection';
import type { BlahMessage } from '../structures';
export default class MessageManager {
export class MessageManager {
connection: BlahChatServerConnection;
roomID: string;
messages: BlahSignedPayload<BlahMessage>[] = $state([]);
@ -15,7 +15,7 @@ export default class MessageManager {
}
async sendMessage(message: BlahRichText): Promise<void> {
if (!this.connection.keypair) throw new Error('Must send message with a keypair');
if (!this.connection.identity) throw new Error('Must send message with a keypair');
const payload: BlahMessage = { room: this.roomID, rich_text: message, typ: 'chat' };
await this.connection.apiCall('POST', `/room/${payload.room}/item`, payload);
}

View file

@ -1,7 +1,7 @@
import type { BlahRoomInfo, BlahUserJoinMessage } from '../structures';
import type { BlahChatServerConnection } from './chatServer';
import type { BlahChatServerConnection } from './connection';
export default class RoomManager {
export class RoomManager {
connection: BlahChatServerConnection;
joinedRooms: BlahRoomInfo[] = $state([]);
publicRooms: BlahRoomInfo[] = $state([]);
@ -11,14 +11,14 @@ export default class RoomManager {
}
async joinRoom(id: string): Promise<void> {
const keypair = this.connection.keypair;
if (!keypair) throw new Error('Must join with a keypair');
const identity = this.connection.identity;
if (!identity) throw new Error('Must join with an identity');
const payload: BlahUserJoinMessage = {
typ: 'add_member',
room: id,
permission: 1,
user: keypair.id
user: identity.idPublicKey.id
};
await this.connection.apiCall('POST', `/room/${id}/admin`, payload);
@ -33,7 +33,7 @@ export default class RoomManager {
}
async fetchJoinedRooms() {
if (!this.connection.keypair) return [];
if (!this.connection.identity) return [];
this.joinedRooms = await this.fetchRoomList('joined');
}

View file

@ -2,16 +2,32 @@ import { z } from 'zod';
export type BlahAuth = { typ: 'auth' };
export type BlahUserRegisterChallenge = {
pow: {
nonce: number;
difficulty: number;
export const blahUserUnregisteredResponseSchema = z.object({
register_challenge: z.object({
pow: z.object({
nonce: z.number().int(),
difficulty: z.number().int()
})
})
});
export type BlahUserUnregisteredResponse = {
register_challenge: {
pow: {
nonce: number;
difficulty: number;
};
};
};
export const blahUserRegisterChallengeSchema = z.object({
pow: z.object({
nonce: z.number().int(),
difficulty: z.number().int()
})
});
export type BlahUserRegisterRequest = {
typ: 'user_register';
server_url: string;
id_url: string;
id_key: string;
challenge: {
pow: {
nonce: number;
};
};
};