···20202121/** the state of a specific peer */
2222export interface PeerState {
2323- /** true if the peer connection is active */
2323+ /** if the peer connection is active */
2424 connected: boolean
25252626- /** true if the peer connection has been destroyed */
2626+ /** if the peer connection has been destroyed */
2727 destroyed: boolean
28282929- /** the peer's address */
2929+ /** the peer's address (ip and port) */
3030 address: ReturnType<SimplePeer.Instance['address']>
3131}
32323333+/** identity info for connecting to a realm */
3334export interface RealmIdentity {
3435 realmid: RealmID
3536 identid: IdentID
3637 keypair: CryptoKeyPair
3738}
38393939-/** A connection manager */
4040+/** manages websocket and webrtc connections for a realm */
4041export class RealmConnection extends EventTarget {
4142 #url: string
4243 #identity: RealmIdentity
···264265 sendSocket(this.#socket, resp)
265266 }
266267267267- /** @returns the current peer state mapping */
268268 getPeerStates(): Record<IdentID, PeerState> {
269269 const states: Record<IdentID, PeerState> = {}
270270 for (const [peerId, peer] of this.#peers) {
···281281282282const peerPingSchema = z.object({type: z.literal('ping'), timestamp: z.number()})
283283284284-/**
285285- * a peer belonging to the connection manager
286286- */
284284+/** a single webrtc peer connection within a realm */
287285export class RealmConnectionPeer extends SimplePeer {
288286 #connection: RealmConnection
289287
+1-1
src/client/realm/context.tsx
···11import {createContext} from 'preact'
22import {useCallback, useEffect, useState} from 'preact/hooks'
3344-import {RealmConnection, RealmIdentity} from '#client/realm/connection.js'
44+import {RealmConnection, RealmIdentity} from '#client/realm/connection'
5566interface RealmConnectionContext {
77 realm?: RealmConnection
···11-import {Semaphore} from './semaphore.js'
11+import {Semaphore} from './semaphore'
2233/**
44 * simple blocking atom, for waiting for a value.
+1-1
src/common/async/blocking-queue.ts
···11-import {Semaphore} from './semaphore.js'
11+import {Semaphore} from './semaphore'
2233/**
44 * simple blocking queue, for turning streams into async pulls.
+6-8
src/common/breaker.ts
···11-import {Callback} from '#common/types'
22-31/**
42 * A Breaker, which allows creating wrapped functions which will only be executed before
53 * the breaker is tripped.
···4947 * @param fn - the function to be wrapped in the breaker
5048 * @returns a wrapped function, controlled by the breaker
5149 */
5252- tripThen<CB extends Callback>(fn: CB): CB {
5353- return ((...args: Parameters<CB>): void => {
5050+ tripThen<T extends unknown[]>(fn: (...args: T) => void): (...args: T) => void {
5151+ return (...args: T): void => {
5452 if (!this.#tripped) {
5553 this.#tripped = true
5654···5856 this.#onTripped?.()
5957 fn(...args)
6058 }
6161- }) as CB
5959+ }
6260 }
63616462 /**
···6866 * @param fn - the function to be wrapped in the breaker
6967 * @returns a wrapped function, controlled by the breaker
7068 */
7171- untilTripped<CB extends Callback>(fn: CB): CB {
7272- return ((...args: Parameters<CB>): void => {
6969+ untilTripped<T extends unknown[]>(fn: (...args: T) => void): (...args: T) => void {
7070+ return (...args: T): void => {
7371 if (!this.#tripped) {
7472 // TODO: if these throw, what to do?
7573 fn(...args)
7674 }
7777- }) as CB
7575+ }
7876 }
7977}
+1-1
src/common/crypto/errors.ts
···11-import {BaseError, BaseErrorOpts} from '#common/errors.js'
11+import {BaseError, BaseErrorOpts} from '#common/errors'
2233/** Common base class for errors in the crypto module */
44export class CryptoError extends BaseError {}
+1-1
src/common/crypto/jwks.ts
···11import * as jose from 'jose'
22import {z} from 'zod/v4'
33-import {CryptoError} from './errors.js'
33+import {CryptoError} from './errors'
4455const subtleSignAlgo = {name: 'ECDSA', namedCurve: 'P-256'}
66const joseSignAlgo = {name: 'ES256'}
+6-6
src/common/errors.ts
···1212 500: 'Internal Server Error',
1313}
14141515-/** Base error options interface */
1515+/** base error options interface */
1616export interface BaseErrorOpts {
1717 /** the cause of the error */
1818 cause?: Error
1919}
20202121/**
2222- * Common base class for Skypod Errors
2222+ * common base class for skypod errors
2323 * only difference is that we explicitly type cause to be Error
2424 */
2525export class BaseError extends Error {
···3232 }
3333}
34343535-/** Common base class for Websocket Errors */
3535+/** common base class for websocket errors */
3636export class ProtocolError extends BaseError {
3737 /** the HTTP status code representing this error */
3838 status: number
···4646 }
4747}
48484949-/** Check if an error is a protocol error with optional status check */
4949+/** check if an error is a protocol error with optional status check */
5050export function isProtocolError(e: Error, status?: number): e is ProtocolError {
5151 return e instanceof ProtocolError && (status === undefined || e.status == status)
5252}
53535454/**
5555- * Normalizes the given error into a protocol error
5555+ * normalizes the given error into a protocol error
5656 * passes through input that is already protocol errors.
5757 */
5858export function normalizeProtocolError(cause: unknown): ProtocolError {
···7373 return new ProtocolError(`Error! ${cause}`, 500, options)
7474}
75757676-/** Error wrapper for unknown errors (not an Error?) */
7676+/** error wrapper for unknown errors (not an Error?) */
7777export class NormalizedError extends Error {}
78787979/**
+4-4
src/common/protocol.ts
···11+import {z} from 'zod/v4'
22+13export * from './protocol/brands'
24export * from './protocol/messages'
35export * from './protocol/messages-preauth'
46export * from './protocol/messages-realm'
5766-import {z} from 'zod/v4'
77-88-/** A zod transformer for parsing json */
99-export const parseJson: z.ZodTransform<unknown, string> = z.transform((input, ctx) => {
88+/** a zod transformer for parsing json */
99+export const parseJson = z.transform<string, unknown>((input, ctx) => {
1010 try {
1111 return JSON.parse(input) as unknown
1212 } catch {
+8-3
src/common/schema/brand.ts
···44export type Branded<T, B> = T & {__brand: B}
5566/**
77- * A brand creates identifiers that are typesafe by construction,
88- * and shouldn't be able to be passed to the wrong resource type.
77+ * creates typesafe identifiers that can't be passed to the wrong resource type.
88+ *
99+ * @example
1010+ * ```
1111+ * const UserBrand = new Brand(Symbol('user'), 'user')
1212+ * type UserID = ReturnType<typeof UserBrand.generate>
1313+ * ```
914 */
1015export class Brand<B extends symbol> {
1116 #prefix: string
···3439 return this.#schema.parse(input) as Branded<string, B>
3540 }
36413737- /** @returns a boolean if the string is valid */
4242+ /** type guard to check if a string is a valid branded id */
3843 validate(input: string): input is Branded<string, B> {
3944 return input != null && typeof input === 'string' && this.#schema.safeParse(input).success
4045 }
+6-13
src/common/socket.ts
···77import {normalizeError, ProtocolError} from '#common/errors'
88import {z} from 'zod/v4'
991010-import {parseJson} from './protocol.js'
1010+import {parseJson} from './protocol'
11111212-/** Send some data in JSON format down the wire. */
1212+/** send some data in json format down the wire */
1313export function sendSocket(ws: WebSocket, data: unknown): void {
1414 ws.send(JSON.stringify(data))
1515}
16161717/**
1818- * Given a websocket, wait and take a single message off and return it.
1818+ * given a websocket, wait and take a single message off and return it
1919 *
2020 * @example
2121 * ```
···7070 }
7171}
72727373-/**
7474- * exactly take socket, but will additionally apply a json decoding
7575- *
7676- * @param ws - the socket to read
7777- * @param schema - an a schema to execute
7878- * @param signal - an abort signal to cancel the block
7979- * @returns the message off the socket
8080- */
7373+/** exactly take socket, but will additionally apply a json decoding */
8174export async function takeSocketJson<T>(
8275 ws: WebSocket,
8376 schema: z.ZodType<T>,
···107100type StreamYield = [typeof yield$, unknown] | [typeof error$, Error] | [typeof end$]
108101109102/**
110110- * Given a websocket, stream messages off the socket as an async generator.
103103+ * given a websocket, stream messages off the socket as an async generator
111104 *
112105 * @example
113113- * ```ts
106106+ * ```
114107 * const ws = new WebSocket("wss://example.com/stream")
115108 * const timeout = timeoutSignal(5000)
116109 *
+4-4
src/common/strict-map.ts
···11-/** A map with methods to ensure key presence and safe update. */
11+/** a map with methods to ensure key presence and safe update */
22export class StrictMap<K, V> extends Map<K, V> {
33 /**
44- * Get a value from the map, throwing if missing
44+ * get a value from the map, throwing if missing
55 * @throws Error if the key is not present in the map
66 */
77 require(key: K): V {
···1212 return value
1313 }
14141515- /** Get a value from the map, creating it if not present */
1515+ /** get a value from the map, creating it if not present */
1616 ensure(key: K, maker: () => V): V {
1717 if (!this.has(key)) {
1818 this.set(key, maker())
···2222 return this.get(key)!
2323 }
24242525- /** Update a value in the map, removing if undefined is returned */
2525+ /** update a value in the map, removing if undefined is returned */
2626 update(key: K, update: (prev?: V) => V | undefined): void {
2727 const prev = this.get(key)
2828 const next = update(prev)
-13
src/common/types.ts
···11-import {NEVER} from 'zod/v4'
22-33-/** A callback function, with arbitrary arguments; use `Parameters` to extract them. */
44-// eslint-disable-next-line @typescript-eslint/no-explicit-any
55-export type Callback = (...args: any[]) => void
66-77-/**
88- * A callback function with no arguments.
99- */
1010-export type VoidCallback = () => void
1111-1212-const output = NEVER
1313-export default output
+1-3
src/server/index.ts
···99import {makeSpaMiddleware, makeStaticMiddleware} from './routes-static'
10101111/**
1212- * configures an http server which hosts the SPA and websocket endpoint
1313- *
1212+ * configures an http server which hosts the spa and websocket endpoint
1413 * @param root - the path to the root public/ directory
1515- * @returns a configured server
1614 */
1715export function buildServer(root: string): http.Server {
1816 const app = express()
+1-1
src/server/routes-socket/handler-realm.ts
···2233import {normalizeProtocolError, ProtocolError} from '#common/errors'
44import * as protocol from '#common/protocol'
55-import {sendSocket, streamSocket} from '#common/socket.js'
55+import {sendSocket, streamSocket} from '#common/socket'
66import * as realm from '#server/routes-socket/state'
7788/**
+1-1
src/server/routes-socket/handler.ts
···77import {realmHandler} from './handler-realm'
88import {attachSocket, detachSocket} from './state'
991010-/** when the socket connects, we drive our protocol through handlers. */
1010+/** when the socket connects, we drive our protocol through handlers */
1111export async function socketHandler(ws: WebSocket) {
1212 console.log('WebSocket connection established')
1313
+1-1
src/server/routes-socket/state.ts
···11import WebSocket from 'isomorphic-ws'
2233-import {IdentID, RealmID} from '#common/protocol.js'
33+import {IdentID, RealmID} from '#common/protocol'
44import {StrictMap} from '#common/strict-map'
5566/** An authenticated identity; only handed out in response to successful authentication. */