ordinary-build 0.6.0

Build & codegen tool for Ordinary
Documentation
// 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 txt_encoder = new TextEncoder();

    const encoded_domain = txt_encoder.encode(domain);
    const encoded_account = txt_encoder.encode(account);
    const encoded_password = txt_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 = txt_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 [encoded_account, new Uint8Array(password_hash), new Uint8Array(mfa_hash)]
    } else {
        return [encoded_account, 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');

    const [encoded_account, password_hash] = await hash(domain, account, password);
    if (encoded_account.length > 255) throw new Error('encoded account cannot be longer than 255 bytes');

    const keypair = await crypto.subtle.generateKey(
        {
            name: 'X25519',
        },
        true,
        ["deriveKey"],
    );

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

    const body = new Uint8Array([
        encoded_account.length,
        ...encoded_account,
        ...password_hash,
        ...pubKeyBytes,
    ]);

    const res = await fetch('/accounts/registration/js', {
        method: 'POST',
        body,
        headers: {
            ...await getHeaders(),
            ...(invite_token ? {'authorization': invite_token} : {}),
        },
    });

    const res_bytes = await res.bytes();

    const ephemeralPubKey = await crypto.subtle.importKey(
        'raw',
        res_bytes.slice(0, 32),
        {
            name: 'X25519',
        },
        false,
        []
    );

    const nonce = res_bytes.slice(32, 44);
    const ciphertext = res_bytes.slice(44);

    const sharedSecret = await crypto.subtle.deriveKey(
        {
            name: 'X25519',
            public: ephemeralPubKey,
        },
        keypair.privateKey,
        {
            name: 'AES-GCM',
            length: 256,
        },
        false,
        ["decrypt"],
    );

    const decrypted = await crypto.subtle.decrypt(
        {name: 'AES-GCM', iv: nonce},
        sharedSecret,
        ciphertext,
    );

    const decoder = new TextDecoder();

    const [recovery_codes_str, mfa_svg] = decoder.decode(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,
    get_cookies,
) {
    const [encoded_account, password_hash, mfa_hash] = await hash(domain, account, password, mfa_code);
    if (encoded_account.length > 255) throw new Error('encoded account cannot be longer than 255 bytes');

    const schema = await getSchema();

    let body;

    if (schema.auth.cookies_enabled) {
        body = new Uint8Array([encoded_account.length, ...encoded_account, ...mfa_hash, ...password_hash]);
    } else {
        const keypair = await crypto.subtle.generateKey("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);

        body = new Uint8Array([
            encoded_account.length,
            ...encoded_account,
            ...mfa_hash,
            ...password_hash,
            ...pubKeyBytes
        ]);
    }

    const res = await fetch('/accounts/login/js', {
        method: 'POST',
        body,
        credentials: get_cookies ? 'include' : 'omit',
        headers: await getHeaders({cookies: get_cookies}),
    });

    const refresh_token = await res.bytes();

    setRefresh(refresh_token);

    return refresh_token;
}

export async function json(
    method,
    path,
    authorization = false,
    object = null,
) {
    if (object && method !== 'GET') {
        const {body, headers} = await tryCompress(
            JSON.stringify(object),
            'application/json'
        );

        return fetch(path, {
            method,
            body,
            headers: {
                ...headers,
                ...(await getHeaders({authorization})),
            },
            credentials: 'omit',
        })
    } else {
        return fetch(path, {
            method,
            headers: {
                ...(await getHeaders({authorization})),
            },
            credentials: 'omit',
        })
    }
}