import { type BlipDomain, type Command, type CommandMethods, type Identity, type Message, type MessageTypes, Node, type NodeLike, type Notification, type UnknownCommandResponse, } from '../types/index.ts' import type { Closable } from './closable.ts' import { EnvelopeResolver, type EventMap, type Listener } from './enveloperesolver.ts' import type { Authentication } from './security.ts' import type { SessionNegotiator } from './sessionnegotiator.ts' export interface Sender { sendMessage(message: Message): Promise sendCommand(command: Command): Promise } export type ConnectionSenderConstructor = new (options: { node: NodeLike authentication: Authentication tenantId?: string }) => TSender export class ConnectionSender { private readonly _domain: BlipDomain constructor(options: ConstructorParameters[0]) { this._domain = (Node.from(options.node).domain as BlipDomain) ?? 'msging.net' } public get domain() { return this._domain } // this method is a mess but provides a good and flexible api with a single method to login protected static login(bot: string | Identity, accessKey: string, tenantId?: string): S protected static login(token: string, tenantId?: string): S protected static login( tokenOrBot: string | Identity, accessKeyOrTenantId?: string, tenantIdOrUndefined?: string, ) { let node: NodeLike let accessKey: string let tenantId: string | undefined const isTokenBasedAuth = !tenantIdOrUndefined && typeof tokenOrBot === 'string' if (isTokenBasedAuth) { try { const { identity, secret } = ConnectionSender.parseToken(tokenOrBot) node = identity accessKey = secret tenantId = accessKeyOrTenantId } catch { if (!accessKeyOrTenantId) { throw new Error('Invalid token format and no access key provided') } node = Node.from(tokenOrBot, 'msging.net') accessKey = accessKeyOrTenantId tenantId = tenantIdOrUndefined } } else { if (!accessKeyOrTenantId) { throw new Error('Access key must be provided for bot-based authentication') } node = Node.from(tokenOrBot, 'msging.net') accessKey = accessKeyOrTenantId tenantId = tenantIdOrUndefined } // biome-ignore lint/complexity/noThisInStatic: fair use-case return new this({ node, authentication: { scheme: 'key', key: accessKey, }, tenantId, }) as S } public static createToken(node: NodeLike, secret: string) { const identity = Node.from(node).toIdentity() return btoa(`${identity}:${atob(secret)}`) } public static parseToken(token: string) { let cleantoken = token.trim() const parts = cleantoken.split(' ') if (parts.length > 1) { cleantoken = parts[1] } const [identityOrIdentifier, key] = atob(cleantoken).split(':') if (!identityOrIdentifier || !key) { throw new Error('Invalid token format') } const identity = Node.isValid(identityOrIdentifier) ? identityOrIdentifier : new Node(identityOrIdentifier, 'msging.net').toIdentity() return { identity, secret: btoa(key), } } } export abstract class OpenConnectionSender extends ConnectionSender implements Sender, Closable { protected readonly envelopeResolver = new EnvelopeResolver(this) protected sessionNegotiator: SessionNegotiator | null = null abstract sendMessage(message: Message): Promise abstract sendCommand(command: Command): Promise abstract sendNotification(notification: Notification): Promise abstract sendCommandResponse(response: UnknownCommandResponse): Promise public on( ev: K, listener: Listener['callback'], predicate?: Listener['predicate'], ): this { this.envelopeResolver.addListener(ev, { callback: listener, predicate: predicate, }) return this } public off(ev: K, listener: Listener['callback']): this { this.envelopeResolver.removeListener(ev, listener) return this } public get session() { return this.sessionNegotiator?.session ?? null } public close(): Promise { this.envelopeResolver.close() return Promise.resolve() } }