@openchambery/relay-server 1.18.7-beta.1 → 1.18.7-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DOCUMENTATION.md +123 -6
- package/README.md +156 -4
- package/bin/openchamber-push-relay.js +11 -0
- package/package.json +11 -5
- package/src/cli.js +20 -1
- package/src/push/apns.js +138 -0
- package/src/push/cli.js +73 -0
- package/src/push/combined.js +34 -0
- package/src/push/config.js +145 -0
- package/src/push/crypto.js +34 -0
- package/src/push/guard.js +145 -0
- package/src/push/handler.js +292 -0
- package/src/push/index.d.ts +111 -0
- package/src/push/index.js +3 -0
- package/src/push/schema.js +185 -0
- package/src/push/server.js +103 -0
- package/src/push/store.js +54 -0
package/DOCUMENTATION.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Relay Server Package Documentation
|
|
2
2
|
|
|
3
|
-
`packages/relay-server/` owns the self-hosted Layer 1 Relay server, the `openchamber-relay`
|
|
3
|
+
`packages/relay-server/` owns the self-hosted Layer 1 Relay server, the isolated Push Relay process, the `openchamber-relay` and `openchamber-push-relay` CLIs, and the package release and deployment contract.
|
|
4
4
|
|
|
5
5
|
## Purpose and security boundary
|
|
6
6
|
|
|
@@ -12,6 +12,8 @@ Relay v1 admission accepts anonymous Client route requests. Per-IP, global, pend
|
|
|
12
12
|
|
|
13
13
|
The Relay keeps process-local routing state only. Hosts reconnect after Relay restarts, and a control disconnect retains its Host route for the 30-second grace period.
|
|
14
14
|
|
|
15
|
+
Layer 1 and Push are separate processes in this package. Layer 1 never holds APNs credentials or the device-token database. Push never sees Relay tunnel frames, pairing secrets, or client bearer credentials. Give Apple secrets only to the Push process. SQLite token storage is single-instance: one Push process per database file.
|
|
16
|
+
|
|
15
17
|
## Quick deployment
|
|
16
18
|
|
|
17
19
|
Install the package, then start the Relay:
|
|
@@ -27,6 +29,50 @@ The default listener is `127.0.0.1:8787` and the WebSocket path is `/ws`. Deploy
|
|
|
27
29
|
openchamber-relay --public-url wss://relay.example.com/ws
|
|
28
30
|
```
|
|
29
31
|
|
|
32
|
+
Push is a second executable from the same package. Default listen address is `127.0.0.1:8788`:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
export OPENCHAMBER_PUSH_RELAY_APNS_KEY_ID='<apns-key-id>'
|
|
36
|
+
export OPENCHAMBER_PUSH_RELAY_APNS_TEAM_ID='<apns-team-id>'
|
|
37
|
+
export OPENCHAMBER_PUSH_RELAY_APNS_BUNDLE_ID=com.yee94.openchamber
|
|
38
|
+
export OPENCHAMBER_PUSH_RELAY_APNS_P8_PATH=/etc/openchamber/AuthKey.p8
|
|
39
|
+
openchamber-push-relay --host 127.0.0.1 --port 8788
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The Host maps the effective Relay `wss://`/`ws://` URL to the same host as `https://`/`http://` `/v1/push/send`. Set `OPENCHAMBER_PUSH_RELAY_URL` on the Host to override. After a Relay switch, the Host re-registers persisted tokens and binds them before the first send. iOS Live Activity uses the same Push origin: `POST /v1/push/register-live-activity-token`, `POST /v1/push/unregister-live-activity-token`, and `POST /v1/push/live-activity`. Each Live Activity APNs request authenticates with the Host signing key, uses topic `{bundleId}.push-type.liveactivity`, and carries only `aps.timestamp`, `aps.event`, `aps.content-state`, and optional `dismissal-date` / `stale-date`. Successful `end` deliveries delete the token binding.
|
|
43
|
+
|
|
44
|
+
### Combined mode (single port)
|
|
45
|
+
|
|
46
|
+
When `openchamber-relay` sees any non-empty `OPENCHAMBER_PUSH_RELAY_APNS_*` variable, it mounts Push HTTP on the same listener at `/v1/push/*`. Missing required APNs fields fail startup instead of silently skipping Push. With no such variables, Layer 1 does not load the Push module.
|
|
47
|
+
|
|
48
|
+
Combined mode ignores `OPENCHAMBER_PUSH_RELAY_HOST` and `OPENCHAMBER_PUSH_RELAY_PORT`. Public `/healthz` and `/readyz` stay Layer 1. The standalone `openchamber-push-relay` entry is unchanged.
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
export OPENCHAMBER_PUSH_RELAY_APNS_KEY_ID='<apns-key-id>'
|
|
52
|
+
export OPENCHAMBER_PUSH_RELAY_APNS_TEAM_ID='<apns-team-id>'
|
|
53
|
+
export OPENCHAMBER_PUSH_RELAY_APNS_BUNDLE_ID=com.yee94.openchamber
|
|
54
|
+
export OPENCHAMBER_PUSH_RELAY_APNS_P8_PATH=/etc/openchamber/AuthKey.p8
|
|
55
|
+
export OPENCHAMBER_PUSH_RELAY_DATABASE_PATH=/var/lib/openchamber/push-relay.sqlite
|
|
56
|
+
openchamber-relay --public-url wss://relay.example.com/ws
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Minimal Compose environment:
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
services:
|
|
63
|
+
relay:
|
|
64
|
+
image: openchamber-relay:<version>
|
|
65
|
+
environment:
|
|
66
|
+
OPENCHAMBER_RELAY_SERVER_PUBLIC_URL: wss://relay.example.com/ws
|
|
67
|
+
OPENCHAMBER_PUSH_RELAY_APNS_KEY_ID: ${OPENCHAMBER_PUSH_RELAY_APNS_KEY_ID}
|
|
68
|
+
OPENCHAMBER_PUSH_RELAY_APNS_TEAM_ID: ${OPENCHAMBER_PUSH_RELAY_APNS_TEAM_ID}
|
|
69
|
+
OPENCHAMBER_PUSH_RELAY_APNS_BUNDLE_ID: com.yee94.openchamber
|
|
70
|
+
OPENCHAMBER_PUSH_RELAY_APNS_P8_PATH: /run/secrets/apns_p8
|
|
71
|
+
OPENCHAMBER_PUSH_RELAY_DATABASE_PATH: /data/push-relay.sqlite
|
|
72
|
+
ports:
|
|
73
|
+
- "127.0.0.1:8787:8787"
|
|
74
|
+
```
|
|
75
|
+
|
|
30
76
|
### Caddy
|
|
31
77
|
|
|
32
78
|
```caddyfile
|
|
@@ -39,6 +85,24 @@ relay.example.com {
|
|
|
39
85
|
|
|
40
86
|
Run the Relay with `--public-url wss://relay.example.com/ws`. Caddy proxies WebSocket upgrades for `/ws` and serves `/healthz` and `/readyz` through the same upstream. `header_up X-Forwarded-For {remote_host}` replaces the inbound value with the single client source IP.
|
|
41
87
|
|
|
88
|
+
Shared hostname with Push:
|
|
89
|
+
|
|
90
|
+
```caddyfile
|
|
91
|
+
relay.example.com {
|
|
92
|
+
handle /v1/push/* {
|
|
93
|
+
reverse_proxy 127.0.0.1:8788 {
|
|
94
|
+
header_up X-Forwarded-For {remote_host}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
handle {
|
|
99
|
+
reverse_proxy 127.0.0.1:8787 {
|
|
100
|
+
header_up X-Forwarded-For {remote_host}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
42
106
|
### Nginx
|
|
43
107
|
|
|
44
108
|
```nginx
|
|
@@ -60,11 +124,11 @@ server {
|
|
|
60
124
|
}
|
|
61
125
|
```
|
|
62
126
|
|
|
63
|
-
Run the Relay with `--public-url wss://relay.example.com/ws`. The `/ws` path in the public URL and Relay configuration must match. `proxy_set_header X-Forwarded-For $remote_addr;` replaces the inbound value with the single client source IP.
|
|
127
|
+
Run the Relay with `--public-url wss://relay.example.com/ws`. The `/ws` path in the public URL and Relay configuration must match. `proxy_set_header X-Forwarded-For $remote_addr;` replaces the inbound value with the single client source IP. For Push on the same hostname, proxy `/v1/push/` to `127.0.0.1:8788` and keep `/` including `/ws` on `127.0.0.1:8787`, replacing `X-Forwarded-For` on both locations.
|
|
64
128
|
|
|
65
129
|
## Docker
|
|
66
130
|
|
|
67
|
-
Each non-dry-run OpenChamber release publishes a multi-platform Relay image for `linux/amd64` and `linux/arm64` to Docker Hub. CI builds each architecture natively in parallel (`ubuntu-latest` and `ubuntu-24.04-arm`), then merges digests into a single multi-arch manifest tagged as:
|
|
131
|
+
Each non-dry-run OpenChamber `v*` release and each `relay/v*` Relay-only release publishes a multi-platform Relay image for `linux/amd64` and `linux/arm64` to Docker Hub. The image default entrypoint is Layer 1. The same image includes Node 24 plus `openchamber-push-relay` source/bin/package files. Layer 1 is compiled with Bun 1.3.14; Push is not Bun-compiled and runs under Node 24 (`node:sqlite`). The container user is non-root, ports `8787` and `8788` are exposed, and the image health check uses Node against Layer 1 `/healthz`. CI builds each architecture natively in parallel (`ubuntu-latest` and `ubuntu-24.04-arm`), then merges digests into a single multi-arch manifest tagged as:
|
|
68
132
|
|
|
69
133
|
```text
|
|
70
134
|
<DOCKERHUB_USERNAME>/openchamber-relay:<version>
|
|
@@ -109,6 +173,27 @@ curl -fsS https://relay.example.com/healthz
|
|
|
109
173
|
curl -fsS https://relay.example.com/readyz
|
|
110
174
|
```
|
|
111
175
|
|
|
176
|
+
Layer 1 plus Push from the same immutable image uses [`docker-compose.relay-push.remote.yml`](../../docker-compose.relay-push.remote.yml). Layer 1 receives no APNs secrets. Push receives only APNs Key ID / Team ID / Bundle ID and the `.p8` Docker secret path, plus a persistent SQLite volume, with a read-only root filesystem. Caddy serves one hostname: `/v1/push/*` to `push:8788`, everything else including `/ws` to `relay:8787`, each replacing `X-Forwarded-For` once. Both services have health checks; Caddy waits until both are healthy.
|
|
177
|
+
|
|
178
|
+
```sh
|
|
179
|
+
OPENCHAMBER_RELAY_IMAGE='<dockerhub-username>/openchamber-relay:<version>@sha256:<manifest-digest>' \
|
|
180
|
+
RELAY_DOMAIN=relay.example.com \
|
|
181
|
+
ACME_EMAIL=admin@example.com \
|
|
182
|
+
OPENCHAMBER_PUSH_RELAY_APNS_KEY_ID='<apns-key-id>' \
|
|
183
|
+
OPENCHAMBER_PUSH_RELAY_APNS_TEAM_ID='<apns-team-id>' \
|
|
184
|
+
OPENCHAMBER_PUSH_RELAY_APNS_BUNDLE_ID=com.yee94.openchamber \
|
|
185
|
+
OPENCHAMBER_PUSH_RELAY_APNS_P8_FILE=/etc/openchamber/AuthKey.p8 \
|
|
186
|
+
docker compose -f docker-compose.relay-push.remote.yml up -d
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
```sh
|
|
190
|
+
docker compose -f docker-compose.relay-push.remote.yml ps
|
|
191
|
+
curl -fsS https://relay.example.com/healthz
|
|
192
|
+
curl -fsS https://relay.example.com/readyz
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Public `/healthz` and `/readyz` are Layer 1. Push health remains on the internal `8788` listener and the Compose health check. Existing Layer-1-only Compose files keep their previous behavior.
|
|
196
|
+
|
|
112
197
|
From the repository root, build and start the supplied service. The compatibility assets are [`Dockerfile.relay`](../../Dockerfile.relay) and [`docker-compose.relay.yml`](../../docker-compose.relay.yml):
|
|
113
198
|
|
|
114
199
|
```sh
|
|
@@ -117,7 +202,7 @@ OPENCHAMBER_RELAY_PUBLISHED_PORT=8787 \
|
|
|
117
202
|
docker compose -f docker-compose.relay.yml up -d --build
|
|
118
203
|
```
|
|
119
204
|
|
|
120
|
-
Compose publishes `127.0.0.1:${OPENCHAMBER_RELAY_PUBLISHED_PORT:-8787}` by default; use `OPENCHAMBER_RELAY_PUBLISHED_PORT` to select the host port. The Compose service uses an ephemeral filesystem and keeps Host identity keys on each OpenChamber Host. Its image health check
|
|
205
|
+
Compose publishes `127.0.0.1:${OPENCHAMBER_RELAY_PUBLISHED_PORT:-8787}` by default; use `OPENCHAMBER_RELAY_PUBLISHED_PORT` to select the host port. The Compose service uses an ephemeral filesystem and keeps Host identity keys on each OpenChamber Host. Its image health check uses Node to call Layer 1 `GET /healthz`. Terminate public TLS at an external reverse proxy and publish `wss://relay.example.com/ws`. A public Relay port binding requires firewall rules and TLS; loopback publishing with a TLS reverse proxy is the deployment path.
|
|
121
206
|
|
|
122
207
|
## Connect Hosts
|
|
123
208
|
|
|
@@ -134,7 +219,7 @@ Existing clients switch to a new Relay after a new pairing flow; generate a fres
|
|
|
134
219
|
|
|
135
220
|
## Configuration
|
|
136
221
|
|
|
137
|
-
Configuration precedence is command flags, then `OPENCHAMBER_RELAY_SERVER_*` variables, then defaults. `--host`, `--port`, `--path`, `--public-url`, `--trust-proxy`, `--no-trust-proxy`, `--json`, and `--quiet` are available.
|
|
222
|
+
Configuration precedence is command flags, then `OPENCHAMBER_RELAY_SERVER_*` variables, then defaults. `--host`, `--port`, `--path`, `--public-url`, `--trust-proxy`, `--no-trust-proxy`, `--json`, and `--quiet` are available. Push flags are `--host`, `--port`, `--trust-proxy`, `--no-trust-proxy`, `--json`, and `--quiet`, with `OPENCHAMBER_PUSH_RELAY_*` variables.
|
|
138
223
|
|
|
139
224
|
`OPENCHAMBER_RELAY_SERVER_PUBLIC_URL` affects startup output. `OPENCHAMBER_RELAY_SERVER_PATH` selects the actual WebSocket upgrade endpoint. Relay listens on loopback by default. Enable `OPENCHAMBER_RELAY_SERVER_TRUST_PROXY=true` when a trusted reverse proxy fully isolates Relay ingress and replaces any client-supplied `X-Forwarded-For` value with one canonical client IP. Relay accepts one forwarded IP in this mode.
|
|
140
225
|
|
|
@@ -179,12 +264,44 @@ For an IPv6 literal in a public URL, enclose the host in brackets: `wss://[2001:
|
|
|
179
264
|
| `OPENCHAMBER_RELAY_SERVER_MAX_ADMISSION_ENTRIES` | `10000` | tracked role/IP admission records |
|
|
180
265
|
| `OPENCHAMBER_RELAY_SERVER_ID_ATTEMPTS` | `4` | random connection-ID attempts |
|
|
181
266
|
|
|
267
|
+
Host Push URL (OpenChamber Host, not the Push process):
|
|
268
|
+
|
|
269
|
+
| Variable | Default | Unit / purpose |
|
|
270
|
+
| --- | --- | --- |
|
|
271
|
+
| `OPENCHAMBER_PUSH_RELAY_URL` | derived from the effective Relay URL | explicit `…/v1/push/send` override |
|
|
272
|
+
| `OPENCHAMBER_PUSH_RELAY_DISABLED` | unset | `true` disables Push Relay on the Host |
|
|
273
|
+
|
|
274
|
+
Push process:
|
|
275
|
+
|
|
276
|
+
| Variable | Default | Unit / purpose |
|
|
277
|
+
| --- | --- | --- |
|
|
278
|
+
| `OPENCHAMBER_PUSH_RELAY_HOST` | `127.0.0.1` | Listener address |
|
|
279
|
+
| `OPENCHAMBER_PUSH_RELAY_PORT` | `8788` | TCP port |
|
|
280
|
+
| `OPENCHAMBER_PUSH_RELAY_TRUST_PROXY` | `false` | Read one canonical client IP from proxy-replaced `X-Forwarded-For` |
|
|
281
|
+
| `OPENCHAMBER_PUSH_RELAY_DATABASE_PATH` | `./data/push-relay.sqlite` | SQLite file |
|
|
282
|
+
| `OPENCHAMBER_PUSH_RELAY_TIMESTAMP_SKEW_MS` | `300000` | ms signed `ts` window |
|
|
283
|
+
| `OPENCHAMBER_PUSH_RELAY_REPLAY_MS` | `600000` | ms replay-record lifetime; at least twice timestamp skew |
|
|
284
|
+
| `OPENCHAMBER_PUSH_RELAY_MAX_REPLAY_ENTRIES` | `10000` | replay records |
|
|
285
|
+
| `OPENCHAMBER_PUSH_RELAY_REGISTER_LIMIT_PER_MINUTE` | `60` | register requests per client IP per minute |
|
|
286
|
+
| `OPENCHAMBER_PUSH_RELAY_SEND_LIMIT_PER_MINUTE` | `60` | send requests per client IP per minute |
|
|
287
|
+
| `OPENCHAMBER_PUSH_RELAY_SERVER_SEND_LIMIT_PER_MINUTE` | `120` | send requests per `serverId` per minute |
|
|
288
|
+
| `OPENCHAMBER_PUSH_RELAY_MAX_TOKENS` | `100000` | persisted device-token bindings |
|
|
289
|
+
| `OPENCHAMBER_PUSH_RELAY_MAX_IN_FLIGHT` | `64` | concurrent APNs deliveries |
|
|
290
|
+
| `OPENCHAMBER_PUSH_RELAY_APNS_KEY_ID` | required | Apple APNs key ID |
|
|
291
|
+
| `OPENCHAMBER_PUSH_RELAY_APNS_TEAM_ID` | required | Apple Team ID |
|
|
292
|
+
| `OPENCHAMBER_PUSH_RELAY_APNS_BUNDLE_ID` | `com.yee94.openchamber` | App bundle ID |
|
|
293
|
+
| `OPENCHAMBER_PUSH_RELAY_APNS_P8` | required unless path set | APNs `.p8` PEM |
|
|
294
|
+
| `OPENCHAMBER_PUSH_RELAY_APNS_P8_PATH` | unset | Path to the `.p8` file |
|
|
295
|
+
|
|
296
|
+
TestFlight and App Store Hosts use `OPENCHAMBER_APNS_ENVIRONMENT=production`. Each send request carries `env`; the Push process does not pick sandbox vs production itself.
|
|
297
|
+
|
|
182
298
|
## Operations
|
|
183
299
|
|
|
184
300
|
- `GET` and `HEAD` requests to `/healthz` return process health. `/readyz` returns ready status after the listener reaches running state.
|
|
185
301
|
- `SIGTERM` and `SIGINT` begin graceful Relay shutdown. Docker grants a 30-second stop period.
|
|
186
302
|
- Hosts automatically reconnect after a Relay process restart. Relay state remains ephemeral.
|
|
187
|
-
- Keep logs and metrics snapshots free of URL query strings, `sig`, `pk`, `grant`, and
|
|
303
|
+
- Keep logs and metrics snapshots free of URL query strings, `sig`, `pk`, `grant`, encrypted payloads, APNs `.p8` material, and device tokens.
|
|
304
|
+
- Run one Push process per SQLite database file.
|
|
188
305
|
|
|
189
306
|
### systemd
|
|
190
307
|
|
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
`openchamber-relay` is the self-hosted Layer 1 relay for OpenChamber remote access. It gives OpenChamber Hosts an outbound Relay connection and carries encrypted client traffic through that connection.
|
|
4
4
|
|
|
5
|
+
The same `@openchambery/relay-server` package also ships `openchamber-push-relay`, an isolated APNs push process. Layer 1 never receives Apple credentials. Push never terminates E2EE tunnels.
|
|
6
|
+
|
|
5
7
|
The Relay routes opaque Layer 2/3 frames verbatim. E2EE terminates at the OpenChamber Host and Client, so the Relay has routing metadata and transport state while the endpoints hold application plaintext, pairing secrets, and client bearer credentials. Host Relay connections authenticate with the Host's long-lived P-256 signing key. Relay reachability grants transport access; endpoint validation continues to enforce pairing and client credentials.
|
|
6
8
|
|
|
7
9
|
## Architecture and transport
|
|
@@ -19,10 +21,19 @@ Relay state lives in process memory. Hosts reconnect after a Relay restart. A di
|
|
|
19
21
|
|
|
20
22
|
Relay v1 accepts anonymous Client route requests. Admission, connection, frame, queue, and socket limits bound this public entry point.
|
|
21
23
|
|
|
24
|
+
## Dual-process security boundary
|
|
25
|
+
|
|
26
|
+
Layer 1 (`openchamber-relay`) and Push (`openchamber-push-relay`) are separate processes in one package:
|
|
27
|
+
|
|
28
|
+
- Layer 1 authenticates Hosts, brokers WebSocket routes, and forwards opaque frames. It has no APNs key, no device-token database, and no `/v1/push/*` handlers.
|
|
29
|
+
- Push verifies Host signatures, binds `token → serverId` in a local SQLite database, and holds the project APNs `.p8` key. It has no access to Relay tunnels, pairing secrets, or client bearer credentials.
|
|
30
|
+
- Deploy them as two containers from the same immutable image. Give APNs Key ID / Team ID / Bundle ID and the `.p8` secret only to the Push container. Keep Layer 1 free of those secrets.
|
|
31
|
+
- SQLite is a single-writer store. Run one Push instance per database file. Do not share that volume across replicas.
|
|
32
|
+
|
|
22
33
|
## Requirements
|
|
23
34
|
|
|
24
|
-
- `@openchambery/relay-server` installation: Node.js 22 or later and a supported package manager such as npm, pnpm, yarn, or Bun.
|
|
25
|
-
- Single-file bundle build: Bun. This repository uses Bun 1.3.14.
|
|
35
|
+
- `@openchambery/relay-server` installation: Node.js 22.13 or later and a supported package manager such as npm, pnpm, yarn, or Bun. Push uses Node's built-in `node:sqlite`.
|
|
36
|
+
- Single-file Layer 1 bundle build: Bun. This repository uses Bun 1.3.14. Do not Bun-compile the Push entry; Docker runs it with Node 24.
|
|
26
37
|
- Public deployment: a DNS name, TLS certificate, reverse proxy, and firewall policy appropriate for the deployment.
|
|
27
38
|
|
|
28
39
|
## Install and quick start
|
|
@@ -36,6 +47,57 @@ openchamber-relay --public-url wss://relay.example.com/ws
|
|
|
36
47
|
|
|
37
48
|
The default listener is `127.0.0.1:8787` and the default WebSocket upgrade path is `/ws`. Keep this loopback listener behind a TLS reverse proxy and set `--public-url` to the public `ws://` or `wss://` URL with the same path.
|
|
38
49
|
|
|
50
|
+
Start the isolated Push process from the same package. The default listener is `127.0.0.1:8788`. APNs Key ID, Team ID, and a `.p8` value or file path are required:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
export OPENCHAMBER_PUSH_RELAY_APNS_KEY_ID='<apns-key-id>'
|
|
54
|
+
export OPENCHAMBER_PUSH_RELAY_APNS_TEAM_ID='<apns-team-id>'
|
|
55
|
+
export OPENCHAMBER_PUSH_RELAY_APNS_BUNDLE_ID=com.yee94.openchamber
|
|
56
|
+
export OPENCHAMBER_PUSH_RELAY_APNS_P8_PATH=/etc/openchamber/AuthKey.p8
|
|
57
|
+
openchamber-push-relay --host 127.0.0.1 --port 8788
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Keep Push on loopback behind the same TLS reverse proxy. Route only `/v1/push/*` to port 8788.
|
|
61
|
+
|
|
62
|
+
### Combined mode (single port)
|
|
63
|
+
|
|
64
|
+
`openchamber-relay` can mount Push on the same listener. If any `OPENCHAMBER_PUSH_RELAY_APNS_*` variable is non-empty, Layer 1 loads Push and serves `/v1/push/*` on the Relay port. Partial APNs configuration fails startup; it does not silently disable Push. With no APNs variables set, Layer 1 behavior is unchanged and Push is not loaded.
|
|
65
|
+
|
|
66
|
+
In combined mode:
|
|
67
|
+
|
|
68
|
+
- Push routes share the Relay port at `/v1/push/*`.
|
|
69
|
+
- `/healthz` and `/readyz` remain Layer 1 endpoints.
|
|
70
|
+
- `OPENCHAMBER_PUSH_RELAY_HOST` and `OPENCHAMBER_PUSH_RELAY_PORT` are ignored.
|
|
71
|
+
- `openchamber-push-relay` is unchanged and remains the isolated two-process option.
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
export OPENCHAMBER_PUSH_RELAY_APNS_KEY_ID='<apns-key-id>'
|
|
75
|
+
export OPENCHAMBER_PUSH_RELAY_APNS_TEAM_ID='<apns-team-id>'
|
|
76
|
+
export OPENCHAMBER_PUSH_RELAY_APNS_BUNDLE_ID=com.yee94.openchamber
|
|
77
|
+
export OPENCHAMBER_PUSH_RELAY_APNS_P8_PATH=/etc/openchamber/AuthKey.p8
|
|
78
|
+
export OPENCHAMBER_PUSH_RELAY_DATABASE_PATH=/var/lib/openchamber/push-relay.sqlite
|
|
79
|
+
openchamber-relay --public-url wss://relay.example.com/ws
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Minimal Compose environment for combined mode:
|
|
83
|
+
|
|
84
|
+
```yaml
|
|
85
|
+
services:
|
|
86
|
+
relay:
|
|
87
|
+
image: openchamber-relay:<version>
|
|
88
|
+
environment:
|
|
89
|
+
OPENCHAMBER_RELAY_SERVER_PUBLIC_URL: wss://relay.example.com/ws
|
|
90
|
+
OPENCHAMBER_PUSH_RELAY_APNS_KEY_ID: ${OPENCHAMBER_PUSH_RELAY_APNS_KEY_ID}
|
|
91
|
+
OPENCHAMBER_PUSH_RELAY_APNS_TEAM_ID: ${OPENCHAMBER_PUSH_RELAY_APNS_TEAM_ID}
|
|
92
|
+
OPENCHAMBER_PUSH_RELAY_APNS_BUNDLE_ID: com.yee94.openchamber
|
|
93
|
+
OPENCHAMBER_PUSH_RELAY_APNS_P8_PATH: /run/secrets/apns_p8
|
|
94
|
+
OPENCHAMBER_PUSH_RELAY_DATABASE_PATH: /data/push-relay.sqlite
|
|
95
|
+
ports:
|
|
96
|
+
- "127.0.0.1:8787:8787"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
OpenChamber Hosts do not need a separate Push URL when they already have a Relay URL. The effective `wss://` or `ws://` Relay URL maps to the same host as `https://` or `http://` `/v1/push/send` (register is `/v1/push/register-token`). Set `OPENCHAMBER_PUSH_RELAY_URL` on the Host to override that mapping. After a Relay switch, the Host re-registers persisted device tokens and binds them again before the first send.
|
|
100
|
+
|
|
39
101
|
### Build a standalone executable
|
|
40
102
|
|
|
41
103
|
Run these commands from the repository root. `bun build --compile` creates a single executable for the current platform and architecture.
|
|
@@ -91,6 +153,18 @@ Available CLI options:
|
|
|
91
153
|
--version, -v
|
|
92
154
|
```
|
|
93
155
|
|
|
156
|
+
Push CLI options:
|
|
157
|
+
|
|
158
|
+
```text
|
|
159
|
+
--host HOST
|
|
160
|
+
--port PORT
|
|
161
|
+
--trust-proxy | --no-trust-proxy
|
|
162
|
+
--json
|
|
163
|
+
--quiet, -q
|
|
164
|
+
--help, -h
|
|
165
|
+
--version, -v
|
|
166
|
+
```
|
|
167
|
+
|
|
94
168
|
## Connect OpenChamber Hosts
|
|
95
169
|
|
|
96
170
|
Set the public Relay URL in every OpenChamber Host environment, then start the Host and create a Relay pairing link or enable Relay pairing in the application.
|
|
@@ -120,6 +194,24 @@ relay.example.com {
|
|
|
120
194
|
|
|
121
195
|
Run the Relay with `--public-url wss://relay.example.com/ws`. Caddy forwards WebSocket upgrades and serves `/healthz` and `/readyz` from the same upstream. The `X-Forwarded-For` rule writes one canonical Client source IP.
|
|
122
196
|
|
|
197
|
+
When Push shares the hostname, send `/v1/push/*` to the Push listener and replace `X-Forwarded-For` once on each upstream:
|
|
198
|
+
|
|
199
|
+
```caddyfile
|
|
200
|
+
relay.example.com {
|
|
201
|
+
handle /v1/push/* {
|
|
202
|
+
reverse_proxy 127.0.0.1:8788 {
|
|
203
|
+
header_up X-Forwarded-For {remote_host}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
handle {
|
|
208
|
+
reverse_proxy 127.0.0.1:8787 {
|
|
209
|
+
header_up X-Forwarded-For {remote_host}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
123
215
|
### Nginx
|
|
124
216
|
|
|
125
217
|
```nginx
|
|
@@ -143,6 +235,8 @@ server {
|
|
|
143
235
|
|
|
144
236
|
Run the Relay with `--public-url wss://relay.example.com/ws`. Nginx writes one canonical Client source IP with `$remote_addr` and forwards WebSocket upgrades over HTTP/1.1.
|
|
145
237
|
|
|
238
|
+
To share the hostname with Push, proxy `/v1/push/` to `127.0.0.1:8788` and keep `/` (including `/ws`) on `127.0.0.1:8787`. Replace `X-Forwarded-For` with `$remote_addr` on both locations.
|
|
239
|
+
|
|
146
240
|
## Trusted proxies and capacity
|
|
147
241
|
|
|
148
242
|
Enable `OPENCHAMBER_RELAY_SERVER_TRUST_PROXY=true` or `--trust-proxy` when a trusted reverse proxy fully isolates Relay ingress and replaces each inbound `X-Forwarded-For` value with one canonical Client IP.
|
|
@@ -195,6 +289,43 @@ Trusted-proxy mode accepts exactly one valid IP address in `X-Forwarded-For`. Cl
|
|
|
195
289
|
| `OPENCHAMBER_RELAY_SERVER_MAX_ADMISSION_ENTRIES` | `10000` | tracked role/IP admission records |
|
|
196
290
|
| `OPENCHAMBER_RELAY_SERVER_ID_ATTEMPTS` | `4` | random connection-ID attempts |
|
|
197
291
|
|
|
292
|
+
### Host Push URL override
|
|
293
|
+
|
|
294
|
+
These variables belong on the OpenChamber Host, not on the Push process:
|
|
295
|
+
|
|
296
|
+
| Variable | Default | Unit / purpose |
|
|
297
|
+
| --- | --- | --- |
|
|
298
|
+
| `OPENCHAMBER_PUSH_RELAY_URL` | derived from the effective Relay `ws`/`wss` URL | Host override for `https://` or `http://` `…/v1/push/send` |
|
|
299
|
+
| `OPENCHAMBER_PUSH_RELAY_DISABLED` | unset | Host-only; `true` skips Push Relay and uses direct APNs |
|
|
300
|
+
|
|
301
|
+
The derived send URL always uses `/v1/push/send` on the same host and port as the Relay URL. `wss` maps to `https`; `ws` maps to `http`. Register is the same origin with `/v1/push/register-token`. iOS Live Activity tokens use `/v1/push/register-live-activity-token`, `/v1/push/unregister-live-activity-token`, and `/v1/push/live-activity` on that same origin. Live Activity APNs requests use topic `{bundleId}.push-type.liveactivity` and never include session IDs, titles, alerts, or collapse IDs.
|
|
302
|
+
|
|
303
|
+
### Push process environment
|
|
304
|
+
|
|
305
|
+
Command flags take precedence over `OPENCHAMBER_PUSH_RELAY_*` variables, which take precedence over defaults. APNs Key ID, Team ID, and `.p8` material are required.
|
|
306
|
+
|
|
307
|
+
| Variable | Default | Unit / purpose |
|
|
308
|
+
| --- | --- | --- |
|
|
309
|
+
| `OPENCHAMBER_PUSH_RELAY_HOST` | `127.0.0.1` | Listener address |
|
|
310
|
+
| `OPENCHAMBER_PUSH_RELAY_PORT` | `8788` | TCP port |
|
|
311
|
+
| `OPENCHAMBER_PUSH_RELAY_TRUST_PROXY` | `false` | Read one canonical Client IP from proxy-replaced `X-Forwarded-For` |
|
|
312
|
+
| `OPENCHAMBER_PUSH_RELAY_DATABASE_PATH` | `./data/push-relay.sqlite` | SQLite file; directory must be writable |
|
|
313
|
+
| `OPENCHAMBER_PUSH_RELAY_TIMESTAMP_SKEW_MS` | `300000` | ms signed `ts` window |
|
|
314
|
+
| `OPENCHAMBER_PUSH_RELAY_REPLAY_MS` | `600000` | ms replay-record lifetime; at least twice timestamp skew |
|
|
315
|
+
| `OPENCHAMBER_PUSH_RELAY_MAX_REPLAY_ENTRIES` | `10000` | replay records |
|
|
316
|
+
| `OPENCHAMBER_PUSH_RELAY_REGISTER_LIMIT_PER_MINUTE` | `60` | register requests per Client IP per minute |
|
|
317
|
+
| `OPENCHAMBER_PUSH_RELAY_SEND_LIMIT_PER_MINUTE` | `60` | send requests per Client IP per minute |
|
|
318
|
+
| `OPENCHAMBER_PUSH_RELAY_SERVER_SEND_LIMIT_PER_MINUTE` | `120` | send requests per `serverId` per minute |
|
|
319
|
+
| `OPENCHAMBER_PUSH_RELAY_MAX_TOKENS` | `100000` | persisted device-token bindings |
|
|
320
|
+
| `OPENCHAMBER_PUSH_RELAY_MAX_IN_FLIGHT` | `64` | concurrent APNs deliveries |
|
|
321
|
+
| `OPENCHAMBER_PUSH_RELAY_APNS_KEY_ID` | required | Apple APNs key ID |
|
|
322
|
+
| `OPENCHAMBER_PUSH_RELAY_APNS_TEAM_ID` | required | Apple Team ID |
|
|
323
|
+
| `OPENCHAMBER_PUSH_RELAY_APNS_BUNDLE_ID` | `com.yee94.openchamber` | App bundle ID |
|
|
324
|
+
| `OPENCHAMBER_PUSH_RELAY_APNS_P8` | required unless path set | APNs `.p8` PEM; literal `\n` accepted |
|
|
325
|
+
| `OPENCHAMBER_PUSH_RELAY_APNS_P8_PATH` | unset | Path to the `.p8` file; used when `APNS_P8` is empty |
|
|
326
|
+
|
|
327
|
+
TestFlight and App Store Hosts send `env: production` (`OPENCHAMBER_APNS_ENVIRONMENT=production` on the Host). Xcode development builds use `sandbox`. The Push process does not choose the APNs environment; each send request carries it.
|
|
328
|
+
|
|
198
329
|
## systemd
|
|
199
330
|
|
|
200
331
|
Create `/etc/openchamber-relay.env`:
|
|
@@ -236,11 +367,12 @@ sudo systemctl status openchamber-relay
|
|
|
236
367
|
- `SIGTERM` and `SIGINT` start graceful Relay shutdown. Hosts reconnect after a process restart.
|
|
237
368
|
- Size Host, Client, pending, socket, frame, and queue limits for expected concurrency and message volume.
|
|
238
369
|
- Keep the default loopback listener, terminate public TLS at a reverse proxy, restrict ingress with firewall rules, and publish the matching `wss://` URL.
|
|
239
|
-
- Keep logs and metrics snapshots free of URL query strings, `sig`, `pk`, `grant`, encrypted payloads, pairing material, and
|
|
370
|
+
- Keep logs and metrics snapshots free of URL query strings, `sig`, `pk`, `grant`, encrypted payloads, pairing material, bearer credentials, APNs `.p8` contents, Key ID / Team ID values, and device tokens.
|
|
371
|
+
- Run a single Push process per SQLite file. WAL mode does not make multi-instance sharing safe.
|
|
240
372
|
|
|
241
373
|
## Docker delivery assets
|
|
242
374
|
|
|
243
|
-
Each non-dry-run OpenChamber release publishes a Docker Hub image for `linux/amd64` and `linux/arm64` as `<DOCKERHUB_USERNAME>/openchamber-relay:<version>` and `<DOCKERHUB_USERNAME>/openchamber-relay:latest`. The release pipeline requires the `DOCKERHUB_USERNAME` GitHub Actions repository variable and a `DOCKERHUB_TOKEN` repository secret with Docker Hub Read and Write permissions. Image publication must succeed before the GitHub Release is finalized.
|
|
375
|
+
Each non-dry-run OpenChamber `v*` release and each `relay/v*` Relay-only release publishes a Docker Hub image for `linux/amd64` and `linux/arm64` as `<DOCKERHUB_USERNAME>/openchamber-relay:<version>` and `<DOCKERHUB_USERNAME>/openchamber-relay:latest`. The image default entrypoint is Layer 1. The same image also contains Node 24 and the `openchamber-push-relay` source/bin/package files. Layer 1 is a Bun 1.3.14 compile standalone; Push is executed with Node 24 and `node:sqlite`. The container runs as a non-root user, exposes `8787` and `8788`, and health-checks Layer 1 with Node. The release pipeline requires the `DOCKERHUB_USERNAME` GitHub Actions repository variable and a `DOCKERHUB_TOKEN` repository secret with Docker Hub Read and Write permissions. Image publication must succeed before the GitHub Release is finalized. `relay/v*` publishes the same npm package and Docker image without desktop, mobile, TestFlight, or OTA artifacts.
|
|
244
376
|
|
|
245
377
|
The `Relay Docker` workflow can republish only the current Relay package version without creating or modifying a GitHub Release or other platform artifacts.
|
|
246
378
|
|
|
@@ -270,6 +402,23 @@ docker compose -f docker-compose.relay.remote.yml up -d
|
|
|
270
402
|
|
|
271
403
|
`OPENCHAMBER_RELAY_IMAGE` is required so the repository never binds this reusable deployment file to a personal registry namespace. Use an immutable version-and-digest reference in production.
|
|
272
404
|
|
|
405
|
+
To run Layer 1 and Push from that same immutable image, with APNs secrets only on Push, a persistent SQLite volume, read-only root filesystems, and Caddy routing `/v1/push/*` to Push, use [`docker-compose.relay-push.remote.yml`](../../docker-compose.relay-push.remote.yml):
|
|
406
|
+
|
|
407
|
+
```sh
|
|
408
|
+
OPENCHAMBER_RELAY_IMAGE='<dockerhub-username>/openchamber-relay:<version>@sha256:<manifest-digest>' \
|
|
409
|
+
RELAY_DOMAIN=relay.example.com \
|
|
410
|
+
ACME_EMAIL=admin@example.com \
|
|
411
|
+
OPENCHAMBER_PUSH_RELAY_APNS_KEY_ID='<apns-key-id>' \
|
|
412
|
+
OPENCHAMBER_PUSH_RELAY_APNS_TEAM_ID='<apns-team-id>' \
|
|
413
|
+
OPENCHAMBER_PUSH_RELAY_APNS_BUNDLE_ID=com.yee94.openchamber \
|
|
414
|
+
OPENCHAMBER_PUSH_RELAY_APNS_P8_FILE=/etc/openchamber/AuthKey.p8 \
|
|
415
|
+
docker compose -f docker-compose.relay-push.remote.yml up -d
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
The Push container reads the `.p8` from the Docker secret path `/run/secrets/apns_p8`. Layer 1 does not receive that secret. Inspect both health checks with `docker compose -f docker-compose.relay-push.remote.yml ps`. Public `/healthz` and `/readyz` are Layer 1. Push `/healthz` stays on the internal listener.
|
|
419
|
+
|
|
420
|
+
Existing Layer-1-only Compose files keep their current behavior: [`docker-compose.relay.yml`](../../docker-compose.relay.yml) for loopback, and [`docker-compose.relay.remote.yml`](../../docker-compose.relay.remote.yml) for public Layer 1 without Push.
|
|
421
|
+
|
|
273
422
|
The repository provides optional Docker delivery assets at [`Dockerfile.relay`](../../Dockerfile.relay) and [`docker-compose.relay.yml`](../../docker-compose.relay.yml). The Compose service publishes `127.0.0.1:${OPENCHAMBER_RELAY_PUBLISHED_PORT:-8787}` and accepts `OPENCHAMBER_RELAY_SERVER_PUBLIC_URL` plus selected Relay limits.
|
|
274
423
|
|
|
275
424
|
```sh
|
|
@@ -291,6 +440,9 @@ These assets define an optional follow-on deployment path. Validate the image, p
|
|
|
291
440
|
| Per-Client IP limits behave as proxy limits | Enable trusted-proxy mode, fully isolate Relay ingress behind that proxy, and configure a single replaced `X-Forwarded-For` IP. |
|
|
292
441
|
| Existing clients continue using an earlier endpoint | Refresh the candidate or create a new pairing link after changing `OPENCHAMBER_RELAY_URL`. |
|
|
293
442
|
| WebSocket application traffic fails while HTTP works | Confirm the Host endpoint mints and supplies a short-lived `oc_url_token` for the WebSocket path. |
|
|
443
|
+
| Push register or send returns 404 on the public hostname | Confirm the reverse proxy sends `/v1/push/*` to the Push listener on port 8788, not to Layer 1. |
|
|
444
|
+
| Push container is unhealthy | Confirm APNs Key ID / Team ID / Bundle ID and the `.p8` secret path, and that the SQLite volume is writable by the non-root user. |
|
|
445
|
+
| Device tokens stop receiving after a Relay URL change | Confirm the Host re-registered against the new Push origin before the first send, or set `OPENCHAMBER_PUSH_RELAY_URL` explicitly. |
|
|
294
446
|
|
|
295
447
|
## Development and test coverage
|
|
296
448
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import packageJson from '../package.json' with { type: 'json' };
|
|
3
|
+
|
|
4
|
+
import { isModuleCliExecution } from './cli-entry.js';
|
|
5
|
+
import { runPushRelayCli } from '../src/push/cli.js';
|
|
6
|
+
|
|
7
|
+
const version = packageJson.version;
|
|
8
|
+
|
|
9
|
+
if (isModuleCliExecution(process.argv[1], import.meta.url, undefined, 'openchamber-push-relay')) {
|
|
10
|
+
runPushRelayCli(process.argv.slice(2), { version }).then((code) => { process.exitCode = code; });
|
|
11
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openchambery/relay-server",
|
|
3
|
-
"version": "1.18.7-beta.
|
|
3
|
+
"version": "1.18.7-beta.3",
|
|
4
4
|
"description": "Self-hosted private relay server for OpenChamber",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -11,16 +11,22 @@
|
|
|
11
11
|
"types": "./src/index.d.ts",
|
|
12
12
|
"import": "./src/index.js",
|
|
13
13
|
"default": "./src/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./push": {
|
|
16
|
+
"types": "./src/push/index.d.ts",
|
|
17
|
+
"import": "./src/push/index.js",
|
|
18
|
+
"default": "./src/push/index.js"
|
|
14
19
|
}
|
|
15
20
|
},
|
|
16
21
|
"bin": {
|
|
17
|
-
"openchamber-relay": "./bin/openchamber-relay.js"
|
|
22
|
+
"openchamber-relay": "./bin/openchamber-relay.js",
|
|
23
|
+
"openchamber-push-relay": "./bin/openchamber-push-relay.js"
|
|
18
24
|
},
|
|
19
25
|
"publishConfig": {
|
|
20
26
|
"access": "public"
|
|
21
27
|
},
|
|
22
28
|
"engines": {
|
|
23
|
-
"node": ">=22.
|
|
29
|
+
"node": ">=22.13.0"
|
|
24
30
|
},
|
|
25
31
|
"license": "MIT",
|
|
26
32
|
"repository": {
|
|
@@ -40,8 +46,8 @@
|
|
|
40
46
|
"scripts": {
|
|
41
47
|
"test": "vitest run",
|
|
42
48
|
"test:node": "node test/node-smoke.js",
|
|
43
|
-
"type-check": "node --check src/index.js && node --check src/cli.js && node --check bin/openchamber-relay.js && node --check bin/cli-entry.js",
|
|
44
|
-
"lint": "node --check src/index.js && node --check src/cli.js && node --check bin/openchamber-relay.js && node --check bin/cli-entry.js",
|
|
49
|
+
"type-check": "node --check src/index.js && node --check src/cli.js && node --check bin/openchamber-relay.js && node --check bin/cli-entry.js && node --check src/push/index.js && node --check src/push/server.js && node --check src/push/cli.js && node --check src/push/config.js && node --check src/push/schema.js && node --check src/push/crypto.js && node --check src/push/store.js && node --check src/push/guard.js && node --check src/push/apns.js && node --check bin/openchamber-push-relay.js && node --check src/push/handler.js && node --check src/push/combined.js",
|
|
50
|
+
"lint": "node --check src/index.js && node --check src/cli.js && node --check bin/openchamber-relay.js && node --check bin/cli-entry.js && node --check src/push/index.js && node --check src/push/server.js && node --check src/push/cli.js && node --check src/push/config.js && node --check src/push/schema.js && node --check src/push/crypto.js && node --check src/push/store.js && node --check src/push/guard.js && node --check src/push/apns.js && node --check bin/openchamber-push-relay.js && node --check src/push/handler.js && node --check src/push/combined.js",
|
|
45
51
|
"build:standalone": "bun build --compile --outfile dist/openchamber-relay bin/openchamber-relay.js"
|
|
46
52
|
},
|
|
47
53
|
"dependencies": {
|
package/src/cli.js
CHANGED
|
@@ -71,6 +71,16 @@ export const buildRelayConfig = (parsed = {}, env = process.env) => {
|
|
|
71
71
|
|
|
72
72
|
const helpText = 'Usage: openchamber-relay [--host HOST] [--port PORT] [--path PATH] [--public-url WS_URL] [--trust-proxy] [--json] [--quiet]\nEnable --trust-proxy only when public ingress reaches this relay through a trusted reverse proxy.\n';
|
|
73
73
|
const writeJson = (stdout, payload) => stdout.write(`${JSON.stringify(payload)}\n`);
|
|
74
|
+
const hasPushApnsEnv = (env) => {
|
|
75
|
+
for (const [key, value] of Object.entries(env ?? {})) {
|
|
76
|
+
if (key.startsWith('OPENCHAMBER_PUSH_RELAY_APNS_') && typeof value === 'string' && value.trim().length > 0) return true;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
};
|
|
80
|
+
const loadCombinedPushMount = async () => {
|
|
81
|
+
// Computed specifier keeps bun --compile from bundling Push/SQLite into the Layer 1 binary.
|
|
82
|
+
return (await import(['.', 'push', 'combined.js'].join('/'))).createCombinedPushMount;
|
|
83
|
+
};
|
|
74
84
|
|
|
75
85
|
export const runRelayServerCli = async (argv, dependencies = {}) => {
|
|
76
86
|
const processLike = dependencies.process ?? process; const stdout = dependencies.stdout ?? process.stdout; const stderr = dependencies.stderr ?? process.stderr; const version = dependencies.version ?? '0.0.0';
|
|
@@ -87,15 +97,24 @@ export const runRelayServerCli = async (argv, dependencies = {}) => {
|
|
|
87
97
|
let config;
|
|
88
98
|
try { config = buildRelayConfig(parsed, processLike.env ?? {}); } catch (error) { respond({ status: 'error', error: error.message, message: error.message }, true); processLike.exitCode = 1; return 1; }
|
|
89
99
|
try {
|
|
100
|
+
const env = processLike.env ?? {};
|
|
101
|
+
const createPushMount = dependencies.createPushMount ?? (
|
|
102
|
+
hasPushApnsEnv(env) ? await loadCombinedPushMount() : () => null
|
|
103
|
+
);
|
|
104
|
+
const mount = createPushMount(env, dependencies.pushMountDeps ?? {});
|
|
105
|
+
if (mount) config.requestHandler = mount.requestHandler;
|
|
90
106
|
const relay = await (dependencies.start ?? startPrivateRelayServer)(config);
|
|
107
|
+
if (mount) await mount.start();
|
|
91
108
|
const url = config.publicUrl ?? relay.wsUrl;
|
|
92
|
-
respond(json ? { status: 'ok', url, host: config.host, port: relay.address?.()?.port ?? config.port, path: config.path } : { message: `Relay listening at ${url}` });
|
|
109
|
+
respond(json ? { status: 'ok', url, host: config.host, port: relay.address?.()?.port ?? config.port, path: config.path, ...(mount ? { push: true } : {}) } : { message: `Relay listening at ${url}` });
|
|
110
|
+
if (mount && !json) respond({ message: 'Push relay mounted at /v1/push/*' });
|
|
93
111
|
let stopping = false;
|
|
94
112
|
const stop = async () => {
|
|
95
113
|
if (stopping) return Promise.resolve();
|
|
96
114
|
stopping = true;
|
|
97
115
|
processLike.off?.('SIGINT', stop); processLike.off?.('SIGTERM', stop);
|
|
98
116
|
try {
|
|
117
|
+
if (mount) await mount.stop();
|
|
99
118
|
await relay.stop();
|
|
100
119
|
processLike.exit?.(0);
|
|
101
120
|
} catch {
|