var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __rest = (this && this.__rest) || function (s, e) {
    var t = {};
    for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
        t[p] = s[p];
    if (s != null && typeof Object.getOwnPropertySymbols === "function")
        for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
            if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
                t[p[i]] = s[p[i]];
        }
    return t;
};
import { DidJwk } from '@enbox/dids';
import { Convert, logger } from '@enbox/common';
import { CryptoUtils, Ed25519, EdDsaAlgorithm, Hkdf, Sha256, X25519, XChaCha20Poly1305, } from '@enbox/crypto';
import { DwnInterfaceName, DwnMethodName } from '@enbox/dwn-sdk-js';
import { AgentPermissionsApi } from './permissions-api.js';
import { concatenateUrl } from './utils.js';
import { DwnInterface } from './types/dwn.js';
import { isRecordPermissionScope } from './dwn-api.js';
/**
 * Gets the correct OIDC endpoint out of the {@link OidcEndpoint} options provided.
 * Handles a trailing slash on baseURL
 *
 * @param {Object} options the options object
 * @param {string} options.baseURL for example `http://foo.com/connect/
 * @param {OidcEndpoint} options.endpoint the OIDC endpoint desired
 * @param {string} options.authParam this is the unique id which must be provided when getting the `authorize` endpoint
 * @param {string} options.tokenParam this is the random state as b64url which must be provided with the `token` endpoint
 */
function buildOidcUrl({ baseURL, endpoint, authParam, tokenParam, }) {
    switch (endpoint) {
        /** 1. client sends {@link PushedAuthRequest} & client receives {@link PushedAuthResponse} */
        case 'pushedAuthorizationRequest':
            return concatenateUrl(baseURL, 'par');
        /** 2. provider gets {@link Web5ConnectAuthRequest} */
        case 'authorize':
            if (!authParam) {
                throw new Error(`authParam must be providied when building a token URL`);
            }
            return concatenateUrl(baseURL, `authorize/${authParam}.jwt`);
        /** 3. provider sends {@link Web5ConnectAuthResponse} */
        case 'callback':
            return concatenateUrl(baseURL, `callback`);
        /**  4. client gets {@link Web5ConnectAuthResponse */
        case 'token':
            if (!tokenParam) {
                throw new Error(`tokenParam must be providied when building a token URL`);
            }
            return concatenateUrl(baseURL, `token/${tokenParam}.jwt`);
        // TODO: metadata endpoints?
        default:
            throw new Error(`No matches for endpoint specified: ${endpoint}`);
    }
}
/**
 * Generates a cryptographically random "code challenge" in
 * accordance with the RFC 7636 PKCE specification.
 *
 * @see {@link https://datatracker.ietf.org/doc/html/rfc7636#section-4.2 | RFC 7636 }
 */
function generateCodeChallenge() {
    return __awaiter(this, void 0, void 0, function* () {
        const codeVerifierBytes = CryptoUtils.randomBytes(32);
        const codeChallengeBytes = yield Sha256.digest({ data: codeVerifierBytes });
        const codeChallengeBase64Url = Convert.uint8Array(codeChallengeBytes).toBase64Url();
        return { codeChallengeBytes, codeChallengeBase64Url };
    });
}
/** Client creates the {@link Web5ConnectAuthRequest} */
function createAuthRequest(options) {
    return __awaiter(this, void 0, void 0, function* () {
        // Generate a random state value to associate the authorization request with the response.
        const stateBytes = CryptoUtils.randomBytes(16);
        // Generate a random nonce value to associate the ID Token with the authorization request.
        const nonceBytes = CryptoUtils.randomBytes(16);
        const requestObject = Object.assign(Object.assign({}, options), { nonce: Convert.uint8Array(nonceBytes).toBase64Url(), response_type: 'id_token', response_mode: 'direct_post', state: Convert.uint8Array(stateBytes).toBase64Url(), client_metadata: {
                subject_syntax_types_supported: ['did:dht', 'did:jwk'],
            } });
        return requestObject;
    });
}
/** Encrypts the auth request with the key which will be passed through QR code */
function encryptAuthRequest(_a) {
    return __awaiter(this, arguments, void 0, function* ({ jwt, encryptionKey, }) {
        const protectedHeader = {
            alg: 'dir',
            cty: 'JWT',
            enc: 'XC20P',
            typ: 'JWT',
        };
        const nonce = CryptoUtils.randomBytes(24);
        const additionalData = Convert.object(protectedHeader).toUint8Array();
        const jwtBytes = Convert.string(jwt).toUint8Array();
        const ciphertextAndTag = yield XChaCha20Poly1305.encryptRaw({ data: jwtBytes, keyBytes: encryptionKey, nonce, additionalData });
        /** The cipher output concatenates the encrypted data and tag
         * so we need to extract the values for use in the JWE. */
        const ciphertext = ciphertextAndTag.subarray(0, -16);
        const authenticationTag = ciphertextAndTag.subarray(-16);
        const compactJwe = [
            Convert.object(protectedHeader).toBase64Url(),
            '', // Empty string since there is no wrapped key.
            Convert.uint8Array(nonce).toBase64Url(),
            Convert.uint8Array(ciphertext).toBase64Url(),
            Convert.uint8Array(authenticationTag).toBase64Url(),
        ].join('.');
        return compactJwe;
    });
}
/** Create a response object compatible with Web5 Connect and OIDC SIOPv2 */
function createResponseObject(options) {
    return __awaiter(this, void 0, void 0, function* () {
        const currentTimeInSeconds = Math.floor(Date.now() / 1000);
        const responseObject = Object.assign(Object.assign({}, options), { iat: currentTimeInSeconds, exp: currentTimeInSeconds + 600 });
        return responseObject;
    });
}
/** sign an object and transform it into a jwt using a did */
function signJwt(_a) {
    return __awaiter(this, arguments, void 0, function* ({ did, data, }) {
        const header = Convert.object({
            alg: 'EdDSA',
            kid: did.document.verificationMethod[0].id,
            typ: 'JWT',
        }).toBase64Url();
        const payload = Convert.object(data).toBase64Url();
        // signs using ed25519 EdDSA
        const signer = yield did.getSigner();
        const signature = yield signer.sign({
            data: Convert.string(`${header}.${payload}`).toUint8Array(),
        });
        const signatureBase64Url = Convert.uint8Array(signature).toBase64Url();
        const jwt = `${header}.${payload}.${signatureBase64Url}`;
        return jwt;
    });
}
/** Take the decrypted JWT and verify it was signed by its public DID. Return parsed object. */
function verifyJwt(_a) {
    return __awaiter(this, arguments, void 0, function* ({ jwt }) {
        var _b, _c;
        const [headerB64U, payloadB64U, signatureB64U] = jwt.split('.');
        // Convert the header back to a JOSE object and verify that the 'kid' header value is present.
        const header = Convert.base64Url(headerB64U).toObject();
        if (!header.kid) {
            throw new Error(`OIDC: Object could not be verified due to missing 'kid' header value.`);
        }
        // Resolve the Client DID document.
        const { didDocument } = yield DidJwk.resolve(header.kid.split('#')[0]);
        if (!didDocument) {
            throw new Error('OIDC: Object could not be verified due to Client DID resolution issue.');
        }
        // Get the public key used to sign the Object from the DID document.
        const { publicKeyJwk } = (_c = (_b = didDocument.verificationMethod) === null || _b === void 0 ? void 0 : _b.find((method) => {
            return method.id === header.kid;
        })) !== null && _c !== void 0 ? _c : {};
        if (!publicKeyJwk) {
            throw new Error('OIDC: Object could not be verified due to missing public key in DID document.');
        }
        const EdDsa = new EdDsaAlgorithm();
        const isValid = yield EdDsa.verify({
            key: publicKeyJwk,
            signature: Convert.base64Url(signatureB64U).toUint8Array(),
            data: Convert.string(`${headerB64U}.${payloadB64U}`).toUint8Array(),
        });
        if (!isValid) {
            throw new Error('OIDC: Object failed verification due to invalid signature.');
        }
        const object = Convert.base64Url(payloadB64U).toObject();
        return object;
    });
}
/**
 * Fetches the {@Web5ConnectAuthRequest} from the authorize endpoint and decrypts it
 * using the encryption key passed via QR code.
 */
const getAuthRequest = (request_uri, encryption_key) => __awaiter(void 0, void 0, void 0, function* () {
    const authRequest = yield fetch(request_uri, { signal: AbortSignal.timeout(30000) });
    const jwe = yield authRequest.text();
    const jwt = yield decryptAuthRequest({
        jwe,
        encryption_key,
    });
    const web5ConnectAuthRequest = (yield verifyJwt({
        jwt,
    }));
    return web5ConnectAuthRequest;
});
/** Take the encrypted JWE, decrypt using the code challenge and return a JWT string which will need to be verified */
function decryptAuthRequest(_a) {
    return __awaiter(this, arguments, void 0, function* ({ jwe, encryption_key, }) {
        const [protectedHeaderB64U, , nonceB64U, ciphertextB64U, authenticationTagB64U,] = jwe.split('.');
        const encryptionKeyBytes = Convert.base64Url(encryption_key).toUint8Array();
        const protectedHeader = Convert.base64Url(protectedHeaderB64U).toUint8Array();
        const additionalData = protectedHeader;
        const nonce = Convert.base64Url(nonceB64U).toUint8Array();
        const ciphertext = Convert.base64Url(ciphertextB64U).toUint8Array();
        const authenticationTag = Convert.base64Url(authenticationTagB64U).toUint8Array();
        // The cipher expects the encrypted data and tag to be concatenated.
        const ciphertextAndTag = new Uint8Array([
            ...ciphertext,
            ...authenticationTag,
        ]);
        const decryptedJwtBytes = yield XChaCha20Poly1305.decryptRaw({ data: ciphertextAndTag, keyBytes: encryptionKeyBytes, nonce, additionalData });
        const jwt = Convert.uint8Array(decryptedJwtBytes).toString();
        return jwt;
    });
}
/**
 * The client uses to decrypt the jwe obtained from the auth server which contains
 * the {@link Web5ConnectAuthResponse} that was sent by the provider to the auth server.
 *
 * @async
 * @param {BearerDid} clientDid - The did that was initially used by the client for ECDH at connect init.
 * @param {string} jwe - The encrypted data as a jwe.
 * @param {string} pin - The pin that was obtained from the user.
 */
function decryptAuthResponse(clientDid, jwe, pin) {
    return __awaiter(this, void 0, void 0, function* () {
        const [protectedHeaderB64U, , nonceB64U, ciphertextB64U, authenticationTagB64U,] = jwe.split('.');
        // get the delegatedid public key from the header
        const header = Convert.base64Url(protectedHeaderB64U).toObject();
        if (!header.kid) {
            throw new Error('JWE protected header is missing required "kid" property');
        }
        const delegateResolvedDid = yield DidJwk.resolve(header.kid.split('#')[0]);
        // derive ECDH shared key using the provider's public key and our clientDid private key
        const sharedKey = yield Oidc.deriveSharedKey(clientDid, delegateResolvedDid.didDocument);
        // add the pin to the AAD
        const additionalData = Object.assign(Object.assign({}, header), { pin: pin });
        const AAD = Convert.object(additionalData).toUint8Array();
        const nonce = Convert.base64Url(nonceB64U).toUint8Array();
        const ciphertext = Convert.base64Url(ciphertextB64U).toUint8Array();
        const authenticationTag = Convert.base64Url(authenticationTagB64U).toUint8Array();
        // The cipher expects the encrypted data and tag to be concatenated.
        const ciphertextAndTag = new Uint8Array([
            ...ciphertext,
            ...authenticationTag,
        ]);
        // decrypt using the sharedKey
        const decryptedJwtBytes = yield XChaCha20Poly1305.decryptRaw({ data: ciphertextAndTag, keyBytes: sharedKey, nonce, additionalData: AAD });
        const jwt = Convert.uint8Array(decryptedJwtBytes).toString();
        return jwt;
    });
}
/** Derives a shared ECDH private key in order to encrypt the {@link Web5ConnectAuthResponse} */
function deriveSharedKey(privateKeyDid, publicKeyDid) {
    return __awaiter(this, void 0, void 0, function* () {
        var _a, _b;
        const privatePortableDid = yield privateKeyDid.export();
        const publicJwk = (_a = publicKeyDid.verificationMethod) === null || _a === void 0 ? void 0 : _a[0].publicKeyJwk;
        const privateJwk = (_b = privatePortableDid.privateKeys) === null || _b === void 0 ? void 0 : _b[0];
        publicJwk.alg = 'EdDSA';
        const publicX25519 = yield Ed25519.convertPublicKeyToX25519({
            publicKey: publicJwk,
        });
        const privateX25519 = yield Ed25519.convertPrivateKeyToX25519({
            privateKey: privateJwk,
        });
        const sharedKey = yield X25519.sharedSecret({
            privateKeyA: privateX25519,
            publicKeyB: publicX25519,
        });
        const sharedEncryptionKey = yield Hkdf.deriveKeyBytes({
            baseKeyBytes: new Uint8Array(sharedKey),
            hash: 'SHA-256',
            salt: new Uint8Array(),
            info: new Uint8Array(),
            length: 256,
        });
        return sharedEncryptionKey;
    });
}
/**
 * Encrypts the auth response jwt. Requires a randomPin is added to the AAD of the
 * encryption algorithm in order to prevent man in the middle and eavesdropping attacks.
 * The keyid of the delegate did is used to pass the public key to the client in order
 * for the client to derive the shared ECDH private key.
 */
function encryptAuthResponse(_a) {
    return __awaiter(this, arguments, void 0, function* ({ jwt, encryptionKey, delegateDidKeyId, randomPin, }) {
        const protectedHeader = {
            alg: 'dir',
            cty: 'JWT',
            enc: 'XC20P',
            typ: 'JWT',
            kid: delegateDidKeyId,
        };
        const nonce = CryptoUtils.randomBytes(24);
        const additionalData = Convert.object(Object.assign(Object.assign({}, protectedHeader), { pin: randomPin })).toUint8Array();
        const jwtBytes = Convert.string(jwt).toUint8Array();
        const ciphertextAndTag = yield XChaCha20Poly1305.encryptRaw({ data: jwtBytes, keyBytes: encryptionKey, nonce, additionalData });
        /** The cipher output concatenates the encrypted data and tag
         * so we need to extract the values for use in the JWE. */
        const ciphertext = ciphertextAndTag.subarray(0, -16);
        const authenticationTag = ciphertextAndTag.subarray(-16);
        const compactJwe = [
            Convert.object(protectedHeader).toBase64Url(),
            '', // Empty string since there is no wrapped key.
            Convert.uint8Array(nonce).toBase64Url(),
            Convert.uint8Array(ciphertext).toBase64Url(),
            Convert.uint8Array(authenticationTag).toBase64Url(),
        ].join('.');
        return compactJwe;
    });
}
function shouldUseDelegatePermission(scope) {
    // Currently all record permissions are treated as delegated permissions
    // In the future only methods that modify state will be delegated and the rest will be normal permissions
    if (isRecordPermissionScope(scope)) {
        return true;
    }
    else if (scope.interface === DwnInterfaceName.Protocols && scope.method === DwnMethodName.Configure) {
        // ProtocolConfigure messages are also delegated, as they modify state
        return true;
    }
    // All other permissions are not treated as delegated
    return false;
}
/**
 * Creates the permission grants that assign to the selectedDid the level of
 * permissions that the web app requested in the {@link Web5ConnectAuthRequest}
 */
function createPermissionGrants(selectedDid, delegateBearerDid, agent, scopes) {
    return __awaiter(this, void 0, void 0, function* () {
        const permissionsApi = new AgentPermissionsApi({ agent });
        // TODO: cleanup all grants if one fails by deleting them from the DWN: https://github.com/enboxorg/enbox/issues/849
        logger.log(`Creating permission grants for ${scopes.length} scopes given...`);
        const permissionGrants = yield Promise.all(scopes.map((scope) => {
            // check if the scope is a records permission scope, or a protocol configure scope, if so it should use a delegated permission.
            const delegated = shouldUseDelegatePermission(scope);
            return permissionsApi.createGrant({
                delegated,
                store: true,
                grantedTo: delegateBearerDid.uri,
                scope,
                dateExpires: '2040-06-25T16:09:16.693356Z', // TODO: make dateExpires optional
                author: selectedDid,
            });
        }));
        logger.log(`Sending ${permissionGrants.length} permission grants to remote DWN...`);
        const messagePromises = permissionGrants.map((grant) => __awaiter(this, void 0, void 0, function* () {
            // Quirk: we have to pull out encodedData out of the message the schema validator doesn't want it there
            const _a = grant.message, { encodedData } = _a, rawMessage = __rest(_a, ["encodedData"]);
            const data = Convert.base64Url(encodedData).toUint8Array();
            const { reply } = yield agent.sendDwnRequest({
                author: selectedDid,
                target: selectedDid,
                messageType: DwnInterface.RecordsWrite,
                dataStream: new Blob([data]),
                rawMessage,
            });
            // check if the message was sent successfully, if the remote returns 409 the message may have come through already via sync
            if (reply.status.code !== 202 && reply.status.code !== 409) {
                logger.error(`Error sending RecordsWrite: ${reply.status.detail}`);
                logger.error(`RecordsWrite message: ${rawMessage}`);
                throw new Error(`Could not send the message. Error details: ${reply.status.detail}`);
            }
            return grant.message;
        }));
        try {
            const messages = yield Promise.all(messagePromises);
            return messages;
        }
        catch (error) {
            logger.error(`Error during batch-send of permission grants: ${error}`);
            throw error;
        }
    });
}
/**
 * Installs the protocol required by the Client on the Provider if it doesn't already exist.
 */
function prepareProtocol(selectedDid, agent, protocolDefinition) {
    return __awaiter(this, void 0, void 0, function* () {
        const queryMessage = yield agent.processDwnRequest({
            author: selectedDid,
            messageType: DwnInterface.ProtocolsQuery,
            target: selectedDid,
            messageParams: { filter: { protocol: protocolDefinition.protocol } },
        });
        if (queryMessage.reply.status.code !== 200) {
            // if the query failed, throw an error
            throw new Error(`Could not fetch protocol: ${queryMessage.reply.status.detail}`);
        }
        else if (queryMessage.reply.entries === undefined || queryMessage.reply.entries.length === 0) {
            logger.log(`Protocol does not exist, creating: ${protocolDefinition.protocol}`);
            // send the protocol definition to the remote DWN first, if it passes we can process it locally
            const { reply: sendReply, message: configureMessage } = yield agent.sendDwnRequest({
                author: selectedDid,
                target: selectedDid,
                messageType: DwnInterface.ProtocolsConfigure,
                messageParams: { definition: protocolDefinition },
            });
            // check if the message was sent successfully, if the remote returns 409 the message may have come through already via sync
            if (sendReply.status.code !== 202 && sendReply.status.code !== 409) {
                throw new Error(`Could not send protocol: ${sendReply.status.detail}`);
            }
            // process the protocol locally, we don't have to check if it exists as this is just a convenience over waiting for sync.
            yield agent.processDwnRequest({
                author: selectedDid,
                target: selectedDid,
                messageType: DwnInterface.ProtocolsConfigure,
                rawMessage: configureMessage
            });
        }
        else {
            logger.log(`Protocol already exists: ${protocolDefinition.protocol}`);
            // the protocol already exists, let's make sure it exists on the remote DWN as the requesting app will need it
            const configureMessage = queryMessage.reply.entries[0];
            const { reply: sendReply } = yield agent.sendDwnRequest({
                author: selectedDid,
                target: selectedDid,
                messageType: DwnInterface.ProtocolsConfigure,
                rawMessage: configureMessage,
            });
            if (sendReply.status.code !== 202 && sendReply.status.code !== 409) {
                throw new Error(`Could not send protocol: ${sendReply.status.detail}`);
            }
        }
    });
}
/**
 * Creates a delegate did which the web app will use as its future indentity.
 * Assigns to that DID the level of permissions that the web app requested in
 * the {@link Web5ConnectAuthRequest}. Encrypts via ECDH key that the web app
 * will have access to because the web app has the public key which it provided
 * in the {@link Web5ConnectAuthRequest}. Then sends the ciphertext of this
 * {@link Web5ConnectAuthResponse} to the callback endpoint. Which the
 * web app will need to retrieve from the token endpoint and decrypt with the pin to access.
 */
function submitAuthResponse(selectedDid, authRequest, randomPin, agent) {
    return __awaiter(this, void 0, void 0, function* () {
        const delegateBearerDid = yield DidJwk.create();
        const delegatePortableDid = yield delegateBearerDid.export();
        // TODO: roll back permissions and protocol configurations if an error occurs. Need a way to delete protocols to achieve this.
        const delegateGrantPromises = authRequest.permissionRequests.map((permissionRequest) => __awaiter(this, void 0, void 0, function* () {
            const { protocolDefinition, permissionScopes } = permissionRequest;
            // We validate that all permission scopes match the protocol uri of the protocol definition they are provided with.
            const grantsMatchProtocolUri = permissionScopes.every(scope => 'protocol' in scope && scope.protocol === protocolDefinition.protocol);
            if (!grantsMatchProtocolUri) {
                throw new Error('All permission scopes must match the protocol uri they are provided with.');
            }
            yield prepareProtocol(selectedDid, agent, protocolDefinition);
            const permissionGrants = yield Oidc.createPermissionGrants(selectedDid, delegateBearerDid, agent, permissionScopes);
            return permissionGrants;
        }));
        const delegateGrants = (yield Promise.all(delegateGrantPromises)).flat();
        logger.log('Generating auth response object...');
        const responseObject = yield Oidc.createResponseObject({
            //* the IDP's did that was selected to be connected
            iss: selectedDid,
            //* the client's new identity
            sub: delegateBearerDid.uri,
            //* the client's temporary ephemeral did used for connect
            aud: authRequest.client_id,
            //* the nonce of the original auth request
            nonce: authRequest.nonce,
            delegateGrants,
            delegatePortableDid,
        });
        // Sign the Response Object using the ephemeral DID's signing key.
        logger.log('Signing auth response object...');
        const responseObjectJwt = yield Oidc.signJwt({
            did: delegateBearerDid,
            data: responseObject,
        });
        const clientDid = yield DidJwk.resolve(authRequest.client_id);
        const sharedKey = yield Oidc.deriveSharedKey(delegateBearerDid, clientDid === null || clientDid === void 0 ? void 0 : clientDid.didDocument);
        logger.log('Encrypting auth response object...');
        const encryptedResponse = yield Oidc.encryptAuthResponse({
            jwt: responseObjectJwt,
            encryptionKey: sharedKey,
            delegateDidKeyId: delegateBearerDid.document.verificationMethod[0].id,
            randomPin,
        });
        const formEncodedRequest = new URLSearchParams({
            id_token: encryptedResponse,
            state: authRequest.state,
        }).toString();
        logger.log(`Sending auth response object to Web5 Connect server: ${authRequest.redirect_uri}`);
        yield fetch(authRequest.redirect_uri, {
            body: formEncodedRequest,
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
            },
            signal: AbortSignal.timeout(30000),
        });
    });
}
export const Oidc = {
    createAuthRequest,
    encryptAuthRequest,
    getAuthRequest,
    decryptAuthRequest,
    createPermissionGrants,
    createResponseObject,
    encryptAuthResponse,
    decryptAuthResponse,
    deriveSharedKey,
    signJwt,
    verifyJwt,
    buildOidcUrl,
    generateCodeChallenge,
    submitAuthResponse,
};
//# sourceMappingURL=oidc.js.map