#include "api.js.h"

#include "file.js.h"
#include "log.h"
#include "mem.h"
#include "ssb.db.h"
#include "ssb.h"
#include "ssb.js.h"
#include "task.h"
#include "taskstub.js.h"
#include "trace.h"
#include "util.js.h"

#include "quickjs.h"
#include "sodium/crypto_box.h"
#include "sodium/crypto_scalarmult.h"
#include "sodium/crypto_scalarmult_curve25519.h"
#include "sodium/crypto_scalarmult_ed25519.h"
#include "sodium/crypto_secretbox.h"
#include "sodium/crypto_sign.h"
#include "sodium/randombytes.h"
#include "sqlite3.h"

#include <assert.h>
#include <stdlib.h>

#if !defined(__APPLE__) && !defined(__OpenBSD__) && !defined(_WIN32)
#include <alloca.h>
#endif

static JSClassID s_permission_test_class_id;
static JSClassID s_core_permission_test_class_id;

typedef void(permission_test_callback_t)(JSContext* context, bool granted, JSValue value, void* user_data);

static void _tf_ssb_permission_test(JSContext* context, JSValue process, const char* permission, const char* description, permission_test_callback_t* callback, void* user_data);
static void _post_message_internal(JSContext* context, JSValue from, JSValue to, const char* event_name, int argc, JSValue* argv, bool from_arg);
static JSValue _tf_api_process_callFunction(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data);

typedef struct _app_path_pair_t
{
	const char* app;
	const char* path;
} app_path_pair_t;

typedef struct _get_apps_t
{
	app_path_pair_t* apps;
	int count;
	JSContext* context;
	JSValue promise[2];
	char user[];
} get_apps_t;

static void _tf_api_core_apps_work(tf_ssb_t* ssb, void* user_data)
{
	get_apps_t* work = user_data;

	JSMallocFunctions funcs = { 0 };
	tf_get_js_malloc_functions(&funcs);
	JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL);
	JSContext* context = JS_NewContext(runtime);

	const char* apps = tf_ssb_db_get_property(ssb, work->user, "apps");
	if (apps)
	{
		JSValue apps_array = JS_ParseJSON(context, apps, strlen(apps), NULL);
		if (JS_IsArray(context, apps_array))
		{
			int length = tf_util_get_length(context, apps_array);
			for (int i = 0; i < length; i++)
			{
				JSValue name = JS_GetPropertyUint32(context, apps_array, i);
				const char* name_string = JS_ToCString(context, name);
				if (name_string)
				{
					work->apps = tf_resize_vec(work->apps, sizeof(app_path_pair_t) * (work->count + 1));
					work->apps[work->count].app = tf_strdup(name_string);
					size_t size = strlen("path:") + strlen(name_string) + 1;
					char* path_key = tf_malloc(size);
					snprintf(path_key, size, "path:%s", name_string);
					work->apps[work->count].path = tf_ssb_db_get_property(ssb, work->user, path_key);
					tf_free(path_key);
					work->count++;
				}
				JS_FreeCString(context, name_string);
				JS_FreeValue(context, name);
			}
		}
		JS_FreeValue(context, apps_array);
	}
	tf_free((void*)apps);

	JS_FreeContext(context);
	JS_FreeRuntime(runtime);
}

static void _tf_api_core_apps_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	get_apps_t* work = user_data;
	JSContext* context = work->context;
	JSValue result = JS_NewObject(context);
	for (int i = 0; i < work->count; i++)
	{
		JS_SetPropertyStr(context, result, work->apps[i].app, JS_NewString(context, work->apps[i].path ? work->apps[i].path : ""));
	}
	JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, result);
	JS_FreeValue(context, work->promise[0]);
	JS_FreeValue(context, work->promise[1]);
	for (int i = 0; i < work->count; i++)
	{
		tf_free((void*)work->apps[i].app);
		tf_free((void*)work->apps[i].path);
	}
	tf_free(work->apps);
	tf_free(work);
}

static const char* _tf_ssb_get_process_credentials_session_name(JSContext* context, JSValue process)
{
	JSValue credentials = JS_IsObject(process) ? JS_GetPropertyStr(context, process, "credentials") : JS_UNDEFINED;
	JSValue session = JS_IsObject(credentials) ? JS_GetPropertyStr(context, credentials, "session") : JS_UNDEFINED;
	const char* result = JS_IsObject(session) ? tf_util_get_property_as_string(context, session, "name") : NULL;
	JS_FreeValue(context, session);
	JS_FreeValue(context, credentials);
	return result;
}

static JSValue _tf_api_core_apps(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	JSValue result = JS_UNDEFINED;
	JSValue user = argv[0];
	JSValue process = data[0];
	const char* user_string = JS_IsString(user) ? JS_ToCString(context, user) : NULL;
	const char* session_name_string = _tf_ssb_get_process_credentials_session_name(context, process);

	if (user_string && session_name_string && strcmp(user_string, session_name_string) && strcmp(user_string, "core"))
	{
		JS_FreeCString(context, user_string);
		user_string = NULL;
	}
	else if (!user_string)
	{
		user_string = session_name_string;
		session_name_string = NULL;
	}
	JS_FreeCString(context, session_name_string);

	if (user_string)
	{
		get_apps_t* work = tf_malloc(sizeof(get_apps_t) + strlen(user_string) + 1);
		*work = (get_apps_t) {
			.context = context,
		};
		memcpy(work->user, user_string, strlen(user_string) + 1);
		result = JS_NewPromiseCapability(context, work->promise);

		tf_task_t* task = tf_task_get(context);
		tf_ssb_t* ssb = tf_task_get_ssb(task);
		tf_ssb_run_work(ssb, _tf_api_core_apps_work, _tf_api_core_apps_after_work, work);
	}
	else
	{
		result = JS_NewObject(context);
	}
	JS_FreeCString(context, user_string);
	return result;
}

typedef struct _users_t
{
	const char* users;
	JSContext* context;
	JSValue promise[2];
} users_t;

static void _tf_api_core_users_work(tf_ssb_t* ssb, void* user_data)
{
	users_t* work = user_data;
	work->users = tf_ssb_db_get_property(ssb, "auth", "users");
}

static void _tf_api_core_users_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	users_t* work = user_data;
	JSContext* context = work->context;
	JSValue result = JS_UNDEFINED;
	if (work->users)
	{
		result = JS_ParseJSON(context, work->users, strlen(work->users), NULL);
		tf_free((void*)work->users);
	}
	if (JS_IsUndefined(result))
	{
		result = JS_NewArray(context);
	}
	JSValue error = JS_Call(context, JS_IsArray(context, result) ? work->promise[0] : work->promise[1], JS_UNDEFINED, 1, &result);
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, result);
	JS_FreeValue(context, work->promise[0]);
	JS_FreeValue(context, work->promise[1]);
	tf_free(work);
}

static JSValue _tf_api_core_users(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	tf_task_t* task = tf_task_get(context);
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	users_t* work = tf_malloc(sizeof(users_t));
	*work = (users_t) {
		.context = context,
	};
	JSValue result = JS_NewPromiseCapability(context, work->promise);
	tf_ssb_run_work(ssb, _tf_api_core_users_work, _tf_api_core_users_after_work, work);
	return result;
}

typedef struct _broadcast_t
{
	JSContext* context;
	JSValue process;
	int argc;
	JSValue* argv;
	const char* event_name;
	const char* only_user;
	bool include_self;
	bool from_arg;
} broadcast_t;

static JSValue _tf_post_message_internal(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	_post_message_internal(context, data[0], data[1], "message", argc, argv, true);
	return JS_UNDEFINED;
}

static JSValue _get_user(JSContext* context, JSValue from, JSValue to)
{
	JSValue user = JS_NewObject(context);
	JS_SetPropertyStr(context, user, "packageOwner", JS_GetPropertyStr(context, to, "packageOwner"));
	JS_SetPropertyStr(context, user, "packageName", JS_GetPropertyStr(context, to, "packageName"));
	JS_SetPropertyStr(context, user, "credentials", JS_GetPropertyStr(context, to, "credentials"));
	JSValue data[] = {
		from,
		to,
	};
	JS_SetPropertyStr(context, user, "postMessageInternal", JS_NewCFunctionData(context, _tf_post_message_internal, 1, 0, tf_countof(data), data));
	return user;
}

static void _post_message_internal(JSContext* context, JSValue from, JSValue to, const char* event_name, int argc, JSValue* argv, bool from_arg)
{
	JSValue event_handlers = JS_GetPropertyStr(context, to, "eventHandlers");
	if (JS_IsObject(event_handlers))
	{
		JSValue message_handlers = JS_GetPropertyStr(context, event_handlers, event_name);
		int count = tf_util_get_length(context, message_handlers);
		JSValue* call_argv = alloca((argc + 1) * sizeof(JSValue));
		for (int i = 0; i < count; i++)
		{
			JSValue handler = JS_GetPropertyUint32(context, message_handlers, i);
			int count = 0;
			if (from_arg)
			{
				call_argv[count++] = _get_user(context, from, from);
			}
			for (int j = 0; j < argc; j++)
			{
				call_argv[count++] = argv[j];
			}
			JSValue result = JS_Call(context, handler, to, count, call_argv);
			tf_util_report_error(context, result);
			JS_FreeValue(context, result);
			JS_FreeValue(context, handler);
			if (from_arg)
			{
				JS_FreeValue(context, call_argv[0]);
			}
		}
		JS_FreeValue(context, message_handlers);
	}
	JS_FreeValue(context, event_handlers);
}

static void _tf_api_core_broadcast_visit(tf_task_t* task, tf_taskstub_t* stub, void* user_data)
{
	broadcast_t* broadcast = user_data;
	JSContext* context = broadcast->context;
	JSValue visit = tf_taskstub_get_task_object(stub);
	if (broadcast->include_self || !JS_StrictEq(context, broadcast->process, visit))
	{
		JSValue from_owner = JS_GetPropertyStr(context, broadcast->process, "packageOwner");
		JSValue from_name = JS_GetPropertyStr(context, broadcast->process, "packageName");
		JSValue to_owner = JS_GetPropertyStr(context, visit, "packageOwner");
		JSValue to_name = JS_GetPropertyStr(context, visit, "packageName");
		const char* to_user = _tf_ssb_get_process_credentials_session_name(context, visit);
		if (JS_StrictEq(context, from_owner, to_owner) && JS_StrictEq(context, from_name, to_name) &&
			(!broadcast->only_user || !to_user || strcmp(broadcast->only_user, to_user) == 0))
		{
			_post_message_internal(context, broadcast->process, visit, broadcast->event_name, broadcast->argc, broadcast->argv, broadcast->from_arg);
		}
		JS_FreeCString(context, to_user);
		JS_FreeValue(context, from_owner);
		JS_FreeValue(context, from_name);
		JS_FreeValue(context, to_owner);
		JS_FreeValue(context, to_name);
	}
}

static JSValue _tf_api_core_broadcast(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	JSValue process = data[0];
	tf_task_t* task = tf_task_get(context);
	broadcast_t broadcast = {
		.context = context,
		.process = process,
		.event_name = "message",
		.argc = argc,
		.argv = argv,
		.from_arg = true,
	};
	tf_task_visit_children(task, _tf_api_core_broadcast_visit, &broadcast);
	return JS_UNDEFINED;
}

static void _tf_api_broadcast_app_event_to_user(JSContext* context, JSValue process, const char* event_name, int argc, JSValue* argv, bool include_self)
{
	tf_task_t* task = tf_task_get(context);
	broadcast_t broadcast = {
		.context = context,
		.process = process,
		.event_name = event_name,
		.argc = argc,
		.argv = argv,
		.only_user = _tf_ssb_get_process_credentials_session_name(context, process),
		.include_self = true,
		.from_arg = false,
	};
	tf_task_visit_children(task, _tf_api_core_broadcast_visit, &broadcast);
	JS_FreeCString(context, broadcast.only_user);
}

static JSValue _tf_api_core_register(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	JSValue event_name = argv[0];
	JSValue handler = argv[1];
	JSValue process = data[0];
	JSValue event_handlers = JS_GetPropertyStr(context, process, "eventHandlers");
	JSAtom atom = JS_ValueToAtom(context, event_name);
	JSValue array = JS_GetProperty(context, event_handlers, atom);
	if (!JS_IsArray(context, array))
	{
		JS_FreeValue(context, array);
		array = JS_NewArray(context);
		JS_SetProperty(context, event_handlers, atom, JS_DupValue(context, array));
	}
	JS_SetPropertyUint32(context, array, tf_util_get_length(context, array), JS_DupValue(context, handler));
	JS_FreeValue(context, array);
	JS_FreeAtom(context, atom);
	JS_FreeValue(context, event_handlers);
	return JS_UNDEFINED;
}

static JSValue _tf_api_core_unregister(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	JSValue event_name = argv[0];
	JSValue handler = argv[1];
	JSValue process = data[0];
	JSValue event_handlers = JS_GetPropertyStr(context, process, "eventHandlers");
	JSAtom atom = JS_ValueToAtom(context, event_name);
	JSValue array = JS_GetProperty(context, event_handlers, atom);

	if (JS_IsArray(context, array))
	{
		JSValue index_of = JS_GetPropertyStr(context, array, "indexOf");

		JSValue index = JS_Call(context, index_of, array, 1, &handler);
		int int_index = -1;
		JS_ToInt32(context, &int_index, index);
		if (int_index != -1)
		{
			JSValue splice = JS_GetPropertyStr(context, array, "splice");
			JSValue splice_args[] = {
				index,
				JS_NewInt32(context, 1),
			};
			JSValue result = JS_Call(context, splice, array, 2, splice_args);
			JS_FreeValue(context, result);
			JS_FreeValue(context, splice);
		}
		JS_FreeValue(context, index);
		JS_FreeValue(context, index_of);

		if (tf_util_get_length(context, array) == 0)
		{
			JS_DeleteProperty(context, event_handlers, atom, 0);
		}
	}

	JS_FreeValue(context, array);
	JS_FreeAtom(context, atom);
	JS_FreeValue(context, event_handlers);
	return JS_UNDEFINED;
}

typedef struct _save_app_t
{
	const char* user;
	const char* package_owner;
	const char* app;
	const char* id;
	bool saved;
	JSContext* context;
	JSValue promise[2];
} save_app_t;

static void _tf_api_core_save_app_work(tf_ssb_t* ssb, void* user_data)
{
	save_app_t* work = user_data;
	if (!work->user || !work->package_owner || !work->app)
	{
		return;
	}

	if (strcmp(work->user, work->package_owner) == 0 || (strcmp(work->package_owner, "core") == 0 && tf_ssb_db_user_has_permission(ssb, NULL, work->user, "administration")))
	{
		size_t length = strlen("path:") + strlen(work->app) + 1;
		char* app_path = alloca(length);
		snprintf(app_path, length, "path:%s", work->app);

		if (work->id)
		{
			const char* old_value = tf_ssb_db_get_property(ssb, work->package_owner, app_path);
			if (old_value && strcmp(old_value, work->id) == 0)
			{
				work->saved = true;
			}
			else
			{
				work->saved =
					tf_ssb_db_set_property(ssb, work->package_owner, app_path, work->id) && tf_ssb_db_add_value_to_array_property(ssb, work->package_owner, "apps", work->app);
			}
			tf_free((void*)old_value);
		}
		else
		{
			if (tf_ssb_db_remove_value_from_array_property(ssb, work->package_owner, "apps", work->app))
			{
				work->saved = true;
			}
			if (tf_ssb_db_remove_property(ssb, work->package_owner, app_path))
			{
				work->saved = true;
			}
		}
	}
}

static void _tf_api_core_save_app_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	save_app_t* work = user_data;
	JSContext* context = work->context;
	JSValue error = JS_Call(context, work->saved ? work->promise[0] : work->promise[1], JS_UNDEFINED, 0, NULL);
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, work->promise[0]);
	JS_FreeValue(context, work->promise[1]);
	JS_FreeCString(context, work->user);
	JS_FreeCString(context, work->package_owner);
	JS_FreeCString(context, work->app);
	JS_FreeCString(context, work->id);
	tf_free(work);
}

static void _tf_api_core_save_app_start_work(JSContext* context, bool granted, JSValue value, void* user_data)
{
	save_app_t* work = user_data;
	tf_task_t* task = tf_task_get(context);
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	if (granted)
	{
		tf_ssb_run_work(ssb, _tf_api_core_save_app_work, _tf_api_core_save_app_after_work, work);
	}
	else
	{
		_tf_api_core_save_app_after_work(ssb, 0, work);
	}
}

static JSValue _tf_api_core_save_app(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	JSValue process = data[0];
	save_app_t* work = tf_malloc(sizeof(save_app_t));
	*work = (save_app_t) {
		.user = _tf_ssb_get_process_credentials_session_name(context, process),
		.package_owner = JS_IsString(argv[0]) ? JS_ToCString(context, argv[0]) : NULL,
		.app = JS_IsString(argv[1]) ? JS_ToCString(context, argv[1]) : NULL,
		.id = JS_IsString(argv[2]) ? JS_ToCString(context, argv[2]) : NULL,
		.context = context,
	};
	JSValue result = JS_NewPromiseCapability(context, work->promise);
	char description[1024];
	snprintf(description, sizeof(description), "Update /~%s/%s/.", work->package_owner, work->app);
	_tf_ssb_permission_test(context, process, "app_save", description, _tf_api_core_save_app_start_work, work);
	return result;
}

typedef struct _permissions_for_user_t
{
	const char* user;
	const char* settings;
	JSContext* context;
	JSValue promise[2];
} permissions_for_user_t;

static void _tf_api_core_permissions_for_user_work(tf_ssb_t* ssb, void* user_data)
{
	permissions_for_user_t* work = user_data;
	work->settings = tf_ssb_db_get_property(ssb, "core", "settings");
}

static void _tf_api_core_permissions_for_user_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	permissions_for_user_t* work = user_data;
	JSContext* context = work->context;
	JSValue result = JS_UNDEFINED;
	if (work->settings)
	{
		JSValue json = JS_ParseJSON(context, work->settings, strlen(work->settings), NULL);
		if (JS_IsObject(json))
		{
			JSValue permissions = JS_GetPropertyStr(context, json, "permissions");
			if (JS_IsObject(permissions))
			{
				result = JS_GetPropertyStr(context, permissions, work->user);
			}
			JS_FreeValue(context, permissions);
		}
		JS_FreeValue(context, json);
		tf_free((void*)work->settings);
	}
	if (JS_IsUndefined(result))
	{
		result = JS_NewArray(context);
	}
	JSValue error = JS_Call(context, JS_IsArray(context, result) ? work->promise[0] : work->promise[1], JS_UNDEFINED, 1, &result);
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, result);
	JS_FreeValue(context, work->promise[0]);
	JS_FreeValue(context, work->promise[1]);
	JS_FreeCString(context, work->user);
	tf_free(work);
}

static JSValue _tf_api_core_permissionsForUser(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	tf_task_t* task = tf_task_get(context);
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	permissions_for_user_t* work = tf_malloc(sizeof(permissions_for_user_t));
	*work = (permissions_for_user_t) {
		.context = context,
		.user = JS_ToCString(context, argv[0]),
	};
	JSValue result = JS_NewPromiseCapability(context, work->promise);
	tf_ssb_run_work(ssb, _tf_api_core_permissions_for_user_work, _tf_api_core_permissions_for_user_after_work, work);
	return result;
}

typedef struct _permissions_granted_t
{
	JSContext* context;
	JSValue process;
	const char* user;
	const char* package_owner;
	const char* package_name;
	const char* settings;
	JSValue promise[2];
} permissions_granted_t;

static void _tf_api_core_permissions_granted_work(tf_ssb_t* ssb, void* user_data)
{
	permissions_granted_t* work = user_data;
	work->settings = tf_ssb_db_get_property(ssb, "core", "settings");
}

static JSValue _tf_api_core_permissions_from_settings(JSContext* context, const char* user, const char* package_owner, const char* package_name, const char* settings)
{
	JSValue result = JS_UNDEFINED;
	if (settings)
	{
		JSValue json = JS_ParseJSON(context, settings, strlen(settings), NULL);
		if (JS_IsObject(json) && user && package_owner && package_name)
		{
			JSValue user_permissions = JS_GetPropertyStr(context, json, "userPermissions");
			if (JS_IsObject(user_permissions))
			{
				JSValue user_value = JS_GetPropertyStr(context, user_permissions, user);
				if (JS_IsObject(user_value))
				{
					JSValue package_owner_value = JS_GetPropertyStr(context, user_value, package_owner);
					if (JS_IsObject(package_owner_value))
					{
						result = JS_GetPropertyStr(context, package_owner_value, package_name);
					}
					JS_FreeValue(context, package_owner_value);
				}
				JS_FreeValue(context, user_value);
			}
			JS_FreeValue(context, user_permissions);
		}
		JS_FreeValue(context, json);
	}
	return result;
}

static void _tf_api_core_permissions_granted_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	permissions_granted_t* work = user_data;
	JSContext* context = work->context;
	JSValue result = _tf_api_core_permissions_from_settings(context, work->user, work->package_owner, work->package_name, work->settings);
	tf_free((void*)work->settings);

	JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, result);
	JS_FreeValue(context, work->promise[0]);
	JS_FreeValue(context, work->promise[1]);
	JS_FreeCString(context, work->user);
	tf_free((void*)work->package_owner);
	tf_free((void*)work->package_name);
	tf_free(work);
}

static JSValue _tf_api_core_permissionsGranted(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	tf_task_t* task = tf_task_get(context);
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	JSValue process = data[0];
	const char* package_owner = tf_util_get_property_as_string(context, process, "packageOwner");
	const char* package_name = tf_util_get_property_as_string(context, process, "packageName");
	permissions_granted_t* work = tf_malloc(sizeof(permissions_granted_t));
	*work = (permissions_granted_t) {
		.context = context,
		.user = _tf_ssb_get_process_credentials_session_name(context, process),
		.package_owner = tf_strdup(package_owner),
		.package_name = tf_strdup(package_name),
	};
	JS_FreeCString(context, package_owner);
	JS_FreeCString(context, package_name);
	JSValue result = JS_NewPromiseCapability(context, work->promise);
	tf_ssb_run_work(ssb, _tf_api_core_permissions_granted_work, _tf_api_core_permissions_granted_after_work, work);
	return result;
}

static void _tf_api_core_all_permissions_granted_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	permissions_granted_t* work = user_data;
	JSContext* context = work->context;
	JSValue result = JS_UNDEFINED;
	if (work->settings)
	{
		JSValue json = JS_ParseJSON(context, work->settings, strlen(work->settings), NULL);
		if (JS_IsObject(json) && work->user)
		{
			JSValue user_permissions = JS_GetPropertyStr(context, json, "userPermissions");
			if (JS_IsObject(user_permissions))
			{
				result = JS_GetPropertyStr(context, user_permissions, work->user);
			}
			JS_FreeValue(context, user_permissions);
		}
		JS_FreeValue(context, json);
		tf_free((void*)work->settings);
	}

	JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, result);
	JS_FreeValue(context, work->promise[0]);
	JS_FreeValue(context, work->promise[1]);
	JS_FreeCString(context, work->user);
	tf_free(work);
}

static JSValue _tf_api_core_allPermissionsGranted(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	tf_task_t* task = tf_task_get(context);
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	JSValue process = data[0];
	permissions_granted_t* work = tf_malloc(sizeof(permissions_granted_t));
	*work = (permissions_granted_t) {
		.context = context,
		.user = _tf_ssb_get_process_credentials_session_name(context, process),
	};
	JSValue result = JS_NewPromiseCapability(context, work->promise);
	tf_ssb_run_work(ssb, _tf_api_core_permissions_granted_work, _tf_api_core_all_permissions_granted_after_work, work);
	return result;
}

typedef struct _core_permission_test_t
{
	JSContext* context;
	taskid_t task_id;
	const char* user;
	const char* package_owner;
	const char* package_name;
	const char* permission;
	const char* description;
	bool is_stored;
	bool is_granted;
	JSValue promise[2];
} core_permission_test_t;

static void _tf_api_core_permissionTest_work(tf_ssb_t* ssb, void* user_data)
{
	core_permission_test_t* work = user_data;

	JSMallocFunctions funcs = { 0 };
	tf_get_js_malloc_functions(&funcs);
	JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL);
	JSContext* context = JS_NewContext(runtime);

	const char* settings = tf_ssb_db_get_property(ssb, "core", "settings");
	if (settings)
	{
		JSValue settings_value = JS_ParseJSON(context, settings, strlen(settings), NULL);
		JSValue user_permissions = JS_UNDEFINED;
		JSValue user = JS_UNDEFINED;
		JSValue package_owner = JS_UNDEFINED;
		JSValue package_name = JS_UNDEFINED;
		JSValue permission = JS_UNDEFINED;
		if (JS_IsObject(settings_value))
		{
			user_permissions = JS_GetPropertyStr(context, settings_value, "userPermissions");
		}
		if (JS_IsObject(user_permissions))
		{
			user = JS_GetPropertyStr(context, user_permissions, work->user);
		}
		if (JS_IsObject(user))
		{
			package_owner = JS_GetPropertyStr(context, user, work->package_owner);
		}
		if (JS_IsObject(package_owner))
		{
			package_name = JS_GetPropertyStr(context, package_owner, work->package_name);
		}
		if (JS_IsObject(package_name))
		{
			permission = JS_GetPropertyStr(context, package_name, work->permission);
		}
		if (JS_IsBool(permission))
		{
			work->is_stored = true;
			work->is_granted = JS_ToBool(context, permission);
		}
		JS_FreeValue(context, permission);
		JS_FreeValue(context, package_name);
		JS_FreeValue(context, package_owner);
		JS_FreeValue(context, user);
		JS_FreeValue(context, user_permissions);
		JS_FreeValue(context, settings_value);
	}
	tf_free((void*)settings);

	JS_FreeContext(context);
	JS_FreeRuntime(runtime);
}

static void _tf_api_core_permissionTest_finish(core_permission_test_t* work, bool allowed)
{
	JSContext* context = work->context;
	JSValue error;

	if (allowed)
	{
		JSValue result = JS_TRUE;
		error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
	}
	else
	{
		JSValue denied = JS_ThrowInternalError(context, "Permission denied: %s.", work->permission);
		JSValue result = JS_GetException(context);
		error = JS_Call(context, work->promise[1], JS_UNDEFINED, 1, &result);
		JS_FreeValue(context, result);
		JS_FreeValue(context, denied);
	}
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, work->promise[0]);
	JS_FreeValue(context, work->promise[1]);
	JS_FreeCString(context, work->user);
	JS_FreeCString(context, work->package_owner);
	JS_FreeCString(context, work->package_name);
	JS_FreeCString(context, work->permission);
	JS_FreeCString(context, work->description);
	tf_free(work);
}

static void _tf_ssb_core_permission_test_finalizer(JSRuntime* runtime, JSValue value)
{
	core_permission_test_t* work = JS_GetOpaque(value, s_core_permission_test_class_id);
	if (work)
	{
		_tf_api_core_permissionTest_finish(work, false);
	}
}

static void _tf_api_core_set_user_permission(JSContext* context, JSValue process, const char* permission, bool allow)
{
	JSValue arguments[] = {
		JS_NewString(context, permission),
		allow ? JS_TRUE : JS_FALSE,
	};
	JSValue set = JS_GetPropertyStr(context, process, "setUserPermission");
	JSValue result = JS_Call(context, set, process, tf_countof(arguments), arguments);
	tf_util_report_error(context, result);
	JS_FreeValue(context, result);
	JS_FreeValue(context, set);
	for (int i = 0; i < tf_countof(arguments); i++)
	{
		JS_FreeValue(context, arguments[i]);
	}
}

static JSValue _tf_api_process_reset_user_permission(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	JSValue process = data[0];
	JSValue arguments[] = {
		argv[0],
		JS_UNDEFINED,
	};
	JSValue set = JS_GetPropertyStr(context, process, "setUserPermission");
	JSValue result = JS_Call(context, set, process, tf_countof(arguments), arguments);
	JS_FreeValue(context, set);
	return result;
}

static JSValue _tf_api_core_permissionTest_then(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	core_permission_test_t* work = JS_GetOpaque(data[0], s_core_permission_test_class_id);

	const char* value = NULL;
	tf_task_t* task = tf_task_get(context);
	tf_taskstub_t* stub = tf_task_get_stub(task, work->task_id);
	if (!JS_IsString(argv[0]) || (value = JS_ToCString(context, argv[0])) == NULL || !stub)
	{
		_tf_api_core_permissionTest_finish(work, false);
		JS_SetOpaque(data[0], NULL);
		return JS_UNDEFINED;
	}

	JSValue process = tf_taskstub_get_task_object(stub);
	if (strcmp(value, "allow") == 0)
	{
		_tf_api_core_set_user_permission(context, process, work->permission, true);
		_tf_api_core_permissionTest_finish(work, true);
	}
	else if (strcmp(value, "allow once") == 0)
	{
		_tf_api_core_permissionTest_finish(work, true);
	}
	else if (strcmp(value, "deny") == 0)
	{
		_tf_api_core_set_user_permission(context, process, work->permission, false);
		_tf_api_core_permissionTest_finish(work, false);
	}
	else if (strcmp(value, "deny once") == 0)
	{
		_tf_api_core_permissionTest_finish(work, false);
	}
	else
	{
		_tf_api_core_permissionTest_finish(work, false);
	}
	JS_SetOpaque(data[0], NULL);
	JS_FreeCString(context, value);
	return JS_UNDEFINED;
}

static JSValue _tf_api_core_permissionTest_catch(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	core_permission_test_t* work = JS_GetOpaque(data[0], s_core_permission_test_class_id);
	_tf_api_core_permissionTest_finish(work, false);
	JS_SetOpaque(data[0], NULL);
	return JS_UNDEFINED;
}

static void _tf_api_core_permissionTest_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	core_permission_test_t* work = user_data;
	JSContext* context = work->context;

	tf_task_t* task = tf_task_get(context);
	tf_taskstub_t* stub = tf_task_get_stub(task, work->task_id);
	if (!stub)
	{
		_tf_api_core_permissionTest_finish(work, false);
	}
	else if (work->is_stored)
	{
		_tf_api_core_permissionTest_finish(work, work->is_granted);
	}
	else
	{
		if (!s_core_permission_test_class_id)
		{
			JSClassDef def = {
				.class_name = "core_permission_test",
				.finalizer = _tf_ssb_core_permission_test_finalizer,
			};
			JS_NewClassID(&s_core_permission_test_class_id);
			JS_NewClass(JS_GetRuntime(context), s_core_permission_test_class_id, &def);
		}

		JSValue opaque = JS_NewObjectClass(context, s_core_permission_test_class_id);
		JS_SetOpaque(opaque, work);

		JSValue array = JS_NewArray(context);
		JS_SetPropertyUint32(context, array, 0, JS_DupValue(context, tf_taskstub_get_task_object(stub)));
		JS_SetPropertyUint32(context, array, 1, JS_NewString(context, "requestPermission"));

		JSValue arguments[] = {
			JS_NewString(context, work->permission),
			JS_NewString(context, work->description),
		};
		JSValue promise = _tf_api_process_callFunction(context, JS_UNDEFINED, tf_countof(arguments), arguments, 1, &array);
		for (int i = 0; i < tf_countof(arguments); i++)
		{
			JS_FreeValue(context, arguments[i]);
		}
		JS_FreeValue(context, array);

		JSValue then = JS_GetPropertyStr(context, promise, "then");
		JSValue then_callback = JS_NewCFunctionData(context, _tf_api_core_permissionTest_then, 1, 0, 1, &opaque);
		JSValue error = JS_Call(context, then, promise, 1, &then_callback);
		tf_util_report_error(context, error);
		JS_FreeValue(context, error);
		JS_FreeValue(context, then_callback);
		JS_FreeValue(context, then);

		JSValue catch = JS_GetPropertyStr(context, promise, "catch");
		JSValue catch_callback = JS_NewCFunctionData(context, _tf_api_core_permissionTest_catch, 1, 0, 1, &opaque);
		error = JS_Call(context, catch, promise, 1, &catch_callback);
		tf_util_report_error(context, error);
		JS_FreeValue(context, error);
		JS_FreeValue(context, catch_callback);
		JS_FreeValue(context, catch);

		JS_FreeValue(context, opaque);
		JS_FreeValue(context, promise);
	}
}

static JSValue _tf_api_core_permissionTest(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	JSValue process = data[0];
	tf_task_t* task = tf_task_get(context);
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	core_permission_test_t* work = tf_malloc(sizeof(core_permission_test_t));
	*work = (core_permission_test_t) {
		.context = context,
		.task_id = tf_taskstub_get_id(tf_taskstub_get(process)),
		.user = _tf_ssb_get_process_credentials_session_name(context, process),
		.package_owner = tf_util_get_property_as_string(context, process, "packageOwner"),
		.package_name = tf_util_get_property_as_string(context, process, "packageName"),
		.permission = JS_ToCString(context, argv[0]),
		.description = JS_ToCString(context, argv[1]),
	};
	JSValue result = JS_NewPromiseCapability(context, work->promise);
	tf_ssb_run_work(ssb, _tf_api_core_permissionTest_work, _tf_api_core_permissionTest_after_work, work);
	return result;
}

typedef struct _active_identity_work_t
{
	JSContext* context;
	const char* name;
	const char* package_owner;
	const char* package_name;
	char identity[k_id_base64_len];
	int result;
	JSValue promise[2];
} active_identity_work_t;

static void _tf_ssb_getActiveIdentity_visit(const char* user, const char* identity, void* user_data)
{
	active_identity_work_t* request = user_data;
	if (!*request->identity)
	{
		snprintf(request->identity, sizeof(request->identity), "@%s", identity);
	}
}

static void _tf_ssb_getActiveIdentity_work(tf_ssb_t* ssb, void* user_data)
{
	active_identity_work_t* request = user_data;
	sqlite3* db = tf_ssb_acquire_db_reader(ssb);
	tf_ssb_db_identity_get_active(db, request->name, request->package_owner, request->package_name, request->identity, sizeof(request->identity));
	tf_ssb_release_db_reader(ssb, db);

	if (!*request->identity && tf_ssb_db_user_has_permission(ssb, NULL, request->name, "administration"))
	{
		tf_ssb_whoami(ssb, request->identity, sizeof(request->identity));
	}

	if (!*request->identity)
	{
		tf_ssb_db_identity_visit(ssb, request->name, _tf_ssb_getActiveIdentity_visit, request);
	}
}

static void _tf_ssb_getActiveIdentity_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	active_identity_work_t* request = user_data;
	JSContext* context = request->context;
	if (request->result == 0)
	{
		JSValue identity = JS_NewString(context, request->identity);
		JSValue error = JS_Call(context, request->promise[0], JS_UNDEFINED, 1, &identity);
		JS_FreeValue(context, identity);
		tf_util_report_error(context, error);
		JS_FreeValue(context, error);
	}
	else
	{
		JSValue error = JS_Call(context, request->promise[1], JS_UNDEFINED, 0, NULL);
		tf_util_report_error(context, error);
		JS_FreeValue(context, error);
	}
	JS_FreeValue(context, request->promise[0]);
	JS_FreeValue(context, request->promise[1]);
	tf_free((void*)request->name);
	tf_free((void*)request->package_owner);
	tf_free((void*)request->package_name);
	tf_free(request);
}

static JSValue _tf_ssb_getActiveIdentity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	tf_task_t* task = tf_task_get(context);
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	JSValue process = data[0];
	const char* package_owner = tf_util_get_property_as_string(context, process, "packageOwner");
	const char* package_name = tf_util_get_property_as_string(context, process, "packageName");

	const char* name = _tf_ssb_get_process_credentials_session_name(context, process);
	active_identity_work_t* work = tf_malloc(sizeof(active_identity_work_t));
	*work = (active_identity_work_t) {
		.context = context,
		.name = tf_strdup(name),
		.package_owner = tf_strdup(package_owner),
		.package_name = tf_strdup(package_name),
	};
	JSValue result = JS_NewPromiseCapability(context, work->promise);
	JS_FreeCString(context, name);
	JS_FreeCString(context, package_owner);
	JS_FreeCString(context, package_name);

	tf_ssb_run_work(ssb, _tf_ssb_getActiveIdentity_work, _tf_ssb_getActiveIdentity_after_work, work);
	return result;
}

typedef struct _identities_visit_t
{
	JSContext* context;
	JSValue promise[2];
	const char** identities;
	int count;
	char user[];
} identities_visit_t;

static void _tf_ssb_getIdentities_visit(const char* user, const char* identity, void* user_data)
{
	identities_visit_t* work = user_data;
	work->identities = tf_resize_vec(work->identities, (work->count + 1) * sizeof(const char*));
	char id[k_id_base64_len];
	snprintf(id, sizeof(id), "@%s", identity);
	work->identities[work->count++] = tf_strdup(id);
}

static void _tf_ssb_get_all_identities_work(tf_ssb_t* ssb, void* user_data)
{
	tf_ssb_db_identity_visit_all(ssb, _tf_ssb_getIdentities_visit, user_data);
}

static void _tf_ssb_get_identities_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	identities_visit_t* work = user_data;
	JSContext* context = tf_ssb_get_context(ssb);
	JSValue result = JS_NewArray(context);
	for (int i = 0; i < work->count; i++)
	{
		JS_SetPropertyUint32(context, result, i, JS_NewString(context, work->identities[i]));
		tf_free((void*)work->identities[i]);
	}
	tf_free(work->identities);

	JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
	JS_FreeValue(context, result);
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, work->promise[0]);
	JS_FreeValue(context, work->promise[1]);
	tf_free(work);
}

static JSValue _tf_ssb_getAllIdentities(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
	JSValue result = JS_UNDEFINED;
	tf_task_t* task = tf_task_get(context);
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	if (ssb)
	{
		identities_visit_t* work = tf_malloc(sizeof(identities_visit_t));
		*work = (identities_visit_t) {
			.context = context,
		};

		result = JS_NewPromiseCapability(context, work->promise);
		tf_ssb_run_work(ssb, _tf_ssb_get_all_identities_work, _tf_ssb_get_identities_after_work, work);
	}
	return result;
}

static void _tf_ssb_get_identities_work(tf_ssb_t* ssb, void* user_data)
{
	identities_visit_t* work = user_data;
	if (tf_ssb_db_user_has_permission(ssb, NULL, work->user, "administration"))
	{
		char id[k_id_base64_len] = "";
		if (tf_ssb_whoami(ssb, id, sizeof(id)))
		{
			_tf_ssb_getIdentities_visit(work->user, *id == '@' ? id + 1 : id, work);
		}
	}
	tf_ssb_db_identity_visit(ssb, work->user, _tf_ssb_getIdentities_visit, user_data);
}

static JSValue _tf_ssb_getIdentities(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	JSValue result = JS_UNDEFINED;
	tf_task_t* task = tf_task_get(context);
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	JSValue process = data[0];
	if (ssb)
	{
		const char* user = _tf_ssb_get_process_credentials_session_name(context, process);
		if (user)
		{
			size_t user_length = user ? strlen(user) : 0;
			identities_visit_t* work = tf_malloc(sizeof(identities_visit_t) + user_length + 1);
			*work = (identities_visit_t) {
				.context = context,
			};
			memcpy(work->user, user, user_length + 1);
			JS_FreeCString(context, user);

			result = JS_NewPromiseCapability(context, work->promise);
			tf_ssb_run_work(ssb, _tf_ssb_get_identities_work, _tf_ssb_get_identities_after_work, work);
		}
	}
	return result;
}

static JSValue _tf_ssb_getOwnerIdentities(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	JSValue result = JS_UNDEFINED;
	tf_task_t* task = tf_task_get(context);
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	JSValue process = data[0];
	if (ssb)
	{
		const char* user = tf_util_get_property_as_string(context, process, "packageOwner");
		size_t user_length = user ? strlen(user) : 0;
		identities_visit_t* work = tf_malloc(sizeof(identities_visit_t) + user_length + 1);
		*work = (identities_visit_t) {
			.context = context,
		};
		memcpy(work->user, user ? user : "", user_length + 1);
		JS_FreeCString(context, user);

		result = JS_NewPromiseCapability(context, work->promise);
		tf_ssb_run_work(ssb, _tf_ssb_get_identities_work, _tf_ssb_get_identities_after_work, work);
	}
	return result;
}

typedef struct _settings_descriptions_get_t
{
	const char* settings;
	JSContext* context;
	JSValue promise[2];
} settings_descriptions_get_t;

static void _tf_ssb_get_settings_descriptions_work(tf_ssb_t* ssb, void* user_data)
{
	settings_descriptions_get_t* work = user_data;
	work->settings = tf_ssb_db_get_property(ssb, "core", "settings");
}

static void _tf_ssb_get_settings_descriptions_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	settings_descriptions_get_t* work = user_data;
	JSContext* context = work->context;
	JSValue result = JS_NewObject(context);
	JSValue settings = JS_ParseJSON(context, work->settings ? work->settings : "", work->settings ? strlen(work->settings) : 0, NULL);
	const char* name;
	const char* type;
	tf_setting_kind_t kind;
	const char* description;
	for (int i = 0; tf_util_get_global_setting_by_index(i, &name, &type, &kind, &description); i++)
	{
		JSValue entry = JS_NewObject(context);
		JS_SetPropertyStr(context, entry, "type", JS_NewString(context, type));
		JS_SetPropertyStr(context, entry, "description", JS_NewString(context, description));
		switch (kind)
		{
		case k_kind_unknown:
			break;
		case k_kind_bool:
			JS_SetPropertyStr(context, entry, "default_value", JS_NewBool(context, tf_util_get_default_global_setting_bool(name)));
			break;
		case k_kind_int:
			JS_SetPropertyStr(context, entry, "default_value", JS_NewInt32(context, tf_util_get_default_global_setting_int(name)));
			break;
		case k_kind_string:
			JS_SetPropertyStr(context, entry, "default_value", JS_NewString(context, tf_util_get_default_global_setting_string(name)));
			break;
		}
		if (JS_IsObject(settings))
		{
			JS_SetPropertyStr(context, entry, "value", JS_GetPropertyStr(context, settings, name));
		}
		JS_SetPropertyStr(context, result, name, entry);
	}
	JS_FreeValue(context, settings);
	JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, result);
	JS_FreeValue(context, work->promise[0]);
	JS_FreeValue(context, work->promise[1]);
	tf_free((void*)work->settings);
	tf_free(work);
}

static JSValue _tf_ssb_globalSettingsDescriptions(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
	tf_task_t* task = tf_task_get(context);
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	settings_descriptions_get_t* work = tf_malloc(sizeof(settings_descriptions_get_t));
	*work = (settings_descriptions_get_t) { .context = context };
	JSValue result = JS_NewPromiseCapability(context, work->promise);
	tf_ssb_run_work(ssb, _tf_ssb_get_settings_descriptions_work, _tf_ssb_get_settings_descriptions_after_work, work);
	return result;
}

typedef struct _settings_get_t
{
	const char* key;
	tf_setting_kind_t kind;
	void* value;
	JSContext* context;
	JSValue promise[2];
} settings_get_t;

static void _tf_ssb_settings_get_work(tf_ssb_t* ssb, void* user_data)
{
	settings_get_t* work = user_data;
	work->kind = tf_util_get_global_setting_kind(work->key);
	switch (work->kind)
	{
	case k_kind_unknown:
		break;
	case k_kind_bool:
		{
			sqlite3* db = tf_ssb_acquire_db_reader(ssb);
			bool value = false;
			tf_ssb_db_get_global_setting_bool(db, work->key, &value);
			work->value = (void*)(intptr_t)value;
			tf_ssb_release_db_reader(ssb, db);
		}
		break;
	case k_kind_int:
		{
			sqlite3* db = tf_ssb_acquire_db_reader(ssb);
			int64_t value = 0;
			tf_ssb_db_get_global_setting_int64(db, work->key, &value);
			work->value = (void*)(intptr_t)value;
			tf_ssb_release_db_reader(ssb, db);
		}
		break;
	case k_kind_string:
		{
			sqlite3* db = tf_ssb_acquire_db_reader(ssb);
			work->value = (void*)tf_ssb_db_get_global_setting_string_alloc(db, work->key);
			tf_ssb_release_db_reader(ssb, db);
		}
		break;
	}
}

static void _tf_ssb_settings_get_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	settings_get_t* work = user_data;
	JSContext* context = tf_ssb_get_context(ssb);
	JSValue result = JS_UNDEFINED;
	switch (work->kind)
	{
	case k_kind_unknown:
		break;
	case k_kind_bool:
		result = work->value ? JS_TRUE : JS_FALSE;
		break;
	case k_kind_int:
		result = JS_NewInt64(context, (int64_t)(intptr_t)work->value);
		break;
	case k_kind_string:
		result = JS_NewString(context, (const char*)work->value);
		tf_free(work->value);
		break;
	}

	JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, result);
	JS_FreeValue(context, work->promise[0]);
	JS_FreeValue(context, work->promise[1]);
	JS_FreeCString(context, work->key);
	tf_free(work);
}

static JSValue _tf_ssb_globalSettingsGet(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
	tf_task_t* task = tf_task_get(context);
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	settings_get_t* work = tf_malloc(sizeof(settings_get_t));
	*work = (settings_get_t) { .context = context, .key = JS_ToCString(context, argv[0]) };
	JSValue result = JS_NewPromiseCapability(context, work->promise);
	tf_ssb_run_work(ssb, _tf_ssb_settings_get_work, _tf_ssb_settings_get_after_work, work);
	return result;
}

typedef struct _modify_block_t
{
	const char* user;
	char id[k_id_base64_len];
	bool add;
	bool completed;
	JSValue result;
	JSValue promise[2];
} modify_block_t;

static void _tf_ssb_modify_block_work(tf_ssb_t* ssb, void* user_data)
{
	modify_block_t* work = user_data;
	sqlite3* db = tf_ssb_acquire_db_writer(ssb);
	if (work->add)
	{
		tf_ssb_db_add_block(db, work->id);
	}
	else
	{
		tf_ssb_db_remove_block(db, work->id);
	}
	tf_ssb_release_db_writer(ssb, db);
	work->completed = true;
}

static void _tf_ssb_modify_block_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	modify_block_t* request = user_data;
	JSContext* context = tf_ssb_get_context(ssb);
	JSValue error = JS_Call(context, request->completed ? request->promise[0] : request->promise[1], JS_UNDEFINED, 1, &request->result);
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, request->promise[0]);
	JS_FreeValue(context, request->promise[1]);
	JS_FreeValue(context, request->result);
	JS_FreeCString(context, request->user);
	tf_free(request);
}

typedef void(permission_test_callback_t)(JSContext* context, bool granted, JSValue value, void* user_data);

typedef struct _permission_test_t
{
	JSContext* context;
	bool completed;
	permission_test_callback_t* callback;
	void* user_data;
	char name[];
} permission_test_t;

static JSValue _tf_ssb_permission_test_resolve(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	permission_test_t* work = JS_GetOpaque(data[0], s_permission_test_class_id);
	if (!work->completed)
	{
		work->completed = true;
		tf_task_t* task = tf_task_get(work->context);
		tf_trace_t* trace = tf_task_get_trace(task);
		tf_trace_begin(trace, work->name);
		work->callback(context, true, argv[0], work->user_data);
		tf_trace_end(trace);
	}
	return JS_UNDEFINED;
}

static JSValue _tf_ssb_permission_test_reject(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	permission_test_t* work = JS_GetOpaque(data[0], s_permission_test_class_id);
	if (!work->completed)
	{
		work->completed = true;
		tf_task_t* task = tf_task_get(work->context);
		tf_trace_t* trace = tf_task_get_trace(task);
		tf_trace_begin(trace, work->name);
		work->callback(context, false, argv[0], work->user_data);
		tf_trace_end(trace);
	}
	return JS_UNDEFINED;
}

static void _tf_ssb_permission_test_finalizer(JSRuntime* runtime, JSValue value)
{
	permission_test_t* work = JS_GetOpaque(value, s_permission_test_class_id);
	if (!work->completed)
	{
		JSValue arg = JS_ThrowInternalError(work->context, "Permission test incomplete.");
		tf_task_t* task = tf_task_get(work->context);
		tf_trace_t* trace = tf_task_get_trace(task);
		tf_trace_begin(trace, work->name);
		work->callback(work->context, false, arg, work->user_data);
		tf_trace_end(trace);
		JS_FreeValue(work->context, arg);
	}
	tf_free(work);
}

static void _tf_ssb_permission_test(JSContext* context, JSValue process, const char* permission, const char* description, permission_test_callback_t* callback, void* user_data)
{
	if (!s_permission_test_class_id)
	{
		JSClassDef def = {
			.class_name = "permission_test",
			.finalizer = _tf_ssb_permission_test_finalizer,
		};
		JS_NewClassID(&s_permission_test_class_id);
		JS_NewClass(JS_GetRuntime(context), s_permission_test_class_id, &def);
	}

	const char* name = tf_util_function_to_string(callback);
	size_t name_length = name ? strlen(name) : 0;
	permission_test_t* payload = tf_malloc(sizeof(permission_test_t) + name_length + 1);
	*payload = (permission_test_t) {
		.context = context,
		.callback = callback,
		.user_data = user_data,
	};
	tf_string_set(payload->name, name_length + 1, name);
	tf_free((void*)name);
	JSValue opaque = JS_NewObjectClass(context, s_permission_test_class_id);
	JS_SetOpaque(opaque, payload);
	JSValue imports = JS_GetPropertyStr(context, process, "imports");
	JSValue core = JS_GetPropertyStr(context, imports, "core");
	JSValue permission_test = JS_GetPropertyStr(context, core, "permissionTest");
	JSValue args[] = {
		JS_NewString(context, permission),
		JS_NewString(context, description),
	};
	JSValue promise = JS_Call(context, permission_test, imports, tf_countof(args), args);
	JSValue then = JS_GetPropertyStr(context, promise, "then");
	JSValue catch = JS_GetPropertyStr(context, promise, "catch");
	JSValue resolve = JS_NewCFunctionData(context, _tf_ssb_permission_test_resolve, 1, 0, 1, &opaque);
	JSValue reject = JS_NewCFunctionData(context, _tf_ssb_permission_test_reject, 1, 0, 1, &opaque);
	JSValue result = JS_Call(context, then, promise, 1, &resolve);
	tf_util_report_error(context, result);
	JS_FreeValue(context, result);
	result = JS_Call(context, catch, promise, 1, &reject);
	tf_util_report_error(context, result);
	JS_FreeValue(context, result);
	JS_FreeValue(context, opaque);
	JS_FreeValue(context, promise);
	JS_FreeValue(context, resolve);
	JS_FreeValue(context, reject);
	JS_FreeValue(context, then);
	JS_FreeValue(context, catch);
	for (int i = 0; i < tf_countof(args); i++)
	{
		JS_FreeValue(context, args[i]);
	}
	JS_FreeValue(context, permission_test);
	JS_FreeValue(context, core);
	JS_FreeValue(context, imports);
}

static void _tf_ssb_modify_block_start_work(JSContext* context, bool granted, JSValue value, void* user_data)
{
	tf_task_t* task = tf_task_get(context);
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	modify_block_t* work = user_data;
	work->result = JS_DupValue(context, value);
	if (granted)
	{
		tf_ssb_run_work(ssb, _tf_ssb_modify_block_work, _tf_ssb_modify_block_after_work, work);
	}
	else
	{
		_tf_ssb_modify_block_after_work(ssb, 0, work);
	}
}

static JSValue _tf_ssb_add_block(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	const char* id = JS_ToCString(context, argv[0]);
	modify_block_t* work = tf_malloc(sizeof(modify_block_t));
	*work = (modify_block_t) {
		.user = _tf_ssb_get_process_credentials_session_name(context, data[0]),
		.add = true,
	};
	tf_string_set(work->id, sizeof(work->id), id);
	JSValue result = JS_NewPromiseCapability(context, work->promise);
	JS_FreeCString(context, id);

	char description[256] = "";
	snprintf(description, sizeof(description), "Block %s.", work->id);
	_tf_ssb_permission_test(context, data[0], "modify_blocks", description, _tf_ssb_modify_block_start_work, work);
	return result;
}

static JSValue _tf_ssb_remove_block(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	const char* id = JS_ToCString(context, argv[0]);
	modify_block_t* work = tf_malloc(sizeof(modify_block_t));
	*work = (modify_block_t) {
		.user = _tf_ssb_get_process_credentials_session_name(context, data[0]),
		.add = false,
	};
	tf_string_set(work->id, sizeof(work->id), id);
	JSValue result = JS_NewPromiseCapability(context, work->promise);
	JS_FreeCString(context, id);

	char description[256] = "";
	snprintf(description, sizeof(description), "Unblock %s.", work->id);
	_tf_ssb_permission_test(context, data[0], "modify_blocks", description, _tf_ssb_modify_block_start_work, work);
	return result;
}

typedef struct _block_t
{
	char id[k_id_base64_len];
	double timestamp;
} block_t;

typedef struct _get_blocks_t
{
	block_t* blocks;
	int count;
	JSValue promise[2];
} get_blocks_t;

static void _get_blocks_callback(const char* id, double timestamp, void* user_data)
{
	get_blocks_t* work = user_data;
	work->blocks = tf_resize_vec(work->blocks, sizeof(block_t) * (work->count + 1));
	work->blocks[work->count] = (block_t) { .timestamp = timestamp };
	tf_string_set(work->blocks[work->count].id, sizeof(work->blocks[work->count].id), id);
	work->count++;
}

static void _tf_ssb_get_blocks_work(tf_ssb_t* ssb, void* user_data)
{
	sqlite3* db = tf_ssb_acquire_db_reader(ssb);
	tf_ssb_db_get_blocks(db, _get_blocks_callback, user_data);
	tf_ssb_release_db_reader(ssb, db);
}

static void _tf_ssb_get_blocks_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	get_blocks_t* work = user_data;
	JSContext* context = tf_ssb_get_context(ssb);

	JSValue result = JS_NewArray(context);
	for (int i = 0; i < work->count; i++)
	{
		JSValue entry = JS_NewObject(context);
		JS_SetPropertyStr(context, entry, "id", JS_NewString(context, work->blocks[i].id));
		JS_SetPropertyStr(context, entry, "timestamp", JS_NewFloat64(context, work->blocks[i].timestamp));
		JS_SetPropertyUint32(context, result, i, entry);
	}

	JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
	JS_FreeValue(context, result);
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, work->promise[0]);
	JS_FreeValue(context, work->promise[1]);
	tf_free(work->blocks);
	tf_free(work);
}

static JSValue _tf_ssb_get_blocks(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	tf_task_t* task = tf_task_get(context);
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	get_blocks_t* work = tf_malloc(sizeof(get_blocks_t));
	*work = (get_blocks_t) { 0 };
	JSValue result = JS_NewPromiseCapability(context, work->promise);
	tf_ssb_run_work(ssb, _tf_ssb_get_blocks_work, _tf_ssb_get_blocks_after_work, work);
	return result;
}

typedef struct _global_setting_set_t
{
	const char* key;
	const char* value;
	JSValue promise[2];
	bool done;
	JSValue result;
} global_setting_set_t;

static void _tf_ssb_globalSettingsSet_work(tf_ssb_t* ssb, void* user_data)
{
	global_setting_set_t* work = user_data;
	sqlite3* db = tf_ssb_acquire_db_writer(ssb);
	work->done = tf_ssb_db_set_global_setting_from_string(db, work->key, work->value);
	tf_ssb_release_db_writer(ssb, db);
}

static void _tf_ssb_globalSettingsSet_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	global_setting_set_t* work = user_data;
	JSContext* context = tf_ssb_get_context(ssb);
	JSValue error = JS_Call(context, work->done ? work->promise[0] : work->promise[1], JS_UNDEFINED, 1, &work->result);
	JS_FreeValue(context, work->result);
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, work->promise[0]);
	JS_FreeValue(context, work->promise[1]);
	JS_FreeCString(context, work->key);
	JS_FreeCString(context, work->value);
	tf_free(work);
}

static void _tf_ssb_globalSettingsSet_permission_callback(JSContext* context, bool granted, JSValue value, void* user_data)
{
	global_setting_set_t* work = user_data;
	tf_task_t* task = tf_task_get(context);
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	work->result = JS_DupValue(context, value);
	if (granted)
	{
		tf_ssb_run_work(ssb, _tf_ssb_globalSettingsSet_work, _tf_ssb_globalSettingsSet_after_work, work);
	}
	else
	{
		_tf_ssb_globalSettingsSet_after_work(ssb, -1, work);
	}
}

static JSValue _tf_ssb_globalSettingsSet(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	const char* key = JS_ToCString(context, argv[0]);
	const char* value = JS_ToCString(context, argv[1]);

	global_setting_set_t* work = tf_malloc(sizeof(global_setting_set_t));
	*work = (global_setting_set_t) {
		.key = key,
		.value = value,
	};
	JSValue result = JS_NewPromiseCapability(context, work->promise);

	char description[256] = "";
	snprintf(description, sizeof(description), "Set %s to %s.", key, value);
	_tf_ssb_permission_test(context, data[0], "set_global_setting", description, _tf_ssb_globalSettingsSet_permission_callback, work);
	return result;
}

typedef struct _delete_user_t
{
	const char* user;
	bool completed;
	JSValue result;
	JSValue promise[2];
} delete_user_t;

static void _tf_ssb_delete_user_work(tf_ssb_t* ssb, void* user_data)
{
	delete_user_t* work = user_data;
	size_t length = strlen("user:") + strlen(work->user) + 1;
	char* buffer = alloca(length);
	snprintf(buffer, length, "user:%s", work->user);
	work->completed = tf_ssb_db_remove_property(ssb, "auth", buffer) || work->completed;
	work->completed = tf_ssb_db_remove_value_from_array_property(ssb, "auth", "users", work->user) || work->completed;
}

static void _tf_ssb_delete_user_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	delete_user_t* work = user_data;
	JSContext* context = tf_ssb_get_context(ssb);
	if (!work->completed && JS_IsUndefined(work->result))
	{
		work->result = JS_NewString(context, "User not found.");
	}
	JSValue error = JS_Call(context, work->completed ? work->promise[0] : work->promise[1], JS_UNDEFINED, 1, &work->result);
	JS_FreeValue(context, work->result);
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, work->promise[0]);
	JS_FreeValue(context, work->promise[1]);
	JS_FreeCString(context, work->user);
	tf_free(work);
}

static void _tf_ssb_delete_user_permission_callback(JSContext* context, bool granted, JSValue value, void* user_data)
{
	delete_user_t* work = user_data;
	tf_task_t* task = tf_task_get(context);
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	if (granted)
	{
		tf_ssb_run_work(ssb, _tf_ssb_delete_user_work, _tf_ssb_delete_user_after_work, work);
	}
	else
	{
		_tf_ssb_delete_user_after_work(ssb, -1, work);
	}
}

static JSValue _tf_ssb_delete_user(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	delete_user_t* work = tf_malloc(sizeof(delete_user_t));
	*work = (delete_user_t) {
		.user = JS_ToCString(context, argv[0]),
	};
	JSValue result = JS_NewPromiseCapability(context, work->promise);

	char description[256] = "";
	snprintf(description, sizeof(description), "Delete user '%s'.", work->user);
	_tf_ssb_permission_test(context, data[0], "delete_user", description, _tf_ssb_delete_user_permission_callback, work);
	return result;
}

typedef struct _swap_with_server_identity_t
{
	char server_id[k_id_base64_len];
	char user_id[k_id_base64_len];
	JSValue promise[2];
	char* error;
	char user[];
} swap_with_server_identity_t;

static void _tf_ssb_swap_with_server_identity_work(tf_ssb_t* ssb, void* user_data)
{
	swap_with_server_identity_t* work = user_data;
	sqlite3* db = tf_ssb_acquire_db_writer(ssb);
	work->error = tf_ssb_db_swap_with_server_identity(db, work->user, work->user_id, work->server_id);
	tf_ssb_release_db_writer(ssb, db);
}

static void _tf_ssb_swap_with_server_identity_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	swap_with_server_identity_t* work = user_data;
	JSContext* context = tf_ssb_get_context(ssb);
	JSValue error = JS_UNDEFINED;
	if (work->error)
	{
		JSValue arg = JS_ThrowInternalError(context, "%s", work->error);
		JSValue exception = JS_GetException(context);
		error = JS_Call(context, work->promise[1], JS_UNDEFINED, 1, &exception);
		tf_free(work->error);
		JS_FreeValue(context, exception);
		JS_FreeValue(context, arg);
	}
	else
	{
		error = JS_Call(context, work->promise[0], JS_UNDEFINED, 0, NULL);
	}
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, work->promise[0]);
	JS_FreeValue(context, work->promise[1]);
	tf_free(work);
}

static void _tf_ssb_swap_with_server_identity_permission_callback(JSContext* context, bool granted, JSValue value, void* user_data)
{
	swap_with_server_identity_t* work = user_data;
	tf_task_t* task = tf_task_get(context);
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	tf_ssb_run_work(ssb, _tf_ssb_swap_with_server_identity_work, _tf_ssb_swap_with_server_identity_after_work, work);
}

static JSValue _tf_ssb_swap_with_server_identity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	tf_task_t* task = tf_task_get(context);
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	const char* user = _tf_ssb_get_process_credentials_session_name(context, data[0]);
	size_t user_length = user ? strlen(user) : 0;
	const char* id = JS_ToCString(context, argv[0]);
	swap_with_server_identity_t* work = tf_malloc(sizeof(swap_with_server_identity_t) + user_length + 1);
	*work = (swap_with_server_identity_t) { 0 };
	tf_ssb_whoami(ssb, work->server_id, sizeof(work->server_id));
	tf_string_set(work->user_id, sizeof(work->user_id), id);
	if (user)
	{
		memcpy(work->user, user, user_length + 1);
	}
	else
	{
		*work->user = '\0';
	}
	JSValue result = JS_NewPromiseCapability(context, work->promise);
	char description[1024];
	snprintf(description, sizeof(description), "Swap identity %s with %s.", work->user_id, work->server_id);
	_tf_ssb_permission_test(context, data[0], "delete_user", description, _tf_ssb_swap_with_server_identity_permission_callback, work);
	JS_FreeCString(context, id);
	JS_FreeCString(context, user);
	return result;
}

static bool _tf_ssb_get_private_key_curve25519_internal(sqlite3* db, const char* user, const char* identity, uint8_t out_private_key[static crypto_sign_SECRETKEYBYTES])
{
	if (!user || !identity)
	{
		tf_printf("user=%p identity=%p out_private_key=%p\n", user, identity, out_private_key);
		return false;
	}

	bool success = false;
	sqlite3_stmt* statement = NULL;
	if (sqlite3_prepare_v2(db, "SELECT private_key FROM identities WHERE user = ? AND public_key = ?", -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, user, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, *identity == '@' ? identity + 1 : identity, -1, NULL) == SQLITE_OK)
		{
			while (sqlite3_step(statement) == SQLITE_ROW)
			{
				uint8_t key[crypto_sign_SECRETKEYBYTES] = { 0 };
				int length = tf_base64_decode((const char*)sqlite3_column_text(statement, 0), sqlite3_column_bytes(statement, 0) - strlen(".ed25519"), key, sizeof(key));
				if (length == crypto_sign_SECRETKEYBYTES)
				{
					success = crypto_sign_ed25519_sk_to_curve25519(out_private_key, key) == 0;
				}
			}
		}
		sqlite3_finalize(statement);
	}
	return success;
}

static bool _tf_ssb_get_private_key_curve25519(tf_ssb_t* ssb, sqlite3* db, const char* user, const char* identity, uint8_t out_private_key[static crypto_sign_SECRETKEYBYTES])
{
	if (_tf_ssb_get_private_key_curve25519_internal(db, user, identity, out_private_key))
	{
		return true;
	}

	if (tf_ssb_db_user_has_permission(ssb, db, user, "administration"))
	{
		return _tf_ssb_get_private_key_curve25519_internal(db, ":admin", identity, out_private_key);
	}

	return false;
}

typedef struct _private_message_encrypt_t
{
	const char* signer_user;
	const char* signer_identity;
	const char* recipients[k_max_private_message_recipients];
	int recipient_count;
	const char* message;
	size_t message_size;
	JSValue promise[2];
	bool error_id_not_found;
	char* encrypted;
	size_t encrypted_length;
} private_message_encrypt_t;

static void _tf_ssb_private_message_encrypt_work(tf_ssb_t* ssb, void* user_data)
{
	private_message_encrypt_t* work = user_data;

	uint8_t private_key[crypto_sign_SECRETKEYBYTES] = { 0 };
	sqlite3* db = tf_ssb_acquire_db_reader(ssb);
	bool found = _tf_ssb_get_private_key_curve25519(ssb, db, work->signer_user, work->signer_identity, private_key);
	tf_ssb_release_db_reader(ssb, db);

	if (found)
	{
		work->encrypted = tf_ssb_private_message_encrypt(private_key, work->recipients, work->recipient_count, work->message, work->message_size);
		work->encrypted_length = work->encrypted ? strlen(work->encrypted) : 0;
	}
	else
	{
		work->error_id_not_found = true;
	}
}

static void _tf_ssb_private_message_encrypt_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	private_message_encrypt_t* work = user_data;
	JSContext* context = tf_ssb_get_context(ssb);
	JSValue result = JS_UNDEFINED;
	bool success = false;
	if (work->error_id_not_found)
	{
		result = JS_ThrowInternalError(context, "Unable to get key for ID %s of user %s.", work->signer_identity, work->signer_user);
	}
	else if (!work->encrypted)
	{
		result = JS_ThrowInternalError(context, "Encrypt failed.");
	}
	else
	{
		result = JS_NewStringLen(context, work->encrypted, work->encrypted_length);
		tf_free((void*)work->encrypted);
		success = true;
	}

	for (int i = 0; i < work->recipient_count; i++)
	{
		tf_free((void*)work->recipients[i]);
	}
	JSValue error = JS_Call(context, work->promise[success ? 0 : 1], JS_UNDEFINED, 1, &result);
	JS_FreeValue(context, result);
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, work->promise[0]);
	JS_FreeValue(context, work->promise[1]);
	JS_FreeCString(context, work->signer_user);
	JS_FreeCString(context, work->signer_identity);
	JS_FreeCString(context, work->message);
	tf_free(work);
}

static JSValue _tf_ssb_private_message_encrypt(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	JSValue result = JS_UNDEFINED;
	JSValue process = data[0];
	int recipient_count = tf_util_get_length(context, argv[1]);
	if (recipient_count < 1 || recipient_count > k_max_private_message_recipients)
	{
		return JS_ThrowRangeError(context, "Number of recipients must be between 1 and %d.", k_max_private_message_recipients);
	}

	const char* session_name_string = _tf_ssb_get_process_credentials_session_name(context, process);
	char* recipients[k_max_private_message_recipients] = { 0 };
	for (int i = 0; i < recipient_count && JS_IsUndefined(result); i++)
	{
		JSValue recipient = JS_GetPropertyUint32(context, argv[1], i);
		const char* id = JS_ToCString(context, recipient);
		if (id)
		{
			recipients[i] = tf_strdup(id);
			JS_FreeCString(context, id);
		}
		JS_FreeValue(context, recipient);
	}

	if (JS_IsUndefined(result))
	{
		const char* signer_identity = JS_ToCString(context, argv[0]);
		size_t message_size = 0;
		const char* message = JS_ToCStringLen(context, &message_size, argv[2]);

		tf_task_t* task = tf_task_get(context);
		tf_ssb_t* ssb = tf_task_get_ssb(task);
		private_message_encrypt_t* work = tf_malloc(sizeof(private_message_encrypt_t));
		*work = (private_message_encrypt_t) {
			.signer_user = session_name_string,
			.signer_identity = signer_identity,
			.recipient_count = recipient_count,
			.message = message,
			.message_size = message_size,
		};
		static_assert(sizeof(work->recipients) == sizeof(recipients), "size mismatch");
		memcpy(work->recipients, recipients, sizeof(recipients));
		result = JS_NewPromiseCapability(context, work->promise);
		tf_ssb_run_work(ssb, _tf_ssb_private_message_encrypt_work, _tf_ssb_private_message_encrypt_after_work, work);
	}

	return result;
}

typedef struct _private_message_decrypt_t
{
	const char* user;
	const char* identity;
	size_t message_size;
	const char* message;
	const char* decrypted;
	size_t decrypted_size;
	const char* error;
	JSValue promise[2];
} private_message_decrypt_t;

static void _tf_ssb_private_message_decrypt_work(tf_ssb_t* ssb, void* user_data)
{
	private_message_decrypt_t* work = user_data;

	uint8_t private_key[crypto_sign_SECRETKEYBYTES] = { 0 };
	sqlite3* db = tf_ssb_acquire_db_reader(ssb);
	bool found = _tf_ssb_get_private_key_curve25519(ssb, db, work->user, work->identity, private_key);
	tf_ssb_release_db_reader(ssb, db);

	if (found)
	{
		if (work->message_size >= strlen(".box") && memcmp(work->message + work->message_size - strlen(".box"), ".box", strlen(".box")) == 0)
		{
			uint8_t* decoded = tf_malloc(work->message_size);
			int decoded_length = tf_base64_decode(work->message, work->message_size - strlen(".box"), decoded, work->message_size);
			uint8_t* nonce = decoded;
			uint8_t* public_key = decoded + crypto_box_NONCEBYTES;
			if (public_key + crypto_secretbox_KEYBYTES < decoded + decoded_length)
			{
				uint8_t shared_secret[crypto_secretbox_KEYBYTES] = { 0 };
				if (crypto_scalarmult(shared_secret, private_key, public_key) == 0)
				{
					enum
					{
						k_recipient_header_bytes = crypto_secretbox_MACBYTES + sizeof(uint8_t) + crypto_secretbox_KEYBYTES
					};
					for (uint8_t* p = decoded + crypto_box_NONCEBYTES + crypto_secretbox_KEYBYTES; p <= decoded + decoded_length - k_recipient_header_bytes;
						 p += k_recipient_header_bytes)
					{
						uint8_t out[k_recipient_header_bytes] = { 0 };
						int opened = crypto_secretbox_open_easy(out, p, k_recipient_header_bytes, nonce, shared_secret);
						if (opened != -1)
						{
							int recipients = (int)out[0];
							uint8_t* body = decoded + crypto_box_NONCEBYTES + crypto_secretbox_KEYBYTES + k_recipient_header_bytes * recipients;
							size_t body_size = decoded + decoded_length - body;
							uint8_t* decrypted = tf_malloc(body_size);
							uint8_t* key = out + 1;
							if (crypto_secretbox_open_easy(decrypted, body, body_size, nonce, key) != -1)
							{
								work->decrypted = (const char*)decrypted;
								work->decrypted_size = body_size - crypto_secretbox_MACBYTES;
								break;
							}
							else
							{
								work->error = "Received key to open secret box containing message body, but it did not work.";
								tf_free(decrypted);
							}
						}
					}
				}
				else
				{
					work->error = "crypto_scalarmult failed.";
				}
			}
			else
			{
				work->error = "Encrypted message was not long enough to contain its one-time public key.";
			}
			tf_free(decoded);
		}
		else
		{
			work->error = "Message does not end in \".box\".";
		}
	}
	else
	{
		work->error = "Private key not found for user.";
	}
}

static void _tf_ssb_private_message_decrypt_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	private_message_decrypt_t* work = user_data;
	JSContext* context = tf_ssb_get_context(ssb);
	JSValue error = JS_UNDEFINED;
	if (work->error)
	{
		JSValue result = JS_ThrowInternalError(context, "%s", work->error);
		error = JS_Call(context, work->promise[1], JS_UNDEFINED, 1, &result);
		JS_FreeValue(context, result);
	}
	else if (work->decrypted)
	{
		JSValue result = JS_NewStringLen(context, work->decrypted, work->decrypted_size);
		error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
		JS_FreeValue(context, result);
	}
	else
	{
		JSValue result = JS_UNDEFINED;
		error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
	}
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, work->promise[0]);
	JS_FreeValue(context, work->promise[1]);
	JS_FreeCString(context, work->user);
	JS_FreeCString(context, work->identity);
	JS_FreeCString(context, work->message);
	tf_free((void*)work->decrypted);
	tf_free(work);
}

static JSValue _tf_ssb_private_message_decrypt(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	tf_task_t* task = tf_task_get(context);
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	JSValue process = data[0];
	const char* identity = JS_ToCString(context, argv[0]);
	size_t message_size = 0;
	const char* message = JS_ToCStringLen(context, &message_size, argv[1]);
	const char* session_name_string = _tf_ssb_get_process_credentials_session_name(context, process);

	private_message_decrypt_t* work = tf_malloc(sizeof(private_message_decrypt_t));
	*work = (private_message_decrypt_t) {
		.user = session_name_string,
		.identity = identity,
		.message_size = message_size,
		.message = message,
	};
	JSValue result = JS_NewPromiseCapability(context, work->promise);
	tf_ssb_run_work(ssb, _tf_ssb_private_message_decrypt_work, _tf_ssb_private_message_decrypt_after_work, work);
	return result;
}

typedef struct _get_one_private_message_t get_one_private_message_t;

typedef struct _get_private_messages_t
{
	tf_ssb_t* ssb;
	const char* user;
	const char* id;
	uint8_t private_key[crypto_sign_SECRETKEYBYTES];
	int64_t rowid_begin;
	int64_t rowid_end;
	int work_count;
	bool error_private_key;
	uv_mutex_t lock;
	char** message_ids;
	int message_id_count;

	uv_mutex_t mutex;
	uv_async_t async;
	get_one_private_message_t* sub_work;

	JSValue promise[2];
} get_private_messages_t;

typedef struct _get_one_private_message_t
{
	get_private_messages_t* work;
	get_one_private_message_t* next;
	char id[k_id_base64_len];
	size_t decoded_length;
	uint8_t decoded[];
} get_one_private_message_t;

static void _tf_ssb_get_private_messages_close(uv_handle_t* handle)
{
	tf_free(handle->data);
}

static void _tf_ssb_get_private_messages_finish(JSContext* context, get_private_messages_t* work)
{
	if (work->work_count == 0)
	{
		if (work->error_private_key)
		{
			JSValue error = JS_ThrowInternalError(context, "Unable to get private key.");
			JSValue result = JS_Call(context, work->promise[1], JS_UNDEFINED, 1, &error);
			tf_util_report_error(context, result);
			JS_FreeValue(context, result);
			JS_FreeValue(context, error);
		}
		else
		{
			JSValue result = JS_NewArray(context);
			for (int i = 0; i < work->message_id_count; i++)
			{
				JS_SetPropertyUint32(context, result, i, JS_NewString(context, work->message_ids[i]));
			}
			JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
			tf_util_report_error(context, error);
			JS_FreeValue(context, error);
			JS_FreeValue(context, result);
		}

		uv_mutex_destroy(&work->mutex);
		uv_mutex_destroy(&work->lock);
		for (int i = 0; i < work->message_id_count; i++)
		{
			tf_free(work->message_ids[i]);
		}
		tf_free(work->message_ids);
		JS_FreeValue(context, work->promise[0]);
		JS_FreeValue(context, work->promise[1]);
		JS_FreeCString(context, work->user);
		JS_FreeCString(context, work->id);
		uv_close((uv_handle_t*)&work->async, _tf_ssb_get_private_messages_close);
	}
}

static void _dump_private_message_work(tf_ssb_t* ssb, void* user_data)
{
	get_one_private_message_t* message = user_data;
	get_private_messages_t* work = message->work;

	uint8_t* nonce = message->decoded;
	uint8_t* public_key = message->decoded + crypto_box_NONCEBYTES;
	if (public_key + crypto_secretbox_KEYBYTES < message->decoded + message->decoded_length)
	{
		uint8_t shared_secret[crypto_secretbox_KEYBYTES] = { 0 };
		if (crypto_scalarmult(shared_secret, work->private_key, public_key) == 0)
		{
			enum
			{
				k_recipient_header_bytes = crypto_secretbox_MACBYTES + sizeof(uint8_t) + crypto_secretbox_KEYBYTES
			};
			for (uint8_t* p = message->decoded + crypto_box_NONCEBYTES + crypto_secretbox_KEYBYTES; p <= message->decoded + message->decoded_length - k_recipient_header_bytes;
				 p += k_recipient_header_bytes)
			{
				uint8_t out[k_recipient_header_bytes] = { 0 };
				int opened = crypto_secretbox_open_easy(out, p, k_recipient_header_bytes, nonce, shared_secret);
				if (opened != -1)
				{
					int recipients = (int)out[0];
					uint8_t* body = message->decoded + crypto_box_NONCEBYTES + crypto_secretbox_KEYBYTES + k_recipient_header_bytes * recipients;
					size_t body_size = message->decoded + message->decoded_length - body;
					uint8_t* decrypted = message->decoded + message->decoded_length;
					uint8_t* key = out + 1;
					if (crypto_secretbox_open_easy(decrypted, body, body_size, nonce, key) != -1)
					{
						uv_mutex_lock(&work->lock);
						work->message_ids = tf_resize_vec(work->message_ids, sizeof(const char*) * (work->message_id_count + 1));
						work->message_ids[work->message_id_count++] = tf_strdup(message->id);
						uv_mutex_unlock(&work->lock);
						break;
					}
				}
			}
		}
	}
}

static void _dump_private_message_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	get_one_private_message_t* message = user_data;
	uv_mutex_lock(&message->work->lock);
	message->work->work_count--;
	uv_mutex_unlock(&message->work->lock);
	_tf_ssb_get_private_messages_finish(tf_ssb_get_context(ssb), message->work);
	tf_free(message);
}

static void _tf_ssb_get_private_messages_work(tf_ssb_t* ssb, void* user_data)
{
	get_private_messages_t* work = user_data;
	uint8_t key[crypto_sign_SECRETKEYBYTES] = { 0 };
	if (!(tf_ssb_db_identity_get_private_key(ssb, work->user, work->id, key, sizeof(key)) ||
			(tf_ssb_db_user_has_permission(ssb, NULL, work->user, "administration") && tf_ssb_db_identity_get_private_key(ssb, ":admin", work->id, key, sizeof(key)))) ||
		crypto_sign_ed25519_sk_to_curve25519(work->private_key, key) != 0)
	{
		tf_printf("Failed to convert private key for %s: %s.\n", work->user, work->id);
		work->error_private_key = true;
		return;
	}

	sqlite3* db = tf_ssb_acquire_db_reader(ssb);
	sqlite3_stmt* statement = NULL;
	if (sqlite3_prepare_v2(
			db, "SELECT id, content ->> '$' FROM messages WHERE json(content) LIKE '\"%.box\"' AND rowid >= ? AND rowid <= ? ORDER BY rowid", -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_int64(statement, 1, work->rowid_begin) == SQLITE_OK && sqlite3_bind_int64(statement, 2, work->rowid_end) == SQLITE_OK)
		{
			while (sqlite3_step(statement) == SQLITE_ROW)
			{
				uv_mutex_lock(&work->lock);
				work->work_count++;
				uv_mutex_unlock(&work->lock);

				size_t length = sqlite3_column_bytes(statement, 1);
				get_one_private_message_t* message = tf_malloc(sizeof(get_one_private_message_t) + length * 2);
				*message = (get_one_private_message_t) {
					.work = work,
				};
				message->decoded_length = tf_base64_decode((const char*)sqlite3_column_text(statement, 1), length - strlen(".box"), message->decoded, length);
				tf_string_set(message->id, sizeof(message->id), (const char*)sqlite3_column_text(statement, 0));

				uv_mutex_lock(&work->mutex);
				message->next = work->sub_work;
				work->sub_work = message;
				uv_mutex_unlock(&work->mutex);
				uv_async_send(&work->async);
			}
		}
		else
		{
			tf_printf("bind failed: %s\n", sqlite3_errmsg(db));
		}
		sqlite3_finalize(statement);
	}
	tf_ssb_release_db_reader(ssb, db);
}

static void _tf_ssb_get_private_messages_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	get_private_messages_t* work = user_data;
	uv_mutex_lock(&work->lock);
	work->work_count--;
	uv_mutex_unlock(&work->lock);
	_tf_ssb_get_private_messages_finish(tf_ssb_get_context(ssb), user_data);
}

static void _tf_ssb_get_private_async(uv_async_t* async)
{
	get_private_messages_t* work = async->data;
	uv_mutex_lock(&work->mutex);
	get_one_private_message_t* sub_work = work->sub_work;
	work->sub_work = NULL;
	uv_mutex_unlock(&work->mutex);

	while (sub_work)
	{
		get_one_private_message_t* next = sub_work->next;
		tf_ssb_run_work(work->ssb, _dump_private_message_work, _dump_private_message_after_work, sub_work);
		sub_work = next;
	}
}

static JSValue _tf_ssb_get_private_messages(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	tf_task_t* task = tf_task_get(context);
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	JSValue process = data[0];
	get_private_messages_t* work = tf_malloc(sizeof(get_private_messages_t));
	*work = (get_private_messages_t) {
		.ssb = ssb,
		.user = _tf_ssb_get_process_credentials_session_name(context, process),
		.id = JS_ToCString(context, argv[0]),
		.rowid_begin = 0,
		.rowid_end = INT64_MAX,
		.work_count = 1,
		.async = { .data = work },
	};
	uv_mutex_init(&work->mutex);
	uv_async_init(tf_ssb_get_loop(ssb), &work->async, _tf_ssb_get_private_async);
	if (!JS_IsUndefined(argv[1]))
	{
		JS_ToInt64(context, &work->rowid_begin, argv[1]);
	}
	if (!JS_IsUndefined(argv[2]))
	{
		JS_ToInt64(context, &work->rowid_end, argv[2]);
	}
	JSValue result = JS_NewPromiseCapability(context, work->promise);
	uv_mutex_init(&work->lock);
	tf_ssb_run_work(ssb, _tf_ssb_get_private_messages_work, _tf_ssb_get_private_messages_after_work, work);
	return result;
}

typedef struct _append_message_t
{
	char id[k_id_base64_len];
	uint8_t private_key[crypto_sign_SECRETKEYBYTES];
	bool got_private_key;
	char previous_id[512];
	int32_t previous_sequence;
	JSContext* context;
	JSValue promise[2];
	JSValue message;
	char user[];
} append_message_t;

static void _tf_ssb_appendMessage_finish(append_message_t* async, bool success, JSValue result)
{
	JSValue error = JS_Call(async->context, success ? async->promise[0] : async->promise[1], JS_UNDEFINED, 1, &result);
	tf_util_report_error(async->context, error);
	JS_FreeValue(async->context, error);
	JS_FreeValue(async->context, async->message);
	JS_FreeValue(async->context, async->promise[0]);
	JS_FreeValue(async->context, async->promise[1]);
	tf_free(async);
}

static void _tf_ssb_appendMessageWithIdentity_callback(const char* id, bool verified, bool is_new, void* user_data)
{
	append_message_t* async = user_data;
	JSValue result = JS_UNDEFINED;
	if (verified)
	{
		result = is_new ? JS_TRUE : JS_FALSE;
	}
	_tf_ssb_appendMessage_finish(async, verified, result);
}

static void _tf_ssb_append_message_with_identity_get_key_work(tf_ssb_t* ssb, void* user_data)
{
	append_message_t* work = user_data;
	work->got_private_key = tf_ssb_db_identity_get_private_key(ssb, work->user, work->id, work->private_key, sizeof(work->private_key));
	if (!work->got_private_key && tf_ssb_db_user_has_permission(ssb, NULL, work->user, "administration"))
	{
		work->got_private_key = tf_ssb_db_identity_get_private_key(ssb, ":admin", work->id, work->private_key, sizeof(work->private_key));
	}
	sqlite3* db = tf_ssb_acquire_db_reader(ssb);
	tf_ssb_db_get_latest_message_by_author(db, NULL, work->id, &work->previous_sequence, work->previous_id, sizeof(work->previous_id));
	tf_ssb_release_db_reader(ssb, db);
}

static void _tf_ssb_append_message_with_identity_get_key_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	append_message_t* work = user_data;
	if (work->got_private_key)
	{
		JSValue signed_message = tf_ssb_sign_message(ssb, work->id, work->private_key, work->message, work->previous_id, work->previous_sequence);
		tf_ssb_verify_strip_and_store_message(ssb, signed_message, _tf_ssb_appendMessageWithIdentity_callback, work);
		JS_FreeValue(work->context, signed_message);
	}
	else
	{
		_tf_ssb_appendMessage_finish(work, false, JS_ThrowInternalError(work->context, "Unable to get private key for user %s with identity %s.", work->user, work->id));
	}
}

static void _tf_ssb_append_message_permission_callback(JSContext* context, bool granted, JSValue value, void* user_data)
{
	tf_task_t* task = tf_task_get(context);
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	append_message_t* work = user_data;
	if (granted)
	{
		tf_ssb_run_work(ssb, _tf_ssb_append_message_with_identity_get_key_work, _tf_ssb_append_message_with_identity_get_key_after_work, work);
	}
	else
	{
		_tf_ssb_appendMessage_finish(work, false, value);
	}
}

static JSValue _tf_ssb_appendMessageWithIdentity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	JSValue process = data[0];

	char description[1024] = "Publish a new message.";
	const char* type = tf_util_get_property_as_string(context, argv[1], "type");
	if (type)
	{
		if (strcmp(type, "vote") == 0)
		{
			JSValue vote = JS_GetPropertyStr(context, argv[1], "vote");
			const char* expression = tf_util_get_property_as_string(context, vote, "expression");
			snprintf(description, sizeof(description), "React with %s.", expression);
			JS_FreeCString(context, expression);
			JS_FreeValue(context, vote);
		}
		else
		{
			snprintf(description, sizeof(description), "Publish a new %s message.", type);
		}
	}
	else if (JS_IsString(argv[1]))
	{
		tf_string_set(description, sizeof(description), "Publish a new private message.");
	}
	JS_FreeCString(context, type);

	const char* user = _tf_ssb_get_process_credentials_session_name(context, process);
	if (!user)
	{
		return JS_ThrowInternalError(context, "Invalid user.");
	}
	else
	{
		size_t user_length = strlen(user);
		const char* id = JS_ToCString(context, argv[0]);

		append_message_t* work = tf_malloc(sizeof(append_message_t) + user_length + 1);
		*work = (append_message_t) { .context = context, .message = JS_DupValue(context, argv[1]) };
		memcpy(work->user, user, user_length + 1);
		tf_string_set(work->id, sizeof(work->id), id);

		JS_FreeCString(context, user);
		JS_FreeCString(context, id);

		JSValue result = JS_NewPromiseCapability(context, work->promise);
		_tf_ssb_permission_test(context, process, "ssb_append", description, _tf_ssb_append_message_permission_callback, work);
		return result;
	}
}

typedef struct _add_identity_t
{
	uint8_t key[crypto_sign_SECRETKEYBYTES / 2];
	bool added;
	JSValue result;
	JSValue promise[2];
	char user[];
} add_identity_t;

static void _tf_ssb_add_identity_work(tf_ssb_t* ssb, void* user_data)
{
	add_identity_t* work = user_data;
	uint8_t public_key[crypto_sign_PUBLICKEYBYTES];
	unsigned char seed[crypto_sign_SEEDBYTES];
	uint8_t secret_key[crypto_sign_SECRETKEYBYTES] = { 0 };
	memcpy(secret_key, work->key, sizeof(secret_key) / 2);
	if (crypto_sign_ed25519_sk_to_seed(seed, secret_key) == 0 && crypto_sign_seed_keypair(public_key, secret_key, seed) == 0)
	{
		char public_key_b64[512];
		tf_base64_encode(public_key, sizeof(public_key), public_key_b64, sizeof(public_key_b64));
		snprintf(public_key_b64 + strlen(public_key_b64), sizeof(public_key_b64) - strlen(public_key_b64), ".ed25519");

		uint8_t combined[crypto_sign_SECRETKEYBYTES];
		memcpy(combined, work->key, sizeof(work->key));
		memcpy(combined + sizeof(work->key), public_key, sizeof(public_key));
		char combined_b64[512];
		tf_base64_encode(combined, sizeof(combined), combined_b64, sizeof(combined_b64));
		snprintf(combined_b64 + strlen(combined_b64), sizeof(combined_b64) - strlen(combined_b64), ".ed25519");

		work->added = tf_ssb_db_identity_add(ssb, work->user, public_key_b64, combined_b64);
	}
}

static void _tf_ssb_add_identity_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	add_identity_t* work = user_data;
	JSContext* context = tf_ssb_get_context(ssb);
	JSValue result = JS_IsUndefined(work->result) ? (work->added ? JS_TRUE : JS_UNDEFINED) : work->result;
	JSValue error = JS_Call(context, work->promise[work->added ? 0 : 1], JS_UNDEFINED, 1, &result);
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, work->result);
	JS_FreeValue(context, work->promise[0]);
	JS_FreeValue(context, work->promise[1]);
	tf_free(work);
}

static void _tf_ssb_add_identity_permission_callback(JSContext* context, bool granted, JSValue value, void* user_data)
{
	add_identity_t* work = user_data;
	tf_task_t* task = tf_task_get(context);
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	if (granted)
	{
		tf_ssb_run_work(ssb, _tf_ssb_add_identity_work, _tf_ssb_add_identity_after_work, work);
	}
	else
	{
		work->result = JS_DupValue(context, value);
		_tf_ssb_add_identity_after_work(ssb, -1, work);
	}
}

static JSValue _tf_ssb_addIdentity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	JSValue process = data[0];
	JSValue result = JS_UNDEFINED;
	const char* user = _tf_ssb_get_process_credentials_session_name(context, process);
	size_t user_length = user ? strlen(user) : 0;

	JSValue buffer = JS_UNDEFINED;
	size_t length = 0;
	uint8_t* array = tf_util_try_get_array_buffer(context, &length, argv[0]);
	if (!array)
	{
		size_t offset;
		size_t element_size;
		buffer = tf_util_try_get_typed_array_buffer(context, argv[0], &offset, &length, &element_size);
		if (!JS_IsException(buffer))
		{
			array = tf_util_try_get_array_buffer(context, &length, buffer);
		}
	}

	if (array)
	{
		if (length == crypto_sign_SECRETKEYBYTES / 2)
		{
			add_identity_t* work = tf_malloc(sizeof(add_identity_t) + user_length + 1);
			*work = (add_identity_t) { .result = JS_UNDEFINED };
			memcpy(work->key, array, sizeof(work->key));
			if (user)
			{
				memcpy(work->user, user, user_length + 1);
			}
			result = JS_NewPromiseCapability(context, work->promise);

			uint8_t public_key[crypto_sign_PUBLICKEYBYTES] = { 0 };
			unsigned char seed[crypto_sign_SEEDBYTES] = { 0 };
			uint8_t secret_key[crypto_sign_SECRETKEYBYTES] = { 0 };
			char public_key_b64[512] = "";
			char description[512] = { 0 };
			memcpy(secret_key, work->key, sizeof(secret_key) / 2);
			if (crypto_sign_ed25519_sk_to_seed(seed, secret_key) == 0 && crypto_sign_seed_keypair(public_key, secret_key, seed) == 0)
			{
				tf_base64_encode(public_key, sizeof(public_key), public_key_b64, sizeof(public_key_b64));
			}
			snprintf(description, sizeof(description), "Add identity @%s.ed25519.", public_key_b64);
			_tf_ssb_permission_test(context, process, "ssb_id_add", description, _tf_ssb_add_identity_permission_callback, work);
		}
		else
		{
			result = JS_ThrowInternalError(context, "Unexpected private key size: %d vs. %d\n", (int)length, crypto_sign_SECRETKEYBYTES);
		}
	}
	else
	{
		result = JS_ThrowInternalError(context, "Expected array argument.");
	}
	JS_FreeValue(context, buffer);
	JS_FreeCString(context, user);
	return result;
}

typedef struct _delete_identity_t
{
	char id[k_id_base64_len];
	bool deleted;
	JSValue result;
	JSValue promise[2];
	char user[];
} delete_identity_t;

static void _tf_ssb_delete_identity_work(tf_ssb_t* ssb, void* user_data)
{
	delete_identity_t* work = user_data;
	work->deleted = tf_ssb_db_identity_delete(ssb, work->user, work->id);
}

static void _tf_ssb_delete_identity_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	delete_identity_t* work = user_data;
	JSContext* context = tf_ssb_get_context(ssb);
	JSValue result = work->deleted ? JS_TRUE : JS_FALSE;
	JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
	JS_FreeValue(context, result);
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, work->promise[0]);
	JS_FreeValue(context, work->promise[1]);
	tf_free(work);
}

static void _tf_ssb_delete_identity_permission_callback(JSContext* context, bool granted, JSValue value, void* user_data)
{
	delete_identity_t* work = user_data;
	tf_task_t* task = tf_task_get(context);
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	if (granted)
	{
		tf_ssb_run_work(ssb, _tf_ssb_delete_identity_work, _tf_ssb_delete_identity_after_work, work);
	}
	else
	{
		work->result = JS_DupValue(context, value);
		_tf_ssb_delete_identity_after_work(ssb, -1, work);
	}
}

static JSValue _tf_ssb_deleteIdentity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	JSValue result = JS_UNDEFINED;
	JSValue process = data[0];
	const char* user = _tf_ssb_get_process_credentials_session_name(context, process);
	const char* id = JS_ToCString(context, argv[0]);
	if (id && user)
	{
		size_t user_length = strlen(user);
		delete_identity_t* work = tf_malloc(sizeof(delete_identity_t) + user_length + 1);
		*work = (delete_identity_t) { .result = JS_UNDEFINED };
		tf_string_set(work->id, sizeof(work->id), *id == '@' ? id + 1 : id);
		memcpy(work->user, user, user_length + 1);
		result = JS_NewPromiseCapability(context, work->promise);
		_tf_ssb_permission_test(context, process, "ssb_id_delete", "Delete an identity.", _tf_ssb_delete_identity_permission_callback, work);
	}
	JS_FreeCString(context, id);
	JS_FreeCString(context, user);
	return result;
}

typedef struct _get_private_key_t
{
	JSContext* context;
	JSValue result;
	JSValue promise[2];
	char id[k_id_base64_len];
	uint8_t private_key[crypto_sign_SECRETKEYBYTES];
	bool got_private_key;
	char user[];
} get_private_key_t;

static void _tf_ssb_get_private_key_work(tf_ssb_t* ssb, void* user_data)
{
	get_private_key_t* work = user_data;
	work->got_private_key = tf_ssb_db_identity_get_private_key(ssb, work->user, work->id, work->private_key, sizeof(work->private_key));
	if (!work->got_private_key && tf_ssb_db_user_has_permission(ssb, NULL, work->user, "administration"))
	{
		work->got_private_key = tf_ssb_db_identity_get_private_key(ssb, ":admin", work->id, work->private_key, sizeof(work->private_key));
	}
}

static void _tf_ssb_get_private_key_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	get_private_key_t* work = user_data;
	JSValue result = JS_UNDEFINED;
	JSContext* context = work->context;
	if (work->got_private_key && JS_IsUndefined(work->result))
	{
		result = tf_util_new_uint8_array(context, work->private_key, sizeof(work->private_key) / 2);
	}
	JSValue error = JS_Call(context, work->promise[work->got_private_key ? 0 : 1], JS_UNDEFINED, 1, &result);
	JS_FreeValue(context, result);
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, work->promise[0]);
	JS_FreeValue(context, work->promise[1]);
	JS_FreeValue(context, work->result);
	tf_free(work);
}
static void _tf_ssb_get_private_key_permission_callback(JSContext* context, bool granted, JSValue value, void* user_data)
{
	get_private_key_t* work = user_data;
	tf_task_t* task = tf_task_get(context);
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	if (granted)
	{
		tf_ssb_run_work(ssb, _tf_ssb_get_private_key_work, _tf_ssb_get_private_key_after_work, work);
	}
	else
	{
		work->result = JS_DupValue(context, value);
		_tf_ssb_get_private_key_after_work(ssb, -1, work);
	}
}

static JSValue _tf_ssb_getPrivateKey(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	JSValue process = data[0];
	const char* user = _tf_ssb_get_process_credentials_session_name(context, process);
	size_t user_length = user ? strlen(user) : 0;
	const char* id = JS_ToCString(context, argv[0]);
	get_private_key_t* work = tf_malloc(sizeof(get_private_key_t) + user_length + 1);
	*work = (get_private_key_t) { .context = context, .result = JS_UNDEFINED };
	if (user)
	{
		memcpy(work->user, user, user_length + 1);
	}
	tf_string_set(work->id, sizeof(work->id), id);
	JSValue result = JS_NewPromiseCapability(context, work->promise);
	_tf_ssb_permission_test(context, process, "ssb_id_export", "Export a private key.", _tf_ssb_get_private_key_permission_callback, work);

	JS_FreeCString(context, user);
	JS_FreeCString(context, id);
	return result;
}

typedef struct _send_identities_t
{
	JSContext* context;
	JSValue process;
	const char* name;
	const char* package_owner;
	const char* package_name;
	tf_ssb_identity_info_t* info;
} send_identities_t;

static void _tf_ssb_sendIdentities_work(tf_ssb_t* ssb, void* user_data)
{
	send_identities_t* request = user_data;
	request->info = tf_ssb_db_get_identity_info(ssb, request->name, request->package_owner, request->package_name);
}

static void _tf_ssb_sendIdentities_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	send_identities_t* request = user_data;
	JSContext* context = request->context;
	JSValue result = JS_NewObject(context);

	JSValue identities = JS_NewArray(context);
	for (int i = 0; i < request->info->count; i++)
	{
		JS_SetPropertyUint32(context, identities, i, JS_NewString(context, request->info->identity[i]));
	}
	JS_SetPropertyStr(context, result, "action", JS_NewString(context, "identities"));
	JS_SetPropertyStr(context, result, "identities", identities);

	JSValue names = JS_NewObject(context);
	for (int i = 0; i < request->info->count; i++)
	{
		JS_SetPropertyStr(context, names, request->info->identity[i], JS_NewString(context, request->info->name[i] ? request->info->name[i] : request->info->identity[i]));
	}
	JS_SetPropertyStr(context, result, "names", names);
	JS_SetPropertyStr(context, result, "identity", JS_NewString(context, request->info->active_identity));

	JSValue process = request->process;
	const char* last_sent = tf_util_get_property_as_string(context, process, "_last_sent_identities");
	JSValue json = JS_JSONStringify(context, result, JS_NULL, JS_NULL);
	const char* string = JS_ToCString(context, json);
	if (!last_sent || !string || strcmp(last_sent, string))
	{
		JSValue send = JS_GetPropertyStr(context, process, "send");
		JSValue error = JS_Call(context, send, process, 1, &result);
		tf_util_report_error(context, error);
		JS_FreeValue(context, error);
		JS_FreeValue(context, send);
		JS_SetPropertyStr(context, process, "_last_sent_identities", json);
	}
	else
	{
		JS_FreeValue(context, json);
	}
	JS_FreeValue(context, result);
	JS_FreeCString(context, string);
	JS_FreeCString(context, last_sent);

	JS_FreeCString(context, (void*)request->name);
	JS_FreeCString(context, (void*)request->package_owner);
	JS_FreeCString(context, (void*)request->package_name);
	JS_FreeValue(context, request->process);
	tf_free(request->info);
	tf_free(request);
}

static JSValue _tf_api_process_sendIdentities(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	tf_task_t* task = tf_task_get(context);
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	JSValue process = data[0];

	const char* name = _tf_ssb_get_process_credentials_session_name(context, process);
	const char* package_owner = tf_util_get_property_as_string(context, process, "packageOwner");
	const char* package_name = tf_util_get_property_as_string(context, process, "packageName");

	send_identities_t* work = tf_malloc(sizeof(send_identities_t));
	*work = (send_identities_t) {
		.context = context,
		.name = name,
		.package_owner = package_owner,
		.package_name = package_name,
		.process = JS_DupValue(context, process),
	};
	tf_ssb_run_work(ssb, _tf_ssb_sendIdentities_work, _tf_ssb_sendIdentities_after_work, work);
	return JS_UNDEFINED;
}

static void _tf_api_process_send_permissions_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	permissions_granted_t* work = user_data;
	JSContext* context = work->context;
	JSValue object = JS_NewObject(context);
	JS_SetPropertyStr(context, object, "action", JS_NewString(context, "permissions"));
	JS_SetPropertyStr(context, object, "permissions", _tf_api_core_permissions_from_settings(context, work->user, work->package_owner, work->package_name, work->settings));
	tf_free((void*)work->settings);

	JSValue send = JS_GetPropertyStr(context, work->process, "send");
	JSValue error = JS_Call(context, send, work->process, 1, &object);
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, send);

	JS_FreeValue(context, object);

	error = JS_Call(context, work->promise[0], JS_UNDEFINED, 0, NULL);
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, work->promise[0]);
	JS_FreeValue(context, work->promise[1]);
	JS_FreeValue(context, work->process);
	JS_FreeCString(context, work->user);
	tf_free((void*)work->package_owner);
	tf_free((void*)work->package_name);
	tf_free(work);
}

static JSValue _tf_api_process_sendPermissions(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	tf_task_t* task = tf_task_get(context);
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	JSValue process = data[0];
	const char* package_owner = tf_util_get_property_as_string(context, process, "packageOwner");
	const char* package_name = tf_util_get_property_as_string(context, process, "packageName");
	permissions_granted_t* work = tf_malloc(sizeof(permissions_granted_t));
	*work = (permissions_granted_t) {
		.context = context,
		.user = _tf_ssb_get_process_credentials_session_name(context, process),
		.package_owner = tf_strdup(package_owner),
		.package_name = tf_strdup(package_name),
		.process = JS_DupValue(context, process),
	};
	JS_FreeCString(context, package_owner);
	JS_FreeCString(context, package_name);
	JSValue result = JS_NewPromiseCapability(context, work->promise);
	tf_ssb_run_work(ssb, _tf_api_core_permissions_granted_work, _tf_api_process_send_permissions_after_work, work);
	return result;
}

typedef struct _set_active_identity_t
{
	JSContext* context;
	JSValue process;
	const char* user;
	const char* id;
	const char* package_owner;
	const char* package_name;
	bool set;
	JSValue promise[2];
} set_active_identity_t;

static void _tf_api_process_set_active_identity_work(tf_ssb_t* ssb, void* user_data)
{
	set_active_identity_t* work = user_data;
	if (work->user && work->id && work->package_owner && work->package_name)
	{
		size_t length = strlen("id:") + strlen(work->package_owner) + 1 + strlen(work->package_name) + 1;
		char* key = alloca(length);
		snprintf(key, length, "id:%s:%s", work->package_owner, work->package_name);
		work->set = tf_ssb_db_set_property(ssb, work->user, key, work->id);
	}
}

static void _tf_api_process_set_active_identity_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	set_active_identity_t* work = user_data;
	JSContext* context = work->context;
	JSValue process = work->process;
	JSValue send_identities = JS_GetPropertyStr(context, process, "sendIdentities");
	JSValue error = JS_Call(context, send_identities, process, 0, NULL);
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, send_identities);

	JSValue argv[] = {
		JS_NewString(context, work->id),
	};
	_tf_api_broadcast_app_event_to_user(context, process, "setActiveIdentity", tf_countof(argv), argv, true);
	for (int i = 0; i < tf_countof(argv); i++)
	{
		JS_FreeValue(context, argv[i]);
	}

	error = JS_Call(context, work->set ? work->promise[0] : work->promise[1], JS_UNDEFINED, 0, NULL);
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, work->promise[0]);
	JS_FreeValue(context, work->promise[1]);
	JS_FreeValue(context, process);

	JS_FreeCString(context, work->user);
	JS_FreeCString(context, work->id);
	JS_FreeCString(context, work->package_owner);
	JS_FreeCString(context, work->package_name);

	tf_free(work);
}

static JSValue _tf_api_process_setActiveIdentity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	tf_task_t* task = tf_task_get(context);
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	JSValue process = data[0];
	set_active_identity_t* work = tf_malloc(sizeof(set_active_identity_t));
	*work = (set_active_identity_t) {
		.context = context,
		.process = JS_DupValue(context, process),
		.user = _tf_ssb_get_process_credentials_session_name(context, process),
		.id = JS_ToCString(context, argv[0]),
		.package_owner = tf_util_get_property_as_string(context, process, "packageOwner"),
		.package_name = tf_util_get_property_as_string(context, process, "packageName"),
	};
	JSValue result = JS_NewPromiseCapability(context, work->promise);
	tf_ssb_run_work(ssb, _tf_api_process_set_active_identity_work, _tf_api_process_set_active_identity_after_work, work);
	return result;
}

static bool _has_property(JSContext* context, JSValue object, int id)
{
	JSValue value = JS_GetPropertyUint32(context, object, id);
	bool found = !JS_IsUndefined(value);
	JS_FreeValue(context, value);
	return found;
}

static JSValue _tf_api_process_callFunction(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	JSValue args = data[0];
	JSValue process = JS_GetPropertyUint32(context, args, 0);
	JSValue name = JS_GetPropertyUint32(context, args, 1);

	int id = 1;
	JSValue next_call_id = JS_GetPropertyStr(context, process, "_next_call_id");
	if (JS_IsNumber(next_call_id))
	{
		JS_ToInt32(context, &id, next_call_id);
	}
	JS_FreeValue(context, next_call_id);

	JSValue calls = JS_GetPropertyStr(context, process, "_calls");
	if (!JS_IsObject(calls))
	{
		calls = JS_NewObject(context);
		JS_SetPropertyStr(context, process, "_calls", JS_DupValue(context, calls));
	}
	while (!id || _has_property(context, calls, id))
	{
		id++;
	}
	JS_SetPropertyStr(context, process, "_next_call_id", JS_NewInt32(context, id));

	JSValue promise[2] = { 0 };
	JSValue result = JS_NewPromiseCapability(context, promise);
	JSValue call_object = JS_NewObject(context);
	JS_SetPropertyStr(context, call_object, "resolve", promise[0]);
	JS_SetPropertyStr(context, call_object, "reject", promise[1]);
	JS_SetPropertyUint32(context, calls, id, call_object);
	JS_FreeValue(context, calls);

	JSValue message = JS_NewObject(context);
	JS_SetPropertyStr(context, message, "action", JS_NewString(context, "tfrpc"));
	JS_SetPropertyStr(context, message, "method", JS_DupValue(context, name));
	JSValue arguments = JS_NewArray(context);
	for (int i = 0; i < argc; i++)
	{
		JS_SetPropertyUint32(context, arguments, i, JS_DupValue(context, argv[i]));
	}
	JS_SetPropertyStr(context, message, "params", arguments);
	JS_SetPropertyStr(context, message, "id", JS_NewInt32(context, id));

	JSValue send = JS_GetPropertyStr(context, process, "send");
	JSValue error = JS_Call(context, send, process, 1, &message);
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, send);
	JS_FreeValue(context, message);

	JS_FreeValue(context, process);
	JS_FreeValue(context, name);
	return result;
}

static JSValue _tf_api_process_makeFunction(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	JSValue api = argv[0];
	JSValue process = data[0];
	JSValue name = JS_GetPropertyUint32(context, api, 0);

	JSValue args = JS_NewArray(context);
	JS_SetPropertyUint32(context, args, 0, JS_DupValue(context, process));
	JS_SetPropertyUint32(context, args, 1, JS_DupValue(context, name));

	JSValue result = JS_NewCFunctionData(context, _tf_api_process_callFunction, 0, 0, 1, &args);
	JS_DefinePropertyValueStr(context, result, "name", JS_DupValue(context, name), JS_PROP_HAS_VALUE);
	JS_FreeValue(context, name);
	JS_FreeValue(context, args);
	return result;
}

static JSValue _tf_api_process_send(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	JSValue process = data[0];
	JSValue send_queue = JS_GetPropertyStr(context, process, "_send_queue");
	JSValue on_output = JS_GetPropertyStr(context, process, "_on_output");
	if (JS_IsArray(context, send_queue))
	{
		if (JS_IsFunction(context, on_output))
		{
			int count = tf_util_get_length(context, send_queue);
			for (int i = 0; i < count; i++)
			{
				JSValue message = JS_GetPropertyUint32(context, send_queue, i);
				JSValue result = JS_Call(context, on_output, process, 1, &message);
				tf_util_report_error(context, result);
				JS_FreeValue(context, result);
				JS_FreeValue(context, message);
			}
			JS_SetPropertyStr(context, process, "_send_queue", JS_UNDEFINED);
		}
		else if (!JS_IsUndefined(argv[0]))
		{
			JS_SetPropertyUint32(context, send_queue, tf_util_get_length(context, send_queue), JS_DupValue(context, argv[0]));
		}
	}

	if (!JS_IsUndefined(argv[0]) && JS_IsFunction(context, on_output))
	{
		JSValue result = JS_Call(context, on_output, process, 1, &argv[0]);
		tf_util_report_error(context, result);
		JS_FreeValue(context, result);
	}
	JS_FreeValue(context, send_queue);
	JS_FreeValue(context, on_output);
	return JS_UNDEFINED;
}

typedef struct _set_user_permission_t
{
	tf_ssb_t* ssb;
	JSContext* context;
	JSValue process;
	const char* user;
	const char* package_owner;
	const char* package_name;
	const char* permission;
	bool allow;
	bool reset;
	bool result;
	JSValue promise[2];
} set_user_permission_t;

static void _tf_api_process_set_user_permission_work(tf_ssb_t* ssb, void* user_data)
{
	set_user_permission_t* work = user_data;

	JSMallocFunctions funcs = { 0 };
	tf_get_js_malloc_functions(&funcs);
	JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL);
	JSContext* context = JS_NewContext(runtime);

	/* XXX: Do this with one DB writer. */
	const char* settings = tf_ssb_db_get_property(ssb, "core", "settings");
	if (settings)
	{
		JSValue settings_value = JS_ParseJSON(context, settings, strlen(settings), NULL);
		JSValue user_permissions = JS_GetPropertyStr(context, settings_value, "userPermissions");
		if (JS_IsUndefined(user_permissions))
		{
			user_permissions = JS_NewObject(context);
			JS_SetPropertyStr(context, settings_value, "userPermissions", JS_DupValue(context, user_permissions));
		}
		JSValue user = JS_GetPropertyStr(context, user_permissions, work->user);
		if (JS_IsUndefined(user))
		{
			user = JS_NewObject(context);
			JS_SetPropertyStr(context, user_permissions, work->user, JS_DupValue(context, user));
		}
		JSValue package_owner = JS_GetPropertyStr(context, user, work->package_owner);
		if (JS_IsUndefined(package_owner))
		{
			package_owner = JS_NewObject(context);
			JS_SetPropertyStr(context, user, work->package_owner, JS_DupValue(context, package_owner));
		}
		JSValue package_name = JS_GetPropertyStr(context, package_owner, work->package_name);
		if (JS_IsUndefined(package_name))
		{
			package_name = JS_NewObject(context);
			JS_SetPropertyStr(context, package_owner, work->package_name, JS_DupValue(context, package_name));
		}
		JSValue permission = JS_GetPropertyStr(context, package_name, work->permission);
		if (work->reset)
		{
			JSAtom atom = JS_NewAtom(context, work->permission);
			JS_DeleteProperty(context, package_name, atom, 0);
			JS_FreeAtom(context, atom);
		}
		else
		{
			JS_SetPropertyStr(context, package_name, work->permission, JS_NewBool(context, work->allow));
		}
		JSValue settings_json = JS_JSONStringify(context, settings_value, JS_NULL, JS_NULL);
		const char* settings_string = JS_ToCString(context, settings_json);
		work->result = tf_ssb_db_set_property(ssb, "core", "settings", settings_string);
		JS_FreeCString(context, settings_string);
		JS_FreeValue(context, settings_json);
		JS_FreeValue(context, permission);
		JS_FreeValue(context, package_owner);
		JS_FreeValue(context, package_name);
		JS_FreeValue(context, user);
		JS_FreeValue(context, user_permissions);
		JS_FreeValue(context, settings_value);
		tf_free((void*)settings);
	}

	JS_FreeContext(context);
	JS_FreeRuntime(runtime);
}

static void _tf_api_process_set_user_permission_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	set_user_permission_t* work = user_data;
	JSContext* context = work->context;
	JS_FreeCString(context, work->user);
	JS_FreeCString(context, work->package_owner);
	JS_FreeCString(context, work->package_name);
	JS_FreeCString(context, work->permission);
	JSValue error = JS_Call(context, work->result ? work->promise[0] : work->promise[0], JS_UNDEFINED, 0, NULL);
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, work->promise[0]);
	JS_FreeValue(context, work->promise[1]);

	JSValue send = JS_GetPropertyStr(context, work->process, "sendPermissions");
	error = JS_Call(context, send, work->process, 0, NULL);
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, send);
	JS_FreeValue(context, work->process);
	tf_free(work);
}

static JSValue _tf_api_process_set_user_permission(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	tf_task_t* task = tf_task_get(context);
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	JSValue process = data[0];

	const char* name = _tf_ssb_get_process_credentials_session_name(context, process);
	const char* package_owner = tf_util_get_property_as_string(context, process, "packageOwner");
	const char* package_name = tf_util_get_property_as_string(context, process, "packageName");

	set_user_permission_t* set = tf_malloc(sizeof(set_user_permission_t));
	*set = (set_user_permission_t) {
		.ssb = ssb,
		.process = JS_DupValue(context, process),
		.context = context,
		.user = name,
		.package_owner = package_owner,
		.package_name = package_name,
		.permission = JS_ToCString(context, argv[0]),
		.allow = JS_ToBool(context, argv[1]),
		.reset = JS_IsUndefined(argv[1]),
	};
	JSValue result = JS_NewPromiseCapability(context, set->promise);
	tf_ssb_run_work(set->ssb, _tf_api_process_set_user_permission_work, _tf_api_process_set_user_permission_after_work, set);
	return result;
}

typedef struct _create_identity_t
{
	JSValue process;
	char id[k_id_base64_len];
	bool error_add;
	bool error_too_many;
	JSValue promise[2];
	char user[];
} create_identity_t;

static void _tf_api_process_create_identity_work(tf_ssb_t* ssb, void* user_data)
{
	create_identity_t* work = user_data;
	int count = tf_ssb_db_identity_get_count_for_user(ssb, work->user);
	if (count < 16)
	{
		char public[k_id_base64_len - 1];
		char private[512];
		tf_ssb_generate_keys_buffer(public, sizeof(public), private, sizeof(private));
		if (tf_ssb_db_identity_add(ssb, work->user, public, private))
		{
			snprintf(work->id, sizeof(work->id), "@%s", public);
		}
		else
		{
			work->error_add = true;
		}
	}
	else
	{
		work->error_too_many = true;
	}
}

static void _tf_api_process_create_identity_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	JSContext* context = tf_ssb_get_context(ssb);
	JSValue result = JS_UNDEFINED;
	create_identity_t* work = user_data;
	if (work->error_too_many)
	{
		result = JS_ThrowInternalError(context, "Too many identities for user.");
	}
	else if (work->error_add)
	{
		result = JS_ThrowInternalError(context, "Unable to add identity.");
	}
	else
	{
		result = JS_NewString(context, work->id);
	}

	JSValue process = work->process;
	JSValue send_identities = JS_GetPropertyStr(context, process, "sendIdentities");
	JSValue error = JS_Call(context, send_identities, process, 0, NULL);
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, send_identities);

	if (*work->id)
	{
		JSValue argv[] = {
			JS_NewString(context, work->id),
		};
		_tf_api_broadcast_app_event_to_user(context, process, "setActiveIdentity", tf_countof(argv), argv, true);
		for (int i = 0; i < tf_countof(argv); i++)
		{
			JS_FreeValue(context, argv[i]);
		}
	}

	error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
	JS_FreeValue(context, result);
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, work->process);
	JS_FreeValue(context, work->promise[0]);
	JS_FreeValue(context, work->promise[1]);
	tf_free(work);
}

static JSValue _tf_api_process_createIdentity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	tf_task_t* task = tf_task_get(context);
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	JSValue process = data[0];

	const char* name = _tf_ssb_get_process_credentials_session_name(context, process);
	if (!name || strcmp(name, "guest") == 0)
	{
		JS_FreeCString(context, name);
		return JS_ThrowInternalError(context, "Must be signed-in to create an account.");
	}

	size_t length = name ? strlen(name) : 0;
	create_identity_t* work = tf_malloc(sizeof(create_identity_t) + length + 1);
	*work = (create_identity_t) {
		.process = JS_DupValue(context, process),
	};
	memcpy(work->user, name, length + 1);
	JS_FreeCString(context, name);
	JSValue result = JS_NewPromiseCapability(context, work->promise);
	tf_ssb_run_work(ssb, _tf_api_process_create_identity_work, _tf_api_process_create_identity_after_work, work);
	return result;
}

static JSValue _tf_api_database(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	JSValue global = JS_GetGlobalObject(context);
	JSValue database = JS_GetPropertyStr(context, global, "Database");
	size_t key_length = 0;
	const char* key = JS_ToCStringLen(context, &key_length, argv[0]);

	JSValue process = data[0];
	const char* session_name_string = _tf_ssb_get_process_credentials_session_name(context, process);
	size_t length = (session_name_string ? strlen(session_name_string) : 0) + 1 + key_length + 1;
	char* buffer = tf_malloc(length);
	snprintf(buffer, length, "%s:%s", session_name_string ? session_name_string : "", key ? key : "");
	JSValue name = JS_NewString(context, buffer);
	JSValue result = JS_CallConstructor(context, database, 1, &name);
	JS_FreeValue(context, name);
	tf_free(buffer);
	JS_FreeCString(context, session_name_string);

	JS_FreeCString(context, key);
	JS_FreeValue(context, database);
	JS_FreeValue(context, global);
	return result;
}

static JSValue _tf_api_my_shared_database(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	JSValue process = data[0];
	JSValue global = JS_GetGlobalObject(context);
	JSValue database = JS_GetPropertyStr(context, global, "Database");
	size_t key_length = 0;
	const char* key = JS_ToCStringLen(context, &key_length, argv[0]);
	const char* package_name = tf_util_get_property_as_string(context, process, "packageName");
	size_t package_name_length = package_name ? strlen(package_name) : 0;

	const char* session_name_string = _tf_ssb_get_process_credentials_session_name(context, process);
	size_t length = (session_name_string ? strlen(session_name_string) : 0) + 1 + package_name_length + 1 + key_length + 1;
	char* buffer = tf_malloc(length);
	snprintf(buffer, length, "%s:%s:%s", session_name_string ? session_name_string : "", package_name ? package_name : "", key ? key : "");
	JSValue name = JS_NewString(context, buffer);
	JSValue result = JS_CallConstructor(context, database, 1, &name);
	JS_FreeValue(context, name);
	tf_free(buffer);
	JS_FreeCString(context, session_name_string);
	JS_FreeCString(context, package_name);

	JS_FreeCString(context, key);
	JS_FreeValue(context, database);
	JS_FreeValue(context, global);
	return result;
}

static JSValue _tf_api_shared_database(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	JSValue process = data[0];
	JSValue global = JS_GetGlobalObject(context);
	JSValue database = JS_GetPropertyStr(context, global, "Database");
	size_t key_length = 0;
	const char* key = JS_ToCStringLen(context, &key_length, argv[0]);
	const char* package_name = tf_util_get_property_as_string(context, process, "packageName");
	size_t package_name_length = package_name ? strlen(package_name) : 0;
	const char* package_owner = tf_util_get_property_as_string(context, process, "packageOwner");
	size_t package_owner_length = package_owner ? strlen(package_owner) : 0;

	size_t length = package_owner_length + 1 + package_name_length + 1 + key_length + 1;
	char* buffer = tf_malloc(length);
	snprintf(buffer, length, "%s:%s:%s", package_owner ? package_owner : "", package_name ? package_name : "", key ? key : "");
	JSValue name = JS_NewString(context, buffer);
	JSValue result = JS_CallConstructor(context, database, 1, &name);
	JS_FreeValue(context, name);
	tf_free(buffer);
	JS_FreeCString(context, package_owner);
	JS_FreeCString(context, package_name);

	JS_FreeCString(context, key);
	JS_FreeValue(context, database);
	JS_FreeValue(context, global);
	return result;
}

static JSValue _tf_api_databases(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	JSValue process = data[0];
	JSValue global = JS_GetGlobalObject(context);
	JSValue databases = JS_GetPropertyStr(context, global, "databases");
	JSValue list = JS_GetPropertyStr(context, databases, "list");
	const char* session_name_string = _tf_ssb_get_process_credentials_session_name(context, process);
	size_t session_name_string_length = session_name_string ? strlen(session_name_string) : 0;

	size_t length0 = strlen(":shared:") + session_name_string_length + 3;
	char* pattern0 = tf_malloc(length0);
	snprintf(pattern0, length0, ":shared:%s:%%", session_name_string);

	size_t length1 = session_name_string_length + 3;
	char* pattern1 = tf_malloc(length1);
	snprintf(pattern1, length1, "%s:%%", session_name_string);

	JSValue args[] = {
		JS_NewString(context, pattern0),
		JS_NewString(context, pattern1),
	};
	JSValue result = JS_Call(context, list, databases, 2, args);
	JS_FreeValue(context, args[0]);
	JS_FreeValue(context, args[1]);
	tf_free(pattern0);
	tf_free(pattern1);
	JS_FreeCString(context, session_name_string);
	JS_FreeValue(context, list);
	JS_FreeValue(context, databases);
	JS_FreeValue(context, global);
	return result;
}

static JSValue _tf_api_process_print(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	JSValue process = data[0];
	JSValue imports = JS_IsObject(process) ? JS_GetPropertyStr(context, process, "imports") : JS_UNDEFINED;
	JSValue app = JS_IsObject(imports) ? JS_GetPropertyStr(context, imports, "app") : JS_UNDEFINED;
	JSValue print = JS_IsObject(app) ? JS_GetPropertyStr(context, app, "print") : JS_UNDEFINED;
	JSValue result = JS_UNDEFINED;
	if (JS_IsFunction(context, print))
	{
		result = JS_Call(context, print, process, argc, argv);
	}
	JS_FreeValue(context, print);
	JS_FreeValue(context, app);
	JS_FreeValue(context, imports);
	return result;
}

static void _tf_api_register_imports(JSContext* context, JSValue process, JSValue options)
{
	JSValue imports = JS_NewObject(context);

	JSValue core = JS_NewObject(context);
	JS_SetPropertyStr(context, core, "apps", JS_NewCFunctionData(context, _tf_api_core_apps, 1, 0, 1, &process));
	JS_SetPropertyStr(context, core, "register", JS_NewCFunctionData(context, _tf_api_core_register, 2, 0, 1, &process));
	JS_SetPropertyStr(context, core, "unregister", JS_NewCFunctionData(context, _tf_api_core_unregister, 2, 0, 1, &process));

	JS_SetPropertyStr(context, core, "saveApp", JS_NewCFunctionData(context, _tf_api_core_save_app, 3, 0, 1, &process));

	JS_SetPropertyStr(context, core, "users", JS_NewCFunctionData(context, _tf_api_core_users, 0, 0, 1, &process));
	JS_SetPropertyStr(context, core, "broadcast", JS_NewCFunctionData(context, _tf_api_core_broadcast, 1, 0, 1, &process));

	JS_SetPropertyStr(context, core, "permissionsForUser", JS_NewCFunctionData(context, _tf_api_core_permissionsForUser, 1, 0, 1, &process));
	JS_SetPropertyStr(context, core, "permissionsGranted", JS_NewCFunctionData(context, _tf_api_core_permissionsGranted, 0, 0, 1, &process));
	JS_SetPropertyStr(context, core, "allPermissionsGranted", JS_NewCFunctionData(context, _tf_api_core_allPermissionsGranted, 0, 0, 1, &process));
	JS_SetPropertyStr(context, core, "permissionTest", JS_NewCFunctionData(context, _tf_api_core_permissionTest, 2, 0, 1, &process));

	JSValue user = JS_NewObject(context);
	JS_SetPropertyStr(context, user, "packageOwner", JS_GetPropertyStr(context, process, "packageOwner"));
	JS_SetPropertyStr(context, user, "packageName", JS_GetPropertyStr(context, process, "packageName"));
	JS_SetPropertyStr(context, user, "credentials", JS_GetPropertyStr(context, process, "credentials"));
	JS_SetPropertyStr(context, core, "user", user);

	JSValue app = JS_NewObject(context);
	JS_SetPropertyStr(context, app, "owner", JS_GetPropertyStr(context, process, "packageOwner"));
	JS_SetPropertyStr(context, app, "name", JS_GetPropertyStr(context, process, "packageName"));
	JS_SetPropertyStr(context, core, "app", app);

	JS_SetPropertyStr(context, core, "url", JS_GetPropertyStr(context, process, "url"));

	JS_SetPropertyStr(context, process, "setUserPermission", JS_NewCFunctionData(context, _tf_api_process_set_user_permission, 2, 0, 1, &process));
	JS_SetPropertyStr(context, process, "sendIdentities", JS_NewCFunctionData(context, _tf_api_process_sendIdentities, 0, 0, 1, &process));
	JS_SetPropertyStr(context, process, "createIdentity", JS_NewCFunctionData(context, _tf_api_process_createIdentity, 0, 0, 1, &process));
	JS_SetPropertyStr(context, process, "sendPermissions", JS_NewCFunctionData(context, _tf_api_process_sendPermissions, 0, 0, 1, &process));
	JS_SetPropertyStr(context, process, "setActiveIdentity", JS_NewCFunctionData(context, _tf_api_process_setActiveIdentity, 1, 0, 1, &process));
	JS_SetPropertyStr(context, process, "makeFunction", JS_NewCFunctionData(context, _tf_api_process_makeFunction, 1, 0, 1, &process));
	JS_SetPropertyStr(context, process, "send", JS_NewCFunctionData(context, _tf_api_process_send, 1, 0, 1, &process));
	JS_SetPropertyStr(context, process, "_send_queue", JS_NewArray(context));

	tf_task_t* task = tf_task_get(context);
	JSValue ssb = tf_ssb_register(context, tf_task_get_ssb(task));
	JS_SetPropertyStr(context, ssb, "getAllIdentities", JS_NewCFunction(context, _tf_ssb_getAllIdentities, "getAllIdentities", 0));
	JS_SetPropertyStr(context, ssb, "getActiveIdentity", JS_NewCFunctionData(context, _tf_ssb_getActiveIdentity, 0, 0, 1, &process));
	JS_SetPropertyStr(context, ssb, "getIdentities", JS_NewCFunctionData(context, _tf_ssb_getIdentities, 0, 0, 1, &process));
	JS_SetPropertyStr(context, ssb, "getOwnerIdentities", JS_NewCFunctionData(context, _tf_ssb_getOwnerIdentities, 0, 0, 1, &process));
	JS_SetPropertyStr(context, ssb, "privateMessageEncrypt", JS_NewCFunctionData(context, _tf_ssb_private_message_encrypt, 3, 0, 1, &process));
	JS_SetPropertyStr(context, ssb, "privateMessageDecrypt", JS_NewCFunctionData(context, _tf_ssb_private_message_decrypt, 2, 0, 1, &process));
	JS_SetPropertyStr(context, ssb, "appendMessageWithIdentity", JS_NewCFunctionData(context, _tf_ssb_appendMessageWithIdentity, 2, 0, 1, &process));
	JS_SetPropertyStr(context, ssb, "addIdentity", JS_NewCFunctionData(context, _tf_ssb_addIdentity, 1, 0, 1, &process));
	JS_SetPropertyStr(context, ssb, "deleteIdentity", JS_NewCFunctionData(context, _tf_ssb_deleteIdentity, 1, 0, 1, &process));
	JS_SetPropertyStr(context, ssb, "getPrivateKey", JS_NewCFunctionData(context, _tf_ssb_getPrivateKey, 1, 0, 1, &process));
	JS_SetPropertyStr(context, ssb, "createIdentity", JS_NewCFunctionData(context, _tf_api_process_createIdentity, 0, 0, 1, &process));
	JS_SetPropertyStr(context, ssb, "setActiveIdentity", JS_NewCFunctionData(context, _tf_api_process_setActiveIdentity, 1, 0, 1, &process));
	JS_SetPropertyStr(context, ssb, "getPrivateMessages", JS_NewCFunctionData(context, _tf_ssb_get_private_messages, 3, 0, 1, &process));

	JS_SetPropertyStr(context, imports, "ssb", ssb);

	const char* session_name_string = _tf_ssb_get_process_credentials_session_name(context, process);
	if (session_name_string)
	{
		JS_SetPropertyStr(context, imports, "database", JS_NewCFunctionData(context, _tf_api_database, 1, 0, 1, &process));
		JS_SetPropertyStr(context, imports, "my_shared_database", JS_NewCFunctionData(context, _tf_api_my_shared_database, 2, 0, 1, &process));
		JS_SetPropertyStr(context, imports, "databases", JS_NewCFunctionData(context, _tf_api_databases, 0, 0, 1, &process));
	}
	JS_FreeCString(context, session_name_string);

	const char* package_owner = tf_util_get_property_as_string(context, process, "packageOwner");
	const char* package_name = tf_util_get_property_as_string(context, process, "packageName");
	if (package_owner && package_name)
	{
		JS_SetPropertyStr(context, imports, "shared_database", JS_NewCFunctionData(context, _tf_api_shared_database, 1, 0, 1, &process));
	}
	JS_FreeCString(context, package_owner);
	JS_FreeCString(context, package_name);

	JSValue credentials = JS_GetPropertyStr(context, process, "credentials");
	JSValue permissions = JS_IsObject(credentials) ? JS_GetPropertyStr(context, credentials, "permissions") : JS_UNDEFINED;
	JSValue administration = JS_IsObject(permissions) ? JS_GetPropertyStr(context, permissions, "administration") : JS_UNDEFINED;
	if (JS_ToBool(context, administration) > 0)
	{
		JS_SetPropertyStr(context, core, "globalSettingsDescriptions", JS_NewCFunction(context, _tf_ssb_globalSettingsDescriptions, "globalSettingsDescriptions", 0));
		JS_SetPropertyStr(context, core, "globalSettingsGet", JS_NewCFunction(context, _tf_ssb_globalSettingsGet, "globalSettingsGet", 1));
		JS_SetPropertyStr(context, core, "globalSettingsSet", JS_NewCFunctionData(context, _tf_ssb_globalSettingsSet, 2, 0, 1, &process));

		JS_SetPropertyStr(context, core, "deleteUser", JS_NewCFunctionData(context, _tf_ssb_delete_user, 0, 0, 1, &process));

		JS_SetPropertyStr(context, ssb, "addBlock", JS_NewCFunctionData(context, _tf_ssb_add_block, 1, 0, 1, &process));
		JS_SetPropertyStr(context, ssb, "removeBlock", JS_NewCFunctionData(context, _tf_ssb_remove_block, 1, 0, 1, &process));
		JS_SetPropertyStr(context, ssb, "getBlocks", JS_NewCFunctionData(context, _tf_ssb_get_blocks, 0, 0, 1, &process));

		JS_SetPropertyStr(context, ssb, "swapWithServerIdentity", JS_NewCFunctionData(context, _tf_ssb_swap_with_server_identity, 1, 0, 1, &process));
	}
	JS_FreeValue(context, administration);
	JS_FreeValue(context, permissions);
	JS_FreeValue(context, credentials);

	JS_SetPropertyStr(context, imports, "core", core);

	if (JS_IsObject(options))
	{
		JSValue options_imports = JS_GetPropertyStr(context, options, "imports");
		if (JS_IsObject(options_imports))
		{
			JSPropertyEnum* ptab;
			uint32_t plen;
			JS_GetOwnPropertyNames(context, &ptab, &plen, options_imports, JS_GPN_STRING_MASK);
			for (uint32_t i = 0; i < plen; ++i)
			{
				JS_SetProperty(context, imports, ptab[i].atom, JS_GetProperty(context, options_imports, ptab[i].atom));
				JS_FreeAtom(context, ptab[i].atom);
			}
			js_free(context, ptab);
		}
		JS_FreeValue(context, options_imports);

		JSValue api = JS_GetPropertyStr(context, options, "api");
		JSValue app = JS_NewObject(context);

		int count = tf_util_get_length(context, api);
		for (int i = 0; i < count; i++)
		{
			JSValue entry = JS_GetPropertyUint32(context, api, i);
			JSValue name = JS_GetPropertyUint32(context, entry, 0);
			JSAtom name_atom = JS_ValueToAtom(context, name);
			JSValue function = _tf_api_process_makeFunction(context, JS_UNDEFINED, 1, &entry, 1, &process);
			JS_SetProperty(context, app, name_atom, function);
			JS_FreeAtom(context, name_atom);
			JS_FreeValue(context, name);
			JS_FreeValue(context, entry);
		}

		JS_SetPropertyStr(context, imports, "app", app);
		JS_FreeValue(context, api);
	}
	JS_SetPropertyStr(context, process, "imports", imports);

	JSValue client_api = JS_NewObject(context);
	JS_SetPropertyStr(context, client_api, "createIdentity", JS_NewCFunctionData(context, _tf_api_process_createIdentity, 0, 0, 1, &process));
	JS_SetPropertyStr(context, client_api, "resetPermission", JS_NewCFunctionData(context, _tf_api_process_reset_user_permission, 1, 0, 1, &process));
	JS_SetPropertyStr(context, client_api, "setActiveIdentity", JS_NewCFunctionData(context, _tf_api_process_setActiveIdentity, 1, 0, 1, &process));
	JS_SetPropertyStr(context, process, "client_api", client_api);

	JS_SetPropertyStr(context, process, "onPrint", JS_NewCFunctionData(context, _tf_api_process_print, 0, 0, 1, &process));

	JSValue error_args = JS_NewArray(context);
	JS_SetPropertyUint32(context, error_args, 0, JS_NewString(context, "error"));
	JS_SetPropertyStr(context, process, "onError", _tf_api_process_makeFunction(context, JS_UNDEFINED, 1, &error_args, 1, &process));
	JS_FreeValue(context, error_args);
}

typedef struct _get_task_t
{
	JSContext* context;
	JSValue promise[2];
	JSValue process;
	char blob_id[k_id_base64_len];
	char** file_names;
	void** file_contents;
	size_t* file_sizes;
	int file_count;
	int files_missing;
	const char* script;
} get_task_t;

static void _tf_api_get_task_work(tf_ssb_t* ssb, void* user_data)
{
	get_task_t* work = user_data;
	const char* blob_id = work->blob_id;

	char message_id[k_id_base64_len] = { 0 };

	if (blob_id && *blob_id == '%')
	{
		sqlite3* db = tf_ssb_acquire_db_reader(ssb);
		if (tf_ssb_db_get_app_blob_from_message(db, blob_id, message_id, sizeof(message_id)))
		{
			blob_id = message_id;
		}
		tf_ssb_release_db_reader(ssb, db);
	}

	if (blob_id && *blob_id == '&')
	{
		uint8_t* data = NULL;
		size_t size = 0;
		if (tf_ssb_db_blob_get(ssb, blob_id, &data, &size))
		{
			JSMallocFunctions funcs = { 0 };
			tf_get_js_malloc_functions(&funcs);
			JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL);
			JSContext* context = JS_NewContext(runtime);

			JSValue json = JS_ParseJSON(context, (const char*)data, size, NULL);
			if (JS_IsObject(json))
			{
				const char* type = tf_util_get_property_as_string(context, json, "type");
				if (type && strcmp(type, "tildefriends-app") == 0)
				{
					/* tfrpc.js */
					JSValue files = JS_GetPropertyStr(context, json, "files");

					JSPropertyEnum* ptab;
					uint32_t plen;
					JS_GetOwnPropertyNames(context, &ptab, &plen, files, JS_GPN_STRING_MASK);
					for (uint32_t i = 0; i < plen; ++i)
					{
						JSValue value = JS_GetProperty(context, files, ptab[i].atom);
						if (!JS_IsUndefined(value))
						{
							const char* file_name = JS_AtomToCString(context, ptab[i].atom);
							const char* file_blob_id = JS_ToCString(context, value);
							uint8_t* file_contents = NULL;
							size_t file_size = 0;

							if (tf_ssb_db_blob_get(ssb, file_blob_id, &file_contents, &file_size))
							{
								work->file_names = tf_resize_vec(work->file_names, sizeof(char*) * (work->file_count + 1));
								work->file_contents = tf_resize_vec(work->file_contents, sizeof(void*) * (work->file_count + 1));
								work->file_sizes = tf_resize_vec(work->file_sizes, sizeof(size_t) * (work->file_count + 1));
								work->file_names[work->file_count] = tf_strdup(file_name);
								work->file_contents[work->file_count] = file_contents;
								work->file_sizes[work->file_count] = file_size;
								work->file_count++;
							}
							else
							{
								work->files_missing++;
							}

							JS_FreeCString(context, file_name);
							JS_FreeCString(context, file_blob_id);
							JS_FreeValue(context, value);
						}
						JS_FreeAtom(context, ptab[i].atom);
					}
					js_free(context, ptab);

					JS_FreeValue(context, files);
				}
				JS_FreeCString(context, type);
			}
			JS_FreeValue(context, json);

			JS_FreeContext(context);
			JS_FreeRuntime(runtime);
			tf_free(data);
		}
	}
}

static void _call_catch(JSContext* context, JSValue promise, JSValue handler)
{
	JSValue catch = JS_GetPropertyStr(context, promise, "catch");
	JSValue error = JS_Call(context, catch, promise, 1, &handler);
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, catch);
}

static void _tf_api_get_task_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	get_task_t* work = user_data;
	JSContext* context = work->context;
	JSValue process = work->process;
	JSValue on_error = JS_GetPropertyStr(context, process, "onError");

	if (work->files_missing)
	{
		JSValue error = JS_ThrowInternalError(context, "Files missing.");
		JSValue exception = JS_GetException(context);
		JSValue result = JS_Call(context, work->promise[1], JS_UNDEFINED, 1, &exception);
		tf_util_report_error(context, result);
		JS_FreeValue(context, result);
		JS_FreeValue(context, exception);
		JS_FreeValue(context, error);
	}
	else
	{
		const char* app_source_name = work->script ? work->script : "app.js";
		const char* app_source = NULL;
		size_t app_source_size = 0;

		JSValue load_file = JS_GetPropertyStr(context, process, "loadFile");
		for (int i = 0; i < work->file_count; i++)
		{
			JSValue args = JS_NewArray(context);
			JS_SetPropertyUint32(context, args, 0, JS_NewString(context, work->file_names[i]));
			JS_SetPropertyUint32(context, args, 1, JS_NewArrayBufferCopy(context, work->file_contents[i], work->file_sizes[i]));
			JSValue promise = JS_Call(context, load_file, process, 1, &args);
			tf_util_report_error(context, promise);
			JS_FreeValue(context, promise);
			JS_FreeValue(context, args);

			if (strcmp(work->file_names[i], app_source_name) == 0)
			{
				app_source = work->file_contents[i];
				app_source_size = work->file_sizes[i];
			}
		}
		JS_FreeValue(context, load_file);

		if (strcmp(app_source_name, "app.js") == 0)
		{
			JSValue global = JS_GetGlobalObject(context);
			JSValue version_function = JS_GetPropertyStr(context, global, "version");
			JSValue version = JS_Call(context, version_function, JS_UNDEFINED, 0, NULL);
			tf_util_report_error(context, version);
			JS_FreeValue(context, version_function);
			JS_FreeValue(context, global);

			JSValue ready = JS_NewObject(context);
			JSValue send = JS_GetPropertyStr(context, process, "send");
			JS_SetPropertyStr(context, ready, "action", JS_NewString(context, "ready"));
			JS_SetPropertyStr(context, ready, "version", version);
			JSValue error = JS_Call(context, send, process, 1, &ready);
			tf_util_report_error(context, error);
			JS_FreeValue(context, error);
			JS_FreeValue(context, send);
			JS_FreeValue(context, ready);

			JSValue send_permissions = JS_GetPropertyStr(context, process, "sendPermissions");
			error = JS_Call(context, send_permissions, process, 0, NULL);
			tf_util_report_error(context, error);
			JS_FreeValue(context, error);
			JS_FreeValue(context, send_permissions);
		}

		JSValue execute_args = JS_NewObject(context);
		JSValue execute = JS_GetPropertyStr(context, process, "execute");
		JS_SetPropertyStr(context, execute_args, "name", JS_NewString(context, app_source_name));
		JS_SetPropertyStr(context, execute_args, "source", JS_NewStringLen(context, app_source, app_source_size));
		JSValue error = JS_Call(context, execute, process, 1, &execute_args);
		_call_catch(context, error, on_error);
		JS_FreeValue(context, error);
		JS_FreeValue(context, execute);
		JS_FreeValue(context, execute_args);
	}

	for (int i = 0; i < work->file_count; i++)
	{
		tf_free(work->file_names[i]);
		tf_free(work->file_contents[i]);
	}

	JSValue result = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &process);
	tf_util_report_error(context, result);
	JS_FreeValue(context, result);

	JS_FreeValue(context, on_error);
	JS_FreeValue(context, work->promise[0]);
	JS_FreeValue(context, work->promise[1]);
	JS_FreeValue(context, process);
	JS_FreeCString(context, work->script);
	tf_free(work->file_names);
	tf_free(work->file_contents);
	tf_free(work->file_sizes);
	tf_free(work);
}

static void _tf_api_get_task_file_callback(tf_task_t* task, const char* path, int result, const void* data, void* user_data)
{
	get_task_t* work = user_data;
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	if (result >= 0)
	{
		work->file_names = tf_resize_vec(work->file_names, sizeof(char*) * (work->file_count + 1));
		work->file_contents = tf_resize_vec(work->file_contents, sizeof(void*) * (work->file_count + 1));
		work->file_sizes = tf_resize_vec(work->file_sizes, sizeof(size_t) * (work->file_count + 1));
		work->file_names[work->file_count] = tf_strdup("/static/tfrpc.js");
		work->file_contents[work->file_count] = tf_malloc(result);
		memcpy(work->file_contents[work->file_count], data, result);
		work->file_sizes[work->file_count] = result;
		work->file_count++;
	}
	tf_ssb_run_work(ssb, _tf_api_get_task_work, _tf_api_get_task_after_work, work);
}

static JSValue _tf_api_get_process_blob(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
	const char* blob_id = JS_ToCString(context, argv[0]);
	tf_printf("Creating task for %s.\n", blob_id);
	JSValue options = argv[1];
	JSValue global = JS_GetGlobalObject(context);
	JSValue constructor = JS_GetPropertyStr(context, global, "Task");
	JSValue task = JS_CallConstructor(context, constructor, 0, NULL);
	JS_FreeValue(context, constructor);
	JSValue credentials = JS_GetPropertyStr(context, options, "credentials");
	JS_SetPropertyStr(context, task, "credentials", JS_IsUndefined(credentials) ? JS_NewObject(context) : credentials);
	JS_SetPropertyStr(context, task, "packageOwner", JS_GetPropertyStr(context, options, "packageOwner"));
	JS_SetPropertyStr(context, task, "packageName", JS_GetPropertyStr(context, options, "packageName"));
	JS_SetPropertyStr(context, task, "url", JS_GetPropertyStr(context, options, "url"));
	JS_SetPropertyStr(context, task, "eventHandlers", JS_NewObject(context));

	_tf_api_register_imports(context, task, options);

	JSValue imports = JS_GetPropertyStr(context, task, "imports");
	JSValue set_imports = JS_GetPropertyStr(context, task, "setImports");
	JS_FreeValue(context, JS_Call(context, set_imports, task, 1, &imports));
	JS_FreeValue(context, set_imports);
	JS_FreeValue(context, imports);

	JSValue activate = JS_GetPropertyStr(context, task, "activate");
	JS_FreeValue(context, JS_Call(context, activate, task, 0, NULL));
	JS_FreeValue(context, activate);

	JS_FreeValue(context, global);

	const char* script = tf_util_get_property_as_string(context, options, "script");

	get_task_t* work = tf_malloc(sizeof(get_task_t));
	*work = (get_task_t) {
		.context = context,
		.process = JS_DupValue(context, task),
		.script = script,
	};
	tf_string_set(work->blob_id, sizeof(work->blob_id), blob_id);
	JS_FreeCString(context, blob_id);

	JSValue promise = JS_NewPromiseCapability(context, work->promise);
	tf_file_read(tf_task_get(context), "core/tfrpc.js", _tf_api_get_task_file_callback, work);
	JS_FreeValue(context, task);
	return promise;
}

void tf_api_register(JSContext* context)
{
	JSValue global = JS_GetGlobalObject(context);

	JSValue exports = JS_GetPropertyStr(context, global, "exports");
	JS_SetPropertyStr(context, exports, "getProcessBlob", JS_NewCFunction(context, _tf_api_get_process_blob, "getProcessBlob", 2));
	JS_FreeValue(context, exports);

	JS_FreeValue(context, global);
}
