api_docs: Document /remotes/push/e2ee/register endpoint.

This commit documents the `/remotes/push/e2ee/register` endpoint.

We use auth_email="ZULIP_ORG_ID" and auth_api_key="ZULIP_ORG_KEY"
instead of "BOT_EMAIL_ADDRESS" and "BOT_API_KEY".
This commit is contained in:
Prakhar Pratyush
2025-08-14 18:14:49 +05:30
committed by Tim Abbott
parent 062a736097
commit f52533795b
6 changed files with 143 additions and 6 deletions

View File

@@ -159,6 +159,7 @@
* [Fetch an API key (development only)](/api/dev-fetch-api-key) * [Fetch an API key (development only)](/api/dev-fetch-api-key)
* [Send an E2EE test notification to mobile device(s)](/api/e2ee-test-notify) * [Send an E2EE test notification to mobile device(s)](/api/e2ee-test-notify)
* [Register E2EE push device](/api/register-push-device) * [Register E2EE push device](/api/register-push-device)
* [Register E2EE push device to bouncer](/api/register-remote-push-device)
* [Mobile notifications](/api/mobile-notifications) * [Mobile notifications](/api/mobile-notifications)
* [Send a test notification to mobile device(s)](/api/test-notify) * [Send a test notification to mobile device(s)](/api/test-notify)
* [Add an APNs device token](/api/add-apns-token) * [Add an APNs device token](/api/add-apns-token)

View File

@@ -319,8 +319,9 @@ def generate_curl_example(
) )
if authentication_required: if authentication_required:
auth_email = DEFAULT_AUTH_EMAIL is_zilencer_endpoint = endpoint.startswith("/remotes/")
auth_api_key = DEFAULT_AUTH_API_KEY auth_email = "ZULIP_ORG_ID" if is_zilencer_endpoint else DEFAULT_AUTH_EMAIL
auth_api_key = "ZULIP_ORG_KEY" if is_zilencer_endpoint else DEFAULT_AUTH_API_KEY
lines.append(" -u " + shlex.quote(f"{auth_email}:{auth_api_key}")) lines.append(" -u " + shlex.quote(f"{auth_email}:{auth_api_key}"))
for parameter in parameters: for parameter in parameters:

View File

@@ -26,9 +26,10 @@ from zerver.openapi.openapi import get_endpoint_from_operationid
UNTESTED_GENERATED_CURL_EXAMPLES = { UNTESTED_GENERATED_CURL_EXAMPLES = {
# Would need push notification bouncer set up to test the # Would need push notification bouncer set up to test the
# generated curl example for the following two endpoints. # generated curl example for the following three endpoints.
"e2ee-test-notify", "e2ee-test-notify",
"test-notify", "test-notify",
"register-remote-push-device",
# Having a message for a specific user available to test this endpoint # Having a message for a specific user available to test this endpoint
# is tricky for testing. # is tricky for testing.
"delete-reminder", "delete-reminder",

View File

@@ -12785,6 +12785,133 @@ paths:
} }
description: | description: |
Error when the server is not configured to use push notification service: Error when the server is not configured to use push notification service:
/remotes/push/e2ee/register:
post:
operationId: register-remote-push-device
summary: Register E2EE push device to bouncer
tags: ["mobile"]
description: |
Register a push device to bouncer to receive end-to-end encrypted
mobile push notifications.
Self-hosted servers use this endpoint to asynchronously register
a push device to the bouncer server after receiving a request from
the mobile client to [register E2EE push device](/api/register-push-device).
It is not meant to be used by mobile clients directly.
**Changes**: New in Zulip 11.0 (feature level 406).
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
realm_uuid:
description: |
The UUID of the realm to which the push device
being registered belongs.
type: string
example: "realm-uuid"
push_account_id:
description: |
The `push_account_id` value provided by the mobile client
to [register E2EE push device](/api/register-push-device).
type: integer
example: 2408
encrypted_push_registration:
description: |
The `encrypted_push_registration` value provided by the mobile client
to [register E2EE push device](/api/register-push-device).
type: string
example: "encrypted-push-registration-data"
bouncer_public_key:
description: |
The `bouncer_public_key` value provided by the mobile client
to [register E2EE push device](/api/register-push-device).
type: string
example: "bouncer-public-key"
required:
- realm_uuid
- push_account_id
- encrypted_push_registration
- bouncer_public_key
responses:
"200":
description: Success
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/JsonSuccessBase"
- required:
- device_id
additionalProperties: false
properties:
result: {}
msg: {}
ignored_parameters_unsupported: {}
device_id:
type: integer
description: |
Unique identifier assigned by the bouncer for the registration.
example: 2408
example: {"device_id": 2408, "msg": "", "result": "success"}
"400":
description: Bad request.
content:
application/json:
schema:
oneOf:
- allOf:
- $ref: "#/components/schemas/CodedError"
- example:
{
"code": "INVALID_BOUNCER_PUBLIC_KEY",
"msg": "Invalid bouncer_public_key",
"result": "error",
}
description: |
An example JSON response for when the given `bouncer_public_key` is invalid:
- allOf:
- $ref: "#/components/schemas/CodedError"
- example:
{
"code": "REQUEST_EXPIRED",
"msg": "Request expired",
"result": "error",
}
description: |
An example JSON response for when the given `encrypted_push_registration` is stale:
- allOf:
- $ref: "#/components/schemas/CodedError"
- example:
{
"code": "BAD_REQUEST",
"msg": "Invalid encrypted_push_registration",
"result": "error",
}
description: |
An example JSON response for when either the bouncer fails to decrypt
the given `encrypted_push_registration` or the decrypted data is invalid:
"403":
description: |
Forbidden.
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/CodedError"
- example:
{
"code": "MISSING_REMOTE_REALM",
"msg": "Organization not registered",
"result": "error",
}
description: |
An example JSON response for when no realm is registered for
the authenticated server on the bouncer for the given `realm_uuid`:
/user_topics: /user_topics:
post: post:
operationId: update-user-topic operationId: update-user-topic

View File

@@ -263,7 +263,6 @@ class OpenAPIArgumentsTest(ZulipTestCase):
"/jwt/fetch_api_key", "/jwt/fetch_api_key",
#### Bouncer endpoints #### Bouncer endpoints
# Higher priority to document # Higher priority to document
"/remotes/push/e2ee/register",
"/remotes/push/e2ee/notify", "/remotes/push/e2ee/notify",
# Lower priority to document # Lower priority to document
"/remotes/server/register", "/remotes/server/register",

View File

@@ -43,9 +43,15 @@ class DocumentationArticle:
endpoint_method: str | None endpoint_method: str | None
def add_api_url_context(context: dict[str, Any], request: HttpRequest) -> None: def add_api_url_context(
context: dict[str, Any], request: HttpRequest, is_zilencer_endpoint: bool = False
) -> None:
context.update(zulip_default_context(request)) context.update(zulip_default_context(request))
if is_zilencer_endpoint:
context["api_url"] = settings.ZULIP_SERVICES_URL + "/api"
return
subdomain = get_subdomain(request) subdomain = get_subdomain(request)
if subdomain != Realm.SUBDOMAIN_FOR_ROOT_DOMAIN or not settings.ROOT_DOMAIN_LANDING_PAGE: if subdomain != Realm.SUBDOMAIN_FOR_ROOT_DOMAIN or not settings.ROOT_DOMAIN_LANDING_PAGE:
display_subdomain = subdomain display_subdomain = subdomain
@@ -226,6 +232,7 @@ class MarkdownDirectoryView(ApiURLView):
# The following is a somewhat hacky approach to extract titles from articles. # The following is a somewhat hacky approach to extract titles from articles.
endpoint_name = None endpoint_name = None
endpoint_method = None endpoint_method = None
is_zilencer_endpoint = False
if os.path.exists(article_absolute_path): if os.path.exists(article_absolute_path):
with open(article_absolute_path) as article_file: with open(article_absolute_path) as article_file:
first_line = article_file.readlines()[0] first_line = article_file.readlines()[0]
@@ -237,6 +244,7 @@ class MarkdownDirectoryView(ApiURLView):
assert endpoint_name is not None assert endpoint_name is not None
assert endpoint_method is not None assert endpoint_method is not None
article_title = get_openapi_summary(endpoint_name, endpoint_method) article_title = get_openapi_summary(endpoint_name, endpoint_method)
is_zilencer_endpoint = endpoint_name.startswith("/remotes/")
elif self.api_doc_view and "{generate_api_header(" in first_line: elif self.api_doc_view and "{generate_api_header(" in first_line:
api_operation = context["PAGE_METADATA_URL"].split("/api/")[1] api_operation = context["PAGE_METADATA_URL"].split("/api/")[1]
endpoint_name, endpoint_method = get_endpoint_from_operationid(api_operation) endpoint_name, endpoint_method = get_endpoint_from_operationid(api_operation)
@@ -268,7 +276,7 @@ class MarkdownDirectoryView(ApiURLView):
# An "article" might require the api_url_context to be rendered # An "article" might require the api_url_context to be rendered
api_url_context: dict[str, Any] = {} api_url_context: dict[str, Any] = {}
add_api_url_context(api_url_context, self.request) add_api_url_context(api_url_context, self.request, is_zilencer_endpoint)
api_url_context["run_content_validators"] = True api_url_context["run_content_validators"] = True
context["api_url_context"] = api_url_context context["api_url_context"] = api_url_context
if endpoint_name and endpoint_method: if endpoint_name and endpoint_method: