1
0

feat: stream private office screens

This commit is contained in:
2026-08-11 21:31:15 -07:00
parent d841575315
commit 5ca214e4bb
28 changed files with 1754 additions and 35 deletions
+100
View File
@@ -0,0 +1,100 @@
import { createHmac, randomBytes } from "node:crypto";
import {
ICE_CONFIG_PROTOCOL_VERSION,
type IceConfigGrant,
type IceConfigRequest,
type IceConfigUnavailable,
type StunIceServer,
type TurnIceServer,
} from "../../../src/media/iceTypes.ts";
import type { IceConfig } from "../config.ts";
export type IceCredentialResult =
| { ok: true; value: IceConfigGrant }
| { ok: false; code: "unavailable" | "rate-limited"; value: IceConfigUnavailable };
export interface IceCredentialProvider {
issue(request: IceConfigRequest, rateKey: string): IceCredentialResult;
}
export interface IceCredentialProviderOptions {
now?: () => number;
nonce?: () => string;
}
interface RateEntry { startedAtMs: number; count: number }
const MAX_RATE_KEYS = 4_096;
export function createIceCredentialProvider(
config: IceConfig,
options: IceCredentialProviderOptions = {},
): IceCredentialProvider {
const now = options.now ?? Date.now;
const nonce = options.nonce ?? (() => randomBytes(18).toString("base64url"));
const rates = new Map<string, RateEntry>();
const windowMs = config.rateWindowSeconds * 1_000;
return {
issue(request, rateKey) {
const at = now();
if (!config.configured) return unavailable(request.requestId, windowMs, "unavailable");
let entry = rates.get(rateKey);
if (!entry && rates.size >= MAX_RATE_KEYS) {
for (const [key, value] of rates) {
if (at - value.startedAtMs >= windowMs) rates.delete(key);
}
if (rates.size >= MAX_RATE_KEYS) {
return unavailable(request.requestId, windowMs, "rate-limited");
}
entry = rates.get(rateKey);
}
if (!entry || at - entry.startedAtMs >= windowMs) {
rates.set(rateKey, { startedAtMs: at, count: 1 });
} else {
entry.count += 1;
if (entry.count > config.rateAttempts) {
return unavailable(request.requestId, Math.max(1_000, windowMs - (at - entry.startedAtMs)), "rate-limited");
}
}
const expiresAtSeconds = Math.floor(at / 1_000) + config.credentialTtlSeconds;
// Coturn REST convention: expiration timestamp, colon, opaque username.
// No auth subject, email, profile id, IP address, or stable browser id.
const username = `${expiresAtSeconds}:${nonce()}`;
const credential = createHmac("sha1", config.sharedSecret).update(username).digest("base64");
const stunUrls = config.urls.filter((url) => url.startsWith("stun:") || url.startsWith("stuns:"));
const turnUrls = config.urls.filter((url) => url.startsWith("turn:") || url.startsWith("turns:"));
const iceServers: Array<StunIceServer | TurnIceServer> = [];
if (stunUrls.length > 0) iceServers.push({ urls: stunUrls });
iceServers.push({ urls: turnUrls, username, credential, credentialType: "password" });
return {
ok: true,
value: {
type: "ice-config-grant",
protocolVersion: ICE_CONFIG_PROTOCOL_VERSION,
requestId: request.requestId,
issuedAtMs: at,
expiresAtMs: expiresAtSeconds * 1_000,
iceServers,
},
};
},
};
}
function unavailable(
requestId: string,
retryAfterMs: number,
code: "unavailable" | "rate-limited",
): IceCredentialResult {
return {
ok: false,
code,
value: {
type: "ice-config-unavailable",
protocolVersion: ICE_CONFIG_PROTOCOL_VERSION,
requestId,
retryAfterMs: Math.min(3_600_000, Math.max(1_000, Math.ceil(retryAfterMs))),
},
};
}