import type { CryptoProvider } from '@shade/core'; import { ValidationError } from '@shade/core'; export const STREAM_ID_BYTES = 16; export const STREAM_SECRET_BYTES = 32; /** Generate a fresh 16-byte random streamId. */ export function generateStreamId(crypto: CryptoProvider): Uint8Array { return crypto.randomBytes(STREAM_ID_BYTES); } /** Generate a fresh 32-byte random streamSecret. */ export function generateStreamSecret(crypto: CryptoProvider): Uint8Array { return crypto.randomBytes(STREAM_SECRET_BYTES); } /** Encode a streamId as URL-safe base64 (no padding). */ export function streamIdToString(streamId: Uint8Array): string { if (streamId.length !== STREAM_ID_BYTES) { throw new ValidationError(`streamId must be ${STREAM_ID_BYTES} bytes`, 'streamId'); } return base64UrlEncode(streamId); } /** Decode a URL-safe base64 streamId back to bytes. */ export function streamIdFromString(s: string): Uint8Array { const bytes = base64UrlDecode(s); if (bytes.length !== STREAM_ID_BYTES) { throw new ValidationError(`streamId must decode to ${STREAM_ID_BYTES} bytes`, 'streamId'); } return bytes; } function base64UrlEncode(bytes: Uint8Array): string { let bin = ''; for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]!); return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); } function base64UrlDecode(s: string): Uint8Array { const padded = s.replace(/-/g, '+').replace(/_/g, '/'); const pad = padded.length % 4 === 0 ? '' : '='.repeat(4 - (padded.length % 4)); const bin = atob(padded + pad); const out = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); return out; }