changeset 13921:33755e085ca5

Merge 13.0->trunk
author Kim Alvefur <zash@zash.se>
date Sun, 03 Aug 2025 15:21:56 +0200
parents 6ad72bd9a7ec (diff) 2169be3581ae (current diff)
children 52cf3ec248a9
files
diffstat 34 files changed, 416 insertions(+), 238 deletions(-) [+]
line wrap: on
line diff
--- a/GNUmakefile	Sun Aug 03 15:14:20 2025 +0200
+++ b/GNUmakefile	Sun Aug 03 15:21:56 2025 +0200
@@ -141,9 +141,10 @@
 vpath %.tl teal-src/prosody
 %.lua: %.tl
 	tl -I teal-src/ --gen-compat off --gen-target 5.1 gen $^ -o $@
-	-lua-format -i $@
+	-lua-format -i --no-keep-simple-control-block-one-line --no-keep-simple-function-one-line $@
+	sed -i "1i-- This file is generated from $<" $@
 
-teal: util/jsonschema.lua util/datamapper.lua util/jsonpointer.lua
+teal: util/jsonschema.lua util/datamapper.lua util/jsonpointer.lua plugins/mod_cron.lua
 
 util/%.so:
 	$(MAKE) install -C util-src
--- a/core/features.lua	Sun Aug 03 15:14:20 2025 +0200
+++ b/core/features.lua	Sun Aug 03 15:21:56 2025 +0200
@@ -20,6 +20,8 @@
 		"loader";
 		-- "keyval+" store
 		"keyval+";
+		-- Support for target filter in net.resolvers.basic
+		"net-connect-filter";
 
 		"s2sout-pre-connect-event";
 
--- a/core/moduleapi.lua	Sun Aug 03 15:14:20 2025 +0200
+++ b/core/moduleapi.lua	Sun Aug 03 15:21:56 2025 +0200
@@ -93,7 +93,14 @@
 end
 
 function api:hook(event, handler, priority)
-	return self:hook_object_event((hosts[self.host] or prosody).events, event, handler, priority);
+	local events;
+	local host = self.host;
+	if host == "*" then
+		events = prosody.events;
+	else
+		events = hosts[host].events;
+	end
+	return self:hook_object_event(events, event, handler, priority);
 end
 
 function api:hook_global(event, handler, priority)
@@ -405,7 +412,14 @@
 end
 
 function api:context(host)
-	return setmetatable({ host = host or "*", global = "*" == host }, { __index = self, __newindex = self });
+	local is_global;
+	if not host or host == "*" then
+		host, is_global = "*", true;
+	end
+	if not is_global and not hosts[host] then
+		error("Cannot create context for unknown host: "..host);
+	end
+	return setmetatable({ host = host, global = is_global }, { __index = self, __newindex = self });
 end
 
 function api:add_item(key, value)
--- a/net/resolvers/basic.lua	Sun Aug 03 15:14:20 2025 +0200
+++ b/net/resolvers/basic.lua	Sun Aug 03 15:21:56 2025 +0200
@@ -66,6 +66,9 @@
 			return;
 		end
 		local next_target = table.remove(self.targets, 1);
+		if self.filter and not self.filter(next_target[1], next_target[2], next_target[3], next_target[4], not not self.targets[1]) then
+			return self:next(cb);
+		end
 		cb(next_target[1], next_target[2], next_target[3], next_target[4], not not self.targets[1]);
 		return;
 	end
@@ -142,6 +145,7 @@
 		port = port;
 		conn_type = conn_type;
 		extra = extra or {};
+		filter = extra and extra.filter;
 		targets = targets;
 	}, resolver_mt);
 end
--- a/plugins/mod_cron.lua	Sun Aug 03 15:14:20 2025 +0200
+++ b/plugins/mod_cron.lua	Sun Aug 03 15:21:56 2025 +0200
@@ -1,3 +1,4 @@
+-- This file is generated from teal-src/prosody/plugins/mod_cron.tl
 module:set_global();
 
 local async = require("prosody.util.async");
@@ -9,7 +10,7 @@
 local active_hosts = {}
 
 if prosody.process_type == "prosodyctl" then
-	return; -- Yes, it happens...
+	return
 end
 
 function module.add_host(host_module)
@@ -17,14 +18,24 @@
 	local last_run_times = host_module:open_store("cron", "map");
 	active_hosts[host_module.host] = true;
 
-	local function save_task(task, started_at) last_run_times:set(nil, task.id, started_at); end
+	local function save_task(task, started_at)
+		last_run_times:set(nil, task.id, started_at);
+	end
 
-	local function restore_task(task) if task.last == nil then task.last = last_run_times:get(nil, task.id); end end
+	local function restore_task(task)
+		if task.last == nil then
+			task.last = last_run_times:get(nil, task.id);
+		end
+	end
 
 	local function task_added(event)
 		local task = event.item;
-		if task.name == nil then task.name = task.when; end
-		if task.id == nil then task.id = event.source.name .. "/" .. task.name:gsub("%W", "_"):lower(); end
+		if task.name == nil then
+			task.name = task.when;
+		end
+		if task.id == nil then
+			task.id = event.source.name .. "/" .. task.name:gsub("%W", "_"):lower();
+		end
 		task.period = host_module:get_option_period(task.id:gsub("/", "_") .. "_period", "1" .. task.when, 60, 86400 * 7 * 53);
 		task.restore = restore_task;
 		task.save = save_task;
@@ -40,14 +51,20 @@
 
 	host_module:handle_items("task", task_added, task_removed, true);
 
-	function host_module.unload() active_hosts[host_module.host] = nil; end
+	function host_module.unload()
+		active_hosts[host_module.host] = nil;
+	end
 end
 
-local function should_run(task, last) return not last or last + task.period * 0.995 <= os.time() end
+local function should_run(task, last)
+	return not last or last + task.period * 0.995 <= os.time()
+end
 
 local function run_task(task)
 	task:restore();
-	if not should_run(task, task.last) then return end
+	if not should_run(task, task.last) then
+		return
+	end
 	local started_at = os.time();
 	task:run(started_at);
 	task.last = started_at;
@@ -55,7 +72,7 @@
 end
 
 local function spread(t, factor)
-	return t * (1 - factor + 2*factor*math.random());
+	return t * (1 - factor + 2 * factor * math.random())
 end
 
 local task_runner = async.runner(run_task);
@@ -64,7 +81,9 @@
 	local delay = spread(cron_check_delay, cron_spread_factor);
 	for host in pairs(active_hosts) do
 		module:log("debug", "Running periodic tasks for host %s", host);
-		for _, task in ipairs(module:context(host):get_host_items("task")) do task_runner:run(task); end
+		for _, task in ipairs(module:context(host):get_host_items("task")) do
+			task_runner:run(task);
+		end
 	end
 	module:log("debug", "Wait %gs", delay);
 	return delay
--- a/plugins/mod_csi_simple.lua	Sun Aug 03 15:14:20 2025 +0200
+++ b/plugins/mod_csi_simple.lua	Sun Aug 03 15:21:56 2025 +0200
@@ -11,6 +11,7 @@
 local dt = require "prosody.util.datetime";
 local filters = require "prosody.util.filters";
 local timer = require "prosody.util.timer";
+local jid_bare = require "prosody.util.jid".bare;
 
 local queue_size = module:get_option_integer("csi_queue_size", 256, 1);
 local resume_delay = module:get_option_period("csi_resume_inactive_delay", 5);
@@ -40,13 +41,18 @@
 		-- TODO Some MUC awareness, e.g. check for the 'this relates to you' status code
 		return true, "subscription request";
 	elseif st_name == "message" then
+		if st_type == "error" then
+			return true, "delivery failure";
+		end
+		if stanza:get_child("event", "http://jabber.org/protocol/pubsub#event") then
+			if stanza.attr.from == nil or stanza.attr.from == jid_bare(stanza.attr.to) then
+				return true, "self-pep-event";
+			end
+		end
 		if st_type == "headline" then
 			-- Headline messages are ephemeral by definition
 			return false, "headline";
 		end
-		if st_type == "error" then
-			return true, "delivery failure";
-		end
 		if stanza:get_child("sent", "urn:xmpp:carbons:2") then
 			return true, "carbon";
 		end
--- a/plugins/mod_external_services.lua	Sun Aug 03 15:14:20 2025 +0200
+++ b/plugins/mod_external_services.lua	Sun Aug 03 15:21:56 2025 +0200
@@ -101,6 +101,9 @@
 			srv.restricted = true;
 		end
 	end
+	if item.extra and st.is_stanza(item.extra) then
+		srv.extra = item.extra;
+	end
 	return srv;
 end
 
@@ -149,7 +152,11 @@
 				password = srv.password;
 				expires = srv.expires and dt.datetime(srv.expires) or nil;
 				restricted = srv.restricted and "1" or nil;
-			}):up();
+			});
+		if srv.extra then
+			reply:add_child(srv.extra);
+		end
+		reply:up();
 	end
 
 	return reply;
@@ -242,3 +249,43 @@
 module:add_feature("urn:xmpp:extdisco:1");
 module:hook("iq-get/host/urn:xmpp:extdisco:1:services", handle_services);
 module:hook("iq-get/host/urn:xmpp:extdisco:1:credentials", handle_credentials);
+
+
+module:add_item("shell-command", {
+	section = "services";
+	section_desc = "Inspect external services";
+	name = "list";
+	desc = "List all external services for a host";
+	args = {
+		{ name = "host", type = "string" };
+		{ name = "service", type = "string" };
+	};
+	host_selector = "host";
+	handler = function (shell, host, service_type) --luacheck: ignore 212/host
+		local c = 0;
+		local print = shell.session.print;
+		local services = get_services();
+		for _, srv in ipairs(services) do
+			if not service_type or service_type:lower() == srv.type:lower() then
+				c = c + 1;
+				print(srv.type.." ("..srv.transport..")", srv.host..(srv.port and (":"..srv.port) or ""));
+				print(" auth:", srv.restricted and "required" or "none");
+				for _, prop in ipairs({ "username", "password", "expires" }) do
+					local val = srv[prop];
+					if val then
+						if prop == "expires" then
+							val = dt.datetime(val);
+						end
+						print(" "..prop..":", val);
+					end
+				end
+				if srv.extra then
+					print(" extra:");
+					print("    "..tostring(srv.extra:indent(3, "  ")));
+				end
+				print("");
+			end
+		end
+		return true, ("%d services"):format(c);
+	end;
+});
--- a/plugins/mod_http_files.lua	Sun Aug 03 15:14:20 2025 +0200
+++ b/plugins/mod_http_files.lua	Sun Aug 03 15:21:56 2025 +0200
@@ -11,7 +11,7 @@
 local open = io.open;
 local fileserver = require"prosody.net.http.files";
 
-local base_path = module:get_option_path("http_files_dir", module:get_option_path("http_path"));
+local base_path = assert(module:get_option_path("http_files_dir", module:get_option_path("http_path")), "missing required setting 'http_files_dir'");
 local cache_size = module:get_option_integer("http_files_cache_size", 128, 1);
 local cache_max_file_size = module:get_option_integer("http_files_cache_max_file_size", 4096, 1);
 local dir_indices = module:get_option_array("http_index_files", { "index.html", "index.htm" });
@@ -55,37 +55,12 @@
 end
 
 -- COMPAT -- TODO deprecate
-function serve(opts)
-	if type(opts) ~= "table" then -- assume path string
-		opts = { path = opts };
-	end
-	if opts.directory_index == nil then
-		opts.directory_index = directory_index;
-	end
-	if opts.mime_map == nil then
-		opts.mime_map = mime_map;
-	end
-	if opts.cache_size == nil then
-		opts.cache_size = cache_size;
-	end
-	if opts.cache_max_file_size == nil then
-		opts.cache_max_file_size = cache_max_file_size;
-	end
-	if opts.index_files == nil then
-		opts.index_files = dir_indices;
-	end
-	module:log("warn", "%s should be updated to use 'prosody.net.http.files' instead of mod_http_files", get_calling_module());
-	return fileserver.serve(opts);
+function serve()
+	error(string.format("%s should be updated to use 'prosody.net.http.files' instead of mod_http_files", get_calling_module()));
 end
 
-function wrap_route(routes)
-	module:log("debug", "%s should be updated to use 'prosody.net.http.files' instead of mod_http_files", get_calling_module());
-	for route,handler in pairs(routes) do
-		if type(handler) ~= "function" then
-			routes[route] = fileserver.serve(handler);
-		end
-	end
-	return routes;
+function wrap_route()
+	error(string.format("%s should be updated to use 'prosody.net.http.files' instead of mod_http_files", get_calling_module()));
 end
 
 module:provides("http", {
--- a/plugins/mod_invites.lua	Sun Aug 03 15:14:20 2025 +0200
+++ b/plugins/mod_invites.lua	Sun Aug 03 15:21:56 2025 +0200
@@ -260,7 +260,7 @@
 	flags = {
 		array_params = { role = true, group = have_group_invites };
 		value_params = { expires_after = true };
-		kv_params = { admin = true };
+		bool_params = { admin = true };
 	};
 
 	handler = function (self, user_jid, opts) --luacheck: ignore 212/self
@@ -326,7 +326,7 @@
 	host_selector = "user_jid";
 	flags = {
 		value_params = { expires_after = true };
-		kv_params = { allow_registration = true };
+		bool_params = { allow_registration = true };
 	};
 
 	handler = function (self, user_jid, opts) --luacheck: ignore 212/self
--- a/plugins/mod_s2s_auth_dane_in.lua	Sun Aug 03 15:14:20 2025 +0200
+++ b/plugins/mod_s2s_auth_dane_in.lua	Sun Aug 03 15:21:56 2025 +0200
@@ -24,11 +24,6 @@
 	return r;
 end
 
-local function ensure_nonempty(r)
-	assert(r[1], "empty");
-	return r;
-end
-
 local function flatten(a)
 	local seen = {};
 	local ret = {};
@@ -95,7 +90,8 @@
 		return promise.all(tlsas):next(flatten);
 	end
 
-	local ret = async.wait_for(resolver:lookup_promise("_xmpp-server." .. dns_domain, "TLSA"):next(ensure_secure):next(ensure_nonempty):catch(function()
+	local ret = async.wait_for(resolver:lookup_promise("_xmpp-server." .. dns_domain, "TLSA"):next(ensure_secure):next(function(ret)
+		if ret[1] then return ret; end
 		return promise.all({
 			resolver:lookup_promise("_xmpps-server._tcp." .. dns_domain, "SRV"):next(ensure_secure):next(fetch_tlsa);
 			resolver:lookup_promise("_xmpp-server._tcp." .. dns_domain, "SRV"):next(ensure_secure):next(fetch_tlsa);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/muc/gc3.lib.lua	Sun Aug 03 15:21:56 2025 +0200
@@ -0,0 +1,57 @@
+
+local jid_bare = require "prosody.util.jid".bare;
+
+local st = require "prosody.util.stanza";
+
+local muc_util = module:require "muc/util";
+local valid_affiliations = muc_util.valid_affiliations;
+
+local xmlns_gc3 = "urn:xmpp:gc3:tmp"
+
+--luacheck: ignore 113/get_room_from_jid
+
+function fetch_participant_list(room, stanza, origin)
+	local from_jid = stanza.attr.from;
+	local from_aff = room:get_affiliation(from_jid);
+	local from_rank = valid_affiliations[from_aff or "none"];
+
+	local required_aff_rank = valid_affiliations[room:get_members_only() and "member" or "none"];
+	if from_rank < required_aff_rank then
+		origin.send(st.error_reply(stanza, "auth", "forbidden"));
+		return true;
+	end
+
+	local can_see_real_jids = not room:is_anonymous_for(from_jid);
+
+	local reply = st.reply(stanza)
+		:tag("participants", { xmlns = xmlns_gc3 });
+
+	for jid, aff in room:each_affiliation() do
+		local nick = room:get_registered_nick(jid);
+		local visible_jid = can_see_real_jids and jid or nil;
+		reply:text_tag("item", nil, {
+			affiliation = aff;
+			jid = visible_jid;
+			nick = nick;
+			id = room:get_occupant_id_from_jid(jid);
+		});
+	end
+
+	origin.send(reply);
+	return true;
+end
+
+local function room_event_handler(handler, allow_nonexistent)
+	return function (event)
+		local origin, stanza = event.origin, event.stanza;
+		local room_jid = jid_bare(stanza.attr.to);
+		local room = get_room_from_jid(room_jid);
+		if not room and allow_nonexistent ~= true then
+			origin.send(st.error_reply(stanza, "cancel", "item-not-found"));
+			return true;
+		end
+		return handler(room, stanza, origin);
+	end;
+end
+
+module:hook("iq-get/bare/"..xmlns_gc3..":participants", room_event_handler(fetch_participant_list));
--- a/plugins/muc/mod_muc.lua	Sun Aug 03 15:14:20 2025 +0200
+++ b/plugins/muc/mod_muc.lua	Sun Aug 03 15:21:56 2025 +0200
@@ -100,6 +100,11 @@
 local occupant_id = module:require "muc/occupant_id";
 room_mt.get_salt = occupant_id.get_room_salt;
 room_mt.get_occupant_id = occupant_id.get_occupant_id;
+room_mt.get_occupant_id_from_jid = occupant_id.get_occupant_id_from_jid;
+
+if module:get_option_boolean("muc_enable_experimental_gc3", false) then
+	module:require "muc/gc3";
+end
 
 local jid_split = require "prosody.util.jid".split;
 local jid_prep = require "prosody.util.jid".prep;
--- a/plugins/muc/muc.lib.lua	Sun Aug 03 15:14:20 2025 +0200
+++ b/plugins/muc/muc.lib.lua	Sun Aug 03 15:21:56 2025 +0200
@@ -24,6 +24,7 @@
 local base64 = require "prosody.util.encodings".base64;
 local hmac_sha256 = require "prosody.util.hashes".hmac_sha256;
 local new_id = require "prosody.util.id".medium;
+local time = require "prosody.util.time";
 
 local log = module._log;
 
@@ -133,11 +134,27 @@
 	return occupant
 end
 
-function room_mt:route_to_occupant(occupant, stanza)
+function room_mt:route_to_occupant(occupant, stanza, session_filter)
 	local to = stanza.attr.to;
-	for jid in occupant:each_session() do
-		stanza.attr.to = jid;
-		self:route_stanza(stanza);
+	for session_jid, session_pres, session_meta in occupant:each_session() do
+		if session_filter then
+			stanza = session_filter(session_jid, session_pres, session_meta, stanza);
+		end
+		if stanza then
+			stanza.attr.to = session_jid;
+			self:route_stanza(stanza);
+		end
+	end
+	stanza.attr.to = to;
+end
+
+function room_mt:route_presence_to_occupant(occupant, stanza)
+	local to = stanza.attr.to;
+	for jid, _, session_meta in occupant:each_session() do
+		if not session_meta.filter_presence then
+			stanza.attr.to = jid;
+			self:route_stanza(stanza);
+		end
 	end
 	stanza.attr.to = to;
 end
@@ -297,7 +314,7 @@
 	end
 
 	if recipient then
-		return self:route_to_occupant(recipient, get_p(recipient));
+		return self:route_presence_to_occupant(recipient, get_p(recipient));
 	end
 
 	local broadcast_roles = self:get_presence_broadcast();
@@ -305,11 +322,11 @@
 	for occupant_nick, n_occupant in self:each_occupant() do
 		if occupant_nick ~= occupant.nick then
 			if broadcast_roles[occupant.role or "none"] or force_unavailable then
-				self:route_to_occupant(n_occupant, get_p(n_occupant));
+				self:route_presence_to_occupant(n_occupant, get_p(n_occupant));
 			elseif prev_role and broadcast_roles[prev_role] then
 				local pr = get_p(n_occupant);
 				pr.attr.type = 'unavailable';
-				self:route_to_occupant(n_occupant, pr);
+				self:route_presence_to_occupant(n_occupant, pr);
 			end
 
 		end
@@ -332,10 +349,14 @@
 	end
 end
 
-function room_mt:send_occupant_list(to, filter)
-	local to_bare = jid_bare(to);
+-- Send the current room list to an occupant (specifically to the real jid 'to',
+-- which is one of the occupant's sessions).
+function room_mt:send_occupant_list(to_occupant, to_jid, filter)
+	local to_session = to_occupant:get_session(to_jid);
+	if to_session.filter_presence then return; end
+	local to_bare = to_occupant.bare_jid;
 	local broadcast_roles = self:get_presence_broadcast();
-	local is_anonymous = self:is_anonymous_for(to);
+	local is_anonymous = self:is_anonymous_for(to_jid);
 	local broadcast_bare_jids = {}; -- Track which bare JIDs we have sent presence for
 	for occupant_jid, occupant in self:each_occupant() do
 		broadcast_bare_jids[occupant.bare_jid] = true;
@@ -343,7 +364,7 @@
 			local x = st.stanza("x", {xmlns='http://jabber.org/protocol/muc#user'});
 			self:build_item_list(occupant, x, is_anonymous and to_bare ~= occupant.bare_jid); -- can always see your own jids
 			local pres = st.clone(occupant:get_presence());
-			pres.attr.to = to;
+			pres.attr.to = to_jid;
 			pres:add_child(x);
 			module:fire_event("muc-build-occupant-presence", { room = self, occupant = occupant, stanza = pres });
 			self:route_stanza(pres);
@@ -356,7 +377,7 @@
 			if (nick or not is_anonymous) and not broadcast_bare_jids[affiliated_jid]
 			and (filter == nil or filter(affiliated_jid, nil)) then
 				local from = nick and (self.jid.."/"..nick) or self.jid;
-				local pres = st.presence({ to = to, from = from, type = "unavailable" })
+				local pres = st.presence({ to = to_jid, from = from, type = "unavailable" })
 					:tag("x", { xmlns = 'http://jabber.org/protocol/muc#user' })
 						:tag("item", {
 							affiliation = affiliation;
@@ -758,7 +779,7 @@
 
 		if orig_occupant == nil or muc_x then
 			-- Send occupant list to newly joined or desynced user
-			self:send_occupant_list(real_jid, function(nick, occupant) -- luacheck: ignore 212
+			self:send_occupant_list(dest_occupant, real_jid, function(nick, occupant) -- luacheck: ignore 212
 				-- Don't include self
 				return (not occupant) or occupant:get_presence(real_jid) == nil;
 			end)
@@ -1437,6 +1458,15 @@
 		end
 	end
 
+	data = data or {};
+	local old_data = self._affiliation_data[jid];
+
+	local current_time = time.now();
+	data.affiliation_created_at = old_data and old_data.affiliation_created_at or current_time;
+	data.affiliation_created_by = old_data and old_data.affiliation_created_by or actor;
+	data.affiliation_modified_at = current_time;
+	data.affiliation_modified_by = actor;
+
 	local event_data = {
 		room = self;
 		actor = actor;
@@ -1444,7 +1474,7 @@
 		affiliation = affiliation or "none";
 		reason = reason;
 		previous_affiliation = target_affiliation or "none";
-		data = data and data or nil; -- coerce false to nil
+		data = data;
 		previous_data = self._affiliation_data[jid] or nil;
 	};
 
@@ -1453,10 +1483,6 @@
 		local err = event_data.error or { type = "cancel", condition = "not-allowed" };
 		return nil, err.type, err.condition;
 	end
-	if affiliation and not data and event_data.data then
-		-- Allow handlers to add data when none was going to be set
-		data = event_data.data;
-	end
 
 	-- Set in 'database'
 	self._affiliations[jid] = affiliation;
@@ -1514,7 +1540,7 @@
 				(old_role ~= "moderator" and occupant.role == "moderator")) then -- Has gained or lost moderator status
 				-- Send everyone else's presences (as jid visibility has changed)
 				for real_jid in occupant:each_session() do
-					self:send_occupant_list(real_jid, function(occupant_jid, occupant) --luacheck: ignore 212 433
+					self:send_occupant_list(occupant, real_jid, function(occupant_jid, occupant) --luacheck: ignore 212 433
 						return (not occupant) or occupant.bare_jid ~= jid;
 					end);
 				end
--- a/plugins/muc/occupant.lib.lua	Sun Aug 03 15:14:20 2025 +0200
+++ b/plugins/muc/occupant.lib.lua	Sun Aug 03 15:21:56 2025 +0200
@@ -23,10 +23,11 @@
 -- Deep copy an occupant
 local function copy_occupant(occupant)
 	local sessions = {};
-	for full_jid, presence_stanza in pairs(occupant.sessions) do
+	for full_jid, session in pairs(occupant.sessions) do
+		local presence_stanza = session.presence;
 		-- Don't keep unavailable presences, as they'll accumulate; unless they're the primary session
 		if presence_stanza.attr.type ~= "unavailable" or full_jid == occupant.jid then
-			sessions[full_jid] = presence_stanza;
+			sessions[full_jid] = session;
 		end
 	end
 	return setmetatable({
@@ -48,12 +49,16 @@
 	return nil;
 end
 
+function occupant_mt:get_session(real_jid)
+	return self.sessions[real_jid];
+end
+
 function occupant_mt:set_session(real_jid, presence_stanza, replace_primary)
 	local pr = get_filtered_presence(presence_stanza);
 	pr.attr.from = self.nick;
 	pr.attr.to = real_jid;
 
-	self.sessions[real_jid] = pr;
+	self.sessions[real_jid] = { presence = pr };
 	if replace_primary then
 		self.jid = real_jid;
 	elseif self.jid == nil or (pr.attr.type == "unavailable" and self.jid == real_jid) then
@@ -70,12 +75,23 @@
 	end
 end
 
+-- An iterator over self.sessions that returns (jid, presence, session)
+local function _session_iter(s, var)
+	local k = next(s, var);
+	if k then
+		local v = s[k];
+		return k, v.presence, v;
+	end
+end
+
 function occupant_mt:each_session()
-	return pairs(self.sessions)
+	return _session_iter, self.sessions;
 end
 
 function occupant_mt:get_presence(real_jid)
-	return self.sessions[real_jid or self.jid]
+	local session = self.sessions[real_jid or self.jid];
+	if not session then return; end
+	return session.presence;
 end
 
 return {
--- a/plugins/muc/occupant_id.lib.lua	Sun Aug 03 15:14:20 2025 +0200
+++ b/plugins/muc/occupant_id.lib.lua	Sun Aug 03 15:21:56 2025 +0200
@@ -31,6 +31,11 @@
 	return occupant.stable_id;
 end
 
+local function get_occupant_id_from_jid(room, real_bare_jid)
+	local salt = get_room_salt(room)
+	return b64encode(hmac_sha256(real_bare_jid, salt));
+end
+
 local function update_occupant(event)
 	local stanza, room, occupant, dest_occupant = event.stanza, event.room, event.occupant, event.dest_occupant;
 
@@ -73,4 +78,5 @@
 return {
 	get_room_salt = get_room_salt;
 	get_occupant_id = get_occupant_id;
+	get_occupant_id_from_jid = get_occupant_id_from_jid;
 };
--- a/plugins/muc/register.lib.lua	Sun Aug 03 15:14:20 2025 +0200
+++ b/plugins/muc/register.lib.lua	Sun Aug 03 15:21:56 2025 +0200
@@ -4,9 +4,10 @@
 local st = require "prosody.util.stanza";
 local dataforms = require "prosody.util.dataforms";
 
-local allow_unaffiliated = module:get_option_boolean("allow_unaffiliated_register", false);
+local gc3_mode = module:get_option_boolean("muc_enable_experimental_gc3", false);
 
-local enforce_nick = module:get_option_boolean("enforce_registered_nickname", false);
+local allow_unaffiliated = module:get_option_boolean("allow_unaffiliated_register", gc3_mode);
+local enforce_nick = module:get_option_boolean("enforce_registered_nickname", gc3_mode);
 
 -- Whether to include the current registration data as a dataform. Disabled
 -- by default currently as it hasn't been widely tested with clients.
@@ -125,9 +126,16 @@
 	if affiliation == "outcast" then
 		origin.send(st.error_reply(stanza, "auth", "forbidden"));
 		return true;
-	elseif not (affiliation or allow_unaffiliated) then
-		origin.send(st.error_reply(stanza, "auth", "registration-required"));
-		return true;
+	elseif not affiliation then
+		if room:get_members_only() then
+			-- Never allow unaffiliated users to register their way into a members-only room
+			origin.send(st.error_reply(stanza, "auth", "forbidden"));
+			return true;
+		elseif not allow_unaffiliated then
+			-- Not members-only, but we're not permitting registration due to config
+			origin.send(st.error_reply(stanza, "auth", "registration-required"));
+			return true;
+		end
 	end
 	local reply = st.reply(stanza);
 	local registered_nick = get_registered_nick(room, user_jid);
--- a/spec/util_argparse_spec.lua	Sun Aug 03 15:14:20 2025 +0200
+++ b/spec/util_argparse_spec.lua	Sun Aug 03 15:21:56 2025 +0200
@@ -42,14 +42,14 @@
 
 		do
 			-- Same test with strict mode enabled and all parameters declared
-			local opts, err = parse({ "--foo"; "-b" }, { kv_params = { foo = true, bar = true }; short_params = { b = "bar" }, strict = true });
+			local opts, err = parse({ "--foo"; "-b" }, { bool_params = { foo = true, bar = true }; short_params = { b = "bar" }, strict = true });
 			assert.falsy(err);
 			assert.same({ foo = true; bar = true }, opts);
 		end
 	end);
 
 	it("supports value arguments", function()
-		local opts, err = parse({ "--foo"; "bar"; "--baz=moo" }, { value_params = { foo = true; bar = true } });
+		local opts, err = parse({ "--foo"; "bar"; "--baz=moo" }, { value_params = { foo = true; bar = true, baz = true } });
 		assert.falsy(err);
 		assert.same({ foo = "bar"; baz = "moo" }, opts);
 	end);
@@ -96,9 +96,9 @@
 		assert.same("--foobar", err2);
 	end);
 
-	it("accepts known kv parameters in strict mode", function ()
-		local opts, err = parse({ "--item=foo" }, { kv_params = { item = true }, strict = true });
+	it("accepts known bool parameters in strict mode", function ()
+		local opts, err = parse({ "--item" }, { bool_params = { item = true }, strict = true });
 		assert.falsy(err);
-		assert.same("foo", opts.item);
+		assert.same(true, opts.item);
 	end);
 end);
--- a/teal-src/prosody/plugins/mod_cron.tl	Sun Aug 03 15:14:20 2025 +0200
+++ b/teal-src/prosody/plugins/mod_cron.tl	Sun Aug 03 15:21:56 2025 +0200
@@ -36,6 +36,10 @@
 
 local active_hosts : { string : boolean } = {  }
 
+if prosody.process_type == "prosodyctl" then
+	return -- Yes, it happens...
+end
+
 function module.add_host(host_module : moduleapi)
 
 	local last_run_times = host_module:open_store("cron", "map") as map_store<string,integer>;
@@ -63,13 +67,13 @@
 		task.restore = restore_task;
 		task.save = save_task;
 		module:log("debug", "%s task %s added", task.when, task.id);
-		return true;
+		return true
 	end
 
 	local function task_removed(event : task_event) : boolean
 		local task = event.item;
 		host_module:log("debug", "Task %s removed", task.id);
-		return true;
+		return true
 	end
 
 	host_module:handle_items("task", task_added, task_removed, true);
@@ -80,13 +84,13 @@
 end
 
 local function should_run(task : task_spec, last : integer) : boolean
-	return not last or last + task.period * 0.995 <= os.time();
+	return not last or last + task.period * 0.995 <= os.time()
 end
 
 local function run_task(task : task_spec)
 	task:restore();
 	if not should_run(task, task.last) then
-		return;
+		return
 	end
 	local started_at = os.time();
 	task:run(started_at);
@@ -95,7 +99,7 @@
 end
 
 local function spread(t : number, factor : number) : number
-	return t * (1 - factor + 2*factor*math.random());
+	return t * (1 - factor + 2*factor*math.random())
 end
 
 local task_runner : async.runner_t<task_spec> = async.runner(run_task);
@@ -109,7 +113,7 @@
 		end
 	end
 	module:log("debug", "Wait %gs", delay);
-	return delay;
+	return delay
 end);
 
 -- TODO measure load, pick a good time to do stuff
@@ -122,7 +126,7 @@
 	args = {};
 	handler = function (self, filter_host : string)
 		local format_table = require "prosody.util.human.io".table;
-		local it = require "util.iterators";
+		local it = require "prosody.util.iterators";
 		local row = format_table({
 			{ title = "Host", width = "2p" };
 			{ title = "Task", width = "3p" };
--- a/teal-src/prosody/util/array.d.tl	Sun Aug 03 15:14:20 2025 +0200
+++ b/teal-src/prosody/util/array.d.tl	Sun Aug 03 15:21:56 2025 +0200
@@ -1,9 +1,10 @@
-local record array_t<T>
-	{ T }
+local record array_t<T> is { T }
+	-- TODO methods
 end
 
 local record lib
 	metamethod __call : function () : array_t
+	-- TODO library functions
 end
 
 return lib
--- a/teal-src/prosody/util/crypto.d.tl	Sun Aug 03 15:14:20 2025 +0200
+++ b/teal-src/prosody/util/crypto.d.tl	Sun Aug 03 15:21:56 2025 +0200
@@ -2,7 +2,9 @@
 	record key
 		private_pem : function (key) : string
 		public_pem : function (key) : string
+		public_raw : function (key) : string
 		get_type : function (key) : string
+		derive : function (key, key) : string
 	end
 
 	type base_evp_sign = function (key, message : string) : string
@@ -44,9 +46,11 @@
 	aes_256_ctr_decrypt : Levp_decrypt
 
 	generate_ed25519_keypair : function () : key
+	generate_p256_keypair : function () : key
 
 	import_private_pem : function (string) : key
 	import_public_pem : function (string) : key
+	import_public_ec_raw : function (string, string) : key
 
 	parse_ecdsa_signature : function (string, integer) : string, string
 	build_ecdsa_signature : function (r : string, s : string) : string
--- a/teal-src/prosody/util/dataforms.d.tl	Sun Aug 03 15:14:20 2025 +0200
+++ b/teal-src/prosody/util/dataforms.d.tl	Sun Aug 03 15:21:56 2025 +0200
@@ -5,7 +5,7 @@
 		title : string
 		instructions : string
 
-		record form_field
+		record form_field is { form_field }
 
 			enum field_type
 				"boolean"
@@ -35,7 +35,6 @@
 			options : table
 		end
 
-		{ form_field }
 
 		enum form_type
 			"form"
--- a/teal-src/prosody/util/datamapper.tl	Sun Aug 03 15:14:20 2025 +0200
+++ b/teal-src/prosody/util/datamapper.tl	Sun Aug 03 15:21:56 2025 +0200
@@ -25,7 +25,7 @@
 local json = require "prosody.util.json"
 local pointer = require "prosody.util.jsonpointer";
 
-local json_type_name = json.json_type_name;
+local type json_type_name = json.json_type_name;
 local json_schema_object = require "prosody.util.jsonschema"
 local type schema_t = boolean | json_schema_object
 
@@ -42,7 +42,7 @@
 local function totype(t : json_type_name, s : string) : any
 	if not s then return nil end
 	if t == "string" then
-		return s;
+		return s
 	elseif t == "boolean" then
 		return toboolean(s)
 	elseif t == "number" or t == "integer" then
@@ -63,10 +63,10 @@
 local function resolve_schema(schema  : schema_t, root : json_schema_object) : schema_t
 	if schema is json_schema_object then
 		if schema["$ref"] and schema["$ref"]:sub(1, 1) == "#" then
-			return pointer.resolve(root as table, schema["$ref"]:sub(2)) as schema_t;
+			return pointer.resolve(root as table, schema["$ref"]:sub(2)) as schema_t
 		end
 	end
-	return schema;
+	return schema
 end
 
 local function guess_schema_type(schema : json_schema_object) : json_type_name
@@ -95,8 +95,6 @@
 
 	if propschema is json_schema_object then
 		proptype = guess_schema_type(propschema);
-	elseif propschema is string then -- Teal says this can never be a string, but it could before so best be sure
-		error("schema as string is not supported: "..propschema.." {"..current_ns.."}"..propname)
 	end
 
 	if proptype == "object" or proptype == "array" then
@@ -158,7 +156,7 @@
 			c = s:get_child(nil, namespace);
 		end
 		if c then
-			return c.name;
+			return c.name
 		end
 	elseif value_where == "in_attribute" then
 		local attr = name
@@ -250,7 +248,7 @@
 			table.insert(out, totype(proptype, value));
 		end
 	end
-	return out;
+	return out
 end
 
 local function parse (schema : json_schema_object, s : st.stanza_t) : table
@@ -269,7 +267,7 @@
 		return  v
 	elseif proptype == "number" and v is number then
 		return  string.format("%g", v)
-	elseif proptype == "integer" and v is number then -- TODO is integer
+	elseif proptype == "integer" and v is integer then
 		return  string.format("%d", v)
 	elseif proptype == "boolean" then
 		return  v and "1" or "0"
@@ -361,7 +359,7 @@
 				unparse_property(out, v, proptype, propschema, value_where, name, namespace, current_ns, prefix, single_attribute, root)
 			end
 		end
-		return out;
+		return out
 
 	elseif s_type == "array" then
 		local itemschema = resolve_schema(schema.items, root)
@@ -369,7 +367,7 @@
 		for _, item in ipairs(t as { string }) do
 			unparse_property(out, item, proptype, itemschema, value_where, name, namespace, current_ns, prefix, single_attribute, root)
 		end
-		return out;
+		return out
 	end
 end
 
--- a/teal-src/prosody/util/jsonschema.tl	Sun Aug 03 15:14:20 2025 +0200
+++ b/teal-src/prosody/util/jsonschema.tl	Sun Aug 03 15:21:56 2025 +0200
@@ -10,12 +10,8 @@
 
 if not math.type then require "prosody.util.mathcompat" end
 
-
-local utf8_enc = rawget(_G, "utf8") or require"prosody.util.encodings".utf8;
-local utf8_len = utf8_enc.len or function(s : string) : integer
-	local _, count = s:gsub("[%z\001-\127\194-\253][\128-\191]*", "");
-	return count;
-end;
+-- XXX util.encodings seems to count differently from the Lua builtin
+local utf8_len = rawget(_G, "utf8") and utf8.len or require"prosody.util.encodings".utf8.length;
 
 local json = require "prosody.util.json"
 local null = json.null;
@@ -166,7 +162,7 @@
 end
 local type errors = { validation_error }
 local function mkerr(sloc:string,iloc:string,err:string) : validation_error
-	return { schemaLocation = sloc; instanceLocation = iloc; error = err };
+	return { schemaLocation = sloc; instanceLocation = iloc; error = err }
 end
 
 local function validate (schema : schema_t, data : any, root : json_schema_object, sloc : string, iloc : string, errs:errors) : boolean, errors
@@ -186,14 +182,14 @@
 		if referenced ~= nil and referenced ~= root and referenced ~= schema then
 			if not validate(referenced, data, root, schema["$ref"], iloc, errs) then
 				table.insert(errs, mkerr(sloc.."/$ref", iloc, "Subschema failed validation"))
-				return false, errs;
+				return false, errs
 			end
 		end
 	end
 
 	if not simple_validate(schema.type, data) then
 		table.insert(errs, mkerr(sloc.."/type", iloc, "unexpected type"));
-		return false, errs;
+		return false, errs
 	end
 
 	if schema.type == "object" then
@@ -202,7 +198,7 @@
 			for k in pairs(data) do
 				if not k is string then
 					table.insert(errs, mkerr(sloc.."/type", iloc, "'object' had non-string keys"));
-					return false, errs;
+					return false, errs
 				end
 			end
 		end
@@ -214,7 +210,7 @@
 			for i in pairs(data) do
 				if not i is integer then
 					table.insert(errs, mkerr(sloc.."/type", iloc, "'array' had non-integer keys"));
-					return false, errs;
+					return false, errs
 				end
 			end
 		end
@@ -231,7 +227,7 @@
 		end
 		if not match then
 			table.insert(errs, mkerr(sloc.."/enum", iloc, "not one of the enumerated values"));
-			return false, errs;
+			return false, errs
 		end
 	end
 
@@ -240,42 +236,42 @@
 	if data is string then
 		if schema.maxLength and utf8_len(data) > schema.maxLength then
 			table.insert(errs, mkerr(sloc.."/maxLength", iloc, "string too long"))
-			return false, errs;
+			return false, errs
 		end
 		if schema.minLength and utf8_len(data) < schema.minLength then
 			table.insert(errs, mkerr(sloc.."/maxLength", iloc, "string too short"))
-			return false, errs;
+			return false, errs
 		end
 		if schema.luaPattern and not data:match(schema.luaPattern) then
 			table.insert(errs, mkerr(sloc.."/luaPattern", iloc, "string does not match pattern"))
-			return false, errs;
+			return false, errs
 		end
 	end
 
 	if data is number then
 		if schema.multipleOf and (data == 0 or data % schema.multipleOf ~= 0) then
 			table.insert(errs, mkerr(sloc.."/luaPattern", iloc, "not a multiple"))
-			return false, errs;
+			return false, errs
 		end
 
 		if schema.maximum and not ( data <= schema.maximum ) then
 			table.insert(errs, mkerr(sloc.."/maximum", iloc, "number exceeds maximum"))
-			return false, errs;
+			return false, errs
 		end
 
 		if schema.exclusiveMaximum and not ( data < schema.exclusiveMaximum ) then
 			table.insert(errs, mkerr(sloc.."/exclusiveMaximum", iloc, "number exceeds exclusive maximum"))
-			return false, errs;
+			return false, errs
 		end
 
 		if schema.minimum and not ( data >= schema.minimum ) then
 			table.insert(errs, mkerr(sloc.."/minimum", iloc, "number below minimum"))
-			return false, errs;
+			return false, errs
 		end
 
 		if schema.exclusiveMinimum and not ( data > schema.exclusiveMinimum ) then
 			table.insert(errs, mkerr(sloc.."/exclusiveMinimum", iloc, "number below exclusive minimum"))
-			return false, errs;
+			return false, errs
 		end
 	end
 
@@ -283,7 +279,7 @@
 		for i, sub in ipairs(schema.allOf) do
 			if not validate(sub, data, root, sloc.."/allOf/"..i, iloc, errs) then
 				table.insert(errs, mkerr(sloc.."/allOf", iloc, "did not match all subschemas"))
-				return false, errs;
+				return false, errs
 			end
 		end
 	end
@@ -297,7 +293,7 @@
 		end
 		if valid ~= 1 then
 			table.insert(errs, mkerr(sloc.."/oneOf", iloc, "did not match exactly one subschema"))
-			return false, errs;
+			return false, errs
 		end
 	end
 
@@ -311,14 +307,14 @@
 		end
 		if not match then
 			table.insert(errs, mkerr(sloc.."/anyOf", iloc, "did not match any subschema"))
-			return false, errs;
+			return false, errs
 		end
 	end
 
 	if schema["not"] ~= nil then
 		if validate(schema["not"], data, root, sloc.."/not", iloc, errs) then
 			table.insert(errs, mkerr(sloc.."/not", iloc, "did match subschema"))
-			return false, errs;
+			return false, errs
 		end
 	end
 
@@ -327,14 +323,14 @@
 			if schema["then"] ~= nil then
 				if not validate(schema["then"], data, root, sloc.."/then", iloc, errs) then
 					table.insert(errs, mkerr(sloc.."/then", iloc, "did not match subschema"))
-					return false, errs;
+					return false, errs
 				end
 			end
 		else
 			if schema["else"] ~= nil then
 				if not validate(schema["else"], data, root, sloc.."/else", iloc, errs) then
 					table.insert(errs, mkerr(sloc.."/else", iloc, "did not match subschema"))
-					return false, errs;
+					return false, errs
 				end
 			end
 		end
@@ -342,7 +338,7 @@
 
 	if schema.const ~= nil and schema.const ~= data then
 		table.insert(errs, mkerr(sloc.."/const", iloc, "did not match constant value"))
-		return false, errs;
+		return false, errs
 	end
 
 	if data is table then
@@ -352,19 +348,19 @@
 
 		if schema.maxItems and #(data as {any}) > schema.maxItems then
 			table.insert(errs, mkerr(sloc.."/maxItems", iloc, "too many items"))
-			return false, errs;
+			return false, errs
 		end
 
 		if schema.minItems and #(data as {any}) < schema.minItems then
 			table.insert(errs, mkerr(sloc.."/minItems", iloc, "too few items"))
-			return false, errs;
+			return false, errs
 		end
 
 		if schema.required then
 			for _, k in ipairs(schema.required) do
 				if data[k] == nil then
 					table.insert(errs, mkerr(sloc.."/required", iloc.."/"..tostring(k), "missing required property"))
-					return false, errs;
+					return false, errs
 				end
 			end
 		end
@@ -375,7 +371,7 @@
 					for _, req in ipairs(reqs) do
 						if data[req] == nil then
 							table.insert(errs, mkerr(sloc.."/dependentRequired", iloc, "missing dependent required property"))
-							return false, errs;
+							return false, errs
 						end
 					end
 				end
@@ -387,7 +383,7 @@
 			for k in pairs(data) do
 				if not validate(schema.propertyNames, k, root, sloc.."/propertyNames", iloc.."/"..tostring(k), errs) then
 					table.insert(errs, mkerr(sloc.."/propertyNames", iloc.."/"..tostring(k), "a property name did not match subschema"))
-					return false, errs;
+					return false, errs
 				end
 			end
 		end
@@ -401,7 +397,7 @@
 			for k, sub in pairs(schema.properties) do
 				if data[k] ~= nil and not validate(sub, data[k], root, sloc.."/"..tostring(k), iloc.."/"..tostring(k), errs) then
 					table.insert(errs, mkerr(sloc.."/"..tostring(k), iloc.."/"..tostring(k), "a property did not match subschema"))
-					return false, errs;
+					return false, errs
 				end
 				seen_properties[k] = true
 			end
@@ -414,7 +410,7 @@
 					if k is string and k:match(pattern) then
 						if not validate(sub, data[k], root, sloc.."/luaPatternProperties", iloc, errs) then
 							table.insert(errs, mkerr(sloc.."/luaPatternProperties/"..pattern, iloc.."/"..tostring(k), "a property did not match subschema"))
-							return false, errs;
+							return false, errs
 						end
 						seen_properties[k] = true
 					end
@@ -427,7 +423,7 @@
 				if not seen_properties[k as string] then
 					if not validate(schema.additionalProperties, v, root, sloc.."/additionalProperties", iloc.."/"..tostring(k), errs) then
 						table.insert(errs, mkerr(sloc.."/additionalProperties", iloc.."/"..tostring(k), "additional property did not match subschema"))
-						return false, errs;
+						return false, errs
 					end
 				end
 			end
@@ -437,7 +433,7 @@
 			for k, sub in pairs(schema.dependentSchemas) do
 				if data[k] ~= nil and not validate(sub, data, root, sloc.."/dependentSchemas/"..k, iloc, errs) then
 					table.insert(errs, mkerr(sloc.."/dependentSchemas", iloc.."/"..tostring(k), "did not match dependent subschema"))
-					return false, errs;
+					return false, errs
 				end
 			end
 		end
@@ -448,7 +444,7 @@
 			for _, v in pairs(data) do
 				if values[v] then
 					table.insert(errs, mkerr(sloc.."/uniqueItems", iloc, "had duplicate items"))
-					return false, errs;
+					return false, errs
 				end
 				values[v] = true
 			end
@@ -463,7 +459,7 @@
 					p = i
 				else
 					table.insert(errs, mkerr(sloc.."/prefixItems/"..i, iloc.."/"..tostring(i), "did not match subschema"))
-					return false, errs;
+					return false, errs
 				end
 			end
 		end
@@ -472,7 +468,7 @@
 			for i = p+1, #(data as {any}) do
 				if not validate(schema.items, data[i], root, sloc, iloc.."/"..i, errs) then
 					table.insert(errs, mkerr(sloc.."/prefixItems/"..i, iloc.."/"..i, "did not match subschema"))
-					return false, errs;
+					return false, errs
 				end
 			end
 		end
@@ -488,18 +484,18 @@
 			end
 			if found < (schema.minContains or 1) then
 				table.insert(errs, mkerr(sloc.."/minContains", iloc, "too few matches"))
-				return false, errs;
+				return false, errs
 			elseif found > (schema.maxContains or math.huge) then
 				table.insert(errs, mkerr(sloc.."/maxContains", iloc, "too many matches"))
-				return false, errs;
+				return false, errs
 			end
 		end
 	end
 
-	return true;
+	return true
 end
 
 
 json_schema_object.validate = validate;
 
-return json_schema_object;
+return json_schema_object
--- a/teal-src/prosody/util/pposix.d.tl	Sun Aug 03 15:14:20 2025 +0200
+++ b/teal-src/prosody/util/pposix.d.tl	Sun Aug 03 15:21:56 2025 +0200
@@ -86,6 +86,14 @@
 
 	mkdir : function (dir : string) : boolean, string
 
+	enum pipe_flag_names
+		"cloexec"
+		"direct"
+		"nonblock"
+	end
+	pipe : function (... : pipe_flag_names) : integer, integer
+	fdopen : function (integer, string) : FILE, string
+
 	setrlimit : function (resource : ulimit_resource, soft : ulimit_limit, hard : ulimit_limit) : boolean, string
 	getrlimit : function (resource : ulimit_resource) : boolean, ulimit_limit, ulimit_limit
 	getrlimit : function (resource : ulimit_resource) : boolean, string
@@ -103,7 +111,7 @@
 
 	ENOENT : integer
 	_NAME : string
-	_VESRION : string
+	_VERSION : string
 end
 
 return pposix
--- a/teal-src/prosody/util/queue.d.tl	Sun Aug 03 15:14:20 2025 +0200
+++ b/teal-src/prosody/util/queue.d.tl	Sun Aug 03 15:21:56 2025 +0200
@@ -18,4 +18,4 @@
 
 	new : function<T> (size:integer, allow_wrapping:boolean) : queue<T>
 end
-return lib;
+return lib
--- a/teal-src/prosody/util/set.d.tl	Sun Aug 03 15:14:20 2025 +0200
+++ b/teal-src/prosody/util/set.d.tl	Sun Aug 03 15:21:56 2025 +0200
@@ -1,22 +1,22 @@
 local record lib
 	record Set<T>
-		add : function<T> (Set<T>, T)
-		contains : function<T> (Set<T>, T) : boolean
-		contains_set : function<T> (Set<T>, Set<T>) : boolean
-		items :  function<T> (Set<T>) : function<T> (Set<T>, T) : T
-		remove : function<T> (Set<T>, T)
-		add_list : function<T> (Set<T>, { T })
-		include : function<T> (Set<T>, Set<T>)
-		exclude : function<T> (Set<T>, Set<T>)
-		empty : function<T> (Set<T>) : boolean
+		add : function (Set, T)
+		contains : function (Set, T) : boolean
+		contains_set : function (Set, Set) : boolean
+		items :  function (Set) : function (Set, T) : T
+		remove : function (Set, T)
+		add_list : function (Set, { T })
+		include : function (Set, Set)
+		exclude : function (Set, Set)
+		empty : function (Set) : boolean
 	end
 
 	new : function<T> ({ T }) : Set<T>
 	is_set : function (any) : boolean
-	union : function<T> (Set<T>, Set<T>) : Set <T>
-	difference : function<T> (Set<T>, Set<T>) : Set <T>
-	intersection : function<T> (Set<T>, Set<T>) : Set <T>
-	xor : function<T> (Set<T>, Set<T>) : Set <T>
+	union : function (Set, Set) : Set
+	difference : function (Set, Set) : Set
+	intersection : function (Set, Set) : Set
+	xor : function (Set, Set) : Set
 end
 
 return lib
--- a/teal-src/prosody/util/signal.d.tl	Sun Aug 03 15:14:20 2025 +0200
+++ b/teal-src/prosody/util/signal.d.tl	Sun Aug 03 15:21:56 2025 +0200
@@ -36,6 +36,7 @@
 	signal : function (integer | Signal, function, boolean) : boolean
 	raise : function (integer | Signal)
 	kill : function (integer, integer | Signal)
+	signalfd : function (integer) : FILE
 	-- enum : integer
 end
 return lib
--- a/teal-src/prosody/util/smqueue.tl	Sun Aug 03 15:14:20 2025 +0200
+++ b/teal-src/prosody/util/smqueue.tl	Sun Aug 03 15:21:56 2025 +0200
@@ -12,12 +12,11 @@
 			"head"
 			"pop"
 		end
-		push : function (smqueue, T)
-		ack : function (smqueue, integer) : { T }, ack_errors
+		push : function (smqueue<T>, T)
+		ack : function (smqueue<T>, integer) : { T }, ack_errors
 		resumable : function (smqueue<T>) : boolean
 		resume : function (smqueue<T>)  : queue.queue.iterator, any, integer
-		type consume_iter = function (smqueue<T>) : T
-		consume : function (smqueue<T>) : consume_iter
+		consume : function (smqueue<T>) : function() : T
 
 		table : function (smqueue<T>) : { T }
 	end
@@ -26,17 +25,17 @@
 
 local type smqueue = lib.smqueue;
 
-function smqueue:push(v)
+function smqueue:push(v : T)
 	self._head = self._head + 1;
 	-- Wraps instead of errors
 	assert(self._queue:push(v));
 end
 
-function smqueue:ack(h : integer) : { any }, smqueue.ack_errors
+function smqueue:ack(h : integer) : { T }, smqueue.ack_errors
 	if h < self._tail then
-		return nil, "tail";
+		return nil, "tail"
 	elseif h > self._head then
-		return nil, "head";
+		return nil, "head"
 	end
 	-- TODO optimize? cache table fields
 	local acked = {};
@@ -44,39 +43,41 @@
 	local expect = self._head - self._tail;
 	while expect < self._queue:count() do
 		local v = self._queue:pop();
-		if not v then return nil, "pop"; end
+		if not v then
+			return nil, "pop"
+		end
 		table.insert(acked, v);
 	end
-	return acked;
+	return acked
 end
 
 function smqueue:count_unacked() : integer
-	return self._head - self._tail;
+	return self._head - self._tail
 end
 
 function smqueue:count_acked() : integer
-	return self._tail;
+	return self._tail
 end
 
 function smqueue:resumable() : boolean
-	return self._queue:count() >= (self._head - self._tail);
+	return self._queue:count() >= (self._head - self._tail)
 end
 
 function smqueue:resume() : queue.queue.iterator, any, integer
-	return self._queue:items();
+	return self._queue:items()
 end
 
-function smqueue:consume() : queue.queue.consume_iter
-	return self._queue:consume()
+function smqueue:consume() : (function() : T)
+	return self._queue:consume() as (function() : T)
 end
 
 -- Compatibility layer, plain ol' table
-function smqueue:table() : { any }
-	local t : { any } = {};
+function smqueue:table() : { T }
+	local t : { T } = {};
 	for i, v in self:resume() do
 		t[i] = v;
 	end
-	return t;
+	return t
 end
 
 local function freeze(q : smqueue<any>) : { string:integer }
@@ -91,9 +92,9 @@
 	__freeze = freeze;
 }
 
-function lib.new<T>(size : integer) : queue.queue<T>
+function lib.new<T>(size : integer) : smqueue<T>
 	assert(size>0);
-	return setmetatable({ _head = 0; _tail = 0; _queue = queue.new(size, true) }, queue_mt);
+	return setmetatable({ _head = 0; _tail = 0; _queue = queue.new(size, true) }, queue_mt)
 end
 
-return lib;
+return lib
--- a/teal-src/prosody/util/stanza.d.tl	Sun Aug 03 15:14:20 2025 +0200
+++ b/teal-src/prosody/util/stanza.d.tl	Sun Aug 03 15:21:56 2025 +0200
@@ -37,10 +37,9 @@
 		"unexpected-request"
 	end
 
-	record stanza_t
+	record stanza_t is { stanza_t | string }
 		name : string
 		attr : { string : string }
-		{ stanza_t | string }
 		tags : { stanza_t }
 
 		query : function ( stanza_t, string ) : stanza_t
@@ -77,10 +76,9 @@
 		indent : function ( stanza_t, integer, string ) : stanza_t
 	end
 
-	record serialized_stanza_t
+	record serialized_stanza_t is { serialized_stanza_t | string }
 		name : string
 		attr : { string : string }
-		{ serialized_stanza_t | string }
 	end
 
 	record message_attr
--- a/teal-src/prosody/util/xtemplate.tl	Sun Aug 03 15:14:20 2025 +0200
+++ b/teal-src/prosody/util/xtemplate.tl	Sun Aug 03 15:21:56 2025 +0200
@@ -56,7 +56,7 @@
 
 			if func == "each" and tmpl then
 				if not st.is_stanza(value) then
-					return pre_blank..post_blank;
+					return pre_blank..post_blank
 				end
 				if not args then value, args = root, path; end
 				local ns, name = s_match(args, "^(%b{})(.*)$");
@@ -94,15 +94,15 @@
 
 		if value is string then
 			if not is_escaped then value = escape(value); end
-			return pre_blank .. value .. post_blank;
+			return pre_blank .. (value as string) .. post_blank
 		elseif st.is_stanza(value) then
 			value = value:get_text();
 			if value then
-				return pre_blank .. escape(value) .. post_blank;
+				return pre_blank .. escape(value) .. post_blank
 			end
 		end
-		return pre_blank .. post_blank;
-	end));
+		return pre_blank .. post_blank
+	end))
 end
 
-return { render = render };
+return { render = render }
--- a/util/argparse.lua	Sun Aug 03 15:14:20 2025 +0200
+++ b/util/argparse.lua	Sun Aug 03 15:21:56 2025 +0200
@@ -2,7 +2,7 @@
 	local short_params = config and config.short_params or {};
 	local value_params = config and config.value_params or {};
 	local array_params = config and config.array_params or {};
-	local kv_params = config and config.kv_params or {};
+	local bool_params = config and (config.bool_params or config.kv_params) or {}; -- COMPAT: kv_params in 13.0 and earlier
 	local strict = config and config.strict;
 	local stop_on_positional = not config or config.stop_on_positional ~= false;
 
@@ -48,16 +48,13 @@
 					end
 				end
 			else
-				param_k, param_v = param:match("^([^=]+)=(.+)$");
-				if not param_k then
-					if param:match("^no%-") then
-						param_k, param_v = param:sub(4), false;
-					else
-						param_k, param_v = param, true;
-					end
+				if param:match("^no%-") then
+					param_k, param_v = param:sub(4), false;
+				else
+					param_k, param_v = param, true;
 				end
 				param_k = param_k:gsub("%-", "_");
-				if strict and not kv_params[param_k] then
+				if strict and not bool_params[param_k] then
 					return nil, "param-not-found", raw_param;
 				end
 			end
--- a/util/datamapper.lua	Sun Aug 03 15:14:20 2025 +0200
+++ b/util/datamapper.lua	Sun Aug 03 15:21:56 2025 +0200
@@ -1,13 +1,13 @@
--- This file is generated from teal-src/util/datamapper.lua
-
+-- This file is generated from teal-src/prosody/util/datamapper.tl
 if not math.type then
 	require("prosody.util.mathcompat")
 end
 
 local st = require("prosody.util.stanza");
+local json = require("prosody.util.json")
 local pointer = require("prosody.util.jsonpointer");
 
-local schema_t = {}
+local json_schema_object = require("prosody.util.jsonschema")
 
 local function toboolean(s)
 	if s == "true" or s == "1" then
@@ -32,8 +32,6 @@
 	end
 end
 
-local value_goes = {}
-
 local function resolve_schema(schema, root)
 	if type(schema) == "table" then
 		if schema["$ref"] and schema["$ref"]:sub(1, 1) == "#" then
@@ -69,8 +67,6 @@
 
 	if type(propschema) == "table" then
 		proptype = guess_schema_type(propschema);
-	elseif type(propschema) == "string" then
-		error("schema as string is not supported: " .. propschema .. " {" .. current_ns .. "}" .. propname)
 	end
 
 	if proptype == "object" or proptype == "array" then
@@ -103,7 +99,7 @@
 			end
 		end
 		if propschema["const"] then
-			enums = {propschema["const"]}
+			enums = { propschema["const"] }
 		elseif propschema["enum"] then
 			enums = propschema["enum"]
 		end
@@ -137,7 +133,7 @@
 	elseif value_where == "in_attribute" then
 		local attr = name
 		if prefix then
-			attr = prefix .. ":" .. name
+			attr = prefix .. ':' .. name
 		elseif namespace and namespace ~= s.attr.xmlns then
 			attr = namespace .. "\1" .. name
 		end
@@ -243,7 +239,7 @@
 		return v
 	elseif proptype == "number" and type(v) == "number" then
 		return string.format("%g", v)
-	elseif proptype == "integer" and type(v) == "number" then
+	elseif proptype == "integer" and math.type(v) == "integer" then
 		return string.format("%d", v)
 	elseif proptype == "boolean" then
 		return v and "1" or "0"
@@ -252,13 +248,12 @@
 
 local unparse
 
-local function unparse_property(out, v, proptype, propschema, value_where, name, namespace, current_ns, prefix,
-	single_attribute, root)
+local function unparse_property(out, v, proptype, propschema, value_where, name, namespace, current_ns, prefix, single_attribute, root)
 
 	if value_where == "in_attribute" then
 		local attr = name
 		if prefix then
-			attr = prefix .. ":" .. name
+			attr = prefix .. ':' .. name
 		elseif namespace and namespace ~= current_ns then
 			attr = namespace .. "\1" .. name
 		end
@@ -280,7 +275,7 @@
 	else
 		local propattr
 		if namespace ~= current_ns then
-			propattr = {xmlns = namespace}
+			propattr = { xmlns = namespace }
 		end
 		if value_where == "in_tag_name" then
 			if proptype == "string" and type(v) == "string" then
@@ -324,7 +319,7 @@
 
 	end
 
-	local out = ctx or st.stanza(current_name, {xmlns = current_ns})
+	local out = ctx or st.stanza(current_name, { xmlns = current_ns })
 
 	local s_type = guess_schema_type(schema)
 	if s_type == "object" then
@@ -350,4 +345,4 @@
 	end
 end
 
-return {parse = parse; unparse = unparse}
+return { parse = parse; unparse = unparse }
--- a/util/jsonpointer.lua	Sun Aug 03 15:14:20 2025 +0200
+++ b/util/jsonpointer.lua	Sun Aug 03 15:21:56 2025 +0200
@@ -1,5 +1,4 @@
-local m_type = math.type;
-
+-- This file is generated from teal-src/prosody/util/jsonpointer.tl
 local function unescape_token(escaped_token)
 	local unescaped = escaped_token:gsub("~1", "/"):gsub("~0", "~")
 	return unescaped
@@ -17,10 +16,10 @@
 
 		if type(idx) == "string" then
 			new_ref = ref[token]
-		elseif m_type(idx) == "integer" then
+		elseif math.type(idx) == "integer" then
 			local i = tonumber(token)
 			if token == "-" then
-				i = #ref + 1
+				i = #(ref) + 1
 			end
 			new_ref = ref[i + 1]
 		else
--- a/util/jsonschema.lua	Sun Aug 03 15:14:20 2025 +0200
+++ b/util/jsonschema.lua	Sun Aug 03 15:21:56 2025 +0200
@@ -1,14 +1,9 @@
--- This file is generated from teal-src/util/jsonschema.lua
-
+-- This file is generated from teal-src/prosody/util/jsonschema.tl
 if not math.type then
 	require("prosody.util.mathcompat")
 end
 
-local utf8_enc = rawget(_G, "utf8") or require("prosody.util.encodings").utf8;
-local utf8_len = utf8_enc.len or function(s)
-	local _, count = s:gsub("[%z\001-\127\194-\253][\128-\191]*", "");
-	return count
-end;
+local utf8_len = rawget(_G, "utf8") and utf8.len or require("prosody.util.encodings").utf8.length;
 
 local json = require("prosody.util.json")
 local null = json.null;