Changeset

6309:342f88e8d522 draft default tip

Merge update
author Trần H. Trung <xmpp:trần.h.trung@trung.fun>
date Sun, 15 Jun 2025 01:08:46 +0700 (10 days ago)
parents 6279:b92aea4e7ff4 (current diff) 6308:e1c54de06905 (diff)
children
files mod_admin_blocklist/README.md mod_csi_grace_period/README.md mod_http_oauth2/mod_http_oauth2.lua mod_http_upload_external/README.md mod_private_adhoc/mod_private_adhoc.lua mod_tls_policy/mod_tls_policy.lua
diffstat 15 files changed, 243 insertions(+), 209 deletions(-) [+]
line wrap: on
line diff
--- a/mod_admin_blocklist/README.md	Sun Jun 01 21:32:49 2025 +0700
+++ b/mod_admin_blocklist/README.md	Sun Jun 15 01:08:46 2025 +0700
@@ -23,9 +23,8 @@
 
 # Compatibility
 
-  Prosody-Version Status
-  --------------- ------
-  trunk*          Works
-  0.12            Works
-
-*as of 2024-12-21
+  Prosody Version   Status
+  ----------------- ------------------------
+  trunk             Works as of 2025-06-13
+  13.0              Works
+  0.12              Works
--- a/mod_audit/README.md	Sun Jun 01 21:32:49 2025 +0700
+++ b/mod_audit/README.md	Sun Jun 15 01:08:46 2025 +0700
@@ -52,6 +52,7 @@
 
 # Compatibilty
 
-Requires Prosody **trunk** as of 2025-02-11.
-
-Does not work with Prosody 0.12 or earlier.
+  Prosody-Version   Status
+  ----------------- ---------------
+  13.0              Works
+  0.12              Does not work
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_auto156/README.md	Sun Jun 15 01:08:46 2025 +0700
@@ -0,0 +1,7 @@
+This module was meant to help convert the deprecated [XEP-0156] TXT records into JSON format.
+
+```shell
+$ prosodyctl mod_auto156 example.com [another.example ...]
+{"links":[{"href":"https://xmpp.example.com/bosh","rel":"urn:xmpp:alt-connections:xbosh"}]}
+...
+```
--- a/mod_cloud_notify/README.md	Sun Jun 01 21:32:49 2025 +0700
+++ b/mod_cloud_notify/README.md	Sun Jun 15 01:08:46 2025 +0700
@@ -4,106 +4,109 @@
 summary: 'XEP-0357: Cloud push notifications'
 ---
 
-Introduction
-============
+# Introduction
 
-This module enables support for sending "push notifications" to clients that
-need it, typically those running on certain mobile devices.
+This module enables support for sending "push notifications" to clients
+that need it, typically those running on certain mobile devices.
 
-As well as this module, your client must support push notifications (the apps
-that need it generally do, of course) and the app developer's push gateway
-must be reachable from your Prosody server (this happens over a normal XMPP
-server-to-server 's2s' connection).
+As well as this module, your client must support push notifications (the
+apps that need it generally do, of course) and the app developer's push
+gateway must be reachable from your Prosody server (this happens over a
+normal XMPP server-to-server 's2s' connection).
 
-Details
-=======
+# Details
 
 Some platforms, notably Apple's iOS and many versions of Android, impose
-limits that prevent applications from running or accessing the network in the
-background. This makes it difficult or impossible for an XMPP application to
-remain reliably connected to a server to receive messages.
+limits that prevent applications from running or accessing the network
+in the background. This makes it difficult or impossible for an XMPP
+application to remain reliably connected to a server to receive
+messages.
 
-In order for messaging and other apps to receive notifications, the OS vendors
-run proprietary servers that their OS maintains a permanent connection to in
-the background. Then they provide APIs to application developers that allow
-sending notifications to specific devices via those servers.
+In order for messaging and other apps to receive notifications, the OS
+vendors run proprietary servers that their OS maintains a permanent
+connection to in the background. Then they provide APIs to application
+developers that allow sending notifications to specific devices via
+those servers.
 
-When you connect to your server with an app that requires push notifications,
-it will use this module to set up a "push registration". When you receive
-a message but your device is not connected to the server, this module will
-generate a notification and send it to the push gateway operated by your
-application's developers). Their gateway will then connect to your device's
-OS vendor and ask them to forward the notification to your device. When your
-device receives the notification, it will display it or wake up the app so it
-can connect to XMPP and receive any pending messages.
+When you connect to your server with an app that requires push
+notifications, it will use this module to set up a "push registration".
+When you receive a message but your device is not connected to the
+server, this module will generate a notification and send it to the push
+gateway operated by your application's developers). Their gateway will
+then connect to your device's OS vendor and ask them to forward the
+notification to your device. When your device receives the notification,
+it will display it or wake up the app so it can connect to XMPP and
+receive any pending messages.
 
-This protocol is described for developers in [XEP-0357: Push Notifications].
-
-For this module to work reliably, you must have [mod_smacks], [mod_mam] and
-[mod_carbons] also enabled on your server.
+This protocol is described for developers in [XEP-0357: Push
+Notifications].
 
-Some clients, notably Siskin and Snikket iOS need some additional extensions
-that are not currently defined in a standard XEP. To support these clients,
-see [mod_cloud_notify_extensions].
+For this module to work reliably, you must have [mod_smacks],
+[mod_mam] and [mod_carbons] also enabled on your server.
 
-Configuration
-=============
+Some clients, notably Siskin and Snikket iOS need some additional
+extensions that are not currently defined in a standard XEP. To support
+these clients, see [mod_cloud_notify_extensions].
+
+# Configuration
 
-  Option                               Default           Description
-  ------------------------------------ ----------------- -------------------------------------------------------------------------------------------------------------------
-  `push_notification_important_body`   `New Message!`    The body text to use when the stanza is important (see above), no message body is sent if this is empty
-  `push_max_errors`                    `16`              How much persistent push errors are tolerated before notifications for the identifier in question are disabled
-  `push_max_devices`                   `5`               The number of allowed devices per user (the oldest devices are automatically removed if this threshold is reached)
-  `push_max_hibernation_timeout`       `259200` (72h)    Number of seconds to extend the smacks timeout if no push was triggered yet (default: 72 hours)
-  `push_notification_with_body` (\*)   `false`           Whether or not to send the real message body to remote pubsub node. Without end-to-end encryption, enabling this may expose your message contents to your client developers and OS vendor. Not recommended.
-  `push_notification_with_sender` (\*) `false`           Whether or not to send the real message sender to remote pubsub node.  Enabling this may expose your contacts to your client developers and OS vendor. Not recommended.
+  Option                                 Default          Description
+  -------------------------------------- ---------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  `push_notification_important_body`     `New Message!`   The body text to use when the stanza is important (see above), no message body is sent if this is empty
+  `push_max_errors`                      `16`             How much persistent push errors are tolerated before notifications for the identifier in question are disabled
+  `push_max_devices`                     `5`              The number of allowed devices per user (the oldest devices are automatically removed if this threshold is reached)
+  `push_max_hibernation_timeout`         `259200` (72h)   Number of seconds to extend the smacks timeout if no push was triggered yet (default: 72 hours)
+  `push_notification_with_body` (\*)     `false`          Whether or not to send the real message body to remote pubsub node. Without end-to-end encryption, enabling this may expose your message contents to your client developers and OS vendor. Not recommended.
+  `push_notification_with_sender` (\*)   `false`          Whether or not to send the real message sender to remote pubsub node. Enabling this may expose your contacts to your client developers and OS vendor. Not recommended.
 
-(\*) There are privacy implications for enabling these options.
+(\*) There are privacy implications for enabling these options.[^1]
+
+# Internal design notes
 
-Internal design notes
-=====================
-
-App servers are notified about offline messages, messages stored by [mod_mam]
-or messages waiting in the smacks queue.
-The business rules outlined [here](//mail.jabber.org/pipermail/standards/2016-February/030925.html) are all honored[^2].
+App servers are notified about offline messages, messages stored by
+[mod_mam] or messages waiting in the smacks queue. The business rules
+outlined
+[here](//mail.jabber.org/pipermail/standards/2016-February/030925.html)
+are all honored[^2].
 
 To cooperate with [mod_smacks] this module consumes some events:
-`smacks-ack-delayed`, `smacks-hibernation-start` and `smacks-hibernation-end`.
-These events allow this module to send out notifications for messages received
-while the session is hibernated by [mod_smacks] or even when smacks
-acknowledgements for messages are delayed by a certain amount of seconds
-configurable with the [mod_smacks] setting `smacks_max_ack_delay`.
+`smacks-ack-delayed`, `smacks-hibernation-start` and
+`smacks-hibernation-end`. These events allow this module to send out
+notifications for messages received while the session is hibernated by
+[mod_smacks] or even when smacks acknowledgements for messages are
+delayed by a certain amount of seconds configurable with the
+[mod_smacks] setting `smacks_max_ack_delay`.
 
-The `smacks_max_ack_delay` setting allows to send out notifications to clients
-which aren't already in smacks hibernation state (because the read timeout or
-connection close didn't already happen) but also aren't responding to acknowledgement
-request in a timely manner. This setting thus allows conversations to be smoother
-under such circumstances.
+The `smacks_max_ack_delay` setting allows to send out notifications to
+clients which aren't already in smacks hibernation state (because the
+read timeout or connection close didn't already happen) but also aren't
+responding to acknowledgement request in a timely manner. This setting
+thus allows conversations to be smoother under such circumstances.
 
-The new event `cloud-notify-ping` can be used by any module to send out a cloud
-notification to either all registered endpoints for the given user or only the endpoints
-given in the event data.
+The new event `cloud-notify-ping` can be used by any module to send out
+a cloud notification to either all registered endpoints for the given
+user or only the endpoints given in the event data.
 
-The config setting `push_notification_important_body` can be used to specify an alternative
-body text to send to the remote pubsub node if the stanza is encrypted or has a body.
-This way the real contents of the message aren't revealed to the push appserver but it
-can still see that the push is important.
-This is used by Chatsecure on iOS to send out high priority pushes in those cases for example.
+The config setting `push_notification_important_body` can be used to
+specify an alternative body text to send to the remote pubsub node if
+the stanza is encrypted or has a body. This way the real contents of the
+message aren't revealed to the push appserver but it can still see that
+the push is important. This is used by Chatsecure on iOS to send out
+high priority pushes in those cases for example.
 
-Compatibility
-=============
-
-**Note:** This module should be used with Lua 5.2 and higher. Using it with
-Lua 5.1 may cause push notifications to not be sent to some clients.
+# Compatibility
 
------- -----------------------------------------------------------------------------
-  trunk  Works
-  0.12   Works
-  0.11   Works
-  0.10   Works
-  0.9    Support dropped, use last supported version [675726ab06d3](//hg.prosody.im/prosody-modules/raw-file/675726ab06d3/mod_cloud_notify/mod_cloud_notify.lua)
------- -----------------------------------------------------------------------------
+**Note:** This module should be used with Lua 5.2 and higher. Using it
+with Lua 5.1 may cause push notifications to not be sent to some
+clients.
 
+  ------- ----------------------
+  trunk   Works as of 25-06-13
+  13.0    Works
+  0.12    Works
+  ------- ----------------------
 
-[^1]: The service which is expected to forward notifications to something like Google Cloud Messaging or Apple Notification Service
-[^2]: [business_rules.markdown](//hg.prosody.im/prosody-modules/file/tip/mod_cloud_notify/business_rules.markdown)
+[^1]: The service which is expected to forward notifications to
+    something like Google Cloud Messaging or Apple Notification Service
+
+[^2]: [business_rules.md](//hg.prosody.im/prosody-modules/file/tip/mod_cloud_notify/business_rules.md)
--- a/mod_csi_grace_period/README.md	Sun Jun 01 21:32:49 2025 +0700
+++ b/mod_csi_grace_period/README.md	Sun Jun 15 01:08:46 2025 +0700
@@ -16,9 +16,8 @@
 Works with [mod_csi_simple][doc:modules:mod_csi_simple] which is
 included with Prosody.
 
-  ------- --------------
-  trunk*   Works
-  0.12     Works
-  ------- --------------
-
-*as of 2024-10-22
+  ------- ------------------------
+  trunk   Works as of 2025-06-13
+  13      Works
+  0.12    Works
+  ------- ------------------------
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_flags/README.md	Sun Jun 15 01:08:46 2025 +0700
@@ -0,0 +1,4 @@
+View and manage flags on user accounts via shell/API.
+See [documentation for mod_flags at Prosody site][doc:modules:mod_flags]
+
+This module is included with Prosody 13.0.x and is provided here for users of Prosody 0.12.x
--- a/mod_http_admin_api/mod_http_admin_api.lua	Sun Jun 01 21:32:49 2025 +0700
+++ b/mod_http_admin_api/mod_http_admin_api.lua	Sun Jun 15 01:08:46 2025 +0700
@@ -15,7 +15,7 @@
 local tokens = module:depends("tokenauth");
 local mod_pep = module:depends("pep");
 local mod_groups = module:depends("groups_internal");
-local mod_lastlog2 = module:depends("lastlog2");
+local mod_account_activity = module:depends("account_activity");
 
 local push_errors = module:shared("cloud_notify/push_errors");
 
@@ -30,8 +30,6 @@
 local xmlns_pubsub = "http://jabber.org/protocol/pubsub";
 local xmlns_nick = "http://jabber.org/protocol/nick";
 
-assert(mod_lastlog2.get_last_active, "Newer version of mod_lastlog2 is required to use this module");
-
 local deleted_users = module:open_store("accounts_cleanup");
 
 local function check_credentials(request)
@@ -242,7 +240,7 @@
 		secondary_roles = secondary_roles;
 		roles = legacy_roles; -- COMPAT w/0.12
 		enabled = enabled;
-		last_active = mod_lastlog2.get_last_active(username);
+		last_active = mod_account_activity.get_last_active(username);
 		deletion_request = not enabled and deleted_users:get(username) or nil;
 		avatar_info = get_user_avatar_info(username);
 	};
--- a/mod_http_oauth2/mod_http_oauth2.lua	Sun Jun 01 21:32:49 2025 +0700
+++ b/mod_http_oauth2/mod_http_oauth2.lua	Sun Jun 15 01:08:46 2025 +0700
@@ -410,7 +410,10 @@
 	local request_username
 
 	if expect_username_jid then
-		local request_jid = assert(params.username, oauth_error("invalid_request", "missing 'username' (JID)"));
+		local request_jid = params.username;
+		if not request_jid then
+			return oauth_error("invalid_request", "missing 'username' (JID)");
+		end
 		local _request_username, request_host = jid.prepped_split(request_jid);
 
 		if not (_request_username and request_host) or request_host ~= module.host then
@@ -419,15 +422,36 @@
 
 		request_username = _request_username
 	else
-		request_username = assert(params.username, oauth_error("invalid_request", "missing 'username'"));
+		request_username = params.username;
+		if not request_username then
+			return oauth_error("invalid_request", "missing 'username'");
+		end
+	end
+
+	local request_password = params.password;
+	if not request_password then
+		return oauth_error("invalid_request", "missing 'password'");
 	end
 
-	local request_password = assert(params.password, oauth_error("invalid_request", "missing 'password'"));
+	local auth_event = {
+		session = {
+			type = "oauth2";
+			ip = "::";
+			username = request_username;
+			host = module.host;
+			log = module._log;
+			sasl_handler = { username = request_username; selected = "x-oauth2-password" };
+			client_id = client.client_name;
+		};
+	};
 
 	if not usermanager.test_password(request_username, module.host, request_password) then
+		module:fire_event("authentication-failure", auth_event);
 		return oauth_error("invalid_grant", "incorrect credentials");
 	end
 
+	module:fire_event("authentication-success", auth_event);
+
 	local granted_jid = jid.join(request_username, module.host);
 	local granted_scopes, granted_role = filter_scopes(request_username, params.scope);
 	return json.encode(new_access_token(granted_jid, granted_role, granted_scopes, client));
@@ -552,21 +576,9 @@
 	return json.encode(new_access_token(code.granted_jid, code.granted_role, code.granted_scopes, client, code.id_token));
 end
 
-function grant_type_handlers.refresh_token(params)
-	if not params.client_id then return oauth_error("invalid_request", "missing 'client_id'"); end
-	if not params.client_secret then return oauth_error("invalid_request", "missing 'client_secret'"); end
+function grant_type_handlers.refresh_token(params, client)
 	if not params.refresh_token then return oauth_error("invalid_request", "missing 'refresh_token'"); end
 
-	local client = check_client(params.client_id);
-	if not client then
-		return oauth_error("invalid_client", "incorrect credentials");
-	end
-
-	if not verify_client_secret(params.client_id, params.client_secret) then
-		module:log("debug", "client_secret mismatch");
-		return oauth_error("invalid_client", "incorrect credentials");
-	end
-
 	local refresh_token_info = tokens.get_token_info(params.refresh_token);
 	if not refresh_token_info or refresh_token_info.purpose ~= "oauth2-refresh" then
 		return oauth_error("invalid_grant", "invalid refresh token");
@@ -598,21 +610,9 @@
 	return json.encode(new_access_token(refresh_token_info.jid, role, new_scopes, client, nil, refresh_token_info));
 end
 
-grant_type_handlers[device_uri] = function(params)
-	if not params.client_id then return oauth_error("invalid_request", "missing 'client_id'"); end
-	if not params.client_secret then return oauth_error("invalid_request", "missing 'client_secret'"); end
+grant_type_handlers[device_uri] = function(params, client)
 	if not params.device_code then return oauth_error("invalid_request", "missing 'device_code'"); end
 
-	local client = check_client(params.client_id);
-	if not client then
-		return oauth_error("invalid_client", "incorrect credentials");
-	end
-
-	if not verify_client_secret(params.client_id, params.client_secret) then
-		module:log("debug", "client_secret mismatch");
-		return oauth_error("invalid_client", "incorrect credentials");
-	end
-
 	local code = codes:get("device_code:" .. params.client_id .. "#" .. params.device_code);
 	if type(code) ~= "table" or code_expired(code) then
 		return oauth_error("expired_token");
@@ -747,8 +747,14 @@
 	local component_secret = assert(module:get_option_string("component_secret"), "'component_secret' is a required setting when loaded on a Component");
 
 	function grant_type_handlers.password(params)
-		local request_jid = assert(params.username, oauth_error("invalid_request", "missing 'username' (JID)"));
-		local request_password = assert(params.password, oauth_error("invalid_request", "missing 'password'"));
+		local request_jid = params.username;
+		if not request_jid then
+			return oauth_error("invalid_request", "missing 'username' (JID)");
+		end
+		local request_password = params.password
+		if not request_password then
+			return oauth_error("invalid_request", "missing 'password'");
+		end
 		local request_username, request_host, request_resource = jid.prepped_split(request_jid);
 		if params.scope then
 			-- TODO shouldn't we support scopes / roles here?
@@ -781,15 +787,28 @@
 -- the redirect_uri is missing or invalid. In those cases, we render an
 -- error directly to the user-agent.
 local function error_response(request, redirect_uri, err)
-	if not redirect_uri or redirect_uri == oob_uri or redirect_uri == device_uri then
+	if not redirect_uri or redirect_uri == oob_uri then
 		return render_error(err);
 	end
-	local q = strict_formdecode(request.url.query);
+	local params = strict_formdecode(request.url.query);
+	if redirect_uri == device_uri then
+		local is_device, device_state = verify_device_token(params.state);
+		if is_device then
+			local device_code = b64url(hashes.hmac_sha256(verification_key, device_state.user_code));
+			local code = codes:get("device_code:" .. params.client_id .. "#" .. device_code);
+			if type(code) == "table" then
+				code.error = err;
+				code.expires = os.time() + 60;
+				codes:set("device_code:" .. params.client_id .. "#" .. device_code, code);
+			end
+		end
+		return render_error(err);
+	end
 	local redirect_query = url.parse(redirect_uri);
 	local sep = redirect_query.query and "&" or "?";
 	redirect_uri = redirect_uri
 		.. sep .. http.formencode(err.extra.oauth2_response)
-		.. "&" .. http.formencode({ state = q.state, iss = get_issuer() });
+		.. "&" .. http.formencode({ state = params.state, iss = get_issuer() });
 	module:log("debug", "Sending error response to client via redirect to %s", redirect_uri);
 	return {
 		status_code = 303;
@@ -842,6 +861,15 @@
 	end
 end
 
+local function array_contains(haystack, needle)
+	for _, item in ipairs(haystack) do
+		if item == needle then
+			return true
+		end
+	end
+	return false
+end
+
 function handle_token_grant(event)
 	local credentials = get_request_credentials(event.request);
 
@@ -874,10 +902,15 @@
 
 
 	local grant_type = params.grant_type
+	if not array_contains(client.grant_types or { "authorization_code" }, grant_type) then
+		return oauth_error("invalid_request", "'grant_type' not registered");
+	end
+
 	local grant_handler = grant_type_handlers[grant_type];
 	if not grant_handler then
-		return oauth_error("invalid_request", "No such grant type.");
+		return oauth_error("invalid_request", "'grant_type' not available");
 	end
+
 	return grant_handler(params, client);
 end
 
@@ -909,10 +942,16 @@
 	end
 	-- From this point we know that redirect_uri is safe to use
 
-	local client_response_types = set.new(array(client.response_types or { "code" }));
-	client_response_types = set.intersection(client_response_types, allowed_response_type_handlers);
-	if not client_response_types:contains(params.response_type) then
-		return error_response(request, redirect_uri, oauth_error("invalid_client", "'response_type' not allowed"));
+	local response_type = params.response_type;
+	if not array_contains(client.response_types or { "code" }, response_type) then
+		return error_response(request, redirect_uri, oauth_error("invalid_client", "'response_type' not registered"));
+	end
+	if not allowed_response_type_handlers:contains(response_type) then
+		return error_response(request, redirect_uri, oauth_error("unsupported_response_type", "'response_type' not allowed"));
+	end
+	local response_handler = response_type_handlers[response_type];
+	if not response_handler then
+		return error_response(request, redirect_uri, oauth_error("unsupported_response_type"));
 	end
 
 	local requested_scopes = parse_scopes(params.scope or "");
@@ -976,16 +1015,6 @@
 		end
 	elseif not auth_state.consent then
 		-- Notify client of rejection
-		if redirect_uri == device_uri then
-			local is_device, device_state = verify_device_token(params.state);
-			if is_device then
-				local device_code = b64url(hashes.hmac_sha256(verification_key, device_state.user_code));
-				local code = codes:get("device_code:" .. params.client_id .. "#" .. device_code);
-				code.error = oauth_error("access_denied");
-				code.expires = os.time() + 60;
-				codes:set("device_code:" .. params.client_id .. "#" .. device_code, code);
-			end
-		end
 		return error_response(request, redirect_uri, oauth_error("access_denied"));
 	end
 	-- else auth_state.consent == true
@@ -1009,13 +1038,8 @@
 		aud = params.client_id;
 		auth_time = auth_state.user.iat;
 		nonce = params.nonce;
-		amr = auth_state.user.amr;
+		amr = auth_state.user.amr; -- RFC 8176: Authentication Method Reference Values
 	});
-	local response_type = params.response_type;
-	local response_handler = response_type_handlers[response_type];
-	if not response_handler then
-		return error_response(request, redirect_uri, oauth_error("unsupported_response_type"));
-	end
 	local ret = response_handler(client, params, user_jid, id_token);
 	if errors.is_err(ret) then
 		return error_response(request, redirect_uri, ret);
@@ -1307,7 +1331,6 @@
 		response_types = {
 			title = "Response Types";
 			type = "array";
-			minItems = 1;
 			uniqueItems = true;
 			items = { type = "string"; enum = { "code"; "token" } };
 			default = { "code" };
@@ -1471,18 +1494,18 @@
 	local grant_types = set.new(client_metadata.grant_types);
 	local response_types = set.new(client_metadata.response_types);
 
+	if not (grant_types - allowed_grant_type_handlers):empty() then
+		return nil, oauth_error("invalid_client_metadata", "Disallowed 'grant_types' specified");
+	elseif not (response_types - allowed_response_type_handlers):empty() then
+		return nil, oauth_error("invalid_client_metadata", "Disallowed 'response_types' specified");
+	end
+
 	if grant_types:contains("authorization_code") and not response_types:contains("code") then
 		return nil, oauth_error("invalid_client_metadata", "Inconsistency between 'grant_types' and 'response_types'");
 	elseif grant_types:contains("implicit") and not response_types:contains("token") then
 		return nil, oauth_error("invalid_client_metadata", "Inconsistency between 'grant_types' and 'response_types'");
 	end
 
-	if set.intersection(grant_types, allowed_grant_type_handlers):empty() then
-		return nil, oauth_error("invalid_client_metadata", "No allowed 'grant_types' specified");
-	elseif set.intersection(response_types, allowed_response_type_handlers):empty() then
-		return nil, oauth_error("invalid_client_metadata", "No allowed 'response_types' specified");
-	end
-
 	if client_metadata.token_endpoint_auth_method ~= "none" then
 		-- Ensure that each client_id JWT with a client_secret is unique.
 		-- A short ID along with the issued at timestamp should be sufficient to
@@ -1669,28 +1692,35 @@
 		issuer = get_issuer();
 		authorization_endpoint = handle_authorization_request and module:http_url() .. "/authorize" or nil;
 		token_endpoint = handle_token_grant and module:http_url() .. "/token" or nil;
+		jwks_uri = nil; -- REQUIRED in OpenID Discovery but not in OAuth 2.0 Metadata
 		registration_endpoint = handle_register_request and module:http_url() .. "/register" or nil;
 		scopes_supported = usermanager.get_all_roles
 			and array(it.keys(usermanager.get_all_roles(module.host))):push("xmpp"):append(array(openid_claims:items()));
 		response_types_supported = array(it.keys(response_type_handlers));
-		token_endpoint_auth_methods_supported = array({ "client_secret_post"; "client_secret_basic" });
+		response_modes_supported = array(it.keys(response_type_handlers)):map(tmap { token = "fragment"; code = "query" });
+		grant_types_supported = array(it.keys(grant_type_handlers));
+		token_endpoint_auth_methods_supported = array({ "client_secret_basic"; "client_secret_post"; "none" });
+		token_endpoint_auth_signing_alg_values_supported = nil;
+		service_documentation = module:get_option_string("oauth2_service_documentation", "https://modules.prosody.im/mod_http_oauth2.html");
+		ui_locales_supported = allowed_locales[1] and allowed_locales;
 		op_policy_uri = module:get_option_string("oauth2_policy_url", nil);
 		op_tos_uri = module:get_option_string("oauth2_terms_url", nil);
 		revocation_endpoint = handle_revocation_request and module:http_url() .. "/revoke" or nil;
-		revocation_endpoint_auth_methods_supported = array({ "client_secret_basic" });
-		device_authorization_endpoint = handle_device_authorization_request and module:http_url() .. "/device";
+		revocation_endpoint_auth_methods_supported = array({ "client_secret_basic"; "client_secret_post"; "none" });
+		revocation_endpoint_auth_signing_alg_values_supported = nil;
 		introspection_endpoint = handle_introspection_request and module:http_url() .. "/introspect";
 		introspection_endpoint_auth_methods_supported = nil;
+		introspection_endpoint_auth_signing_alg_values_supported = nil;
 		code_challenge_methods_supported = array(it.keys(verifier_transforms));
-		grant_types_supported = array(it.keys(grant_type_handlers));
-		response_modes_supported = array(it.keys(response_type_handlers)):map(tmap { token = "fragment"; code = "query" });
+
+		-- RFC 8628: OAuth 2.0 Device Authorization Grant
+		device_authorization_endpoint = handle_device_authorization_request and module:http_url() .. "/device";
+
+		-- RFC 9207: OAuth 2.0 Authorization Server Issuer Identification
 		authorization_response_iss_parameter_supported = true;
-		service_documentation = module:get_option_string("oauth2_service_documentation", "https://modules.prosody.im/mod_http_oauth2.html");
-		ui_locales_supported = allowed_locales[1] and allowed_locales;
 
-		-- OpenID
+		-- OpenID Connect Discovery 1.0
 		userinfo_endpoint = handle_userinfo_request and module:http_url() .. "/userinfo" or nil;
-		jwks_uri = nil; -- REQUIRED in OpenID Discovery but not in OAuth 2.0 Metadata
 		id_token_signing_alg_values_supported = { "HS256" }; -- The algorithm RS256 MUST be included, but we use HS256 and client_secret as shared key.
 	}
 	return authorization_server_metadata;
--- a/mod_http_upload_external/README.md	Sun Jun 01 21:32:49 2025 +0700
+++ b/mod_http_upload_external/README.md	Sun Jun 15 01:08:46 2025 +0700
@@ -19,7 +19,6 @@
 * [PHP implementation](https://hg.prosody.im/prosody-modules/raw-file/tip/mod_http_upload_external/share.php)
 * [Python3+Flask implementation](https://github.com/horazont/xmpp-http-upload)
 * [Go implementation, Prosody Filer](https://github.com/ThomasLeister/prosody-filer)
-* [Go implementation, HMAC File Server](https://github.com/PlusOne/hmac-file-server)
 * [Perl implementation for nginx](https://github.com/weiss/ngx_http_upload)
 * [Rust implementation](https://gitlab.com/nyovaya/xmpp-http-upload)
 
@@ -88,8 +87,9 @@
 =============
 
   Prosody-Version   Status
-  ----------------  --------------------
-  trunk             Works as of 24-12-12
+  ----------------- ----------------------
+  trunk             Works as of 25-06-13
+  13.0              Works
   0.12              Works
 
 Implementation
--- a/mod_muc_moderation/README.md	Sun Jun 01 21:32:49 2025 +0700
+++ b/mod_muc_moderation/README.md	Sun Jun 15 01:08:46 2025 +0700
@@ -27,10 +27,11 @@
 
 # Compatibility
 
-  ------- ---------------
-  trunk   Works^[as of 2024-10-22]
+  ------- ------------------------
+  trunk   Works as of 2025-06-13
+  13      Works
   0.12    Works
-  ------- ---------------
+  ------- ------------------------
 
 ## XEP version
 
--- a/mod_pastebin/README.md	Sun Jun 01 21:32:49 2025 +0700
+++ b/mod_pastebin/README.md	Sun Jun 15 01:08:46 2025 +0700
@@ -37,12 +37,14 @@
 Pastes will be available by default at
 `http://<your-prosody>:5280/pastebin/` by default.
 
-In Prosody 0.9 and later this can be changed with [HTTP
-settings](https://prosody.im/doc/http).
+Ports and path can be changed with [HTTP
+settings](https://prosody.im/doc/http), for example like:
 
-In 0.8 and older this can be changed with `pastebin_ports` (see below),
-or you can forward another external URL from your web server to Prosody,
-use `pastebin_url` to set that URL.
+``` {.lua}
+  http_paths = {
+    pastebin = "/$host-paste";
+  }
+```
 
 # Discovery
 
@@ -82,27 +84,16 @@
   pastebin_line_threshold   The maximum number of lines a message may have before it is sent to the pastebin. (default 4 lines)
   pastebin_trigger          A string of characters (e.g. "!paste ") which if detected at the start of a message, always sends the message to the pastebin, regardless of length. (default: not set)
   pastebin_expire_after     Number of hours after which to expire (remove) a paste, defaults to 24. Set to 0 to store pastes permanently on disk.
-  pastebin_ports            List of ports to run the HTTP server on, same format as mod_httpserver's http_ports[^1]
-  pastebin_url              Base URL to display for pastebin links, must end with / and redirect to Prosody's built-in HTTP server[^2]
 
 # Compatibility
 
-  ------ -------
-  trunk  Works
-  0.12   Works
-  0.11   Works
-  0.10   Works
-  0.9    Works
-  0.8    Works
-  ------ -------
+  ------- ----------------------
+  trunk   Works as of 25-06-13
+  13.0    Works
+  0.12    Works
+  ------- ----------------------
 
 # Todo
 
 -   Maximum paste length
 -   Web interface to submit pastes?
-
-[^1]: As of Prosody 0.9, `pastebin_ports` is replaced by `http_ports`,
-    see [Prosody HTTP server documentation](https://prosody.im/doc/http)
-
-[^2]: See also
-    [http_external_url](https://prosody.im/doc/http#external_url)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_s2s_auth_samecert/README.md	Sun Jun 15 01:08:46 2025 +0700
@@ -0,0 +1,2 @@
+This module authenticates server-to-server connections by looking for an already established connection that uses the exact same certificate, reusing
+the earlier validation results and letting Prosody skip performing slower validation methods such as [POSH][mod_s2s_auth_posh] twice.
--- a/mod_s2s_idle_timeout/README.md	Sun Jun 01 21:32:49 2025 +0700
+++ b/mod_s2s_idle_timeout/README.md	Sun Jun 15 01:08:46 2025 +0700
@@ -25,9 +25,7 @@
 =============
 
   Prosody Version   Status
-  ----------------- -----------
-  trunk[^1]         Works
+  ----------------- ------------------------
+  trunk             Works as of 2025-06-13
+  13.0              Works
   0.12              Works
-  ----------------- -----------
-
-[^1]: as of 2024-10-22
--- a/mod_s2s_keepalive/README.md	Sun Jun 01 21:32:49 2025 +0700
+++ b/mod_s2s_keepalive/README.md	Sun Jun 15 01:08:46 2025 +0700
@@ -34,9 +34,7 @@
 =============
 
   Prosody Version   Status
-  ----------------- -----------
-  trunk[^1]         Works
+  ----------------- ------------------------
+  trunk             Works as of 2025-06-13
+  13.0              Works
   0.12              Works
-  ----------------- -----------
-
-[^1]: as of 2024-11-11
--- a/mod_s2soutinjection/README.md	Sun Jun 01 21:32:49 2025 +0700
+++ b/mod_s2soutinjection/README.md	Sun Jun 15 01:08:46 2025 +0700
@@ -28,4 +28,7 @@
 
 # Compatibility
 
-Requires 0.9.x or later. Tested on 0.12.0
+  Prosody version   Status
+  ----------------- ---------------
+  13.0.x            Does not work
+  0.12.x            Works