ordinary-build 0.7.0

Build & codegen tool for Ordinary
// Copyright (C) 2026 Ordinary Labs, LLC.
//
// SPDX-License-Identifier: AGPL-3.0-only

import {
    ALG,
    base64decode,
    getHeaders,
    getSchema,
    setRefresh,
    setTokenSigningKey,
    tryCompress
} from '/assets/{{ version }}/js/core.js';

async function hash(
    domain,
    account,
    password,
    mfa_code,
) {
    const encoder = new TextEncoder();

    const encoded_domain = encoder.encode(domain);
    const encoded_account = encoder.encode(account);
    const encoded_password = encoder.encode(password);

    const all = new Uint8Array([...encoded_domain, ...encoded_account, ...encoded_password]);

    const schema = await getSchema();

    const alg = ALG[schema.auth.client_hash]
    const password_hash = await crypto.subtle.digest(alg, all);

    if (mfa_code) {
        const encoded_mfa_code = encoder.encode(mfa_code);
        const mfa_all = new Uint8Array([...encoded_domain, ...encoded_account, ...encoded_mfa_code]);

        const mfa_hash = await crypto.subtle.digest(alg, mfa_all);

        return [new Uint8Array(password_hash), new Uint8Array(mfa_hash)];
    } else {
        return new Uint8Array(password_hash);
    }
}

export async function register(
    account,
    password,
    domain,
    invite_token = null,
) {
    const schema = await getSchema();
    if (schema.auth.invite && !invite_token) throw new Error('invite token must be included');

    await __wbg_init();

    const password_hash = await hash(domain, account, password);
    const start_req = registration_start_req(
        account,
        password_hash,
    );

    const start_res = await fetch('/accounts/registration/start', {
        method: 'POST',
        body: start_req.request,
        headers: {
            ...await getHeaders({cookies: false}),
            ...(invite_token ? {'authorization': invite_token} : {}),
        },
    });
    const server_message = await start_res.bytes();

    const finish_req = registration_finish_req(
        account, password_hash, start_req.client_state, server_message
    );

    const finish_res = await fetch('/accounts/registration/finish', {
        method: 'POST',
        body: finish_req.request,
        headers: await getHeaders({cookies: false}),
    });
    const encrypted_totp_mfa = await finish_res.bytes();

    const decrypted = decrypt_totp_mfa(encrypted_totp_mfa, finish_req.private_key, domain, account);

    const [recovery_codes_str, mfa_svg] = decrypted.split('__');
    const recovery_codes = recovery_codes_str.match(/.{1,11}/g);

    return [mfa_svg, recovery_codes];
}

export async function login(
    account,
    password,
    mfa_code,
    domain,
) {
    await __wbg_init();

    const [password_hash, mfa_hash] = await hash(domain, account, password, mfa_code);
    const start_req = login_start_req(account, password_hash);

    const start_res = await fetch('/accounts/login/start', {
        method: 'POST',
        body: start_req.request,
        headers: await getHeaders({cookies: false}),
    });
    const server_message = await start_res.bytes();

    const keypair = await crypto.subtle.generateKey({name: "Ed25519"}, true, ["sign", "verify"]);

    const pubKeyBytes = new Uint8Array(
        await crypto.subtle.exportKey("raw", keypair.publicKey),
    );

    const privKeyBytes = new Uint8Array(
        await crypto.subtle.exportKey("pkcs8", keypair.privateKey),
    );

    setTokenSigningKey(privKeyBytes);

    const finish_req = login_finish_req(
        account, password_hash, mfa_hash, start_req.client_state, server_message, pubKeyBytes
    );

    const finish_res = await fetch('/accounts/login/finish', {
        method: 'POST',
        body: finish_req.request,
        credentials: 'omit',
        headers: await getHeaders({cookies: false}),
    });
    const encrypted_token = await finish_res.bytes();

    const refresh_token = decrypt_token(encrypted_token, finish_req.session_key);

    setRefresh(refresh_token);
}

export async function invoke(actionId, payload) {
    await __wbg_init();

    const schema = await getSchema();

    for (let i = 0; i < schema.actions.length; i += 1) {
        const action = schema.actions[i];

        if ((action.name === actionId || action.idx === actionId) && action.triggered_by.includes("Ordinary")) {
            const req = invoke_req(
                JSON.stringify(payload),
                JSON.stringify(action.accepts),
            );

            const send = await tryCompress(req, 'application/octet-stream');

            const res = await fetch(`/action/invoke/${action.idx}`, {
                method: 'POST',
                body: send.body,
                headers: {
                    ...(await getHeaders({cookies: false, authorization: action.protected})),
                    ...send.headers,
                },
            });

            const res_bytes = await res.bytes();

            const res_json = invoke_res(JSON.stringify(action.returns), res_bytes);
            return JSON.parse(res_json);
        }
    }
}