"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.__tsfTypeAliases = exports.MeshLinkAuthenticator = void 0;
exports.createMeshLinkEndpointKeyPair = createMeshLinkEndpointKeyPair;
exports.signMeshLinkEndpointProof = signMeshLinkEndpointProof;
exports.verifyMeshLinkEndpointProof = verifyMeshLinkEndpointProof;
const tslib_1 = require("tslib");
const _a = tslib_1.__importStar(require("@zyno-io/ts-server-foundation/type-metadata-runtime"));
const node_crypto_1 = require("node:crypto");
const types_1 = require("../../srpc/types");
const protocol_1 = require("./protocol");
const __tsf_metadata_type = _a.createCompactMetadataRegistryV1("[1,[{\"kind\":6},{\"kind\":12,\"types\":[{\"kind\":10,\"literal\":\"request\"},{\"kind\":10,\"literal\":\"response\"}],\"typeName\":\"MeshLinkAuthPurpose\"},{\"kind\":2},{\"kind\":18,\"types\":[{\"kind\":20,\"name\":\"endpointId\",\"type\":{\"kind\":6},\"optional\":false},{\"kind\":20,\"name\":\"nonce\",\"type\":{\"kind\":6},\"optional\":false}]},{\"kind\":18,\"typeName\":\"MeshLinkAuthIdentity\",\"types\":[{\"kind\":20,\"name\":\"processId\",\"type\":{\"kind\":6},\"optional\":false},{\"kind\":20,\"name\":\"endpointId\",\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"requesterEndpointId\",\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"requestNonce\",\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"audienceEndpointId\",\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"endpointPublicKey\",\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"endpointSignature\",\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"timestamp\",\"type\":{\"kind\":7},\"optional\":false},{\"kind\":20,\"name\":\"nonce\",\"type\":{\"kind\":6},\"optional\":false},{\"kind\":20,\"name\":\"signature\",\"type\":{\"kind\":6},\"optional\":false}]},{\"kind\":3}]]", []);
const DefaultClockSkewMs = 30000;
const NonceTtlMs = 60000;
const MaxRememberedNonces = 10000;
const NonceBucketMs = 1000;
const NonceBucketCount = Math.ceil(NonceTtlMs / NonceBucketMs) + 1;
function createMeshLinkEndpointKeyPair() {
    const keyPair = (0, node_crypto_1.generateKeyPairSync)('ed25519');
    return { privateKey: keyPair.privateKey, publicKey: keyPair.publicKey.export({ type: 'spki', format: 'der' }).toString('base64') };
}
function signMeshLinkEndpointProof(privateKey, identity, path, purpose, responseTo) {
    return (0, node_crypto_1.sign)(null, Buffer.from(endpointProofPayload(identity, path, purpose, responseTo)), privateKey).toString('base64');
}
function verifyMeshLinkEndpointProof(identity, path, purpose, responseTo) {
    if (!identity.endpointPublicKey || !identity.endpointSignature)
        return false;
    try {
        return (0, node_crypto_1.verify)(null, Buffer.from(endpointProofPayload(identity, path, purpose, responseTo)), (0, node_crypto_1.createPublicKey)({ key: Buffer.from(identity.endpointPublicKey, 'base64'), format: 'der', type: 'spki' }), Buffer.from(identity.endpointSignature, 'base64'));
    }
    catch {
        return false;
    }
}
let _MeshLinkAuthenticatorMetadata_1;
class MeshLinkAuthenticator {
    secret;
    clockSkewMs;
    seenNonces = new Map();
    /** A timing wheel makes expiry bounded by one bucket, rather than scanning the
     * entire cache on every handshake.  When full we reject new handshakes: evicting
     * a live nonce would turn a resource limit into a replay vulnerability. */
    nonceBuckets = Array.from({ length: NonceBucketCount }, () => new Set());
    lastPrunedBucket = Math.floor(Date.now() / NonceBucketMs);
    constructor(secret, clockSkewMs = DefaultClockSkewMs) {
        this.secret = secret;
        this.clockSkewMs = clockSkewMs;
        if (Buffer.byteLength(secret) < 16)
            throw new Error('MESH_LINK_SECRET must contain at least 16 bytes');
    }
    createIdentity(processId, path, purpose = 'request', endpointId = processId, responseTo, audienceEndpointId) {
        const timestamp = Date.now();
        const nonce = (0, node_crypto_1.randomBytes)(16).toString('hex');
        return {
            processId,
            endpointId,
            requesterEndpointId: purpose === 'response' ? responseTo?.endpointId : undefined,
            requestNonce: purpose === 'response' ? responseTo?.nonce : undefined,
            audienceEndpointId: purpose === 'request' ? audienceEndpointId : undefined,
            timestamp,
            nonce,
            signature: this.sign(processId, endpointId, timestamp, nonce, path, purpose, responseTo, audienceEndpointId)
        };
    }
    verify(identity, path, purpose = 'request', responseTo) {
        const now = Date.now();
        this.pruneNonces(now);
        if (!identity.processId || !identity.nonce || !Number.isFinite(identity.timestamp)) {
            throw new types_1.SrpcMeshAuthenticationError();
        }
        if (!identity.endpointId)
            throw new types_1.SrpcMeshAuthenticationError();
        if (purpose === 'response' &&
            (!identity.requesterEndpointId ||
                !identity.requestNonce ||
                !responseTo ||
                identity.requesterEndpointId !== responseTo.endpointId ||
                identity.requestNonce !== responseTo.nonce)) {
            throw new types_1.SrpcMeshAuthenticationError('sRPC mesh response proof does not match its request');
        }
        if (Math.abs(now - identity.timestamp) > this.clockSkewMs) {
            throw new types_1.SrpcMeshAuthenticationError('sRPC mesh authentication timestamp is outside the allowed window');
        }
        const nonceKey = `${identity.processId}:${identity.nonce}`;
        if (this.seenNonces.has(nonceKey))
            throw new types_1.SrpcMeshAuthenticationError('sRPC mesh authentication nonce was replayed');
        const expected = Buffer.from(this.sign(identity.processId, identity.endpointId ?? identity.processId, identity.timestamp, identity.nonce, path, purpose, purpose === 'response' ? { endpointId: identity.requesterEndpointId ?? '', nonce: identity.requestNonce ?? '' } : undefined, identity.audienceEndpointId), 'hex');
        const actual = Buffer.from(identity.signature, 'hex');
        if (expected.length !== actual.length || !(0, node_crypto_1.timingSafeEqual)(expected, actual))
            throw new types_1.SrpcMeshAuthenticationError();
        if (this.seenNonces.size >= MaxRememberedNonces) {
            throw new types_1.SrpcMeshAuthenticationError('sRPC mesh authentication replay cache is saturated');
        }
        const expiresAt = now + NonceTtlMs;
        this.seenNonces.set(nonceKey, expiresAt);
        this.nonceBuckets[Math.floor(expiresAt / NonceBucketMs) % NonceBucketCount].add(nonceKey);
    }
    sign(processId, endpointId, timestamp, nonce, path, purpose, responseTo, audienceEndpointId) {
        const signature = (0, node_crypto_1.createHmac)('sha256', this.secret).update(lengthPrefixed(processId)).update(lengthPrefixed(endpointId));
        signature
            .update(lengthPrefixed(String(timestamp)))
            .update(lengthPrefixed(nonce))
            .update(lengthPrefixed(path))
            .update(lengthPrefixed(purpose));
        if (purpose === 'response') {
            signature.update(lengthPrefixed(responseTo?.endpointId ?? ''));
            signature.update(lengthPrefixed(responseTo?.nonce ?? ''));
        }
        if (purpose === 'request')
            signature.update(lengthPrefixed(audienceEndpointId ?? ''));
        return signature.update(lengthPrefixed(String(protocol_1.MeshLinkProtocolVersion))).digest('hex');
    }
    pruneNonces(now) {
        const nowBucket = Math.floor(now / NonceBucketMs);
        if (nowBucket <= this.lastPrunedBucket)
            return;
        // A large clock jump only needs to visit each fixed bucket once.  The
        // expiry comparison protects against bucket wrap-around.
        const steps = Math.min(nowBucket - this.lastPrunedBucket, NonceBucketCount);
        for (let offset = 0; offset < steps; offset++) {
            const bucket = this.nonceBuckets[(this.lastPrunedBucket + offset) % NonceBucketCount];
            for (const nonce of bucket) {
                const expiresAt = this.seenNonces.get(nonce);
                if (expiresAt !== undefined && expiresAt <= now)
                    this.seenNonces.delete(nonce);
            }
            bucket.clear();
        }
        this.lastPrunedBucket = nowBucket;
    }
    static __tsfType = _MeshLinkAuthenticatorMetadata_1 = _a.decodeCompactMetadataV1("[1,{\"kind\":16,\"name\":\"MeshLinkAuthenticator\",\"typeName\":\"MeshLinkAuthenticator\",\"classType\":{\"$tsf\":0},\"properties\":[],\"methods\":[{\"name\":\"createIdentity\",\"parameters\":[{\"name\":\"processId\",\"type\":{\"$tsfType\":0},\"optional\":false,\"default\":false},{\"name\":\"path\",\"type\":{\"$tsfType\":0},\"optional\":false,\"default\":false},{\"name\":\"purpose\",\"type\":{\"$tsfType\":1},\"optional\":true,\"default\":true},{\"name\":\"endpointId\",\"type\":{\"$tsfType\":2},\"optional\":true,\"default\":true},{\"name\":\"responseTo\",\"type\":{\"$tsfType\":3},\"optional\":true,\"default\":false},{\"name\":\"audienceEndpointId\",\"type\":{\"$tsfType\":0},\"optional\":true,\"default\":false}],\"returnType\":{\"$tsfType\":4}},{\"name\":\"verify\",\"parameters\":[{\"name\":\"identity\",\"type\":{\"$tsfType\":4},\"optional\":false,\"default\":false},{\"name\":\"path\",\"type\":{\"$tsfType\":0},\"optional\":false,\"default\":false},{\"name\":\"purpose\",\"type\":{\"$tsfType\":1},\"optional\":true,\"default\":true},{\"name\":\"responseTo\",\"type\":{\"$tsfType\":3},\"optional\":true,\"default\":false}],\"returnType\":{\"$tsfType\":5}}],\"hasConstructor\":true,\"constructorParameters\":[{\"name\":\"secret\",\"type\":{\"$tsfType\":0},\"optional\":false,\"default\":false},{\"name\":\"clockSkewMs\",\"type\":{\"$tsfType\":2},\"optional\":true,\"default\":true}]}]", [() => MeshLinkAuthenticator], __tsf_metadata_type);
}
exports.MeshLinkAuthenticator = MeshLinkAuthenticator;
_MeshLinkAuthenticatorMetadata_1.classType = MeshLinkAuthenticator;
MeshLinkAuthenticator.__tsfType = _MeshLinkAuthenticatorMetadata_1;
function endpointProofPayload(identity, path, purpose, responseTo) {
    return [
        identity.processId,
        identity.endpointId ?? '',
        identity.timestamp,
        identity.nonce,
        identity.signature,
        identity.audienceEndpointId ?? '',
        path,
        purpose,
        protocol_1.MeshLinkProtocolVersion,
        responseTo?.endpointId ?? '',
        responseTo?.nonce ?? ''
    ]
        .map(value => lengthPrefixed(String(value)))
        .join('');
}
function lengthPrefixed(value) {
    return `${Buffer.byteLength(value)}:${value}`;
}
exports.__tsfTypeAliases = _a.decodeCompactMetadataV1("[1,{\"MeshLinkAuthIdentity\":{\"kind\":18,\"typeName\":\"MeshLinkAuthIdentity\",\"types\":[{\"kind\":20,\"name\":\"processId\",\"type\":{\"kind\":6},\"optional\":false},{\"kind\":20,\"name\":\"endpointId\",\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"requesterEndpointId\",\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"requestNonce\",\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"audienceEndpointId\",\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"endpointPublicKey\",\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"endpointSignature\",\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"timestamp\",\"type\":{\"kind\":7},\"optional\":false},{\"kind\":20,\"name\":\"nonce\",\"type\":{\"kind\":6},\"optional\":false},{\"kind\":20,\"name\":\"signature\",\"type\":{\"kind\":6},\"optional\":false}]},\"MeshLinkAuthPurpose\":{\"kind\":12,\"types\":[{\"kind\":10,\"literal\":\"request\"},{\"kind\":10,\"literal\":\"response\"}],\"typeName\":\"MeshLinkAuthPurpose\"},\"MeshLinkEndpointKeyPair\":{\"kind\":18,\"typeName\":\"MeshLinkEndpointKeyPair\",\"types\":[{\"kind\":20,\"name\":\"privateKey\",\"type\":{\"$tsfAlias\":[0,\"node:crypto\",\"KeyObject\",\"KeyObject\"]},\"optional\":false},{\"kind\":20,\"name\":\"publicKey\",\"type\":{\"kind\":6},\"optional\":false}]}}]", [require], __tsf_metadata_type);
//# sourceMappingURL=auth.js.map