mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-08 03:32:27 +09:00
Compare commits
112
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6730a242db | ||
|
|
e62ae77643 | ||
|
|
f4f39e6a89 | ||
|
|
00bf74cef5 | ||
|
|
c1c45d835f | ||
|
|
bbfe809bef | ||
|
|
e0843ac4f5 | ||
|
|
43741cdad8 | ||
|
|
b8e3807262 | ||
|
|
3304f01a84 | ||
|
|
2ba463235b | ||
|
|
15136fed59 | ||
|
|
6013581dd9 | ||
|
|
7a91f128e9 | ||
|
|
963ffc5550 | ||
|
|
a90991612c | ||
|
|
50ad23b760 | ||
|
|
425dab983b | ||
|
|
a0825e77c4 | ||
|
|
88038a1d5b | ||
|
|
c2c0fdb445 | ||
|
|
dcd5f09d6a | ||
|
|
590b1f36fd | ||
|
|
168ac727f1 | ||
|
|
deb86dd92e | ||
|
|
7ccec4d3b8 | ||
|
|
2f38bcdf26 | ||
|
|
f2785941aa | ||
|
|
ea9f83a443 | ||
|
|
bd6ca7290e | ||
|
|
b85e975fb5 | ||
|
|
b6e504f68c | ||
|
|
5fde6eb484 | ||
|
|
b16989d567 | ||
|
|
c9754ac11a | ||
|
|
f34e4a5115 | ||
|
|
44b3615298 | ||
|
|
211e98307d | ||
|
|
18c303abf6 | ||
|
|
7021a58090 | ||
|
|
320725a587 | ||
|
|
8450edc072 | ||
|
|
a1e2bf2c8d | ||
|
|
82b2f4ec5e | ||
|
|
c92e5d03a7 | ||
|
|
91340c5c84 | ||
|
|
045dd5d027 | ||
|
|
a21b9c4659 | ||
|
|
4b1b869802 | ||
|
|
1ab7e7dfcc | ||
|
|
31c53d2dff | ||
|
|
412a1ae79d | ||
|
|
0b2306ec3d | ||
|
|
242ed3a934 | ||
|
|
70e1ce682a | ||
|
|
7601bf98ee | ||
|
|
c7ec2a0f58 | ||
|
|
6a5e0056a8 | ||
|
|
78d105b46e | ||
|
|
c68d62b8a0 | ||
|
|
df58020f4c | ||
|
|
f052ce05aa | ||
|
|
eedfd9275f | ||
|
|
416af4bec4 | ||
|
|
108d282ddd | ||
|
|
a6103244b0 | ||
|
|
38935c83c5 | ||
|
|
c157ab5752 | ||
|
|
574a93257c | ||
|
|
ba7d8781cf | ||
|
|
3256af8d92 | ||
|
|
86043212f2 | ||
|
|
5d85e88532 | ||
|
|
e2abfd476a | ||
|
|
487febac8e | ||
|
|
a3454e8245 | ||
|
|
bf7567b768 | ||
|
|
ac3450ab32 | ||
|
|
5d034becb8 | ||
|
|
f9397d0db9 | ||
|
|
9005139dc8 | ||
|
|
d93604afa2 | ||
|
|
c4f0b2ece0 | ||
|
|
98a42f612b | ||
|
|
cc75e1318d | ||
|
|
9027cbdf3e | ||
|
|
2119e10ed5 | ||
|
|
87f3eb3c81 | ||
|
|
f9bb8bd585 | ||
|
|
bc47a724af | ||
|
|
f32356801d | ||
|
|
efd677f32b | ||
|
|
3cec27ba57 | ||
|
|
1f810ba04d | ||
|
|
522cf08e61 | ||
|
|
025c01ab13 | ||
|
|
dc41b53d60 | ||
|
|
3b552e00ef | ||
|
|
56e04e7b53 | ||
|
|
deac653a9e | ||
|
|
ed9528834d | ||
|
|
4cecbf1f43 | ||
|
|
b019f4a91f | ||
|
|
34b6ecfbd2 | ||
|
|
4ef9c4c65b | ||
|
|
3276039e41 | ||
|
|
03d1354562 | ||
|
|
964845d7a7 | ||
|
|
3bc5dd8e0f | ||
|
|
17292fd6a5 | ||
|
|
f753659899 | ||
|
|
7412ec3395 |
@@ -1,14 +1,14 @@
|
||||
FROM chrislusf/seaweedfs:4.31 AS seaweedfs
|
||||
FROM chrislusf/seaweedfs:4.47 AS seaweedfs
|
||||
|
||||
FROM erlang:28.5.0.1
|
||||
FROM erlang:28.5.0.6
|
||||
|
||||
ARG USERNAME=vscode
|
||||
ARG USER_UID=1000
|
||||
ARG USER_GID=1000
|
||||
ARG NODE_MAJOR=24
|
||||
ARG ELP_VERSION=2026-02-27
|
||||
ARG PNPM_VERSION=10.29.3
|
||||
ARG WASM_BINDGEN_VERSION=0.2.123
|
||||
ARG NODE_MAJOR=26
|
||||
ARG ELP_VERSION=2026-08-10
|
||||
ARG PNPM_VERSION=11.27.0
|
||||
ARG WASM_BINDGEN_VERSION=0.2.128
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
@@ -131,7 +131,8 @@ RUN apt-get update \
|
||||
RUN curl --retry 5 --retry-delay 2 --retry-all-errors -fsSL https://deb.nodesource.com/setup_${NODE_MAJOR}.x | bash - \
|
||||
&& apt-get install -y --no-install-recommends nodejs \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& corepack enable
|
||||
&& npm install -g "pnpm@${PNPM_VERSION}" \
|
||||
&& pnpm --version
|
||||
|
||||
RUN python3 -m pip install --break-system-packages --no-cache-dir awscli
|
||||
|
||||
@@ -167,7 +168,7 @@ RUN ARCH="$(dpkg --print-architecture)" \
|
||||
arm64) ELP_ARCH="aarch64" ;; \
|
||||
*) echo "Unsupported architecture for ELP: $ARCH" >&2; exit 1 ;; \
|
||||
esac \
|
||||
&& curl --retry 5 --retry-delay 2 --retry-all-errors -fsSL "https://github.com/WhatsApp/erlang-language-platform/releases/download/${ELP_VERSION}/elp-linux-${ELP_ARCH}-unknown-linux-gnu-otp-28.tar.gz" -o /tmp/elp.tgz \
|
||||
&& curl --retry 5 --retry-delay 2 --retry-all-errors -fsSL "https://github.com/WhatsApp/erlang-language-platform/releases/download/${ELP_VERSION}/elp-linux-${ELP_ARCH}-unknown-linux-gnu-otp-28.5.tar.gz" -o /tmp/elp.tgz \
|
||||
&& tar -C /usr/local/bin -xzf /tmp/elp.tgz elp \
|
||||
&& chmod +x /usr/local/bin/elp \
|
||||
&& rm /tmp/elp.tgz
|
||||
@@ -194,7 +195,4 @@ RUN curl --retry 5 --retry-delay 2 --retry-all-errors -fsSL https://sh.rustup.rs
|
||||
&& cargo install wasm-bindgen-cli --version "${WASM_BINDGEN_VERSION}" --locked \
|
||||
&& rm -rf "/home/${USERNAME}/.cargo/registry" "/home/${USERNAME}/.cargo/git"
|
||||
|
||||
RUN corepack prepare "pnpm@${PNPM_VERSION}" --activate \
|
||||
&& pnpm --version
|
||||
|
||||
WORKDIR /workspaces/fluxer
|
||||
|
||||
@@ -38,7 +38,11 @@
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"settings": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
"editor.defaultFormatter": "biomejs.biome",
|
||||
"erlang.includePaths": ["."],
|
||||
"search.exclude": {
|
||||
"**/_build/default/lib/fluxer_gateway": true
|
||||
}
|
||||
},
|
||||
"extensions": [
|
||||
"biomejs.biome",
|
||||
|
||||
@@ -9,7 +9,7 @@ services:
|
||||
init: true
|
||||
environment:
|
||||
DOCKER_HOST: unix:///var/run/docker.sock
|
||||
npm_config_store_dir: /home/vscode/.local/share/pnpm/store
|
||||
pnpm_config_store_dir: /home/vscode/.local/share/pnpm/store
|
||||
FLUXER_PUBLIC_PORT: "${FLUXER_DEV_PROXY_PORT:-8088}"
|
||||
FLUXER_PUBLIC_URL: "http://localhost:${FLUXER_DEV_PROXY_PORT:-8088}"
|
||||
FLUXER_API_ENDPOINT: "http://localhost:${FLUXER_DEV_PROXY_PORT:-8088}/api"
|
||||
@@ -292,13 +292,13 @@ services:
|
||||
start_period: 5s
|
||||
|
||||
valkey:
|
||||
image: valkey/valkey:8.1.7-alpine
|
||||
image: valkey/valkey:9.1.2-alpine
|
||||
command: ["valkey-server", "--save", "", "--appendonly", "no"]
|
||||
ports:
|
||||
- "127.0.0.1:${FLUXER_DEV_VALKEY_PORT:-6379}:6379"
|
||||
|
||||
nats:
|
||||
image: nats:2.14.2-alpine
|
||||
image: nats:2.14.7-alpine
|
||||
command: ["-js", "-sd", "/data", "-m", "8222"]
|
||||
volumes:
|
||||
- nats-data:/data
|
||||
@@ -321,9 +321,10 @@ services:
|
||||
- "127.0.0.1:${FLUXER_DEV_LIVEKIT_UDP_PORT:-7882}:${FLUXER_DEV_LIVEKIT_UDP_PORT:-7882}/udp"
|
||||
|
||||
meilisearch:
|
||||
image: getmeili/meilisearch:v1.12
|
||||
image: getmeili/meilisearch:v1.53
|
||||
environment:
|
||||
MEILI_NO_ANALYTICS: "true"
|
||||
MEILI_UPGRADE_DB: "true"
|
||||
MEILI_MASTER_KEY: fluxer-dev-meilisearch
|
||||
volumes:
|
||||
- meilisearch-data:/meili_data
|
||||
@@ -337,7 +338,7 @@ services:
|
||||
start_period: 5s
|
||||
|
||||
mailpit:
|
||||
image: axllent/mailpit:v1.30
|
||||
image: axllent/mailpit:v1.31
|
||||
environment:
|
||||
MP_DATABASE: /data/mailpit.db
|
||||
MP_MAX_MESSAGES: 5000
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
/fluxer_docs/.astro/
|
||||
/fluxer_app/.devserver-cache.json
|
||||
/fluxer_app/pkgs/libfluxcore/
|
||||
/fluxer_app/pkgs/libfluxwebp/
|
||||
/fluxer_app/src/features/i18n/locales/*/messages.mjs
|
||||
/fluxer_app/src/features/messaging/utils/markdown/parser/MarkdownParserWasmBytes.ts
|
||||
/fluxer_app/src/features/theme/styles/generated/
|
||||
|
||||
@@ -28,9 +28,9 @@ f:media_proxy:
|
||||
f:messages:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: fluxer_messages/**/*
|
||||
f:recon:
|
||||
f:push:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: fluxer_recon/**/*
|
||||
- any-glob-to-any-file: fluxer_push/**/*
|
||||
f:snowflakes:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: fluxer_snowflakes/**/*
|
||||
|
||||
@@ -58,13 +58,13 @@ jobs:
|
||||
outputs:
|
||||
build_version: ${{ steps.vars.outputs.build_version }}
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
- name: Create token
|
||||
id: create-token
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
|
||||
@@ -101,19 +101,19 @@ jobs:
|
||||
- platform: arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
|
||||
- name: resolve source date
|
||||
id: source
|
||||
run: echo "date=$(TZ=UTC git log -1 --no-show-signature --pretty=%cd --date=format-local:%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
|
||||
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
|
||||
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee
|
||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069
|
||||
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ github.token }}
|
||||
- uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf
|
||||
- uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc
|
||||
with:
|
||||
context: ${{ inputs.context }}
|
||||
file: ${{ inputs.dockerfile }}
|
||||
@@ -141,15 +141,15 @@ jobs:
|
||||
contents: write
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
|
||||
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee
|
||||
toolchain: "1.98.1"
|
||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069
|
||||
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -43,13 +43,13 @@ jobs:
|
||||
outputs:
|
||||
build_version: ${{ steps.vars.outputs.build_version }}
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
- name: set variables
|
||||
id: vars
|
||||
run: >-
|
||||
@@ -71,20 +71,19 @@ jobs:
|
||||
BUILD_VERSION: ${{ needs.meta.outputs.build_version }}
|
||||
PUBLIC_ASSET_BASE_URL: ""
|
||||
BUNDLE_LOCAL_ASSETS: "true"
|
||||
FLUXER_APP_PROXY_TIME_FREEZE_ENABLED: "false"
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
- name: prepare docker config
|
||||
run: >-
|
||||
tools/ci/run.sh build-app-proxy
|
||||
--step prepare_docker_config
|
||||
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
|
||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069
|
||||
- name: configure ghcr auth
|
||||
env:
|
||||
GHCR_USERNAME: ${{ github.actor }}
|
||||
@@ -131,19 +130,19 @@ jobs:
|
||||
- platform: arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
|
||||
- name: resolve source date
|
||||
id: source
|
||||
run: echo "date=$(TZ=UTC git log -1 --no-show-signature --pretty=%cd --date=format-local:%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
|
||||
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
|
||||
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee
|
||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069
|
||||
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf
|
||||
- uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc
|
||||
with:
|
||||
context: .
|
||||
file: fluxer_app_proxy/Dockerfile
|
||||
@@ -155,7 +154,6 @@ jobs:
|
||||
BUILD_VERSION=${{ needs.meta.outputs.build_version }}
|
||||
SOURCE_SHA=${{ github.sha }}
|
||||
SOURCE_DATE=${{ steps.source.outputs.date }}
|
||||
FLUXER_APP_PROXY_TIME_FREEZE_ENABLED=false
|
||||
APP_ASSETS_REF=ghcr.io/${{ env.GHCR_OWNER }}/fluxer-app-proxy-self-hosted:${{ needs.meta.outputs.build_version }}-assets
|
||||
APP_ASSETS_PLATFORM=linux/amd64
|
||||
cache-from: type=registry,ref=ghcr.io/${{ env.GHCR_OWNER }}/fluxer-app-proxy-self-hosted:buildcache-${{ matrix.platform }}
|
||||
@@ -173,15 +171,15 @@ jobs:
|
||||
contents: write
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
|
||||
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee
|
||||
toolchain: "1.98.1"
|
||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069
|
||||
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -43,13 +43,13 @@ jobs:
|
||||
outputs:
|
||||
build_version: ${{ steps.vars.outputs.build_version }}
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
- name: set variables
|
||||
id: vars
|
||||
run: >-
|
||||
@@ -67,18 +67,18 @@ jobs:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
- name: prepare docker config
|
||||
run: >-
|
||||
tools/ci/run.sh build-app-proxy
|
||||
--step prepare_docker_config
|
||||
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
|
||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069
|
||||
- name: configure ghcr auth
|
||||
env:
|
||||
GHCR_USERNAME: ${{ github.actor }}
|
||||
@@ -131,13 +131,13 @@ jobs:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
- name: resolve source date
|
||||
id: source
|
||||
run: echo "date=$(TZ=UTC git log -1 --no-show-signature --pretty=%cd --date=format-local:%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
|
||||
@@ -145,7 +145,7 @@ jobs:
|
||||
run: >-
|
||||
tools/ci/run.sh build-app-proxy
|
||||
--step prepare_docker_config
|
||||
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
|
||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069
|
||||
- name: configure ghcr auth
|
||||
env:
|
||||
GHCR_USERNAME: ${{ github.actor }}
|
||||
@@ -178,19 +178,19 @@ jobs:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
|
||||
- name: resolve source date
|
||||
id: source
|
||||
run: echo "date=$(TZ=UTC git log -1 --no-show-signature --pretty=%cd --date=format-local:%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
|
||||
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
|
||||
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee
|
||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069
|
||||
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf
|
||||
- uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc
|
||||
with:
|
||||
context: .
|
||||
file: fluxer_app_proxy/Dockerfile
|
||||
@@ -219,15 +219,15 @@ jobs:
|
||||
contents: write
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
|
||||
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee
|
||||
toolchain: "1.98.1"
|
||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069
|
||||
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -11,11 +11,6 @@ on:
|
||||
- stable
|
||||
- canary
|
||||
default: stable
|
||||
test_build:
|
||||
description: Stash artifacts under desktop-test/ instead of desktop/ (API will not pick these up as a release).
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
build_version:
|
||||
description: Explicit Fluxer CalVer build version (YYYY.MDD.MICRO, UTC HHMMSS without leading zeroes) to use instead of automatic UTC clock allocation.
|
||||
required: false
|
||||
@@ -32,13 +27,12 @@ permissions:
|
||||
actions: read
|
||||
|
||||
concurrency:
|
||||
group: desktop-${{ inputs.channel }}-${{ inputs.test_build && 'test' || 'release' }}
|
||||
group: desktop-${{ inputs.channel }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CHANNEL: ${{ inputs.channel }}
|
||||
BUILD_CHANNEL: ${{ inputs.channel == 'canary' && 'canary' || 'stable' }}
|
||||
TEST_BUILD: ${{ inputs.test_build && 'true' || 'false' }}
|
||||
|
||||
jobs:
|
||||
meta:
|
||||
@@ -53,19 +47,17 @@ jobs:
|
||||
pub_date: ${{ steps.meta.outputs.pub_date }}
|
||||
channel: ${{ steps.meta.outputs.channel }}
|
||||
build_channel: ${{ steps.meta.outputs.build_channel }}
|
||||
test_build: ${{ steps.meta.outputs.test_build }}
|
||||
s3_prefix: ${{ steps.meta.outputs.s3_prefix }}
|
||||
source_sha: ${{ steps.meta.outputs.source_sha }}
|
||||
steps:
|
||||
- name: Checkout source
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
ref: main
|
||||
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
|
||||
- name: Create token
|
||||
id: create-token
|
||||
@@ -85,7 +77,6 @@ jobs:
|
||||
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
|
||||
--step set_metadata
|
||||
--channel "${{ inputs.channel }}"
|
||||
--test-build "${{ inputs.test_build }}"
|
||||
|
||||
matrix:
|
||||
name: Resolve build matrix
|
||||
@@ -98,12 +89,12 @@ jobs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout source
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
|
||||
- name: Build platform matrix
|
||||
id: set-matrix
|
||||
@@ -113,7 +104,7 @@ jobs:
|
||||
--skip-targets "${{ inputs.skip_targets }}"
|
||||
|
||||
build:
|
||||
name: Build ${{ matrix.platform }} (${{ matrix.arch }}, ${{ matrix.desktop_variant }})
|
||||
name: Build ${{ matrix.platform }} (${{ matrix.arch }})
|
||||
needs:
|
||||
- meta
|
||||
- matrix
|
||||
@@ -137,41 +128,34 @@ jobs:
|
||||
PUBLIC_BUILD_VERSION: ${{ needs.meta.outputs.version }}
|
||||
PUB_DATE: ${{ needs.meta.outputs.pub_date }}
|
||||
SOURCE_SHA: ${{ needs.meta.outputs.source_sha }}
|
||||
S3_DESKTOP_PREFIX: ${{ needs.meta.outputs.s3_prefix }}
|
||||
DESKTOP_HANDOFF_PREFIX: _handoff/desktop/${{ needs.meta.outputs.build_channel }}/${{ needs.meta.outputs.version }}/${{ needs.meta.outputs.source_sha }}
|
||||
S3_ENDPOINT: ${{ vars.DOWNLOADS_S3_ENDPOINT }}
|
||||
S3_BUCKET: ${{ vars.DOWNLOADS_S3_BUCKET }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.DOWNLOADS_AWS_ACCESS_KEY_ID || secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.DOWNLOADS_AWS_SECRET_ACCESS_KEY || secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
DESKTOP_PLATFORM: ${{ matrix.platform }}
|
||||
DESKTOP_ARCH: ${{ matrix.arch }}
|
||||
DESKTOP_VARIANT: ${{ matrix.desktop_variant }}
|
||||
PLATFORM: ${{ matrix.platform }}
|
||||
ARCH: ${{ matrix.arch }}
|
||||
ELECTRON_ARCH: ${{ matrix.electron_arch }}
|
||||
steps:
|
||||
- name: Checkout CI helpers
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
ref: ${{ needs.meta.outputs.source_sha }}
|
||||
path: _ci
|
||||
|
||||
- name: Checkout source
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
ref: ${{ needs.meta.outputs.source_sha }}
|
||||
path: source
|
||||
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
|
||||
- name: Set up Python (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
|
||||
with:
|
||||
python-version: "3.13"
|
||||
python-version: "3.14"
|
||||
|
||||
- name: Ensure python3 command (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
@@ -196,14 +180,14 @@ jobs:
|
||||
--step set_workdir_unix
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 26
|
||||
|
||||
- name: Set up pnpm via corepack
|
||||
- name: Set up pnpm
|
||||
run: >-
|
||||
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
|
||||
--step setup_pnpm_corepack
|
||||
--step setup_pnpm
|
||||
|
||||
- name: Resolve pnpm store path (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
@@ -247,9 +231,9 @@ jobs:
|
||||
|
||||
- name: Set up Rust toolchain (Unix)
|
||||
if: matrix.platform != 'windows'
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
targets: ${{ matrix.platform == 'macos' && 'aarch64-apple-darwin,x86_64-apple-darwin' || (matrix.arch == 'arm64' && 'aarch64-unknown-linux-gnu' || 'x86_64-unknown-linux-gnu') }}
|
||||
|
||||
- name: Install MSVC ARM64 build tools
|
||||
@@ -260,7 +244,7 @@ jobs:
|
||||
|
||||
- name: Set up MSVC env (Windows)
|
||||
if: matrix.platform == 'windows'
|
||||
uses: TheMrMilchmann/setup-msvc-dev@79dac248aac9d0059f86eae9d8b5bfab4e95e97c
|
||||
uses: TheMrMilchmann/setup-msvc-dev@368ef7d1ee4d1171b31d4a7f67f4d954f903f5a9
|
||||
with:
|
||||
arch: ${{ matrix.arch == 'arm64' && 'amd64_arm64' || 'amd64' }}
|
||||
|
||||
@@ -302,9 +286,9 @@ jobs:
|
||||
|
||||
- name: Set up .NET SDK (Windows)
|
||||
if: matrix.platform == 'windows'
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1
|
||||
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68
|
||||
with:
|
||||
dotnet-version: "8.0.x"
|
||||
dotnet-version: "10.0.x"
|
||||
|
||||
- name: Install Velopack CLI
|
||||
if: matrix.platform == 'windows'
|
||||
@@ -363,7 +347,7 @@ jobs:
|
||||
|
||||
- name: Azure login for Artifact Signing
|
||||
if: matrix.platform == 'windows'
|
||||
uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43
|
||||
uses: azure/login@a641126d1b8aa4d1fa005f4f92df94a3a4c4c906
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
@@ -477,6 +461,12 @@ jobs:
|
||||
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
|
||||
--step prepare_artifacts_unix
|
||||
|
||||
- name: Build AppImage update feed (Linux)
|
||||
if: matrix.platform == 'linux'
|
||||
run: >-
|
||||
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
|
||||
--step build_appimage_update_feed
|
||||
|
||||
- name: Normalize updater YAML (macOS)
|
||||
if: matrix.platform == 'macos'
|
||||
run: >-
|
||||
@@ -495,13 +485,23 @@ jobs:
|
||||
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
|
||||
--step generate_checksums_windows
|
||||
|
||||
- name: Upload artifacts to S3 handoff
|
||||
- name: Stage build artifacts
|
||||
id: handoff
|
||||
run: >-
|
||||
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
|
||||
--step upload_handoff
|
||||
--step stage_handoff
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: ${{ steps.handoff.outputs.artifact_name }}
|
||||
path: upload_staging
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
compression-level: 0
|
||||
|
||||
upload:
|
||||
name: Upload to S3
|
||||
name: Assemble desktop release assets
|
||||
if: ${{ !cancelled() && needs.build.result == 'success' }}
|
||||
needs:
|
||||
- meta
|
||||
@@ -520,34 +520,26 @@ jobs:
|
||||
BUILD_VERSION: ${{ needs.meta.outputs.version }}
|
||||
PUBLIC_BUILD_VERSION: ${{ needs.meta.outputs.version }}
|
||||
PUB_DATE: ${{ needs.meta.outputs.pub_date }}
|
||||
TEST_BUILD: ${{ needs.meta.outputs.test_build }}
|
||||
SOURCE_SHA: ${{ needs.meta.outputs.source_sha }}
|
||||
S3_DESKTOP_PREFIX: ${{ needs.meta.outputs.s3_prefix }}
|
||||
DESKTOP_HANDOFF_PREFIX: _handoff/desktop/${{ needs.meta.outputs.build_channel }}/${{ needs.meta.outputs.version }}/${{ needs.meta.outputs.source_sha }}
|
||||
DESKTOP_RELEASE_ASSETS_PREFIX: _handoff/desktop-release-assets/${{ needs.meta.outputs.build_channel }}/${{ needs.meta.outputs.version }}/${{ needs.meta.outputs.source_sha }}
|
||||
DESKTOP_METADATA_PREFIX: _handoff/desktop-metadata/${{ needs.meta.outputs.build_channel }}/${{ needs.meta.outputs.version }}/${{ needs.meta.outputs.source_sha }}
|
||||
S3_ENDPOINT: ${{ vars.DOWNLOADS_S3_ENDPOINT }}
|
||||
S3_BUCKET: ${{ vars.DOWNLOADS_S3_BUCKET }}
|
||||
PUBLIC_DL_BASE: https://api.fluxer.app/dl
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.DOWNLOADS_AWS_ACCESS_KEY_ID || secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.DOWNLOADS_AWS_SECRET_ACCESS_KEY || secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
steps:
|
||||
- name: Checkout source
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
ref: ${{ needs.meta.outputs.source_sha }}
|
||||
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
|
||||
- name: Download S3 handoff artifacts
|
||||
run: >-
|
||||
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
|
||||
--step download_handoff
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
with:
|
||||
path: artifacts
|
||||
pattern: fluxer-desktop-${{ needs.meta.outputs.build_channel }}-*
|
||||
|
||||
- name: Build S3 payload layout (+ manifest.json)
|
||||
- name: Build payload layout (+ manifest.json)
|
||||
env:
|
||||
VERSION: ${{ needs.meta.outputs.version }}
|
||||
PUB_DATE: ${{ needs.meta.outputs.pub_date }}
|
||||
@@ -556,42 +548,27 @@ jobs:
|
||||
--step build_payload
|
||||
|
||||
- name: Prepare GitHub release assets
|
||||
if: needs.meta.outputs.test_build != 'true'
|
||||
run: >-
|
||||
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
|
||||
--step prepare_release_assets
|
||||
|
||||
- name: Publish GitHub release descriptor
|
||||
if: needs.meta.outputs.test_build != 'true'
|
||||
run: >-
|
||||
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
|
||||
--step publish_release_descriptor
|
||||
|
||||
- name: Upload payload to S3
|
||||
run: >-
|
||||
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
|
||||
--step upload_payload
|
||||
|
||||
- name: Upload GitHub release asset handoff
|
||||
if: needs.meta.outputs.test_build != 'true'
|
||||
run: >-
|
||||
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
|
||||
--step upload_release_assets
|
||||
- name: Upload GitHub release assets
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: fluxer-desktop-release-assets
|
||||
path: release_assets
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
compression-level: 0
|
||||
|
||||
- name: Build summary
|
||||
run: >-
|
||||
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
|
||||
--step build_summary
|
||||
|
||||
- name: Cleanup S3 handoff
|
||||
if: ${{ success() }}
|
||||
run: >-
|
||||
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
|
||||
--step cleanup_handoff
|
||||
|
||||
publish_release:
|
||||
name: Publish GitHub desktop release
|
||||
if: ${{ !cancelled() && needs.upload.result == 'success' && needs.meta.outputs.test_build != 'true' }}
|
||||
if: ${{ !cancelled() && needs.upload.result == 'success' }}
|
||||
needs:
|
||||
- meta
|
||||
- upload
|
||||
@@ -603,28 +580,22 @@ jobs:
|
||||
env:
|
||||
CHANNEL: ${{ needs.meta.outputs.build_channel }}
|
||||
VERSION: ${{ needs.meta.outputs.version }}
|
||||
S3_DESKTOP_PREFIX: ${{ needs.meta.outputs.s3_prefix }}
|
||||
DESKTOP_RELEASE_ASSETS_PREFIX: _handoff/desktop-release-assets/${{ needs.meta.outputs.build_channel }}/${{ needs.meta.outputs.version }}/${{ needs.meta.outputs.source_sha }}
|
||||
DESKTOP_METADATA_PREFIX: _handoff/desktop-metadata/${{ needs.meta.outputs.build_channel }}/${{ needs.meta.outputs.version }}/${{ needs.meta.outputs.source_sha }}
|
||||
S3_ENDPOINT: ${{ vars.DOWNLOADS_S3_ENDPOINT }}
|
||||
S3_BUCKET: ${{ vars.DOWNLOADS_S3_BUCKET }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.DOWNLOADS_AWS_ACCESS_KEY_ID || secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.DOWNLOADS_AWS_SECRET_ACCESS_KEY || secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
steps:
|
||||
- name: Checkout source
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
ref: ${{ needs.meta.outputs.source_sha }}
|
||||
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
|
||||
- name: Download GitHub release assets
|
||||
run: >-
|
||||
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
|
||||
--step download_release_assets
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
with:
|
||||
name: fluxer-desktop-release-assets
|
||||
path: release_assets
|
||||
|
||||
- name: Create token
|
||||
id: create-token
|
||||
@@ -656,15 +627,3 @@ jobs:
|
||||
release_args+=(--prerelease)
|
||||
fi
|
||||
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- "${release_args[@]}"
|
||||
|
||||
- name: Publish GitHub release readiness marker
|
||||
env:
|
||||
SOURCE_SHA: ${{ needs.meta.outputs.source_sha }}
|
||||
run: >-
|
||||
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
|
||||
--step publish_release_marker
|
||||
|
||||
- name: Publish payload metadata to S3
|
||||
run: >-
|
||||
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
|
||||
--step publish_payload_metadata
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
name: build recon
|
||||
name: build push
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -31,6 +31,6 @@ jobs:
|
||||
uses: ./.github/workflows/_build-image.yaml
|
||||
secrets: inherit
|
||||
with:
|
||||
image: fluxer-recon
|
||||
dockerfile: fluxer_recon/Dockerfile
|
||||
image: fluxer-push
|
||||
dockerfile: fluxer_push/Dockerfile
|
||||
build-version: ${{ inputs['build-version'] }}
|
||||
@@ -19,22 +19,22 @@ jobs:
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout fluxer
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271
|
||||
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
||||
with:
|
||||
node-version: '24'
|
||||
node-version: '26'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -35,24 +35,24 @@ jobs:
|
||||
permission-pull-requests: write
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
token: ${{ steps.create-token.outputs.token }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271
|
||||
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
||||
with:
|
||||
node-version: "24"
|
||||
node-version: "26"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
permission-pull-requests: write
|
||||
|
||||
- name: Checkout Weblate branch
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
token: ${{ steps.create-token.outputs.token }}
|
||||
ref: ${{ env.WEBLATE_BRANCH }}
|
||||
@@ -48,17 +48,17 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271
|
||||
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
||||
with:
|
||||
node-version: "24"
|
||||
node-version: "26"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
permission-pull-requests: write
|
||||
|
||||
- name: Label pull request
|
||||
uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213
|
||||
uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13
|
||||
with:
|
||||
repo-token: ${{ steps.create-token.outputs.token }}
|
||||
configuration-path: .github/labeller.yaml
|
||||
|
||||
@@ -56,15 +56,15 @@ jobs:
|
||||
contents: write
|
||||
packages: read
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
|
||||
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee
|
||||
toolchain: "1.98.1"
|
||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069
|
||||
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -28,12 +28,12 @@ jobs:
|
||||
FLUXER_CI_BIN: ${{ github.workspace }}/target/debug/fluxer-ci
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
targets: wasm32-unknown-unknown
|
||||
|
||||
- name: Restore ci helper
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
with:
|
||||
path: target/debug/fluxer-ci
|
||||
key: >-
|
||||
fluxer-ci-bin-${{ runner.os }}-1.93.0-${{ hashFiles('Cargo.lock', 'Cargo.toml',
|
||||
fluxer-ci-bin-${{ runner.os }}-1.98.1-${{ hashFiles('Cargo.lock', 'Cargo.toml',
|
||||
'tools/ci/Cargo.toml', 'tools/ci/src/**', 'tools/ci/templates/**') }}
|
||||
|
||||
- name: Build ci helper
|
||||
@@ -55,16 +55,16 @@ jobs:
|
||||
with:
|
||||
path: target/debug/fluxer-ci
|
||||
key: >-
|
||||
fluxer-ci-bin-${{ runner.os }}-1.93.0-${{ hashFiles('Cargo.lock', 'Cargo.toml',
|
||||
fluxer-ci-bin-${{ runner.os }}-1.98.1-${{ hashFiles('Cargo.lock', 'Cargo.toml',
|
||||
'tools/ci/Cargo.toml', 'tools/ci/src/**', 'tools/ci/templates/**') }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271
|
||||
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
||||
with:
|
||||
node-version: '24'
|
||||
node-version: '26'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -83,12 +83,12 @@ jobs:
|
||||
PNPM_TEST_WORKSPACE_CONCURRENCY: '2'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
targets: wasm32-unknown-unknown
|
||||
|
||||
- name: Restore ci helper
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
with:
|
||||
path: target/debug/fluxer-ci
|
||||
key: >-
|
||||
fluxer-ci-bin-${{ runner.os }}-1.93.0-${{ hashFiles('Cargo.lock', 'Cargo.toml',
|
||||
fluxer-ci-bin-${{ runner.os }}-1.98.1-${{ hashFiles('Cargo.lock', 'Cargo.toml',
|
||||
'tools/ci/Cargo.toml', 'tools/ci/src/**', 'tools/ci/templates/**') }}
|
||||
|
||||
- name: Build ci helper
|
||||
@@ -105,12 +105,12 @@ jobs:
|
||||
run: cargo build --locked --package fluxer-ci
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271
|
||||
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
||||
with:
|
||||
node-version: '24'
|
||||
node-version: '26'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -123,12 +123,16 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
fluxer_app/pkgs/libfluxcore
|
||||
fluxer_app/pkgs/libfluxwebp
|
||||
fluxer_app/src/features/messaging/utils/markdown/parser/MarkdownParserWasmBytes.ts
|
||||
key: >-
|
||||
app-wasm-${{ runner.os }}-1.93.0-${{ hashFiles('Cargo.lock', 'tools/ci/src/app_wasm.rs',
|
||||
app-wasm-${{ runner.os }}-1.98.1-${{ hashFiles('Cargo.lock', 'tools/ci/src/app_wasm.rs',
|
||||
'tools/ci/templates/libfluxcore_wrapper.js', 'tools/ci/templates/libfluxcore_wrapper.d.ts',
|
||||
'fluxer_app/rust/libfluxcore/Cargo.toml', 'fluxer_app/rust/libfluxcore/Cargo.lock',
|
||||
'fluxer_app/rust/libfluxcore/.cargo/config.toml', 'fluxer_app/rust/libfluxcore/src/**',
|
||||
'fluxer_app/rust/libfluxwebp/Cargo.toml', 'fluxer_app/rust/libfluxwebp/Cargo.lock',
|
||||
'fluxer_app/rust/libfluxwebp/src/**', 'fluxer_app/rust/libfluxwebp/shim/**',
|
||||
'fluxer_app/rust/libfluxwebp/simd/**',
|
||||
'packages/markdown_parser/rust/Cargo.toml', 'packages/markdown_parser/rust/.cargo/config.toml',
|
||||
'packages/markdown_parser/rust/src/**') }}
|
||||
|
||||
@@ -142,12 +146,16 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
fluxer_app/pkgs/libfluxcore
|
||||
fluxer_app/pkgs/libfluxwebp
|
||||
fluxer_app/src/features/messaging/utils/markdown/parser/MarkdownParserWasmBytes.ts
|
||||
key: >-
|
||||
app-wasm-${{ runner.os }}-1.93.0-${{ hashFiles('Cargo.lock', 'tools/ci/src/app_wasm.rs',
|
||||
app-wasm-${{ runner.os }}-1.98.1-${{ hashFiles('Cargo.lock', 'tools/ci/src/app_wasm.rs',
|
||||
'tools/ci/templates/libfluxcore_wrapper.js', 'tools/ci/templates/libfluxcore_wrapper.d.ts',
|
||||
'fluxer_app/rust/libfluxcore/Cargo.toml', 'fluxer_app/rust/libfluxcore/Cargo.lock',
|
||||
'fluxer_app/rust/libfluxcore/.cargo/config.toml', 'fluxer_app/rust/libfluxcore/src/**',
|
||||
'fluxer_app/rust/libfluxwebp/Cargo.toml', 'fluxer_app/rust/libfluxwebp/Cargo.lock',
|
||||
'fluxer_app/rust/libfluxwebp/src/**', 'fluxer_app/rust/libfluxwebp/shim/**',
|
||||
'fluxer_app/rust/libfluxwebp/simd/**',
|
||||
'packages/markdown_parser/rust/Cargo.toml', 'packages/markdown_parser/rust/.cargo/config.toml',
|
||||
'packages/markdown_parser/rust/src/**') }}
|
||||
|
||||
@@ -156,21 +164,21 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
components: clippy, rustfmt
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271
|
||||
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
||||
with:
|
||||
node-version: '24'
|
||||
node-version: '26'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache cargo
|
||||
@@ -185,11 +193,14 @@ jobs:
|
||||
rust-${{ runner.os }}-${{ hashFiles('fluxer_media_proxy/tools/install-native-deps.sh') }}-
|
||||
|
||||
- name: Install cargo-deny
|
||||
run: cargo install cargo-deny --version 0.19.6 --locked
|
||||
run: cargo install cargo-deny --version 0.20.2 --locked
|
||||
|
||||
- name: Check Rust dependencies
|
||||
run: cargo deny --locked check -D warnings
|
||||
|
||||
- name: Check libfluxwebp dependencies
|
||||
run: cargo deny --manifest-path fluxer_app/rust/libfluxwebp/Cargo.toml --config deny.toml --locked check licenses bans sources
|
||||
|
||||
- name: Check desktop native dependencies
|
||||
run: tools/ci/check-desktop-native-workspaces.sh dependencies
|
||||
|
||||
@@ -242,6 +253,9 @@ jobs:
|
||||
- name: Check formatting
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
- name: Check formatting (libfluxwebp)
|
||||
run: cargo fmt --manifest-path fluxer_app/rust/libfluxwebp/Cargo.toml -- --check
|
||||
|
||||
- name: Check formatting (desktop native workspaces)
|
||||
run: tools/ci/check-desktop-native-workspaces.sh fmt
|
||||
|
||||
@@ -273,12 +287,12 @@ jobs:
|
||||
FLUXER_CI_BIN: ${{ github.workspace }}/target/debug/fluxer-ci
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
|
||||
- name: Cache cargo (gateway NIFs)
|
||||
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
|
||||
@@ -294,7 +308,7 @@ jobs:
|
||||
with:
|
||||
path: target/debug/fluxer-ci
|
||||
key: >-
|
||||
fluxer-ci-bin-${{ runner.os }}-1.93.0-${{ hashFiles('Cargo.lock', 'Cargo.toml',
|
||||
fluxer-ci-bin-${{ runner.os }}-1.98.1-${{ hashFiles('Cargo.lock', 'Cargo.toml',
|
||||
'tools/ci/Cargo.toml', 'tools/ci/src/**', 'tools/ci/templates/**') }}
|
||||
|
||||
- name: Build ci helper
|
||||
@@ -305,7 +319,7 @@ jobs:
|
||||
uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124
|
||||
with:
|
||||
otp-version: '28'
|
||||
rebar3-version: '3.24.0'
|
||||
rebar3-version: '3.27.0'
|
||||
|
||||
- name: Restore rebar3 dependencies
|
||||
id: rebar3-cache
|
||||
@@ -317,10 +331,10 @@ jobs:
|
||||
!fluxer_gateway/_build/default/lib/fluxer_gateway/**
|
||||
!fluxer_gateway/_build/test/lib/fluxer_gateway/**
|
||||
key: >-
|
||||
rebar3-${{ runner.os }}-otp28-rebar3.24.0-${{ hashFiles('fluxer_gateway/rebar.lock',
|
||||
rebar3-${{ runner.os }}-otp28-rebar3.27.0-${{ hashFiles('fluxer_gateway/rebar.lock',
|
||||
'fluxer_gateway/rebar.config') }}
|
||||
restore-keys: |
|
||||
rebar3-${{ runner.os }}-otp28-rebar3.24.0-
|
||||
rebar3-${{ runner.os }}-otp28-rebar3.27.0-
|
||||
|
||||
- name: Check formatting
|
||||
run: |
|
||||
@@ -348,7 +362,7 @@ jobs:
|
||||
!fluxer_gateway/_build/default/lib/fluxer_gateway/**
|
||||
!fluxer_gateway/_build/test/lib/fluxer_gateway/**
|
||||
key: >-
|
||||
rebar3-${{ runner.os }}-otp28-rebar3.24.0-${{ hashFiles('fluxer_gateway/rebar.lock',
|
||||
rebar3-${{ runner.os }}-otp28-rebar3.27.0-${{ hashFiles('fluxer_gateway/rebar.lock',
|
||||
'fluxer_gateway/rebar.config') }}
|
||||
|
||||
knip:
|
||||
@@ -358,12 +372,12 @@ jobs:
|
||||
FLUXER_CI_BIN: ${{ github.workspace }}/target/debug/fluxer-ci
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
targets: wasm32-unknown-unknown
|
||||
|
||||
- name: Restore ci helper
|
||||
@@ -372,7 +386,7 @@ jobs:
|
||||
with:
|
||||
path: target/debug/fluxer-ci
|
||||
key: >-
|
||||
fluxer-ci-bin-${{ runner.os }}-1.93.0-${{ hashFiles('Cargo.lock', 'Cargo.toml',
|
||||
fluxer-ci-bin-${{ runner.os }}-1.98.1-${{ hashFiles('Cargo.lock', 'Cargo.toml',
|
||||
'tools/ci/Cargo.toml', 'tools/ci/src/**', 'tools/ci/templates/**') }}
|
||||
|
||||
- name: Build ci helper
|
||||
@@ -380,12 +394,12 @@ jobs:
|
||||
run: cargo build --locked --package fluxer-ci
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271
|
||||
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
||||
with:
|
||||
node-version: '24'
|
||||
node-version: '26'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -398,12 +412,16 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
fluxer_app/pkgs/libfluxcore
|
||||
fluxer_app/pkgs/libfluxwebp
|
||||
fluxer_app/src/features/messaging/utils/markdown/parser/MarkdownParserWasmBytes.ts
|
||||
key: >-
|
||||
app-wasm-${{ runner.os }}-1.93.0-${{ hashFiles('Cargo.lock', 'tools/ci/src/app_wasm.rs',
|
||||
app-wasm-${{ runner.os }}-1.98.1-${{ hashFiles('Cargo.lock', 'tools/ci/src/app_wasm.rs',
|
||||
'tools/ci/templates/libfluxcore_wrapper.js', 'tools/ci/templates/libfluxcore_wrapper.d.ts',
|
||||
'fluxer_app/rust/libfluxcore/Cargo.toml', 'fluxer_app/rust/libfluxcore/Cargo.lock',
|
||||
'fluxer_app/rust/libfluxcore/.cargo/config.toml', 'fluxer_app/rust/libfluxcore/src/**',
|
||||
'fluxer_app/rust/libfluxwebp/Cargo.toml', 'fluxer_app/rust/libfluxwebp/Cargo.lock',
|
||||
'fluxer_app/rust/libfluxwebp/src/**', 'fluxer_app/rust/libfluxwebp/shim/**',
|
||||
'fluxer_app/rust/libfluxwebp/simd/**',
|
||||
'packages/markdown_parser/rust/Cargo.toml', 'packages/markdown_parser/rust/.cargo/config.toml',
|
||||
'packages/markdown_parser/rust/src/**') }}
|
||||
|
||||
@@ -417,12 +435,16 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
fluxer_app/pkgs/libfluxcore
|
||||
fluxer_app/pkgs/libfluxwebp
|
||||
fluxer_app/src/features/messaging/utils/markdown/parser/MarkdownParserWasmBytes.ts
|
||||
key: >-
|
||||
app-wasm-${{ runner.os }}-1.93.0-${{ hashFiles('Cargo.lock', 'tools/ci/src/app_wasm.rs',
|
||||
app-wasm-${{ runner.os }}-1.98.1-${{ hashFiles('Cargo.lock', 'tools/ci/src/app_wasm.rs',
|
||||
'tools/ci/templates/libfluxcore_wrapper.js', 'tools/ci/templates/libfluxcore_wrapper.d.ts',
|
||||
'fluxer_app/rust/libfluxcore/Cargo.toml', 'fluxer_app/rust/libfluxcore/Cargo.lock',
|
||||
'fluxer_app/rust/libfluxcore/.cargo/config.toml', 'fluxer_app/rust/libfluxcore/src/**',
|
||||
'fluxer_app/rust/libfluxwebp/Cargo.toml', 'fluxer_app/rust/libfluxwebp/Cargo.lock',
|
||||
'fluxer_app/rust/libfluxwebp/src/**', 'fluxer_app/rust/libfluxwebp/shim/**',
|
||||
'fluxer_app/rust/libfluxwebp/simd/**',
|
||||
'packages/markdown_parser/rust/Cargo.toml', 'packages/markdown_parser/rust/.cargo/config.toml',
|
||||
'packages/markdown_parser/rust/src/**') }}
|
||||
|
||||
@@ -431,15 +453,15 @@ jobs:
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271
|
||||
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
||||
with:
|
||||
node-version: '24'
|
||||
node-version: '26'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -456,15 +478,15 @@ jobs:
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271
|
||||
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
||||
with:
|
||||
node-version: '24'
|
||||
node-version: '26'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -490,15 +512,15 @@ jobs:
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271
|
||||
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
||||
with:
|
||||
node-version: '24'
|
||||
node-version: '26'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -515,12 +537,12 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
|
||||
with:
|
||||
python-version: "3.13"
|
||||
python-version: "3.14"
|
||||
|
||||
- name: Install font tooling
|
||||
run: python3 -m pip install -r tools/fonts/requirements.txt
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
/.direnv/
|
||||
/.fluxer/
|
||||
/.pnpm-store/
|
||||
/.vscode/
|
||||
|
||||
**/*.css.d.ts
|
||||
**/*.tsbuildinfo
|
||||
@@ -25,6 +26,7 @@
|
||||
|
||||
/fluxer_app/.devserver-cache.json
|
||||
/fluxer_app/pkgs/libfluxcore/
|
||||
/fluxer_app/pkgs/libfluxwebp/
|
||||
/fluxer_app/src/features/i18n/locales/*/messages.mjs
|
||||
/fluxer_app/src/features/messaging/utils/markdown/parser/MarkdownParserWasmBytes.ts
|
||||
/fluxer_app/src/features/theme/styles/generated/
|
||||
|
||||
Generated
+850
-859
File diff suppressed because it is too large
Load Diff
+1
-2
@@ -7,15 +7,14 @@ members = [
|
||||
"fluxer_gifs",
|
||||
"fluxer_svc",
|
||||
"fluxer_messages",
|
||||
"fluxer_push",
|
||||
"fluxer_snowflakes",
|
||||
"tools/ci",
|
||||
"tools/content/update-frozen-snapshot",
|
||||
"tools/dev",
|
||||
"tools/i18n_auto",
|
||||
"fluxer_users",
|
||||
"fluxer_unfurl",
|
||||
"packages/markdown_parser/rust",
|
||||
"fluxer_recon",
|
||||
]
|
||||
exclude = [
|
||||
"packages/markdown_parser/rust/fuzz",
|
||||
|
||||
@@ -6,18 +6,172 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://fluxer.app/donate">
|
||||
<img src="https://img.shields.io/badge/Donate-fluxer.app%2Fdonate-brightgreen" alt="Donate" /></a>
|
||||
<a href="https://fluxer.app/download">
|
||||
<img src="https://img.shields.io/badge/Download-fluxer.app-4641D9" alt="Download" /></a>
|
||||
<a href="https://docs.fluxer.app">
|
||||
<img src="https://img.shields.io/badge/Docs-docs.fluxer.app-blue" alt="Documentation" /></a>
|
||||
<a href="https://fluxer.app/donate">
|
||||
<img src="https://img.shields.io/badge/Donate-fluxer.app%2Fdonate-brightgreen" alt="Donate" /></a>
|
||||
<a href="./LICENSE">
|
||||
<img src="https://img.shields.io/badge/License-AGPLv3-purple" alt="AGPLv3 License" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://flathub.org/apps/app.fluxer.Fluxer">
|
||||
<img src="https://dl.flathub.org/assets/badges/flathub-badge-en.svg" alt="Get it on Flathub" height="60" /></a>
|
||||
</p>
|
||||
|
||||
# Fluxer
|
||||
|
||||
Fluxer is a free and open source instant messaging and VoIP chat app built for friends, groups, and communities.
|
||||
|
||||
<p align="center">
|
||||
<img src="./fluxer_static/marketing/screenshots/desktop-readme-1920w.png" alt="Fluxer app showcase" width="900">
|
||||
<img src="./fluxer_static/marketing/screenshots/desktop-readme-1920w.png" alt="Fluxer running side by side on a desktop monitor and a phone" width="640">
|
||||
</p>
|
||||
|
||||
## Download
|
||||
|
||||
| Windows | macOS | Linux | Android | iOS |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [Installer (x64)][win-setup-x64] | [Disk image][mac-dmg] | [Flathub][flathub] | [APK][android-apk] | [TestFlight][ios-testflight] |
|
||||
| [Installer (ARM64)][win-setup-arm64] | | [deb (x64)][linux-deb-x64] | [Obtainium][obtainium] | |
|
||||
| [Portable (x64)][win-portable-x64] | | [deb (ARM64)][linux-deb-arm64] | | |
|
||||
| [Portable (ARM64)][win-portable-arm64] | | [rpm (x64)][linux-rpm-x64] | | |
|
||||
| | | [rpm (ARM64)][linux-rpm-arm64] | | |
|
||||
| | | [AppImage (x64)][linux-appimage-x64] | | |
|
||||
| | | [AppImage (ARM64)][linux-appimage-arm64] | | |
|
||||
| | | [tar.gz (x64)][linux-targz-x64] | | |
|
||||
| | | [tar.gz (ARM64)][linux-targz-arm64] | | |
|
||||
|
||||
The macOS disk image runs on both Apple silicon and Intel. Windows and Linux need the build matching your processor.
|
||||
|
||||
On Linux, prefer a repository over a single file so Fluxer updates with the rest of your system.
|
||||
|
||||
## Linux package repositories
|
||||
|
||||
The package is `fluxer` for stable and `fluxer-canary` for canary. apt and dnf subscribe to one channel per entry file. pacman and Flatpak serve both from one repository.
|
||||
|
||||
### Flatpak
|
||||
|
||||
Stable is on [Flathub][flathub], the easiest route on most desktops:
|
||||
|
||||
```sh
|
||||
flatpak install flathub app.fluxer.Fluxer
|
||||
```
|
||||
|
||||
Flathub has stable only. To use Fluxer's own repository, open [the stable][flatpak-ref] or [the canary][flatpak-canary-ref] reference file and your software manager takes over. Some desktops also accept `flatpak+https://pkgs.fluxer.com/flatpak/fluxer.flatpakref` in the address bar.
|
||||
|
||||
From a terminal:
|
||||
|
||||
```sh
|
||||
flatpak install https://pkgs.fluxer.com/flatpak/fluxer.flatpakref
|
||||
```
|
||||
|
||||
### Debian and Ubuntu
|
||||
|
||||
```sh
|
||||
sudo install -d -m 0755 /etc/apt/keyrings
|
||||
sudo curl -fsSL -o /etc/apt/keyrings/fluxer-archive-keyring.gpg https://pkgs.fluxer.com/keys/fluxer-archive-keyring.gpg
|
||||
sudo curl -fsSL -o /etc/apt/sources.list.d/fluxer.sources https://pkgs.fluxer.com/deb/fluxer.sources
|
||||
sudo apt update && sudo apt install fluxer
|
||||
```
|
||||
|
||||
For canary, use the canary entry file and package.
|
||||
|
||||
```sh
|
||||
sudo curl -fsSL -o /etc/apt/sources.list.d/fluxer-canary.sources https://pkgs.fluxer.com/deb/fluxer-canary.sources
|
||||
sudo apt update && sudo apt install fluxer-canary
|
||||
```
|
||||
|
||||
A `.deb` installed from a download only updates once its channel's entry is added.
|
||||
|
||||
### Fedora and RHEL
|
||||
|
||||
```sh
|
||||
sudo curl -fsSL -o /etc/yum.repos.d/fluxer.repo https://pkgs.fluxer.com/rpm/fluxer.repo
|
||||
sudo dnf install fluxer
|
||||
```
|
||||
|
||||
For canary, use the canary entry file and package.
|
||||
|
||||
```sh
|
||||
sudo curl -fsSL -o /etc/yum.repos.d/fluxer-canary.repo https://pkgs.fluxer.com/rpm/fluxer-canary.repo
|
||||
sudo dnf install fluxer-canary
|
||||
```
|
||||
|
||||
RHEL, Rocky, Alma and CentOS Stream need `sudo dnf install epel-release` first, because their base repositories lack `libXScrnSaver`. Fedora does not.
|
||||
|
||||
### Arch Linux
|
||||
|
||||
The repository is signed, so pacman needs the key once:
|
||||
|
||||
```sh
|
||||
sudo pacman-key --init
|
||||
curl -fsSL -o /tmp/fluxer-archive-keyring.asc https://pkgs.fluxer.com/keys/fluxer-archive-keyring.asc
|
||||
sudo pacman-key --add /tmp/fluxer-archive-keyring.asc
|
||||
sudo pacman-key --lsign-key 09D01339EE128925F75E675C855C5BDE34D205D2
|
||||
```
|
||||
|
||||
`--lsign-key` is what makes pacman trust it. Then add the repository:
|
||||
|
||||
```sh
|
||||
sudo tee -a /etc/pacman.conf >/dev/null <<'REPO'
|
||||
|
||||
[fluxer]
|
||||
SigLevel = Required TrustedOnly
|
||||
Server = https://pkgs.fluxer.com/arch/$repo/os/$arch
|
||||
REPO
|
||||
sudo pacman -Syu fluxer
|
||||
```
|
||||
|
||||
Write `$repo` and `$arch` literally. Both are pacman variables, not shell ones, hence the quoted heredoc.
|
||||
|
||||
Full setup notes, including canary, are in the [Linux repositories documentation][docs-linux].
|
||||
|
||||
## Other ways to run it
|
||||
|
||||
- [Open Fluxer in a browser](https://web.fluxer.app), no install needed.
|
||||
- [Host your own instance][docs-selfhost] from this repository.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Documentation home][docs]
|
||||
- [Downloads][docs-downloads]
|
||||
- [Self-hosting][docs-selfhost]
|
||||
|
||||
## License
|
||||
|
||||
The source is licensed under the [AGPL-3.0-or-later](./LICENSE) license.
|
||||
|
||||
Fluxer branding, icons, default avatars, badge artwork, screenshots and marketing
|
||||
imagery are copyright Fluxer, all rights reserved, as set out in
|
||||
[fluxer_static/LICENSE](./fluxer_static/LICENSE). Third-party material keeps its own
|
||||
terms, listed in
|
||||
[fluxer_static/THIRD_PARTY_LICENSES.md](./fluxer_static/THIRD_PARTY_LICENSES.md).
|
||||
|
||||
Public availability of this repository does not grant trademark, brand, or
|
||||
endorsement rights.
|
||||
|
||||
[win-setup-x64]: https://pkgs.fluxer.com/desktop/stable/win32/x64/latest/setup
|
||||
[win-setup-arm64]: https://pkgs.fluxer.com/desktop/stable/win32/arm64/latest/setup
|
||||
[win-portable-x64]: https://pkgs.fluxer.com/desktop/stable/win32/x64/latest/portable
|
||||
[win-portable-arm64]: https://pkgs.fluxer.com/desktop/stable/win32/arm64/latest/portable
|
||||
[mac-dmg]: https://pkgs.fluxer.com/desktop/stable/darwin/arm64/latest/dmg
|
||||
[linux-deb-x64]: https://pkgs.fluxer.com/desktop/stable/linux/x64/latest/deb
|
||||
[linux-deb-arm64]: https://pkgs.fluxer.com/desktop/stable/linux/arm64/latest/deb
|
||||
[linux-rpm-x64]: https://pkgs.fluxer.com/desktop/stable/linux/x64/latest/rpm
|
||||
[linux-rpm-arm64]: https://pkgs.fluxer.com/desktop/stable/linux/arm64/latest/rpm
|
||||
[linux-appimage-x64]: https://pkgs.fluxer.com/desktop/stable/linux/x64/latest/appimage
|
||||
[linux-appimage-arm64]: https://pkgs.fluxer.com/desktop/stable/linux/arm64/latest/appimage
|
||||
[linux-targz-x64]: https://pkgs.fluxer.com/desktop/stable/linux/x64/latest/tar_gz
|
||||
[linux-targz-arm64]: https://pkgs.fluxer.com/desktop/stable/linux/arm64/latest/tar_gz
|
||||
[flatpak-ref]: https://pkgs.fluxer.com/flatpak/fluxer.flatpakref
|
||||
[flatpak-canary-ref]: https://pkgs.fluxer.com/flatpak/fluxer-canary.flatpakref
|
||||
[flathub]: https://flathub.org/apps/app.fluxer.Fluxer
|
||||
[android-apk]: https://github.com/fluxerapp/flutter_client/releases
|
||||
[obtainium]: https://obtainium.imranr.dev/
|
||||
[ios-testflight]: https://testflight.apple.com/join/PKZR6pK9
|
||||
[docs]: https://docs.fluxer.app
|
||||
[docs-downloads]: https://docs.fluxer.app/downloads/overview/
|
||||
[docs-linux]: https://docs.fluxer.app/downloads/linux-repositories/
|
||||
[docs-selfhost]: https://docs.fluxer.app/operator/get-started/
|
||||
|
||||
+3
-2
@@ -48,7 +48,7 @@
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"preset": "recommended",
|
||||
"complexity": {
|
||||
"noForEach": "off",
|
||||
"noImportantStyles": "off",
|
||||
@@ -83,6 +83,7 @@
|
||||
}
|
||||
},
|
||||
"useConst": "error",
|
||||
"noDescendingSpecificity": "off",
|
||||
"noNonNullAssertion": "off",
|
||||
"noParameterAssign": "off",
|
||||
"noRestrictedImports": {
|
||||
@@ -98,7 +99,7 @@
|
||||
}
|
||||
},
|
||||
"a11y": {
|
||||
"recommended": true,
|
||||
"preset": "recommended",
|
||||
"useAriaPropsForRole": "error",
|
||||
"useValidAriaRole": "error",
|
||||
"useValidAriaValues": "error",
|
||||
|
||||
Vendored
+1
-6
@@ -52,7 +52,6 @@ FLUXER_S3_SECRET_ACCESS_KEY=fluxer-secret
|
||||
FLUXER_S3_FORCE_PATH_STYLE=true
|
||||
FLUXER_S3_BUCKET_CDN=fluxer
|
||||
FLUXER_S3_BUCKET_UPLOADS=fluxer-uploads
|
||||
FLUXER_S3_BUCKET_DOWNLOADS=fluxer-downloads
|
||||
FLUXER_S3_BUCKET_REPORTS=fluxer-reports
|
||||
FLUXER_S3_BUCKET_HARVESTS=fluxer-harvests
|
||||
FLUXER_S3_BUCKET_STATIC=fluxer-static
|
||||
@@ -65,11 +64,6 @@ FLUXER_LIVEKIT_API_SECRET=fluxer-livekit-development-secret
|
||||
FLUXER_LIVEKIT_WEBHOOK_URL=http://localhost:8088/api/webhooks/livekit
|
||||
FLUXER_LIVEKIT_DEFAULT_REGION={"id":"local","name":"Local","emoji":"LC","latitude":59.3293,"longitude":18.0686}
|
||||
|
||||
FLUXER_RECON_MODE=observing
|
||||
FLUXER_RECON_EXPECTED_ROOMS=64
|
||||
FLUXER_RECON_WARMUP_SECONDS=15
|
||||
FLUXER_RECON_MAX_HOT_ROOMS=8
|
||||
|
||||
FLUXER_API_PORT=8080
|
||||
FLUXER_API_PRESIGNED_ATTACHMENT_UPLOADS_ENABLED=true
|
||||
FLUXER_API_WORKER_MODE=all_lanes
|
||||
@@ -135,6 +129,7 @@ PUBLIC_RELEASE_CHANNEL=canary
|
||||
PUBLIC_BOOTSTRAP_API_ENDPOINT=/api
|
||||
PUBLIC_BOOTSTRAP_API_PUBLIC_ENDPOINT=http://localhost:8088/api
|
||||
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64=Zmx1eGVyLWRldi11cGxvYWQtcmVsYXktc2VjcmV0LTAwMDA=
|
||||
FLUXER_MEDIA_PROXY_ATTACHMENT_URL_SECRETS_BASE64=Zmx1eGVyLWRldi1hdHRhY2htZW50LXVybC1zZWNyZXQ=
|
||||
AWS_EC2_METADATA_DISABLED=true
|
||||
AWS_ACCESS_KEY_ID=fluxer
|
||||
AWS_SECRET_ACCESS_KEY=fluxer-secret
|
||||
|
||||
@@ -79,34 +79,42 @@ deny = [
|
||||
{ crate = "fuse-sys", reason = "libfuse2 FFI crate; Fluxer AppImages must not reintroduce libfuse2 through native Rust dependencies" },
|
||||
]
|
||||
skip = [
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older digest API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older digest API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older crypto API" },
|
||||
{ crate = "[email protected].7", reason = "transitive dependency requires the older digest API" },
|
||||
{ crate = "[email protected].6", reason = "transitive dependency requires the older digest API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older digest API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older hashbrown API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older randomness API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the prior randomness API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older hashbrown API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older hashbrown API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the prior hashbrown API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older digest API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older HTTP API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older HTTP body API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older WASI API" },
|
||||
{ crate = "[email protected].6", reason = "transitive dependency requires the older randomness API" },
|
||||
{ crate = "[email protected].4", reason = "transitive dependency requires the prior randomness API" },
|
||||
{ crate = "[email protected].8", reason = "transitive dependency requires the older randomness API" },
|
||||
{ crate = "[email protected].5", reason = "transitive dependency requires the prior randomness API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older randomness API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the prior randomness API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older randomness API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the prior randomness API" },
|
||||
{ crate = "[email protected].6", reason = "transitive dependency requires the older digest API" },
|
||||
{ crate = "[email protected].7", reason = "transitive dependency requires the older digest API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older digest API" },
|
||||
{ crate = "s[email protected]0", reason = "transitive dependency requires the older socket API" },
|
||||
{ crate = "s[email protected].0", reason = "transitive dependency requires the older release line" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
|
||||
{ crate = "[email protected]+wasi-snapshot-preview1", reason = "transitive dependency requires the legacy WASI API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older Windows API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the prior Windows API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older WASI binding API" },
|
||||
]
|
||||
skip-tree = []
|
||||
|
||||
|
||||
+117
-186
@@ -1,108 +1,64 @@
|
||||
# Every variable docker-compose.yml reads is named here, uncommented when it has
|
||||
# no default and commented with its default when it has one. A name absent from
|
||||
# this file reaches a service only through a Compose override. Compose expands
|
||||
# top to bottom, so a line using ${...} must sit below every name it reads.
|
||||
# Every variable docker-compose.yml reads, uncommented when it has no default and
|
||||
# commented with its default when it has one. Compose expands top to bottom, so a
|
||||
# line using ${...} must sit below every name it reads.
|
||||
|
||||
FLUXER_DOMAIN=chat.example.com
|
||||
FLUXER_PUBLIC_SCHEME=https
|
||||
FLUXER_PUBLIC_PORT=443
|
||||
|
||||
# The lines above are the address browsers use, and every advertised endpoint
|
||||
# carries FLUXER_PUBLIC_PORT. They do not move what the host publishes.
|
||||
# FLUXER_HTTP_PORT and FLUXER_HTTPS_PORT below do that, and a non-default port
|
||||
# needs the matching one set as well. Complete recipes sit beside them.
|
||||
# The address browsers use. FLUXER_HTTP_PORT and FLUXER_HTTPS_PORT below decide
|
||||
# which host ports Fluxer binds.
|
||||
|
||||
# How browsers reach this instance.
|
||||
#
|
||||
# Default: Fluxer binds 80 and 443 and gets its own Let's Encrypt certificate.
|
||||
# Point DNS at this host.
|
||||
#
|
||||
# Behind your own reverse proxy (nginx, Traefik, HAProxy, Cloudflare Tunnel,
|
||||
# another Caddy): uncomment COMPOSE_FILE below. Fluxer then serves plain HTTP on
|
||||
# 127.0.0.1:8080 instead, and your proxy forwards everything to it. Keep
|
||||
# FLUXER_PUBLIC_SCHEME and FLUXER_PUBLIC_PORT describing the PUBLIC address your
|
||||
# proxy serves, not this local port.
|
||||
# By default Fluxer binds 80 and 443 and gets its own certificate. Point DNS here.
|
||||
# Behind your own reverse proxy, uncomment this instead: Fluxer then serves plain
|
||||
# HTTP on 127.0.0.1:8080. Keep the scheme and port above describing the public
|
||||
# address, not this one.
|
||||
#COMPOSE_FILE=docker-compose.yml:docker-compose.proxy.yml
|
||||
|
||||
# Where the plain-HTTP port binds when the proxy overlay is in use. Leave it on
|
||||
# loopback when the proxy runs on this host. Use 0.0.0.0:8080 only when the proxy
|
||||
# is on another machine, and firewall the port to that machine.
|
||||
# Where that plain-HTTP port binds. Use 0.0.0.0:8080 only when the proxy is on
|
||||
# another machine, and firewall it to that machine.
|
||||
#FLUXER_EDGE_BIND=127.0.0.1:8080
|
||||
|
||||
# Which upstream hops may set X-Forwarded-For. Fluxer rewrites the header from
|
||||
# this to the real client address, so IP bans, rate limits and abuse detection
|
||||
# see the caller rather than the proxy. The default covers proxies on private or
|
||||
# loopback addresses, which is every same-host setup. Set it to your proxy's
|
||||
# address if it reaches Fluxer from a public IP.
|
||||
# Which hops may set X-Forwarded-For. The default covers private and loopback
|
||||
# addresses. Set your proxy's address if it reaches Fluxer from a public IP.
|
||||
#FLUXER_EDGE_TRUSTED_PROXIES=private_ranges
|
||||
|
||||
# The origin browsers see, without a trailing slash. Leave it unset and each
|
||||
# service builds one from the three values at the top of this file. Set it and it
|
||||
# wins: every service reads the host, the scheme and the port out of it and
|
||||
# ignores those three names. Use it when browsers reach the instance on a host
|
||||
# FLUXER_DOMAIN does not name. It has to be a bare origin, a scheme and a host
|
||||
# and an optional port and nothing after them, or the services refuse to start.
|
||||
# It does not move the edge listener or the published ports either, so set the
|
||||
# publish below to the port written here.
|
||||
# The origin browsers see, no trailing slash. Set it when browsers reach the
|
||||
# instance on a host FLUXER_DOMAIN does not name, and it wins over the three
|
||||
# values above. Scheme, host and optional port only. It does not move the
|
||||
# published ports.
|
||||
#FLUXER_PUBLIC_ORIGIN=https://chat.example.com
|
||||
|
||||
# Overrides the address the edge listens on inside its container. Compose builds
|
||||
# it from FLUXER_PUBLIC_SCHEME and FLUXER_DOMAIN with no port, and the edge keeps
|
||||
# its container ports at 80 and 443 whatever the public port is. Caddy matches a
|
||||
# site by host and ignores the port in the Host header, so a request arriving on
|
||||
# a non-default published port still lands on this site. Put a port in this value
|
||||
# only if you also publish that same container port below, or nothing will be
|
||||
# listening where the publish points. Honoured in the default mode only:
|
||||
# docker-compose.proxy.yml sets the literal :8080 and tunnel.compose.yml the
|
||||
# literal :80, and Compose lets the last file win, so a value here is discarded
|
||||
# under either overlay with no warning. Set it for an unusual default-mode
|
||||
# layout, such as serving several hostnames. Write the scheme into it: a bare
|
||||
# hostname means automatic HTTPS on 443 whatever FLUXER_PUBLIC_SCHEME says.
|
||||
# The address the edge listens on inside its container. Both proxy overlays set
|
||||
# this themselves, so a value here is ignored under either. Include the scheme.
|
||||
#FLUXER_EDGE_SITE_ADDRESS=https://chat.example.com
|
||||
|
||||
# The old name for the value above, read only when FLUXER_EDGE_SITE_ADDRESS is
|
||||
# unset, so an existing .env keeps the listener it already had.
|
||||
# The old name for the line above, read only when it is unset.
|
||||
#FLUXER_CADDY_SITE_ADDRESS=
|
||||
|
||||
# Host side of the edge's publishes, and the only names that decide which host
|
||||
# ports Fluxer binds. The container side is fixed. Container 80 carries the
|
||||
# HTTP to HTTPS redirect and the Let's Encrypt HTTP challenge under an https
|
||||
# scheme, and the site itself under an http one. Container 443 carries the TLS
|
||||
# site. FLUXER_HTTPS_PORT moves the TCP and the UDP publish together, because
|
||||
# HTTP/3 needs both on the same port. Both take an optional bind address in front
|
||||
# of the port, and 127.0.0.1 keeps the publish off every public interface. Give
|
||||
# them different host ports: the same host port on both is two publishes of one
|
||||
# port and the edge refuses to start.
|
||||
# Host ports. Container 80 handles the redirect and the certificate challenge,
|
||||
# container 443 the TLS site. FLUXER_HTTPS_PORT moves TCP and UDP together, since
|
||||
# HTTP/3 needs both. Both accept a bind address. Give them different host ports.
|
||||
#FLUXER_HTTP_PORT=80
|
||||
#FLUXER_HTTPS_PORT=443
|
||||
#FLUXER_HTTP_PORT=127.0.0.1:80
|
||||
#FLUXER_HTTPS_PORT=127.0.0.1:443
|
||||
|
||||
# HTTPS on 8443, complete. Host 80 stays published and still answers the ACME
|
||||
# challenge. Let's Encrypt only ever connects to the public 80 or 443, so the
|
||||
# certificate is issued if a router in front forwards public 80 to this host and
|
||||
# is not issued otherwise. Serve your own certificate from the Caddyfile when it
|
||||
# cannot.
|
||||
# HTTPS on 8443. Host 80 stays published for the certificate challenge, which
|
||||
# only ever arrives on public 80 or 443. Serve your own certificate if nothing
|
||||
# forwards those.
|
||||
#FLUXER_PUBLIC_PORT=8443
|
||||
#FLUXER_HTTPS_PORT=8443
|
||||
|
||||
# Plain HTTP on 19080, complete. The port 80 publish moves to 19080, so nothing
|
||||
# binds host 80. Under an http scheme nothing listens on container 443, so the
|
||||
# last line parks that publish on loopback for a host that wants 443 for
|
||||
# something else. Drop it and 443 is published and idle, which is what earlier
|
||||
# releases did.
|
||||
# Plain HTTP on 19080. Nothing binds host 80, and the last line parks the idle
|
||||
# 443 publish on loopback.
|
||||
#FLUXER_PUBLIC_SCHEME=http
|
||||
#FLUXER_PUBLIC_PORT=19080
|
||||
#FLUXER_HTTP_PORT=19080
|
||||
#FLUXER_HTTPS_PORT=127.0.0.1:443
|
||||
|
||||
# A tunnel or another proxy in front of the stack needs no HTTPS publish at all.
|
||||
# tunnel.compose.yml ships beside this file and replaces Caddy's published ports
|
||||
# with a single loopback HTTP publish, so nothing binds 443, and points the edge
|
||||
# at plain HTTP on that publish so it stops redirecting to https. FLUXER_HTTP_PORT
|
||||
# still moves that one publish. Set the line below and plain docker compose
|
||||
# commands pick the file up, or add it to your own -f flags if you pass any. The
|
||||
# file uses the !override tag, which needs Compose 2.24.4 or newer.
|
||||
# A tunnel needs no HTTPS publish. tunnel.compose.yml ships beside this file and
|
||||
# leaves one loopback HTTP publish. Needs Compose 2.24.4 or newer.
|
||||
#COMPOSE_FILE=docker-compose.yml:tunnel.compose.yml
|
||||
|
||||
FLUXER_REGISTRY_OWNER=fluxerapp
|
||||
@@ -111,11 +67,8 @@ FLUXER_IMAGE_TAG=v1
|
||||
|
||||
POSTGRES_PASSWORD=CHANGE_ME
|
||||
MEILI_MASTER_KEY=CHANGE_ME
|
||||
# The stack ships its own Postgres and its own object store, and points at both
|
||||
# by service name. Set these to run either one outside the stack. Leave them
|
||||
# unset and the bundled services are used. Taking a service out of the stack
|
||||
# means an upgrade skips the backup step that reaches into it, and backing that
|
||||
# store up belongs to whoever runs it.
|
||||
# Set these to run Postgres or the object store outside the stack. Backing up a
|
||||
# store you moved out is yours to arrange, and an upgrade skips it.
|
||||
#FLUXER_POSTGRES_HOST=db.example.com
|
||||
#FLUXER_POSTGRES_PORT=5432
|
||||
#FLUXER_POSTGRES_DATABASE=fluxer
|
||||
@@ -125,19 +78,15 @@ MEILI_MASTER_KEY=CHANGE_ME
|
||||
#FLUXER_S3_PUBLIC_ENDPOINT=https://cdn.example.com
|
||||
#FLUXER_S3_REGION=eu-central-1
|
||||
#FLUXER_S3_FORCE_PATH_STYLE=false
|
||||
# Bucket names. The bundled object store creates whichever names these hold, so
|
||||
# the two stay in step. An object store outside the stack needs the buckets to
|
||||
# Bucket names. The bundled store creates these. An outside store needs them to
|
||||
# exist already.
|
||||
#FLUXER_S3_BUCKET_CDN=fluxer
|
||||
#FLUXER_S3_BUCKET_UPLOADS=fluxer-uploads
|
||||
#FLUXER_S3_BUCKET_DOWNLOADS=fluxer-downloads
|
||||
#FLUXER_S3_BUCKET_REPORTS=fluxer-reports
|
||||
#FLUXER_S3_BUCKET_HARVESTS=fluxer-harvests
|
||||
|
||||
# The rest of the bundled services, pointed somewhere else the same way. Leave a
|
||||
# line unset and the service in the stack is used. Taking a service out of the
|
||||
# stack goes in an override file listed in COMPOSE_FILE, because an upgrade
|
||||
# replaces docker-compose.yml.
|
||||
# The other bundled services, pointed elsewhere. Removing a service from the
|
||||
# stack belongs in an override file, since an upgrade replaces docker-compose.yml.
|
||||
#FLUXER_KV_URL=redis://cache.example.com:6379/0
|
||||
#FLUXER_NATS_URL=nats://mq.example.com:4222
|
||||
#FLUXER_NATS_JETSTREAM_URL=nats://mq.example.com:4222
|
||||
@@ -145,24 +94,27 @@ MEILI_MASTER_KEY=CHANGE_ME
|
||||
#FLUXER_SEARCH_URL=https://search.example.com
|
||||
#FLUXER_LIVEKIT_INTERNAL_URL=http://livekit.example.com:7880
|
||||
|
||||
# Voice off. The livekit service still runs until an override file takes it out.
|
||||
# Voice off. The livekit service still runs until an override removes it.
|
||||
#FLUXER_LIVEKIT_ENABLED=false
|
||||
|
||||
# Optional systems, each off unless the instance is configured for it.
|
||||
# Optional systems, each off unless configured.
|
||||
#FLUXER_SMS_ENABLED=false
|
||||
#FLUXER_STRIPE_ENABLED=false
|
||||
#FLUXER_NCMEC_ENABLED=false
|
||||
#FLUXER_CLAMAV_ENABLED=false
|
||||
|
||||
# The client address. Set the header name a proxy in front actually writes, and
|
||||
# turn the trust off when nothing sits in front, because a trusted header an
|
||||
# attacker can set is a spoofed client address.
|
||||
# Outside lookups, off unless turned on. The Tor exit list comes from
|
||||
# onionoo.torproject.org and the breached password check asks
|
||||
# api.pwnedpasswords.com.
|
||||
#FLUXER_TOR_EXIT_LIST_ENABLED=true
|
||||
#FLUXER_BREACHED_PASSWORD_CHECK_ENABLED=true
|
||||
|
||||
# The client address. Name the header your proxy actually writes, and turn the
|
||||
# trust off when nothing sits in front.
|
||||
#FLUXER_CLIENT_IP_HEADER_NAME=cf-connecting-ip
|
||||
#FLUXER_TRUST_CLIENT_IP_HEADER=true
|
||||
|
||||
# How much the services write. trace, debug, info, warn, error or fatal. Every
|
||||
# service names the object storage endpoint and its addressing at info on start,
|
||||
# so a bucket that answers 404 is visible without raising this.
|
||||
# How much the services write. trace, debug, info, warn, error or fatal.
|
||||
#LOG_LEVEL=debug
|
||||
|
||||
FLUXER_S3_ACCESS_KEY=fluxer
|
||||
@@ -177,32 +129,49 @@ FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64=CHANGE_ME
|
||||
FLUXER_ADMIN_SECRET_KEY_BASE=CHANGE_ME
|
||||
FLUXER_ADMIN_OAUTH_CLIENT_SECRET=CHANGE_ME
|
||||
|
||||
# The token every service sends to NATS. The bundled NATS runs without
|
||||
# authentication, so this stays empty unless a Compose override points the stack
|
||||
# at an external NATS that requires a token. Compose forwards the name to every
|
||||
# container that connects.
|
||||
# The token every service sends to NATS. The bundled NATS needs none, so this
|
||||
# stays empty unless an override points at an external one.
|
||||
#FLUXER_NATS_AUTH_TOKEN=
|
||||
|
||||
FLUXER_VAPID_PUBLIC_KEY=CHANGE_ME
|
||||
FLUXER_VAPID_PRIVATE_KEY=CHANGE_ME
|
||||
|
||||
# The VAPID contact address defaults to admin@ followed by FLUXER_DOMAIN. Set it
|
||||
# only if that mailbox does not exist.
|
||||
# Defaults to admin@ followed by FLUXER_DOMAIN. Set it if that mailbox does not
|
||||
# exist.
|
||||
#[email protected]
|
||||
|
||||
# Passkeys follow FLUXER_DOMAIN by default. Set these only if browsers reach the
|
||||
# instance on a different host, and note that changing FLUXER_PASSKEY_RP_ID
|
||||
# invalidates every passkey already registered against the old value.
|
||||
# Passkeys follow FLUXER_DOMAIN. Set these only if browsers use another host.
|
||||
# Changing the RP ID invalidates every passkey registered against the old value.
|
||||
#FLUXER_PASSKEY_RP_ID=chat.example.com
|
||||
#FLUXER_PASSKEY_RP_NAME=Fluxer
|
||||
#FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS=https://chat.example.com
|
||||
#FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS=http://chat.example.com:19080
|
||||
|
||||
# Extra Content-Security-Policy sources, appended to the built-in ones. Set these
|
||||
# only when a browser must reach an origin the defaults do not cover, such as a
|
||||
# voice server hosted on a domain other than FLUXER_DOMAIN. Separate several
|
||||
# sources with spaces or commas. Every one of them is empty by default, and the
|
||||
# three carrying a value below are illustrations, not defaults.
|
||||
# Notification jobs the push container holds at once, 1 to 1000000.
|
||||
#FLUXER_PUSH_SERVICE_QUEUE_CAPACITY=10000
|
||||
# Provider requests the push container sends at once, 1 to 65536.
|
||||
#FLUXER_PUSH_SERVICE_SEND_CONCURRENCY=256
|
||||
|
||||
|
||||
# Optional media policies, both off by default. See the operator docs.
|
||||
#
|
||||
# CORS limits which web origins may read media. A request with no Origin is
|
||||
# always served. Add https://web.fluxer.app if people use the hosted client.
|
||||
#
|
||||
# Signatures make an attachment read need a signed URL, so a copied link stops
|
||||
# working. Needs a secret from openssl rand -base64 32, first entry signs and
|
||||
# every entry verifies.
|
||||
#
|
||||
# Each mode is off, report or enforce. Start at report. media-proxy reads these
|
||||
# at start, so apply with docker compose up -d media-proxy.
|
||||
#FLUXER_MEDIA_PROXY_CORS_MODE=enforce
|
||||
#FLUXER_MEDIA_PROXY_CORS_ALLOWED_ORIGINS=https://chat.example.com,https://web.fluxer.app
|
||||
#FLUXER_MEDIA_PROXY_ATTACHMENT_URL_SECRETS_BASE64=
|
||||
#FLUXER_MEDIA_PROXY_ATTACHMENT_SIGNATURE_MODE=enforce
|
||||
|
||||
# Extra Content-Security-Policy sources, appended to the built-in ones. Set one
|
||||
# only when a browser must reach an origin the defaults do not cover. Separate
|
||||
# several with spaces or commas. The three values below are illustrations.
|
||||
#FLUXER_CSP_EXTRA_DEFAULT_SRC=
|
||||
#FLUXER_CSP_EXTRA_CONNECT_SRC=wss://livekit.example.com:7881
|
||||
#FLUXER_CSP_EXTRA_IMG_SRC=https://cdn.example.com
|
||||
@@ -214,39 +183,29 @@ FLUXER_VAPID_PRIVATE_KEY=CHANGE_ME
|
||||
#FLUXER_CSP_EXTRA_WORKER_SRC=
|
||||
#FLUXER_CSP_EXTRA_MANIFEST_SRC=
|
||||
|
||||
# One report-uri for Content-Security-Policy violation reports. Empty leaves the
|
||||
# directive off the header.
|
||||
# One report-uri for CSP violation reports. Empty leaves the directive off.
|
||||
#FLUXER_CSP_REPORT_URI=
|
||||
|
||||
# Allow the SSO identity provider to resolve to a private or internal address.
|
||||
# Off by default: the API refuses to call non-public addresses so a misconfigured
|
||||
# provider URL cannot be used to reach internal services. Turn it on only when the
|
||||
# provider genuinely lives on your own network, such as split-horizon DNS or a LAN
|
||||
# identity provider, and only when you trust everyone who can configure SSO.
|
||||
# Let the SSO provider resolve to a private address. Off by default, so a
|
||||
# misconfigured provider URL cannot reach internal services. Turn it on only for
|
||||
# a provider on your own network.
|
||||
#FLUXER_SSO_ALLOW_PRIVATE_ADDRESSES=true
|
||||
|
||||
# Both reach LiveKit as LIVEKIT_KEYS and the webhook signing key, and the API as
|
||||
# FLUXER_LIVEKIT_API_KEY and FLUXER_LIVEKIT_API_SECRET. Change them together.
|
||||
# These reach both LiveKit and the api. Change them together.
|
||||
LIVEKIT_API_KEY=fluxer
|
||||
LIVEKIT_API_SECRET=CHANGE_ME
|
||||
|
||||
# The URL browsers use for voice signalling. Compose builds it from
|
||||
# FLUXER_PUBLIC_ORIGIN, or from FLUXER_PUBLIC_SCHEME, FLUXER_DOMAIN and
|
||||
# FLUXER_PUBLIC_PORT, as that origin followed by /livekit. The client rewrites a
|
||||
# leading http to ws itself. Set it only when LiveKit is served from another
|
||||
# host.
|
||||
# The URL browsers use for voice signalling. Built from the public origin plus
|
||||
# /livekit. Set it only when LiveKit is served from another host.
|
||||
#FLUXER_LIVEKIT_URL=
|
||||
|
||||
# Media ports. LiveKit advertises these in ICE candidates, so the host must
|
||||
# forward the same numbers.
|
||||
# Media ports. LiveKit advertises these, so forward the same numbers.
|
||||
#FLUXER_LIVEKIT_TCP_PORT=7881
|
||||
#FLUXER_LIVEKIT_UDP_PORT=7882
|
||||
|
||||
# LiveKit finds the address browsers dial by asking a STUN server. A host that
|
||||
# cannot reach one over UDP stops with "could not resolve external IP", and the
|
||||
# address is then set by hand: put it in FLUXER_LIVEKIT_NODE_IP and set
|
||||
# FLUXER_LIVEKIT_USE_EXTERNAL_IP to false. Point the STUN entries at another
|
||||
# server to keep the lookup and leave Google out of it.
|
||||
# LiveKit finds its public address over STUN. A host that cannot reach one stops
|
||||
# with "could not resolve external IP", so set the address by hand instead, or
|
||||
# point STUN elsewhere.
|
||||
#FLUXER_LIVEKIT_USE_EXTERNAL_IP=false
|
||||
#FLUXER_LIVEKIT_NODE_IP=203.0.113.10
|
||||
#FLUXER_LIVEKIT_STUN_PRIMARY=stun.l.google.com:19302
|
||||
@@ -273,11 +232,9 @@ FLUXER_CAPTCHA_TURNSTILE_SITE_KEY=
|
||||
FLUXER_CAPTCHA_TURNSTILE_SECRET_KEY=
|
||||
FLUXER_DISCOVERY_ENABLED=true
|
||||
|
||||
# Container memory. The limits sum to 18.25 GiB, which is a sum of ceilings and
|
||||
# not an allocation, so the defaults fit a host with 8 GB and are sized for 16 GB.
|
||||
# The reservations are cgroup memory.low, which biases the kernel away from
|
||||
# reclaiming from services whose death takes the instance down. They reserve
|
||||
# nothing. Lower the limits on a smaller host.
|
||||
# Container memory. These are ceilings, not allocations, and the defaults suit a
|
||||
# 16 GB host. The reservations bias the kernel away from reclaiming from services
|
||||
# whose death takes the instance down. Lower the limits on a smaller host.
|
||||
#FLUXER_CADDY_MEMORY_LIMIT=256mb
|
||||
#FLUXER_POSTGRES_MEMORY_LIMIT=5gb
|
||||
#FLUXER_POSTGRES_MEMORY_RESERVATION=3gb
|
||||
@@ -294,6 +251,7 @@ FLUXER_DISCOVERY_ENABLED=true
|
||||
#FLUXER_GATEWAY_MEMORY_LIMIT=1gb
|
||||
#FLUXER_GATEWAY_MEMORY_RESERVATION=384mb
|
||||
#FLUXER_MEDIA_PROXY_MEMORY_LIMIT=512mb
|
||||
#FLUXER_PUSH_MEMORY_LIMIT=256mb
|
||||
#FLUXER_STATIC_PROXY_MEMORY_LIMIT=256mb
|
||||
#FLUXER_APP_PROXY_MEMORY_LIMIT=256mb
|
||||
#FLUXER_SNOWFLAKES_MEMORY_LIMIT=128mb
|
||||
@@ -308,81 +266,54 @@ FLUXER_DISCOVERY_ENABLED=true
|
||||
#FLUXER_UNFURL_SHARD_MEMORY_LIMIT=256mb
|
||||
#FLUXER_ADMIN_MEMORY_LIMIT=256mb
|
||||
|
||||
# Meilisearch indexing memory. Keep it well under FLUXER_MEILISEARCH_MEMORY_LIMIT,
|
||||
# which is the container ceiling the indexer shares with the search process.
|
||||
# Meilisearch indexing memory. Keep it well under the container limit above.
|
||||
#FLUXER_MEILISEARCH_MAX_INDEXING_MEMORY=384mb
|
||||
|
||||
# SeaweedFS heap ceiling. Go collects against this value instead of against the
|
||||
# container limit, which it cannot see, so without it an upload burst grows the
|
||||
# heap past FLUXER_SEAWEEDFS_MEMORY_LIMIT and the kernel OOM-kills the container
|
||||
# mid-upload (exit 137). Keep it near three quarters of that limit, and raise both
|
||||
# together: the peak is the parts of one upload in flight at once, which is 25 MB
|
||||
# times 20 for a 500 MB attachment.
|
||||
# SeaweedFS heap ceiling. Go cannot see the container limit, so without this an
|
||||
# upload burst gets the container OOM-killed. Keep it near three quarters of
|
||||
# FLUXER_SEAWEEDFS_MEMORY_LIMIT and raise both together.
|
||||
#FLUXER_SEAWEEDFS_GOMEMLIMIT=1536MiB
|
||||
|
||||
# Node sizes its own heap from the container memory limit by default, at roughly
|
||||
# 55 percent of it, which always leaves room for the buffers and stacks that live
|
||||
# outside the heap. Leave these unset unless you have a reason to pin the value.
|
||||
# Any value set here must stay well below the container limit above: a heap ceiling
|
||||
# above the container limit makes the kernel OOM-kill the container (exit 137, no
|
||||
# diagnostics) instead of Node reporting a JavaScript heap out of memory error.
|
||||
# Node sizes its heap from the container limit by default. Leave these unset
|
||||
# unless you need to pin it. A heap ceiling above the container limit gets the
|
||||
# container OOM-killed instead of reporting a heap error.
|
||||
#FLUXER_API_NODE_HEAP_MB=1792
|
||||
#FLUXER_WORKER_NODE_HEAP_MB=1792
|
||||
|
||||
# Bundled Postgres tuning. Keep these consistent with FLUXER_POSTGRES_MEMORY_LIMIT:
|
||||
# budget roughly shared_buffers + (server max_connections x 12 MB) +
|
||||
# (3 x autovacuum_work_mem) + 300 MB for page cache and WAL. Note this is the
|
||||
# server setting, distinct from the per-service FLUXER_POSTGRES_MAX_CONNECTIONS
|
||||
# pool sizes used by the api, worker and shards.
|
||||
# Bundled Postgres tuning. Keep it consistent with the memory limit above. This
|
||||
# is the server setting, not the per-service pool sizes.
|
||||
#FLUXER_POSTGRES_SERVER_MAX_CONNECTIONS=150
|
||||
#FLUXER_POSTGRES_SHARED_BUFFERS=512MB
|
||||
#FLUXER_POSTGRES_EFFECTIVE_CACHE_SIZE=2GB
|
||||
#FLUXER_POSTGRES_WORK_MEM=8MB
|
||||
#FLUXER_POSTGRES_MAINTENANCE_WORK_MEM=256MB
|
||||
#FLUXER_POSTGRES_AUTOVACUUM_WORK_MEM=128MB
|
||||
#FLUXER_POSTGRES_SHM_SIZE=1gb
|
||||
|
||||
# The bundled Valkey holds durable state as well as cache. The bulk message
|
||||
# deletion queue and the account deletion queue are sorted sets with no expiry,
|
||||
# and nothing else stores the first of the two. It therefore runs with an
|
||||
# append-only file on a named volume and with noeviction, so an over-limit write
|
||||
# fails loudly instead of silently deleting queued work. Distributed locks all
|
||||
# carry a TTL and are not what the durability is for. Only change the policy if
|
||||
# you have moved that durable state elsewhere.
|
||||
# The bundled Valkey holds durable state as well as cache, so it runs with an
|
||||
# append-only file and with noeviction, which fails an over-limit write instead
|
||||
# of dropping queued work. Change the policy only if that state lives elsewhere.
|
||||
#FLUXER_VALKEY_MAXMEMORY=192mb
|
||||
#FLUXER_VALKEY_MAXMEMORY_POLICY=noeviction
|
||||
|
||||
# The gateway derives its BEAM scheduler count from the container CPU quota,
|
||||
# clamped to this range. The floor matters: a single scheduler lets one blocking
|
||||
# operation stall every websocket on the node. The ceiling stops a large host
|
||||
# from starting far more schedulers than the container can actually use.
|
||||
# The gateway derives its scheduler count from the CPU quota, clamped here. One
|
||||
# scheduler lets a single blocking operation stall every websocket on the node.
|
||||
#FLUXER_ERLANG_SCHEDULERS_MIN=2
|
||||
#FLUXER_ERLANG_SCHEDULERS_MAX=16
|
||||
|
||||
# In-flight request ceiling for the services Compose forwards it to: the users
|
||||
# and messages routers and their shards. Leave it unset and each service uses its
|
||||
# built-in default. Set it and the one value replaces that default on all of
|
||||
# them, so size it for the busiest. The built-in defaults are 192 for
|
||||
# messages, 320 for snowflakes and 64 elsewhere, and they govern every service
|
||||
# Compose does not forward this to. A router holds a slot for the whole round
|
||||
# trip to its shard, so this is a ceiling on requests in flight at once and not a
|
||||
# rate: too low a value does not slow requests down, it rejects them. The api
|
||||
# turns that rejection into a 503 and logs "shard rejected the request because
|
||||
# it is at its concurrency limit".
|
||||
# In-flight request ceiling for the users and messages routers and their shards.
|
||||
# One value replaces the built-in default on all of them, so size it for the
|
||||
# busiest. Too low a value rejects requests rather than slowing them, and the api
|
||||
# turns that into a 503.
|
||||
#FLUXER_SVC_MAX_CONCURRENT_REQUESTS=192
|
||||
|
||||
# The api and the Rust services name their fixed Postgres statement shapes so the
|
||||
# server can reuse their plans. Named prepared statements require a session that
|
||||
# outlives the transaction, so set this to false if you put a transaction-pooling
|
||||
# connection pooler such as PgBouncer in front of Postgres. One setting governs
|
||||
# every service. The bundled compose talks to Postgres directly, where naming is
|
||||
# a win and the default is correct.
|
||||
# Named prepared statements need a session that outlives the transaction, so set
|
||||
# this to false behind a transaction-pooling connection pooler. The bundled
|
||||
# compose talks to Postgres directly, where the default is correct.
|
||||
#FLUXER_POSTGRES_PREPARED_STATEMENTS=true
|
||||
|
||||
# The api bounds how long a client may take to send a request. The header timeout
|
||||
# covers the request line and headers only, while the request timeout covers the
|
||||
# whole exchange, so a slow uploader is bounded by the second value and not by
|
||||
# the first. Raise both if you front large uploads or serve clients on high
|
||||
# latency links. The header timeout is clamped down to the request timeout, so
|
||||
# raising it alone does nothing. Both are milliseconds, between 1000 and 3600000.
|
||||
# How long a client may take to send a request. The header timeout covers the
|
||||
# request line and headers, the request timeout the whole exchange, and the first
|
||||
# is clamped down to the second. Milliseconds, 1000 to 3600000.
|
||||
#FLUXER_API_HEADERS_TIMEOUT_MS=30000
|
||||
#FLUXER_API_REQUEST_TIMEOUT_MS=120000
|
||||
|
||||
@@ -24,6 +24,8 @@ x-fluxer-env: &fluxer-env
|
||||
FLUXER_CLIENT_IP_HEADER_NAME: ${FLUXER_CLIENT_IP_HEADER_NAME:-x-forwarded-for}
|
||||
FLUXER_API_HEADERS_TIMEOUT_MS: ${FLUXER_API_HEADERS_TIMEOUT_MS:-30000}
|
||||
FLUXER_API_REQUEST_TIMEOUT_MS: ${FLUXER_API_REQUEST_TIMEOUT_MS:-120000}
|
||||
FLUXER_TOR_EXIT_LIST_ENABLED: "${FLUXER_TOR_EXIT_LIST_ENABLED:-false}"
|
||||
FLUXER_BREACHED_PASSWORD_CHECK_ENABLED: "${FLUXER_BREACHED_PASSWORD_CHECK_ENABLED:-false}"
|
||||
|
||||
FLUXER_KV_URL: ${FLUXER_KV_URL:-redis://valkey:6379/0}
|
||||
FLUXER_NATS_URL: ${FLUXER_NATS_URL:-nats://nats:4222}
|
||||
@@ -44,7 +46,6 @@ x-fluxer-env: &fluxer-env
|
||||
FLUXER_S3_FORCE_PATH_STYLE: "${FLUXER_S3_FORCE_PATH_STYLE:-true}"
|
||||
FLUXER_S3_BUCKET_CDN: ${FLUXER_S3_BUCKET_CDN:-fluxer}
|
||||
FLUXER_S3_BUCKET_UPLOADS: ${FLUXER_S3_BUCKET_UPLOADS:-fluxer-uploads}
|
||||
FLUXER_S3_BUCKET_DOWNLOADS: ${FLUXER_S3_BUCKET_DOWNLOADS:-fluxer-downloads}
|
||||
FLUXER_S3_BUCKET_REPORTS: ${FLUXER_S3_BUCKET_REPORTS:-fluxer-reports}
|
||||
FLUXER_S3_BUCKET_HARVESTS: ${FLUXER_S3_BUCKET_HARVESTS:-fluxer-harvests}
|
||||
AWS_ACCESS_KEY_ID: ${FLUXER_S3_ACCESS_KEY:?set FLUXER_S3_ACCESS_KEY in .env}
|
||||
@@ -97,6 +98,7 @@ x-fluxer-env: &fluxer-env
|
||||
FLUXER_GATEWAY_RPC_AUTH_TOKEN: ${FLUXER_GATEWAY_RPC_AUTH_TOKEN:?set FLUXER_GATEWAY_RPC_AUTH_TOKEN in .env}
|
||||
FLUXER_MEDIA_PROXY_SECRET_KEY: ${FLUXER_MEDIA_PROXY_SECRET_KEY:?set FLUXER_MEDIA_PROXY_SECRET_KEY in .env}
|
||||
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64: ${FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64:?set FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64 in .env}
|
||||
FLUXER_MEDIA_PROXY_ATTACHMENT_URL_SECRETS_BASE64: ${FLUXER_MEDIA_PROXY_ATTACHMENT_URL_SECRETS_BASE64:-}
|
||||
FLUXER_ADMIN_SECRET_KEY_BASE: ${FLUXER_ADMIN_SECRET_KEY_BASE:?set FLUXER_ADMIN_SECRET_KEY_BASE in .env}
|
||||
FLUXER_ADMIN_OAUTH_CLIENT_SECRET: ${FLUXER_ADMIN_OAUTH_CLIENT_SECRET:?set FLUXER_ADMIN_OAUTH_CLIENT_SECRET in .env}
|
||||
|
||||
@@ -122,7 +124,7 @@ x-fluxer-svc-healthcheck: &fluxer-svc-healthcheck
|
||||
|
||||
services:
|
||||
edge:
|
||||
image: caddy:2.10-alpine
|
||||
image: caddy:2.11-alpine
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
@@ -186,7 +188,7 @@ services:
|
||||
-c autovacuum_vacuum_cost_limit=2000
|
||||
-c track_io_timing=on
|
||||
-c shared_preload_libraries=pg_stat_statements
|
||||
shm_size: 256mb
|
||||
shm_size: ${FLUXER_POSTGRES_SHM_SIZE:-1gb}
|
||||
environment:
|
||||
POSTGRES_DB: fluxer
|
||||
POSTGRES_USER: fluxer
|
||||
@@ -200,7 +202,7 @@ services:
|
||||
retries: 10
|
||||
|
||||
valkey:
|
||||
image: valkey/valkey:8.1-alpine
|
||||
image: valkey/valkey:9.1-alpine
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
@@ -236,7 +238,7 @@ services:
|
||||
retries: 10
|
||||
|
||||
meilisearch:
|
||||
image: getmeili/meilisearch:v1.12
|
||||
image: getmeili/meilisearch:v1.53
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
@@ -246,6 +248,7 @@ services:
|
||||
environment:
|
||||
MEILI_ENV: production
|
||||
MEILI_NO_ANALYTICS: "true"
|
||||
MEILI_UPGRADE_DB: "true"
|
||||
MEILI_MAX_INDEXING_MEMORY: ${FLUXER_MEILISEARCH_MAX_INDEXING_MEMORY:-384mb}
|
||||
MEILI_MASTER_KEY: ${MEILI_MASTER_KEY:?set MEILI_MASTER_KEY in .env}
|
||||
volumes:
|
||||
@@ -257,7 +260,7 @@ services:
|
||||
retries: 10
|
||||
|
||||
seaweedfs:
|
||||
image: chrislusf/seaweedfs:4.34
|
||||
image: chrislusf/seaweedfs:4.47
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
@@ -266,7 +269,7 @@ services:
|
||||
networks: [fluxer]
|
||||
environment:
|
||||
GOMEMLIMIT: ${FLUXER_SEAWEEDFS_GOMEMLIMIT:-1536MiB}
|
||||
command: ["server", "-s3", "-dir=/data"]
|
||||
command: ["server", "-s3", "-dir=/data", "-master.telemetry=false"]
|
||||
volumes:
|
||||
- seaweedfs-data:/data
|
||||
healthcheck:
|
||||
@@ -277,7 +280,7 @@ services:
|
||||
start_period: 60s
|
||||
|
||||
seaweedfs-init:
|
||||
image: chrislusf/seaweedfs:4.34
|
||||
image: chrislusf/seaweedfs:4.47
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
@@ -291,14 +294,13 @@ services:
|
||||
FLUXER_S3_SECRET_KEY: ${FLUXER_S3_SECRET_KEY:?set FLUXER_S3_SECRET_KEY in .env}
|
||||
FLUXER_S3_BUCKET_CDN: ${FLUXER_S3_BUCKET_CDN:-fluxer}
|
||||
FLUXER_S3_BUCKET_UPLOADS: ${FLUXER_S3_BUCKET_UPLOADS:-fluxer-uploads}
|
||||
FLUXER_S3_BUCKET_DOWNLOADS: ${FLUXER_S3_BUCKET_DOWNLOADS:-fluxer-downloads}
|
||||
FLUXER_S3_BUCKET_REPORTS: ${FLUXER_S3_BUCKET_REPORTS:-fluxer-reports}
|
||||
FLUXER_S3_BUCKET_HARVESTS: ${FLUXER_S3_BUCKET_HARVESTS:-fluxer-harvests}
|
||||
entrypoint:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- >
|
||||
buckets="$$FLUXER_S3_BUCKET_CDN $$FLUXER_S3_BUCKET_UPLOADS $$FLUXER_S3_BUCKET_DOWNLOADS $$FLUXER_S3_BUCKET_REPORTS $$FLUXER_S3_BUCKET_HARVESTS";
|
||||
buckets="$$FLUXER_S3_BUCKET_CDN $$FLUXER_S3_BUCKET_UPLOADS $$FLUXER_S3_BUCKET_REPORTS $$FLUXER_S3_BUCKET_HARVESTS";
|
||||
missing="$$buckets";
|
||||
for attempt in $$(seq 1 60); do
|
||||
if ! nc -z seaweedfs 9333 2>/dev/null; then
|
||||
@@ -413,7 +415,6 @@ services:
|
||||
NODE_OPTIONS: --enable-source-maps${FLUXER_WORKER_NODE_HEAP_MB:+ --max-old-space-size=$FLUXER_WORKER_NODE_HEAP_MB}
|
||||
FLUXER_API_WORKER_MODE: all_lanes
|
||||
FLUXER_API_WORKER_ENABLE_CRON_SCHEDULER: "true"
|
||||
FLUXER_API_WORKER_ENABLE_VOICE_RECONCILIATION: "true"
|
||||
FLUXER_POSTGRES_MAX_CONNECTIONS: "25"
|
||||
healthcheck:
|
||||
test: ["CMD", "node", "-e", "const age=Date.now()-require('node:fs').statSync('/tmp/fluxer-worker-heartbeat').mtimeMs;if(age>30000){console.error('worker heartbeat is '+Math.round(age)+'ms old');process.exit(1)}"]
|
||||
@@ -473,11 +474,38 @@ services:
|
||||
FLUXER_MEDIA_PROXY_MODE: upload
|
||||
FLUXER_MEDIA_PROXY_STORAGE_BACKEND: s3
|
||||
FLUXER_MEDIA_PROXY_PUBLIC_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}/media
|
||||
FLUXER_MEDIA_PROXY_CORS_MODE: ${FLUXER_MEDIA_PROXY_CORS_MODE:-off}
|
||||
FLUXER_MEDIA_PROXY_CORS_ALLOWED_ORIGINS: ${FLUXER_MEDIA_PROXY_CORS_ALLOWED_ORIGINS:-${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}:${FLUXER_PUBLIC_PORT:-443}}}
|
||||
FLUXER_MEDIA_PROXY_ATTACHMENT_SIGNATURE_MODE: ${FLUXER_MEDIA_PROXY_ATTACHMENT_SIGNATURE_MODE:-off}
|
||||
FLUXER_S3_READ_SIGNED: "true"
|
||||
depends_on:
|
||||
seaweedfs-init: {condition: service_completed_successfully}
|
||||
nats: {condition: service_healthy}
|
||||
|
||||
push:
|
||||
<<: *fluxer-service
|
||||
image: ${FLUXER_REGISTRY:-ghcr.io/${FLUXER_REGISTRY_OWNER:-fluxerapp}}/fluxer-push:${FLUXER_IMAGE_TAG:-v1}
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: ${FLUXER_PUSH_MEMORY_LIMIT:-256mb}
|
||||
environment:
|
||||
<<: *fluxer-env
|
||||
FLUXER_PUSH_SERVICE_HOST: 0.0.0.0
|
||||
FLUXER_PUSH_SERVICE_PORT: "8126"
|
||||
FLUXER_PUSH_SERVICE_QUEUE_CAPACITY: "${FLUXER_PUSH_SERVICE_QUEUE_CAPACITY:-}"
|
||||
FLUXER_PUSH_SERVICE_SEND_CONCURRENCY: "${FLUXER_PUSH_SERVICE_SEND_CONCURRENCY:-}"
|
||||
healthcheck:
|
||||
test: ["CMD", "/usr/local/bin/fluxer-push", "healthcheck"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
start_period: 60s
|
||||
start_interval: 1s
|
||||
depends_on:
|
||||
nats: {condition: service_healthy}
|
||||
api: {condition: service_healthy}
|
||||
|
||||
static-proxy:
|
||||
<<: *fluxer-service
|
||||
image: ${FLUXER_REGISTRY:-ghcr.io/${FLUXER_REGISTRY_OWNER:-fluxerapp}}/fluxer-static:${FLUXER_IMAGE_TAG:-v1}
|
||||
|
||||
+13
-13
@@ -9,32 +9,32 @@ build = "build.rs"
|
||||
[dependencies]
|
||||
anyhow = "1.0.104"
|
||||
axum = { version = "0.8.9", features = ["macros"] }
|
||||
base64 = "0.22.1"
|
||||
base64 = "0.23.1"
|
||||
chrono = { version = "0.4", default-features = false, features = ["serde"] }
|
||||
cookie = "0.18.1"
|
||||
cookie = "0.18.2"
|
||||
fluxer_common = { path = "../fluxer_common" }
|
||||
hmac = "0.13.0"
|
||||
maud = { version = "0.27.0", features = ["axum"] }
|
||||
rand = "0.10"
|
||||
regress = "0.11"
|
||||
reqwest = { version = "0.13.4", default-features = false, features = ["json", "rustls"] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.150"
|
||||
regress = "0.12"
|
||||
reqwest = { version = "0.13.5", default-features = false, features = ["json", "rustls"] }
|
||||
serde = { version = "1.0.229", features = ["derive"] }
|
||||
serde_json = "1.0.151"
|
||||
sha2 = "0.11.0"
|
||||
time = { version = "0.3.47", features = ["formatting", "parsing"] }
|
||||
tokio = { version = "1.52.3", features = ["macros", "net", "rt-multi-thread", "signal"] }
|
||||
time = { version = "0.3.55", features = ["formatting", "parsing"] }
|
||||
tokio = { version = "1.53.1", features = ["macros", "net", "rt-multi-thread", "signal"] }
|
||||
tower = { version = "0.5.3", features = ["util"] }
|
||||
tower-http = { version = "0.6.11", features = ["compression-gzip", "trace"] }
|
||||
tower-http = { version = "0.7.1", features = ["compression-gzip", "trace"] }
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
||||
url = "2.5"
|
||||
urlencoding = "2.1.3"
|
||||
progenitor-client = { version = "0.14.0", default-features = false }
|
||||
progenitor-client = { version = "0.15.0", default-features = false }
|
||||
|
||||
[build-dependencies]
|
||||
openapiv3 = "2.2.0"
|
||||
prettyplease = "0.2"
|
||||
progenitor = { version = "0.14.0", default-features = false }
|
||||
prettyplease = "0.3"
|
||||
progenitor = { version = "0.15.0", default-features = false }
|
||||
serde_json = "1"
|
||||
sha2 = "0.11.0"
|
||||
syn = "2"
|
||||
syn = "3"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
FROM rust:1-bookworm AS builder
|
||||
FROM rust:1-trixie AS builder
|
||||
|
||||
ARG BUILD_VERSION=""
|
||||
ARG TARGETARCH
|
||||
@@ -9,7 +9,7 @@ WORKDIR /usr/src/app
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates nodejs npm pkg-config \
|
||||
&& npm install -g pnpm@10.29.3 \
|
||||
&& npm install -g pnpm@11.27.0 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN npm install --no-audit --no-fund @tailwindcss/[email protected] [email protected]
|
||||
@@ -57,7 +57,7 @@ RUN test "$(ls target/release/build/fluxer_admin-*/out/static/fonts/*.woff2 | wc
|
||||
&& ls target/release/build/fluxer_admin-*/out/static/fonts/fonts.*.css \
|
||||
&& echo "Latin-core fonts bundled successfully"
|
||||
|
||||
FROM debian:bookworm-slim AS runtime
|
||||
FROM debian:trixie-slim AS runtime
|
||||
|
||||
ARG BUILD_VERSION=""
|
||||
ARG SOURCE_SHA=""
|
||||
|
||||
+12
-2
@@ -54,9 +54,9 @@ fn generate_admin_api(manifest_dir: &Path, out_dir: &Path) {
|
||||
.generate_tokens(&spec)
|
||||
.expect("failed to generate admin API client");
|
||||
|
||||
let content = prettyplease::unparse(
|
||||
let content = relax_required_nullable_fields(&prettyplease::unparse(
|
||||
&syn::parse2::<syn::File>(tokens).expect("failed to parse generated tokens"),
|
||||
);
|
||||
));
|
||||
|
||||
let output_path = out_dir.join("admin_api_generated.rs");
|
||||
fs::write(&output_path, content).expect("failed to write generated API code");
|
||||
@@ -190,6 +190,16 @@ fn object_schema_mut<'a>(
|
||||
|
||||
const MAX_SCHEMA_REFERENCE_DEPTH: usize = 32;
|
||||
|
||||
fn relax_required_nullable_fields(generated: &str) -> String {
|
||||
const PRESENCE_CHECK: &str =
|
||||
"#[serde(deserialize_with = \"::std::option::Option::deserialize\")]";
|
||||
generated
|
||||
.lines()
|
||||
.filter(|line| line.trim() != PRESENCE_CHECK)
|
||||
.flat_map(|line| [line, "\n"])
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn relax_progenitor_schema_strictness(spec: &mut openapiv3::OpenAPI) {
|
||||
let registry = spec.components.clone().unwrap_or_default();
|
||||
|
||||
|
||||
+223
-98
@@ -933,6 +933,22 @@
|
||||
},
|
||||
"description": "Filter by target entity ID (user, channel, role, invite code, etc.)"
|
||||
},
|
||||
{
|
||||
"name": "access",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"description": "Only return entries recorded by reads or only entries recorded by writes",
|
||||
"x-enumNames": ["read", "write"],
|
||||
"x-enumDescriptions": [
|
||||
"An entry recorded by an operation that only reads data",
|
||||
"An entry recorded by an operation that changes data or triggers work"
|
||||
],
|
||||
"enum": ["read", "write"],
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Only return entries recorded by reads or only entries recorded by writes"
|
||||
},
|
||||
{
|
||||
"name": "sort_by",
|
||||
"in": "query",
|
||||
@@ -5427,59 +5443,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/admin/system/heap-snapshots": {
|
||||
"post": {
|
||||
"operationId": "create_admin_system_heap_snapshot",
|
||||
"summary": "Create a V8 heap snapshot",
|
||||
"tags": ["Admin"],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {"application/octet-stream": {"schema": {"$ref": "#/components/schemas/HeapSnapshotResponse"}}}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request - The request was malformed or contained invalid data",
|
||||
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized - Authentication is required or the token is invalid",
|
||||
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden - You do not have permission to perform this action",
|
||||
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
|
||||
},
|
||||
"429": {
|
||||
"description": "Too Many Requests - You are being rate limited",
|
||||
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/ThrottledError"}}},
|
||||
"headers": {
|
||||
"Retry-After": {
|
||||
"description": "Number of seconds to wait before retrying (only on 429)",
|
||||
"schema": {"type": "integer"}
|
||||
},
|
||||
"X-RateLimit-Limit": {
|
||||
"description": "The number of requests that can be made in the current window",
|
||||
"schema": {"type": "integer"}
|
||||
},
|
||||
"X-RateLimit-Remaining": {
|
||||
"description": "The number of remaining requests that can be made",
|
||||
"schema": {"type": "integer"}
|
||||
},
|
||||
"X-RateLimit-Reset": {
|
||||
"description": "Unix timestamp when the rate limit resets",
|
||||
"schema": {"type": "integer"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error - An unexpected error occurred",
|
||||
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
|
||||
}
|
||||
},
|
||||
"description": "Writes a V8 heap snapshot of the current process and returns the snapshot file. Used for diagnosing memory leaks. Requires SYSTEM_HEAP_SNAPSHOT permission.",
|
||||
"security": [{"adminApiKey": []}]
|
||||
}
|
||||
},
|
||||
"/admin/users": {
|
||||
"get": {
|
||||
"operationId": "list_admin_users",
|
||||
@@ -9874,7 +9837,7 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"acls": {
|
||||
"maxItems": 112,
|
||||
"maxItems": 111,
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/AdminAclType"},
|
||||
"description": "List of access control permissions to assign"
|
||||
@@ -9904,7 +9867,6 @@
|
||||
"required": ["users", "total"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"HeapSnapshotResponse": {"type": "string", "format": "binary", "description": "V8 heap snapshot file"},
|
||||
"SendSystemDmRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -10562,6 +10524,8 @@
|
||||
},
|
||||
"gateway_rollout": {"$ref": "#/components/schemas/GatewayRolloutConfigResponse"},
|
||||
"voice_noise_suppression": {"$ref": "#/components/schemas/VoiceNoiseSuppressionConfigResponse"},
|
||||
"push_service_delivery": {"$ref": "#/components/schemas/PushServiceDeliveryConfigResponse"},
|
||||
"domain_migration": {"$ref": "#/components/schemas/DomainMigrationConfigResponse"},
|
||||
"experiment_delivery": {"$ref": "#/components/schemas/ExperimentDeliveryConfigResponse"},
|
||||
"registration": {
|
||||
"type": "object",
|
||||
@@ -10579,13 +10543,13 @@
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"expires_at": {
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"max_uses": {"nullable": true, "type": "integer", "minimum": 1, "maximum": 9007199254740991},
|
||||
"use_count": {"type": "integer", "minimum": 0, "maximum": 9007199254740991},
|
||||
@@ -10593,14 +10557,14 @@
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"approval_required": {"type": "boolean"},
|
||||
"last_used_at": {
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"last_used_by_user_id": {
|
||||
"nullable": true,
|
||||
@@ -10636,7 +10600,7 @@
|
||||
"requested_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"registration_url_id": {"nullable": true, "type": "string", "minLength": 1, "maxLength": 128},
|
||||
"client_ip": {"nullable": true, "type": "string"}
|
||||
@@ -10671,7 +10635,9 @@
|
||||
"logo_url": {"nullable": true, "type": "string"},
|
||||
"wordmark_url": {"nullable": true, "type": "string"},
|
||||
"favicon_url": {"nullable": true, "type": "string"},
|
||||
"theme_color": {"nullable": true, "type": "string"}
|
||||
"theme_color": {"nullable": true, "type": "string"},
|
||||
"status_page_url": {"nullable": true, "type": "string"},
|
||||
"status_page_incident_history_url": {"nullable": true, "type": "string"}
|
||||
},
|
||||
"required": [
|
||||
"product_name",
|
||||
@@ -10680,7 +10646,9 @@
|
||||
"logo_url",
|
||||
"wordmark_url",
|
||||
"favicon_url",
|
||||
"theme_color"
|
||||
"theme_color",
|
||||
"status_page_url",
|
||||
"status_page_incident_history_url"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
@@ -10889,12 +10857,14 @@
|
||||
"min_lifetime_days": {
|
||||
"nullable": true,
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true,
|
||||
"maximum": 9007199254740991
|
||||
},
|
||||
"max_lifetime_days": {
|
||||
"nullable": true,
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true,
|
||||
"maximum": 9007199254740991
|
||||
},
|
||||
@@ -10902,12 +10872,14 @@
|
||||
"renew_threshold_days": {
|
||||
"nullable": true,
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true,
|
||||
"maximum": 9007199254740991
|
||||
},
|
||||
"renew_window_days": {
|
||||
"nullable": true,
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true,
|
||||
"maximum": 9007199254740991
|
||||
},
|
||||
@@ -10918,15 +10890,31 @@
|
||||
"min_size_mb": {"type": "number", "minimum": 0, "exclusiveMinimum": true},
|
||||
"max_size_mb": {"type": "number", "minimum": 0, "exclusiveMinimum": true},
|
||||
"max_eligible_size_mb": {"type": "number", "minimum": 0, "exclusiveMinimum": true},
|
||||
"min_lifetime_days": {"type": "integer", "exclusiveMinimum": true, "maximum": 9007199254740991},
|
||||
"max_lifetime_days": {"type": "integer", "exclusiveMinimum": true, "maximum": 9007199254740991},
|
||||
"curve": {"type": "number", "minimum": 0, "maximum": 1},
|
||||
"renew_threshold_days": {
|
||||
"min_lifetime_days": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true,
|
||||
"maximum": 9007199254740991
|
||||
},
|
||||
"renew_window_days": {"type": "integer", "exclusiveMinimum": true, "maximum": 9007199254740991}
|
||||
"max_lifetime_days": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true,
|
||||
"maximum": 9007199254740991
|
||||
},
|
||||
"curve": {"type": "number", "minimum": 0, "maximum": 1},
|
||||
"renew_threshold_days": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true,
|
||||
"maximum": 9007199254740991
|
||||
},
|
||||
"renew_window_days": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true,
|
||||
"maximum": 9007199254740991
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
@@ -10965,6 +10953,8 @@
|
||||
"sso",
|
||||
"gateway_rollout",
|
||||
"voice_noise_suppression",
|
||||
"push_service_delivery",
|
||||
"domain_migration",
|
||||
"experiment_delivery",
|
||||
"registration",
|
||||
"self_hosted",
|
||||
@@ -10983,7 +10973,7 @@
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"max_uses": {"nullable": true, "type": "integer", "minimum": 1, "maximum": 1000000},
|
||||
"approval_required": {"default": false, "type": "boolean"}
|
||||
@@ -11001,13 +10991,13 @@
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"expires_at": {
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"max_uses": {"nullable": true, "type": "integer", "minimum": 1, "maximum": 9007199254740991},
|
||||
"use_count": {"type": "integer", "minimum": 0, "maximum": 9007199254740991},
|
||||
@@ -11015,14 +11005,14 @@
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"approval_required": {"type": "boolean"},
|
||||
"last_used_at": {
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"last_used_by_user_id": {
|
||||
"nullable": true,
|
||||
@@ -11099,6 +11089,14 @@
|
||||
"nullable": true,
|
||||
"allOf": [{"$ref": "#/components/schemas/VoiceNoiseSuppressionConfigUpdateRequest"}]
|
||||
},
|
||||
"push_service_delivery": {
|
||||
"nullable": true,
|
||||
"allOf": [{"$ref": "#/components/schemas/PushServiceDeliveryConfigUpdateRequest"}]
|
||||
},
|
||||
"domain_migration": {
|
||||
"nullable": true,
|
||||
"allOf": [{"$ref": "#/components/schemas/DomainMigrationConfigUpdateRequest"}]
|
||||
},
|
||||
"experiment_delivery": {
|
||||
"nullable": true,
|
||||
"allOf": [{"$ref": "#/components/schemas/ExperimentDeliveryConfigUpdateRequest"}]
|
||||
@@ -11144,7 +11142,9 @@
|
||||
"logo_url": {"nullable": true, "type": "string", "maxLength": 2048},
|
||||
"wordmark_url": {"nullable": true, "type": "string", "maxLength": 2048},
|
||||
"favicon_url": {"nullable": true, "type": "string", "maxLength": 2048},
|
||||
"theme_color": {"nullable": true, "type": "string", "maxLength": 64}
|
||||
"theme_color": {"nullable": true, "type": "string", "maxLength": 64},
|
||||
"status_page_url": {"nullable": true, "type": "string", "maxLength": 2048},
|
||||
"status_page_incident_history_url": {"nullable": true, "type": "string", "maxLength": 2048}
|
||||
}
|
||||
},
|
||||
"setup": {"nullable": true, "type": "object", "properties": {"configured": {"type": "boolean"}}},
|
||||
@@ -11254,12 +11254,14 @@
|
||||
"min_lifetime_days": {
|
||||
"nullable": true,
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true,
|
||||
"maximum": 9007199254740991
|
||||
},
|
||||
"max_lifetime_days": {
|
||||
"nullable": true,
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true,
|
||||
"maximum": 9007199254740991
|
||||
},
|
||||
@@ -11267,12 +11269,14 @@
|
||||
"renew_threshold_days": {
|
||||
"nullable": true,
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true,
|
||||
"maximum": 9007199254740991
|
||||
},
|
||||
"renew_window_days": {
|
||||
"nullable": true,
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true,
|
||||
"maximum": 9007199254740991
|
||||
}
|
||||
@@ -11304,7 +11308,7 @@
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean"},
|
||||
"window_hours": {"type": "number", "minimum": 0, "exclusiveMinimum": true, "maximum": 8760},
|
||||
"member_threshold": {"type": "integer", "exclusiveMinimum": true, "maximum": 1000000}
|
||||
"member_threshold": {"type": "integer", "minimum": 0, "exclusiveMinimum": true, "maximum": 1000000}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12621,6 +12625,16 @@
|
||||
}
|
||||
},
|
||||
"action": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||
"access": {
|
||||
"description": "Whether the recorded operation read data or changed it",
|
||||
"x-enumNames": ["read", "write"],
|
||||
"x-enumDescriptions": [
|
||||
"An entry recorded by an operation that only reads data",
|
||||
"An entry recorded by an operation that changes data or triggers work"
|
||||
],
|
||||
"enum": ["read", "write"],
|
||||
"type": "string"
|
||||
},
|
||||
"audit_log_reason": {"nullable": true, "type": "string", "minLength": 1, "maxLength": 4000},
|
||||
"metadata": {"type": "object", "additionalProperties": {"type": "string", "maxLength": 4000}},
|
||||
"created_at": {"type": "string"}
|
||||
@@ -12638,6 +12652,7 @@
|
||||
"related_guilds",
|
||||
"related_channels",
|
||||
"action",
|
||||
"access",
|
||||
"audit_log_reason",
|
||||
"metadata",
|
||||
"created_at"
|
||||
@@ -12726,7 +12741,7 @@
|
||||
},
|
||||
"acls": {
|
||||
"description": "Replacement list of access control permissions for the key",
|
||||
"maxItems": 112,
|
||||
"maxItems": 111,
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/AdminAclType"}
|
||||
}
|
||||
@@ -12744,7 +12759,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"acls": {
|
||||
"maxItems": 112,
|
||||
"maxItems": 111,
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of access control permissions for the key"
|
||||
@@ -12774,7 +12789,7 @@
|
||||
"maximum": 365
|
||||
},
|
||||
"acls": {
|
||||
"maxItems": 112,
|
||||
"maxItems": 111,
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/AdminAclType"},
|
||||
"description": "List of access control permissions for the key"
|
||||
@@ -12795,7 +12810,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"acls": {
|
||||
"maxItems": 112,
|
||||
"maxItems": 111,
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of access control permissions for the key"
|
||||
@@ -12808,7 +12823,7 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"acls": {
|
||||
"maxItems": 112,
|
||||
"maxItems": 111,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||
"description": "Every admin access control permission the admin API recognises"
|
||||
@@ -12899,7 +12914,6 @@
|
||||
"report:view",
|
||||
"report:view:reporter_pii",
|
||||
"system_dm:send",
|
||||
"system:heap_snapshot",
|
||||
"user:cancel:bulk_message_deletion",
|
||||
"user:delete",
|
||||
"user:disable:suspicious",
|
||||
@@ -13011,14 +13025,14 @@
|
||||
"description": "ISO 8601 timestamp when the bot token was created",
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"client_secret_created_at": {
|
||||
"nullable": true,
|
||||
"description": "ISO 8601 timestamp when the client secret was created",
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"version": {
|
||||
"description": "The optimistic locking version of the application record",
|
||||
@@ -13446,7 +13460,7 @@
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
|
||||
"description": "The ISO 8601 timestamp of when the message was created"
|
||||
},
|
||||
"edited_timestamp": {
|
||||
@@ -13454,7 +13468,7 @@
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"pinned": {"type": "boolean", "description": "Whether the message is pinned"},
|
||||
"mention_everyone": {"type": "boolean", "description": "Whether the message mentions @everyone"},
|
||||
@@ -13554,7 +13568,7 @@
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
}
|
||||
},
|
||||
"required": ["participants"],
|
||||
@@ -13591,7 +13605,7 @@
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
|
||||
"description": "The ISO 8601 timestamp of when the message was created"
|
||||
},
|
||||
"edited_timestamp": {
|
||||
@@ -13599,7 +13613,7 @@
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"pinned": {"type": "boolean", "description": "Whether the message is pinned"},
|
||||
"mention_everyone": {"type": "boolean", "description": "Whether the message mentions @everyone"},
|
||||
@@ -13699,7 +13713,7 @@
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
}
|
||||
},
|
||||
"required": ["participants"],
|
||||
@@ -13863,7 +13877,7 @@
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
|
||||
"description": "The ISO 8601 timestamp of when the original message was created"
|
||||
},
|
||||
"edited_timestamp": {
|
||||
@@ -13871,7 +13885,7 @@
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"mentions": {
|
||||
"description": "The user IDs mentioned in the snapshot",
|
||||
@@ -14040,7 +14054,7 @@
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"description": {"description": "The description of the embed", "nullable": true, "type": "string"},
|
||||
"author": {
|
||||
@@ -14252,7 +14266,7 @@
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"description": {"description": "The description of the embed", "nullable": true, "type": "string"},
|
||||
"author": {
|
||||
@@ -15124,7 +15138,7 @@
|
||||
"joined_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
|
||||
"description": "ISO8601 timestamp of when the user joined the guild"
|
||||
},
|
||||
"mute": {"type": "boolean", "description": "Whether the member is muted in voice channels"},
|
||||
@@ -15134,7 +15148,7 @@
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"profile_flags": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/GuildMemberProfileFlags"}]},
|
||||
"mention_flags": {
|
||||
@@ -15176,6 +15190,44 @@
|
||||
"poll_jitter_percent": {"type": "integer", "minimum": 0, "maximum": 50}
|
||||
}
|
||||
},
|
||||
"DomainMigrationConfigUpdateRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean"},
|
||||
"rollout_basis_points": {"type": "integer", "minimum": 0, "maximum": 10000},
|
||||
"rollout_salt": {"type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[\\x20-\\x7e]+$"},
|
||||
"included_user_ids": {
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
},
|
||||
"excluded_user_ids": {
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
},
|
||||
"anonymous_rollout_basis_points": {"type": "integer", "minimum": 0, "maximum": 10000},
|
||||
"standalone_forwarding": {"type": "boolean"}
|
||||
}
|
||||
},
|
||||
"PushServiceDeliveryConfigUpdateRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean"},
|
||||
"rollout_basis_points": {"type": "integer", "minimum": 0, "maximum": 10000},
|
||||
"rollout_salt": {"type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[\\x20-\\x7e]+$"},
|
||||
"included_user_ids": {
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
},
|
||||
"excluded_user_ids": {
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"VoiceNoiseSuppressionConfigUpdateRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -15211,7 +15263,6 @@
|
||||
"required": ["guild_id", "backend"]
|
||||
}
|
||||
},
|
||||
"stereo_enabled": {"type": "boolean"},
|
||||
"suppression_strength": {"type": "integer", "minimum": 0, "maximum": 100}
|
||||
}
|
||||
},
|
||||
@@ -15242,6 +15293,82 @@
|
||||
"required": ["poll_interval_seconds", "poll_jitter_percent"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"DomainMigrationConfigResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"default": false, "type": "boolean"},
|
||||
"config_version": {"default": 0, "type": "integer", "minimum": 0, "maximum": 9007199254740991},
|
||||
"rollout_basis_points": {"default": 0, "type": "integer", "minimum": 0, "maximum": 10000},
|
||||
"rollout_salt": {
|
||||
"default": "domain-migration-v1",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 64,
|
||||
"pattern": "^[\\x20-\\x7e]+$"
|
||||
},
|
||||
"included_user_ids": {
|
||||
"default": [],
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
},
|
||||
"excluded_user_ids": {
|
||||
"default": [],
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
},
|
||||
"anonymous_rollout_basis_points": {"default": 0, "type": "integer", "minimum": 0, "maximum": 10000},
|
||||
"standalone_forwarding": {"default": false, "type": "boolean"}
|
||||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
"config_version",
|
||||
"rollout_basis_points",
|
||||
"rollout_salt",
|
||||
"included_user_ids",
|
||||
"excluded_user_ids",
|
||||
"anonymous_rollout_basis_points",
|
||||
"standalone_forwarding"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"PushServiceDeliveryConfigResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"default": false, "type": "boolean"},
|
||||
"config_version": {"default": 0, "type": "integer", "minimum": 0, "maximum": 9007199254740991},
|
||||
"rollout_basis_points": {"default": 0, "type": "integer", "minimum": 0, "maximum": 10000},
|
||||
"rollout_salt": {
|
||||
"default": "push-service-delivery-v1",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 64,
|
||||
"pattern": "^[\\x20-\\x7e]+$"
|
||||
},
|
||||
"included_user_ids": {
|
||||
"default": [],
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
},
|
||||
"excluded_user_ids": {
|
||||
"default": [],
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
"config_version",
|
||||
"rollout_basis_points",
|
||||
"rollout_salt",
|
||||
"included_user_ids",
|
||||
"excluded_user_ids"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"VoiceNoiseSuppressionConfigResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -15286,7 +15413,6 @@
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"stereo_enabled": {"default": false, "type": "boolean"},
|
||||
"suppression_strength": {"default": 80, "type": "integer", "minimum": 0, "maximum": 100}
|
||||
},
|
||||
"required": [
|
||||
@@ -15300,7 +15426,6 @@
|
||||
"included_user_ids",
|
||||
"excluded_user_ids",
|
||||
"guild_overrides",
|
||||
"stereo_enabled",
|
||||
"suppression_strength"
|
||||
],
|
||||
"additionalProperties": false
|
||||
@@ -15438,7 +15563,7 @@
|
||||
"pending_bulk_message_deletion_at": {"nullable": true, "type": "string"},
|
||||
"deletion_reason_code": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/Int32Type"}]},
|
||||
"deletion_public_reason": {"nullable": true, "type": "string"},
|
||||
"acls": {"maxItems": 112, "type": "array", "items": {"type": "string"}},
|
||||
"acls": {"maxItems": 111, "type": "array", "items": {"type": "string"}},
|
||||
"traits": {"maxItems": 100, "type": "array", "items": {"type": "string"}},
|
||||
"has_totp": {"type": "boolean"},
|
||||
"authenticator_types": {"maxItems": 10, "type": "array", "items": {"$ref": "#/components/schemas/Int32Type"}},
|
||||
|
||||
@@ -79,7 +79,6 @@ pub const REPORT_RESOLVE: &str = "report:resolve";
|
||||
pub const REPORT_VIEW: &str = "report:view";
|
||||
pub const REPORT_VIEW_REPORTER_PII: &str = "report:view:reporter_pii";
|
||||
pub const SYSTEM_DM_SEND: &str = "system_dm:send";
|
||||
pub const SYSTEM_HEAP_SNAPSHOT: &str = "system:heap_snapshot";
|
||||
pub const USER_CANCEL_BULK_MESSAGE_DELETION: &str = "user:cancel:bulk_message_deletion";
|
||||
pub const USER_DELETE: &str = "user:delete";
|
||||
pub const USER_DISABLE_SUSPICIOUS: &str = "user:disable:suspicious";
|
||||
@@ -192,7 +191,6 @@ pub const ALL_ACLS: &[&str] = &[
|
||||
REPORT_VIEW,
|
||||
REPORT_VIEW_REPORTER_PII,
|
||||
SYSTEM_DM_SEND,
|
||||
SYSTEM_HEAP_SNAPSHOT,
|
||||
USER_CANCEL_BULK_MESSAGE_DELETION,
|
||||
USER_DELETE,
|
||||
USER_DISABLE_SUSPICIOUS,
|
||||
|
||||
@@ -10,6 +10,7 @@ pub struct SearchAuditLogsParams {
|
||||
pub admin_user_id: Option<String>,
|
||||
pub target_id: Option<String>,
|
||||
pub target_type: Option<String>,
|
||||
pub access: Option<String>,
|
||||
pub sort_by: Option<String>,
|
||||
pub sort_order: Option<String>,
|
||||
pub limit: u32,
|
||||
@@ -21,6 +22,12 @@ impl AdminApiClient {
|
||||
&self,
|
||||
params: &SearchAuditLogsParams,
|
||||
) -> ApiResult<AuditLogsListResponse> {
|
||||
let access = params
|
||||
.access
|
||||
.as_deref()
|
||||
.map(audit_access)
|
||||
.transpose()?
|
||||
.map(|value| value.to_string());
|
||||
let sort_by = params
|
||||
.sort_by
|
||||
.as_deref()
|
||||
@@ -46,6 +53,7 @@ impl AdminApiClient {
|
||||
params.target_type.as_deref().unwrap_or_default(),
|
||||
),
|
||||
("target_id", params.target_id.as_deref().unwrap_or_default()),
|
||||
("access", access.as_deref().unwrap_or_default()),
|
||||
("sort_by", sort_by.as_deref().unwrap_or_default()),
|
||||
("sort_order", sort_order.as_deref().unwrap_or_default()),
|
||||
("limit", limit.as_str()),
|
||||
@@ -55,6 +63,11 @@ impl AdminApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
fn audit_access(value: &str) -> ApiResult<generated_types::ListAdminAuditLogsAccess> {
|
||||
generated_types::ListAdminAuditLogsAccess::try_from(value)
|
||||
.map_err(|e| ApiError::Parse(e.to_string()))
|
||||
}
|
||||
|
||||
fn audit_sort_by(value: &str) -> ApiResult<generated_types::ListAdminAuditLogsSortBy> {
|
||||
let value = match value {
|
||||
"created_at" => "createdAt",
|
||||
@@ -83,6 +96,13 @@ mod tests {
|
||||
assert_eq!(audit_sort_order("desc").unwrap().to_string(), "desc");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_only_known_access_filters() {
|
||||
assert_eq!(audit_access("read").unwrap().to_string(), "read");
|
||||
assert_eq!(audit_access("write").unwrap().to_string(), "write");
|
||||
assert!(audit_access("all").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_lossy_audit_totals() {
|
||||
for total in [serde_json::json!(1.5), serde_json::json!(-1)] {
|
||||
@@ -99,6 +119,7 @@ mod tests {
|
||||
"admin_user_id": "234567890123456789",
|
||||
"admin_user": null,
|
||||
"action": "USER_UPDATE",
|
||||
"access": "write",
|
||||
"target_id": "345678901234567890",
|
||||
"target_type": "user",
|
||||
"target_user": null,
|
||||
@@ -118,6 +139,26 @@ mod tests {
|
||||
assert_eq!(generated.logs[0].action.to_string(), "USER_UPDATE");
|
||||
|
||||
let response: AuditLogsListResponse = serde_json::from_value(json.clone()).unwrap();
|
||||
assert_eq!(response.logs[0].access.as_deref(), Some("write"));
|
||||
assert_eq!(serde_json::to_value(response).unwrap(), json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserializes_audit_entries_from_an_api_without_access() {
|
||||
let json = serde_json::json!({
|
||||
"logs": [{
|
||||
"log_id": "123456789012345678",
|
||||
"admin_user_id": "234567890123456789",
|
||||
"action": "USER_UPDATE",
|
||||
"target_id": "345678901234567890",
|
||||
"target_type": "user",
|
||||
"audit_log_reason": null,
|
||||
"metadata": {},
|
||||
"created_at": "2026-09-11T12:00:00.000Z"
|
||||
}],
|
||||
"total": 1
|
||||
});
|
||||
let response: AuditLogsListResponse = serde_json::from_value(json).unwrap();
|
||||
assert_eq!(response.logs[0].access, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ impl AdminApiClient {
|
||||
audit_log_reason: Option<&str>,
|
||||
) -> ApiResult<BulkJobResponse> {
|
||||
let body = generated_types::AdminBulkJobCreateRequest::UpdateUserFlags {
|
||||
add_flags: user_flags(add_flags),
|
||||
remove_flags: user_flags(remove_flags),
|
||||
add_flags: user_flags(add_flags)?,
|
||||
remove_flags: user_flags(remove_flags)?,
|
||||
user_ids: snowflakes(user_ids),
|
||||
};
|
||||
self.post_typed_with_reason("/admin/bulk-jobs", &body, audit_log_reason)
|
||||
@@ -112,11 +112,13 @@ fn snowflakes(values: &[String]) -> Vec<generated_types::SnowflakeType> {
|
||||
values.iter().map(|value| snowflake(value)).collect()
|
||||
}
|
||||
|
||||
fn user_flags(values: &[String]) -> Vec<generated_types::UserFlags> {
|
||||
fn user_flags(values: &[String]) -> ApiResult<Vec<generated_types::UserFlags>> {
|
||||
values
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(generated_types::UserFlags::from)
|
||||
.map(|value| {
|
||||
generated_types::UserFlags::try_from(value.as_str())
|
||||
.map_err(|error| ApiError::Parse(error.to_string()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ pub struct AuditLogEntry {
|
||||
#[serde(default)]
|
||||
pub admin_user: Option<AuditLogUserSummary>,
|
||||
pub action: String,
|
||||
#[serde(default)]
|
||||
pub access: Option<String>,
|
||||
pub target_id: String,
|
||||
pub target_type: String,
|
||||
#[serde(default)]
|
||||
|
||||
@@ -23,6 +23,10 @@ pub struct InstanceConfigResponse {
|
||||
#[serde(default)]
|
||||
pub voice_noise_suppression: VoiceNoiseSuppressionConfigResponse,
|
||||
#[serde(default)]
|
||||
pub push_service_delivery: PushServiceDeliveryConfigResponse,
|
||||
#[serde(default)]
|
||||
pub domain_migration: DomainMigrationConfigResponse,
|
||||
#[serde(default)]
|
||||
pub experiment_delivery: ExperimentDeliveryConfigResponse,
|
||||
}
|
||||
|
||||
@@ -326,6 +330,8 @@ pub struct AppBrandingConfigResponse {
|
||||
pub wordmark_url: Option<String>,
|
||||
pub favicon_url: Option<String>,
|
||||
pub theme_color: Option<String>,
|
||||
pub status_page_url: Option<String>,
|
||||
pub status_page_incident_history_url: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for AppBrandingConfigResponse {
|
||||
@@ -338,6 +344,8 @@ impl Default for AppBrandingConfigResponse {
|
||||
wordmark_url: None,
|
||||
favicon_url: None,
|
||||
theme_color: None,
|
||||
status_page_url: None,
|
||||
status_page_incident_history_url: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -442,7 +450,9 @@ impl VoiceE2eeScope {
|
||||
}
|
||||
}
|
||||
|
||||
pub const VOICE_NS_MAX_TARGETED_USERS: usize = 1_000;
|
||||
pub const EXPERIMENT_MAX_TARGETED_USERS: usize = 1_000;
|
||||
pub const PUSH_SERVICE_DELIVERY_DEFAULT_SALT: &str = "push-service-delivery-v1";
|
||||
pub const DOMAIN_MIGRATION_DEFAULT_SALT: &str = "domain-migration-v1";
|
||||
pub const VOICE_NS_MAX_GUILD_OVERRIDES: usize = 200;
|
||||
|
||||
impl NoiseSuppressionBackend {
|
||||
@@ -488,7 +498,6 @@ pub struct VoiceNoiseSuppressionConfigResponse {
|
||||
pub included_user_ids: Vec<String>,
|
||||
pub excluded_user_ids: Vec<String>,
|
||||
pub guild_overrides: Vec<VoiceNoiseSuppressionGuildOverride>,
|
||||
pub stereo_enabled: bool,
|
||||
pub suppression_strength: u32,
|
||||
}
|
||||
|
||||
@@ -505,7 +514,6 @@ impl Default for VoiceNoiseSuppressionConfigResponse {
|
||||
included_user_ids: Vec::new(),
|
||||
excluded_user_ids: Vec::new(),
|
||||
guild_overrides: Vec::new(),
|
||||
stereo_enabled: false,
|
||||
suppression_strength: 80,
|
||||
}
|
||||
}
|
||||
@@ -532,11 +540,93 @@ pub struct VoiceNoiseSuppressionConfigUpdateRequest {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub guild_overrides: Option<Vec<VoiceNoiseSuppressionGuildOverride>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stereo_enabled: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub suppression_strength: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct PushServiceDeliveryConfigResponse {
|
||||
pub enabled: bool,
|
||||
pub config_version: u64,
|
||||
pub rollout_basis_points: u32,
|
||||
pub rollout_salt: String,
|
||||
pub included_user_ids: Vec<String>,
|
||||
pub excluded_user_ids: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for PushServiceDeliveryConfigResponse {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
config_version: 0,
|
||||
rollout_basis_points: 0,
|
||||
rollout_salt: PUSH_SERVICE_DELIVERY_DEFAULT_SALT.to_owned(),
|
||||
included_user_ids: Vec::new(),
|
||||
excluded_user_ids: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct PushServiceDeliveryConfigUpdateRequest {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rollout_basis_points: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rollout_salt: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub included_user_ids: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub excluded_user_ids: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct DomainMigrationConfigResponse {
|
||||
pub enabled: bool,
|
||||
pub config_version: u64,
|
||||
pub rollout_basis_points: u32,
|
||||
pub rollout_salt: String,
|
||||
pub included_user_ids: Vec<String>,
|
||||
pub excluded_user_ids: Vec<String>,
|
||||
pub anonymous_rollout_basis_points: u32,
|
||||
pub standalone_forwarding: bool,
|
||||
}
|
||||
|
||||
impl Default for DomainMigrationConfigResponse {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
config_version: 0,
|
||||
rollout_basis_points: 0,
|
||||
rollout_salt: DOMAIN_MIGRATION_DEFAULT_SALT.to_owned(),
|
||||
included_user_ids: Vec::new(),
|
||||
excluded_user_ids: Vec::new(),
|
||||
anonymous_rollout_basis_points: 0,
|
||||
standalone_forwarding: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct DomainMigrationConfigUpdateRequest {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rollout_basis_points: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rollout_salt: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub included_user_ids: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub excluded_user_ids: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub anonymous_rollout_basis_points: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub standalone_forwarding: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct ExperimentDeliveryConfigResponse {
|
||||
@@ -653,6 +743,10 @@ pub struct InstanceConfigUpdateRequest {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub voice_noise_suppression: Option<VoiceNoiseSuppressionConfigUpdateRequest>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub push_service_delivery: Option<PushServiceDeliveryConfigUpdateRequest>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub domain_migration: Option<DomainMigrationConfigUpdateRequest>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub experiment_delivery: Option<ExperimentDeliveryConfigUpdateRequest>,
|
||||
}
|
||||
|
||||
@@ -858,6 +952,10 @@ pub struct AppBrandingConfigUpdateRequest {
|
||||
pub favicon_url: Option<Option<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub theme_color: Option<Option<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub status_page_url: Option<Option<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub status_page_incident_history_url: Option<Option<String>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
@@ -986,18 +1084,30 @@ mod tests {
|
||||
.expect("admin schema");
|
||||
let noise = serde_json::from_value::<VoiceNoiseSuppressionConfigResponse>(json!({}))
|
||||
.expect("default noise config");
|
||||
let domain_migration = serde_json::from_value::<DomainMigrationConfigResponse>(json!({}))
|
||||
.expect("default domain migration config");
|
||||
let delivery = serde_json::from_value::<ExperimentDeliveryConfigResponse>(json!({}))
|
||||
.expect("default delivery config");
|
||||
let noise = serde_json::to_value(noise).expect("serializable noise config");
|
||||
let domain_migration =
|
||||
serde_json::to_value(domain_migration).expect("serializable domain migration config");
|
||||
let delivery = serde_json::to_value(delivery).expect("serializable delivery config");
|
||||
let generated_noise: generated_types::VoiceNoiseSuppressionConfigResponse =
|
||||
serde_json::from_value(noise.clone()).expect("generated noise config contract");
|
||||
let generated_domain_migration: generated_types::DomainMigrationConfigResponse =
|
||||
serde_json::from_value(domain_migration.clone())
|
||||
.expect("generated domain migration config contract");
|
||||
let generated_delivery: generated_types::ExperimentDeliveryConfigResponse =
|
||||
serde_json::from_value(delivery.clone()).expect("generated delivery config contract");
|
||||
assert_eq!(
|
||||
serde_json::to_value(generated_noise).expect("serializable generated noise config"),
|
||||
noise
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(generated_domain_migration)
|
||||
.expect("serializable generated domain migration config"),
|
||||
domain_migration
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(generated_delivery)
|
||||
.expect("serializable generated delivery config"),
|
||||
@@ -1005,6 +1115,7 @@ mod tests {
|
||||
);
|
||||
for (name, value) in [
|
||||
("VoiceNoiseSuppressionConfigResponse", noise),
|
||||
("DomainMigrationConfigResponse", domain_migration),
|
||||
("ExperimentDeliveryConfigResponse", delivery),
|
||||
] {
|
||||
for (field, value) in value.as_object().expect("config object") {
|
||||
@@ -1040,4 +1151,27 @@ mod tests {
|
||||
json!({})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn domain_migration_update_preserves_empty_lists_and_omitted_fields() {
|
||||
let update = DomainMigrationConfigUpdateRequest {
|
||||
included_user_ids: Some(Vec::new()),
|
||||
excluded_user_ids: Some(Vec::new()),
|
||||
..Default::default()
|
||||
};
|
||||
let value = serde_json::to_value(update).expect("serializable update");
|
||||
serde_json::from_value::<generated_types::DomainMigrationConfigUpdateRequest>(
|
||||
value.clone(),
|
||||
)
|
||||
.expect("generated update contract");
|
||||
assert_eq!(
|
||||
value,
|
||||
json!({"included_user_ids": [], "excluded_user_ids": []})
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(DomainMigrationConfigUpdateRequest::default())
|
||||
.expect("serializable update"),
|
||||
json!({})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,8 +95,8 @@ impl AdminApiClient {
|
||||
remove_flags: &[String],
|
||||
) -> ApiResult<AdminUser> {
|
||||
let body = generated_types::AdminUserFlagsUpdateRequest {
|
||||
add_flags: user_flags(add_flags),
|
||||
remove_flags: user_flags(remove_flags),
|
||||
add_flags: user_flags(add_flags)?,
|
||||
remove_flags: user_flags(remove_flags)?,
|
||||
};
|
||||
let response = self
|
||||
.generated()
|
||||
@@ -567,11 +567,13 @@ fn bool_param(value: bool) -> &'static str {
|
||||
if value { "true" } else { "false" }
|
||||
}
|
||||
|
||||
fn user_flags(values: &[String]) -> Vec<generated_types::UserFlags> {
|
||||
fn user_flags(values: &[String]) -> ApiResult<Vec<generated_types::UserFlags>> {
|
||||
values
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(generated_types::UserFlags::from)
|
||||
.map(|value| {
|
||||
generated_types::UserFlags::try_from(value.as_str())
|
||||
.map_err(|error| ApiError::Parse(error.to_string()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
||||
@@ -121,6 +121,7 @@ pub async fn render(
|
||||
admin_user_id: None,
|
||||
target_id: Some(guild_id.to_owned()),
|
||||
target_type: Some("guild".to_owned()),
|
||||
access: Some("write".to_owned()),
|
||||
sort_by: Some("created_at".to_owned()),
|
||||
sort_order: Some("desc".to_owned()),
|
||||
limit: 50,
|
||||
@@ -134,7 +135,7 @@ pub async fn render(
|
||||
@if let Some(resp) = resp {
|
||||
@if resp.logs.is_empty() {
|
||||
p class="text-sm text-neutral-500" {
|
||||
"No admin audit log entries for this guild."
|
||||
"No admin write actions have been recorded for this guild."
|
||||
}
|
||||
} @else {
|
||||
(table_container(table(maud::html! {
|
||||
|
||||
@@ -80,7 +80,7 @@ async fn reports_list(
|
||||
return reports_error_page(
|
||||
config,
|
||||
&auth.0,
|
||||
"That page is out of range. The reports search returns at most the first 10000 reports, so narrow the filters and start again.",
|
||||
"That page is out of range. The reports search returns at most the first 10000 reports. Narrow the filters and start again.",
|
||||
);
|
||||
}
|
||||
let search_query = query.q.as_deref().and_then(clean_string);
|
||||
|
||||
@@ -28,6 +28,7 @@ struct AuditLogsQuery {
|
||||
admin_user_id: Option<String>,
|
||||
target_id: Option<String>,
|
||||
target_type: Option<String>,
|
||||
access: Option<String>,
|
||||
sort_by: Option<String>,
|
||||
sort_order: Option<String>,
|
||||
limit: Option<u32>,
|
||||
@@ -119,6 +120,7 @@ async fn audit_logs_page(
|
||||
admin_user_id: query.admin_user_id.as_deref().unwrap_or(""),
|
||||
target_id: query.target_id.as_deref().unwrap_or(""),
|
||||
target_type: query.target_type.as_deref().unwrap_or(""),
|
||||
access: query.access.as_deref().unwrap_or(""),
|
||||
sort_by: query.sort_by.as_deref().unwrap_or("createdAt"),
|
||||
sort_order: query.sort_order.as_deref().unwrap_or("desc"),
|
||||
limit,
|
||||
@@ -131,6 +133,7 @@ async fn audit_logs_page(
|
||||
admin_user_id: nonempty(params.admin_user_id),
|
||||
target_id: nonempty(params.target_id),
|
||||
target_type: nonempty(params.target_type),
|
||||
access: nonempty(params.access),
|
||||
sort_by: Some(params.sort_by.to_owned()),
|
||||
sort_order: Some(params.sort_order.to_owned()),
|
||||
limit,
|
||||
|
||||
@@ -7,7 +7,8 @@ use crate::{
|
||||
AppBrandingConfigUpdateRequest, AppLegalConfigUpdateRequest,
|
||||
AppPublicConfigUpdateRequest, AppRegistrationConfigUpdateRequest,
|
||||
AppSetupConfigUpdateRequest, CreateRegistrationUrlRequest,
|
||||
DeferredPhoneGateUpdateRequest, ExperimentDeliveryConfigUpdateRequest,
|
||||
DeferredPhoneGateUpdateRequest, DomainMigrationConfigUpdateRequest,
|
||||
EXPERIMENT_MAX_TARGETED_USERS, ExperimentDeliveryConfigUpdateRequest,
|
||||
GatewayRolloutConfigUpdateRequest, GatewayRolloutMode,
|
||||
InstanceAttachmentDecayUpdateRequest, InstanceBlueskyIntegrationUpdateRequest,
|
||||
InstanceBlueskyKeyIntegrationUpdateRequest, InstanceCaptchaIntegrationUpdateRequest,
|
||||
@@ -17,9 +18,9 @@ use crate::{
|
||||
InstanceMediaUpdateRequest, InstancePolicyUpdateRequest,
|
||||
InstanceRegistrationConfigUpdateRequest, InstanceServicesUpdateRequest,
|
||||
InstanceYoutubeIntegrationUpdateRequest, LimitConfigUpdateRequest, LimitRule,
|
||||
LimitRuleFilters, NoiseSuppressionBackend, PremiumMode, RegistrationMode,
|
||||
SsoConfigUpdateRequest, VOICE_NS_MAX_GUILD_OVERRIDES, VOICE_NS_MAX_TARGETED_USERS,
|
||||
VoiceE2eeScope, VoiceNoiseSuppressionConfigUpdateRequest,
|
||||
LimitRuleFilters, NoiseSuppressionBackend, PremiumMode,
|
||||
PushServiceDeliveryConfigUpdateRequest, RegistrationMode, SsoConfigUpdateRequest,
|
||||
VOICE_NS_MAX_GUILD_OVERRIDES, VoiceE2eeScope, VoiceNoiseSuppressionConfigUpdateRequest,
|
||||
VoiceNoiseSuppressionGuildOverride,
|
||||
},
|
||||
},
|
||||
@@ -207,6 +208,14 @@ pub async fn instance_config_post(
|
||||
Ok(update) => instance_config_result(client.update_instance_config(&update).await),
|
||||
Err(message) => FlashData::error(message),
|
||||
},
|
||||
"update_push_service_delivery" => match build_push_service_delivery_update(&form) {
|
||||
Ok(update) => instance_config_result(client.update_instance_config(&update).await),
|
||||
Err(message) => FlashData::error(message),
|
||||
},
|
||||
"update_domain_migration" => match build_domain_migration_update(&form) {
|
||||
Ok(update) => instance_config_result(client.update_instance_config(&update).await),
|
||||
Err(message) => FlashData::error(message),
|
||||
},
|
||||
"update_experiment_delivery" => match build_experiment_delivery_update(&form) {
|
||||
Ok(update) => instance_config_result(client.update_instance_config(&update).await),
|
||||
Err(message) => FlashData::error(message),
|
||||
@@ -447,10 +456,10 @@ fn build_gateway_rollout_update(form: &MultiValueForm) -> InstanceConfigUpdateRe
|
||||
}
|
||||
}
|
||||
|
||||
const VOICE_NS_ROLLOUT_BASIS_POINTS_MAX: u32 = 10_000;
|
||||
const EXPERIMENT_ROLLOUT_BASIS_POINTS_MAX: u32 = 10_000;
|
||||
const VOICE_NS_SUPPRESSION_STRENGTH_MAX: u32 = 100;
|
||||
const VOICE_NS_MAX_ROLLOUT_SALT_CHARS: usize = 64;
|
||||
const VOICE_NS_MAX_SNOWFLAKE_LENGTH: usize = 20;
|
||||
const EXPERIMENT_MAX_ROLLOUT_SALT_CHARS: usize = 64;
|
||||
const EXPERIMENT_MAX_SNOWFLAKE_LENGTH: usize = 20;
|
||||
const EXPERIMENT_MIN_POLL_INTERVAL_SECONDS: u64 = 60;
|
||||
const EXPERIMENT_MAX_POLL_INTERVAL_SECONDS: u64 = 86_400;
|
||||
const EXPERIMENT_MAX_POLL_JITTER_PERCENT: u32 = 50;
|
||||
@@ -476,35 +485,51 @@ where
|
||||
Ok(Some(value))
|
||||
}
|
||||
|
||||
fn parse_voice_noise_suppression_rollout_salt(
|
||||
fn parse_experiment_rollout_salt(
|
||||
form: &MultiValueForm,
|
||||
key: &str,
|
||||
) -> Result<Option<String>, String> {
|
||||
let Some(raw) = form.first("voice_ns_rollout_salt") else {
|
||||
let Some(raw) = form.first(key) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let salt = raw.trim();
|
||||
if salt.is_empty() || salt.encode_utf16().count() > VOICE_NS_MAX_ROLLOUT_SALT_CHARS {
|
||||
if salt.is_empty() || salt.encode_utf16().count() > EXPERIMENT_MAX_ROLLOUT_SALT_CHARS {
|
||||
return Err(format!(
|
||||
"Rollout salt must be between 1 and {VOICE_NS_MAX_ROLLOUT_SALT_CHARS} characters"
|
||||
"Rollout salt must be between 1 and {EXPERIMENT_MAX_ROLLOUT_SALT_CHARS} characters"
|
||||
));
|
||||
}
|
||||
Ok(Some(salt.to_owned()))
|
||||
}
|
||||
|
||||
fn is_voice_noise_suppression_snowflake(value: &str) -> bool {
|
||||
fn parse_ascii_experiment_rollout_salt(
|
||||
form: &MultiValueForm,
|
||||
key: &str,
|
||||
) -> Result<Option<String>, String> {
|
||||
let salt = parse_experiment_rollout_salt(form, key)?;
|
||||
if let Some(value) = salt.as_deref()
|
||||
&& !value
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_graphic() || byte == b' ')
|
||||
{
|
||||
return Err("Rollout salt must use printable ASCII".to_owned());
|
||||
}
|
||||
Ok(salt)
|
||||
}
|
||||
|
||||
fn is_experiment_snowflake(value: &str) -> bool {
|
||||
!value.is_empty()
|
||||
&& value.len() <= VOICE_NS_MAX_SNOWFLAKE_LENGTH
|
||||
&& value.len() <= EXPERIMENT_MAX_SNOWFLAKE_LENGTH
|
||||
&& value.bytes().all(|byte| byte.is_ascii_digit())
|
||||
}
|
||||
|
||||
fn parse_voice_noise_suppression_user_ids(value: &str, label: &str) -> Result<Vec<String>, String> {
|
||||
fn parse_experiment_user_ids(value: &str, label: &str) -> Result<Vec<String>, String> {
|
||||
let mut ids: Vec<String> = Vec::new();
|
||||
for (index, candidate) in value.split([',', '\n', '\r']).enumerate() {
|
||||
let candidate = candidate.trim();
|
||||
if candidate.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if !is_voice_noise_suppression_snowflake(candidate) {
|
||||
if !is_experiment_snowflake(candidate) {
|
||||
return Err(format!(
|
||||
"{label} entry {} must contain 1 to 20 decimal digits",
|
||||
index + 1
|
||||
@@ -513,9 +538,9 @@ fn parse_voice_noise_suppression_user_ids(value: &str, label: &str) -> Result<Ve
|
||||
if ids.iter().any(|existing| existing == candidate) {
|
||||
continue;
|
||||
}
|
||||
if ids.len() == VOICE_NS_MAX_TARGETED_USERS {
|
||||
if ids.len() == EXPERIMENT_MAX_TARGETED_USERS {
|
||||
return Err(format!(
|
||||
"{label} must contain at most {VOICE_NS_MAX_TARGETED_USERS} unique IDs"
|
||||
"{label} must contain at most {EXPERIMENT_MAX_TARGETED_USERS} unique IDs"
|
||||
));
|
||||
}
|
||||
ids.push(candidate.to_owned());
|
||||
@@ -536,7 +561,7 @@ fn parse_voice_noise_suppression_guild_overrides(
|
||||
format!("Guild overrides line {line_number} must use guild_id=backend")
|
||||
})?;
|
||||
let guild_id = guild_id.trim();
|
||||
if !is_voice_noise_suppression_snowflake(guild_id) {
|
||||
if !is_experiment_snowflake(guild_id) {
|
||||
return Err(format!(
|
||||
"Guild overrides line {line_number} must use a guild ID with 1 to 20 decimal digits"
|
||||
));
|
||||
@@ -602,21 +627,20 @@ fn build_voice_noise_suppression_update(
|
||||
"voice_ns_rollout_basis_points",
|
||||
"Rollout basis points",
|
||||
0,
|
||||
VOICE_NS_ROLLOUT_BASIS_POINTS_MAX,
|
||||
EXPERIMENT_ROLLOUT_BASIS_POINTS_MAX,
|
||||
)?,
|
||||
rollout_salt: parse_voice_noise_suppression_rollout_salt(form)?,
|
||||
included_user_ids: Some(parse_voice_noise_suppression_user_ids(
|
||||
rollout_salt: parse_experiment_rollout_salt(form, "voice_ns_rollout_salt")?,
|
||||
included_user_ids: Some(parse_experiment_user_ids(
|
||||
form.first("voice_ns_included_user_ids").unwrap_or_default(),
|
||||
"Included user IDs",
|
||||
)?),
|
||||
excluded_user_ids: Some(parse_voice_noise_suppression_user_ids(
|
||||
excluded_user_ids: Some(parse_experiment_user_ids(
|
||||
form.first("voice_ns_excluded_user_ids").unwrap_or_default(),
|
||||
"Excluded user IDs",
|
||||
)?),
|
||||
guild_overrides: Some(parse_voice_noise_suppression_guild_overrides(
|
||||
form.first("voice_ns_guild_overrides").unwrap_or_default(),
|
||||
)?),
|
||||
stereo_enabled: Some(form.bool_value("voice_ns_stereo_enabled")),
|
||||
suppression_strength: parse_form_number(
|
||||
form,
|
||||
"voice_ns_suppression_strength",
|
||||
@@ -629,6 +653,78 @@ fn build_voice_noise_suppression_update(
|
||||
})
|
||||
}
|
||||
|
||||
fn build_push_service_delivery_update(
|
||||
form: &MultiValueForm,
|
||||
) -> Result<InstanceConfigUpdateRequest, String> {
|
||||
Ok(InstanceConfigUpdateRequest {
|
||||
push_service_delivery: Some(PushServiceDeliveryConfigUpdateRequest {
|
||||
enabled: Some(form.bool_value("push_service_delivery_enabled")),
|
||||
rollout_basis_points: parse_form_number(
|
||||
form,
|
||||
"push_service_delivery_rollout_basis_points",
|
||||
"Rollout basis points",
|
||||
0,
|
||||
EXPERIMENT_ROLLOUT_BASIS_POINTS_MAX,
|
||||
)?,
|
||||
rollout_salt: parse_ascii_experiment_rollout_salt(
|
||||
form,
|
||||
"push_service_delivery_rollout_salt",
|
||||
)?,
|
||||
included_user_ids: Some(parse_experiment_user_ids(
|
||||
form.first("push_service_delivery_included_user_ids")
|
||||
.unwrap_or_default(),
|
||||
"Included user IDs",
|
||||
)?),
|
||||
excluded_user_ids: Some(parse_experiment_user_ids(
|
||||
form.first("push_service_delivery_excluded_user_ids")
|
||||
.unwrap_or_default(),
|
||||
"Excluded user IDs",
|
||||
)?),
|
||||
}),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn build_domain_migration_update(
|
||||
form: &MultiValueForm,
|
||||
) -> Result<InstanceConfigUpdateRequest, String> {
|
||||
Ok(InstanceConfigUpdateRequest {
|
||||
domain_migration: Some(DomainMigrationConfigUpdateRequest {
|
||||
enabled: Some(form.bool_value("domain_migration_enabled")),
|
||||
rollout_basis_points: parse_form_number(
|
||||
form,
|
||||
"domain_migration_rollout_basis_points",
|
||||
"Rollout basis points",
|
||||
0,
|
||||
EXPERIMENT_ROLLOUT_BASIS_POINTS_MAX,
|
||||
)?,
|
||||
rollout_salt: parse_ascii_experiment_rollout_salt(
|
||||
form,
|
||||
"domain_migration_rollout_salt",
|
||||
)?,
|
||||
included_user_ids: Some(parse_experiment_user_ids(
|
||||
form.first("domain_migration_included_user_ids")
|
||||
.unwrap_or_default(),
|
||||
"Included user IDs",
|
||||
)?),
|
||||
excluded_user_ids: Some(parse_experiment_user_ids(
|
||||
form.first("domain_migration_excluded_user_ids")
|
||||
.unwrap_or_default(),
|
||||
"Excluded user IDs",
|
||||
)?),
|
||||
anonymous_rollout_basis_points: parse_form_number(
|
||||
form,
|
||||
"domain_migration_anonymous_rollout_basis_points",
|
||||
"Anonymous rollout basis points",
|
||||
0,
|
||||
EXPERIMENT_ROLLOUT_BASIS_POINTS_MAX,
|
||||
)?,
|
||||
standalone_forwarding: Some(form.bool_value("domain_migration_standalone_forwarding")),
|
||||
}),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn build_experiment_delivery_update(
|
||||
form: &MultiValueForm,
|
||||
) -> Result<InstanceConfigUpdateRequest, String> {
|
||||
@@ -683,6 +779,8 @@ fn build_app_public_update(form: &MultiValueForm) -> InstanceConfigUpdateRequest
|
||||
wordmark_url: optional("app_wordmark_url"),
|
||||
favicon_url: optional("app_favicon_url"),
|
||||
theme_color: optional("app_theme_color"),
|
||||
status_page_url: optional("app_status_page_url"),
|
||||
status_page_incident_history_url: optional("app_status_page_incident_history_url"),
|
||||
}),
|
||||
setup: Some(AppSetupConfigUpdateRequest {
|
||||
configured: Some(form.bool_value("app_setup_configured")),
|
||||
@@ -1244,7 +1342,6 @@ mod tests {
|
||||
.expect("voice noise suppression update");
|
||||
assert_eq!(update.enabled, Some(true));
|
||||
assert_eq!(update.allow_user_override, Some(true));
|
||||
assert_eq!(update.stereo_enabled, Some(false));
|
||||
assert_eq!(
|
||||
update.default_backend,
|
||||
Some(NoiseSuppressionBackend::Rnnoise)
|
||||
@@ -1270,7 +1367,6 @@ mod tests {
|
||||
serde_json::json!({"voice_noise_suppression": {
|
||||
"enabled": false,
|
||||
"allow_user_override": false,
|
||||
"stereo_enabled": false,
|
||||
"enabled_backends": [],
|
||||
"included_user_ids": [],
|
||||
"excluded_user_ids": [],
|
||||
@@ -1305,9 +1401,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_voice_noise_suppression_user_ids_splits_newlines_and_commas() {
|
||||
fn parse_experiment_user_ids_splits_newlines_and_commas() {
|
||||
assert_eq!(
|
||||
parse_voice_noise_suppression_user_ids(" 1 ,2\n3\r\n 4 ,, 5 ", "Included user IDs")
|
||||
parse_experiment_user_ids(" 1 ,2\n3\r\n 4 ,, 5 ", "Included user IDs")
|
||||
.expect("valid IDs"),
|
||||
vec![
|
||||
"1".to_owned(),
|
||||
@@ -1320,16 +1416,15 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_voice_noise_suppression_user_ids_dedupes_preserving_order() {
|
||||
fn parse_experiment_user_ids_dedupes_preserving_order() {
|
||||
assert_eq!(
|
||||
parse_voice_noise_suppression_user_ids("20,10,20,10,30", "Included user IDs")
|
||||
.expect("valid IDs"),
|
||||
parse_experiment_user_ids("20,10,20,10,30", "Included user IDs").expect("valid IDs"),
|
||||
vec!["20".to_owned(), "10".to_owned(), "30".to_owned()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_voice_noise_suppression_user_ids_rejects_non_digit_and_overlong_values() {
|
||||
fn parse_experiment_user_ids_rejects_non_digit_and_overlong_values() {
|
||||
for value in [
|
||||
"abc",
|
||||
"12a",
|
||||
@@ -1339,11 +1434,8 @@ mod tests {
|
||||
"<script>",
|
||||
] {
|
||||
assert_eq!(
|
||||
parse_voice_noise_suppression_user_ids(
|
||||
&format!("123,{value}"),
|
||||
"Included user IDs"
|
||||
)
|
||||
.expect_err("invalid ID"),
|
||||
parse_experiment_user_ids(&format!("123,{value}"), "Included user IDs")
|
||||
.expect_err("invalid ID"),
|
||||
"Included user IDs entry 2 must contain 1 to 20 decimal digits",
|
||||
"{value}"
|
||||
);
|
||||
@@ -1351,18 +1443,17 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_voice_noise_suppression_user_ids_rejects_exceeding_the_cap() {
|
||||
let value = (0..VOICE_NS_MAX_TARGETED_USERS)
|
||||
fn parse_experiment_user_ids_rejects_exceeding_the_cap() {
|
||||
let value = (0..EXPERIMENT_MAX_TARGETED_USERS)
|
||||
.map(|index| index.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let ids =
|
||||
parse_voice_noise_suppression_user_ids(&format!("{value}\n999"), "Included user IDs")
|
||||
.expect("valid IDs at cap");
|
||||
assert_eq!(ids.len(), VOICE_NS_MAX_TARGETED_USERS);
|
||||
let ids = parse_experiment_user_ids(&format!("{value}\n999"), "Included user IDs")
|
||||
.expect("valid IDs at cap");
|
||||
assert_eq!(ids.len(), EXPERIMENT_MAX_TARGETED_USERS);
|
||||
assert_eq!(ids.last(), Some(&"999".to_owned()));
|
||||
assert_eq!(
|
||||
parse_voice_noise_suppression_user_ids(&format!("{value}\n1000"), "Included user IDs")
|
||||
parse_experiment_user_ids(&format!("{value}\n1000"), "Included user IDs")
|
||||
.expect_err("too many IDs"),
|
||||
"Included user IDs must contain at most 1000 unique IDs"
|
||||
);
|
||||
@@ -1555,6 +1646,91 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_domain_migration_update_reads_the_rollout_fields() {
|
||||
let form = MultiValueForm::parse(
|
||||
b"domain_migration_enabled=true&domain_migration_rollout_basis_points=%20250%20&domain_migration_rollout_salt=%20domain-migration-v2%20&domain_migration_included_user_ids=1500000000000000001%0A1500000000000000002&domain_migration_excluded_user_ids=1500000000000000003%2C%201500000000000000004&domain_migration_anonymous_rollout_basis_points=%20100%20&domain_migration_standalone_forwarding=true",
|
||||
);
|
||||
let update = build_domain_migration_update(&form)
|
||||
.expect("valid form")
|
||||
.domain_migration
|
||||
.expect("domain migration update");
|
||||
assert_eq!(update.enabled, Some(true));
|
||||
assert_eq!(update.rollout_basis_points, Some(250));
|
||||
assert_eq!(update.rollout_salt, Some("domain-migration-v2".to_owned()));
|
||||
assert_eq!(
|
||||
update.included_user_ids,
|
||||
Some(vec![
|
||||
"1500000000000000001".to_owned(),
|
||||
"1500000000000000002".to_owned()
|
||||
])
|
||||
);
|
||||
assert_eq!(
|
||||
update.excluded_user_ids,
|
||||
Some(vec![
|
||||
"1500000000000000003".to_owned(),
|
||||
"1500000000000000004".to_owned()
|
||||
])
|
||||
);
|
||||
assert_eq!(update.anonymous_rollout_basis_points, Some(100));
|
||||
assert_eq!(update.standalone_forwarding, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_domain_migration_update_leaves_the_feature_inert_when_nothing_is_submitted() {
|
||||
let form = MultiValueForm::parse(b"_csrf=token");
|
||||
let request = build_domain_migration_update(&form).expect("valid form");
|
||||
assert_eq!(
|
||||
serde_json::to_value(request).expect("serializable update"),
|
||||
serde_json::json!({"domain_migration": {
|
||||
"enabled": false,
|
||||
"included_user_ids": [],
|
||||
"excluded_user_ids": [],
|
||||
"standalone_forwarding": false,
|
||||
}})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_domain_migration_update_rejects_invalid_rollout_fields() {
|
||||
for (form, message) in [
|
||||
(
|
||||
"domain_migration_rollout_basis_points=10001",
|
||||
"Rollout basis points must be a whole number between 0 and 10000",
|
||||
),
|
||||
(
|
||||
"domain_migration_anonymous_rollout_basis_points=10001",
|
||||
"Anonymous rollout basis points must be a whole number between 0 and 10000",
|
||||
),
|
||||
(
|
||||
"domain_migration_anonymous_rollout_basis_points=abc",
|
||||
"Anonymous rollout basis points must be a whole number between 0 and 10000",
|
||||
),
|
||||
(
|
||||
"domain_migration_rollout_salt=%20%20",
|
||||
"Rollout salt must be between 1 and 64 characters",
|
||||
),
|
||||
(
|
||||
"domain_migration_rollout_salt=caf%C3%A9",
|
||||
"Rollout salt must use printable ASCII",
|
||||
),
|
||||
(
|
||||
"domain_migration_included_user_ids=123%2Cinvalid",
|
||||
"Included user IDs entry 2 must contain 1 to 20 decimal digits",
|
||||
),
|
||||
(
|
||||
"domain_migration_excluded_user_ids=123%2Cinvalid",
|
||||
"Excluded user IDs entry 2 must contain 1 to 20 decimal digits",
|
||||
),
|
||||
] {
|
||||
let form = MultiValueForm::parse(form.as_bytes());
|
||||
assert_eq!(
|
||||
build_domain_migration_update(&form).expect_err("invalid rollout field"),
|
||||
message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_experiment_delivery_update_leaves_both_fields_unchanged_when_absent() {
|
||||
let form = MultiValueForm::parse(b"_csrf=token");
|
||||
|
||||
@@ -73,15 +73,11 @@ pub async fn render(
|
||||
.map(|r| r.sessions)
|
||||
.map_err(|error| tracing::warn!(%error, user_id, "admin API request failed: list user sessions"))
|
||||
.unwrap_or_default();
|
||||
let webauthn_credentials = if u.authenticator_types.contains(&2) {
|
||||
client
|
||||
.list_webauthn_credentials(user_id)
|
||||
.await
|
||||
.map_err(|error| tracing::warn!(%error, user_id, "admin API request failed: list webauthn credentials"))
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let webauthn_credentials = client
|
||||
.list_webauthn_credentials(user_id)
|
||||
.await
|
||||
.map_err(|error| tracing::warn!(%error, user_id, "admin API request failed: list webauthn credentials"))
|
||||
.unwrap_or_default();
|
||||
Some(tabs::account::account_tab(
|
||||
config,
|
||||
&u,
|
||||
@@ -245,6 +241,7 @@ pub async fn render(
|
||||
admin_user_id: None,
|
||||
target_id: Some(user_id.to_owned()),
|
||||
target_type: None,
|
||||
access: Some("write".to_owned()),
|
||||
sort_by: Some("created_at".to_owned()),
|
||||
sort_order: Some("desc".to_owned()),
|
||||
limit,
|
||||
|
||||
@@ -72,7 +72,7 @@ pub fn audit_logs_for_target(
|
||||
let total_pages = total.div_ceil(u64::from(PAGE_SIZE)).max(1);
|
||||
let page_number = u64::from(current_page) + 1;
|
||||
let all_logs_href = format!(
|
||||
"{base_path}/audit-logs?target_id={}",
|
||||
"{base_path}/audit-logs?target_id={}&access=write",
|
||||
urlencoding::encode(target_id)
|
||||
);
|
||||
|
||||
@@ -94,7 +94,7 @@ pub fn audit_logs_for_target(
|
||||
}
|
||||
}
|
||||
@if entries.is_empty() {
|
||||
(empty_state("No admin actions have been recorded against this entity."))
|
||||
(empty_state("No admin write actions have been recorded against this entity."))
|
||||
} @else {
|
||||
(table_container(html! {
|
||||
(table(html! {
|
||||
|
||||
@@ -22,6 +22,7 @@ pub struct AuditLogsParams<'a> {
|
||||
pub admin_user_id: &'a str,
|
||||
pub target_id: &'a str,
|
||||
pub target_type: &'a str,
|
||||
pub access: &'a str,
|
||||
pub sort_by: &'a str,
|
||||
pub sort_order: &'a str,
|
||||
pub limit: u32,
|
||||
@@ -42,6 +43,11 @@ fn filters_section(base: &str, params: &AuditLogsParams<'_>) -> Markup {
|
||||
("file_sha", "File SHA"),
|
||||
("email", "Email"),
|
||||
];
|
||||
let access_options: &[(&str, &str)] = &[
|
||||
("", "All entries"),
|
||||
("write", "Writes only"),
|
||||
("read", "Reads only"),
|
||||
];
|
||||
let sort_options: &[(&str, &str)] = &[("createdAt", "Created at"), ("relevance", "Relevance")];
|
||||
let order_options: &[(&str, &str)] = &[("desc", "Newest first"), ("asc", "Oldest first")];
|
||||
let limit_options: &[(&str, &str)] =
|
||||
@@ -60,6 +66,7 @@ fn filters_section(base: &str, params: &AuditLogsParams<'_>) -> Markup {
|
||||
"Filter by admin user ID..."))
|
||||
(select_input("target_type", "Target type",
|
||||
target_type_options, params.target_type))
|
||||
(select_input("access", "Access", access_options, params.access))
|
||||
(select_input("sort_by", "Sort by", sort_options, params.sort_by))
|
||||
(select_input("sort_order", "Order", order_options, params.sort_order))
|
||||
(select_input("limit", "Page size", limit_options, &limit_str))
|
||||
@@ -81,6 +88,7 @@ fn build_pagination_url(base: &str, page: u32, params: &AuditLogsParams<'_>) ->
|
||||
("admin_user_id", params.admin_user_id),
|
||||
("target_id", params.target_id),
|
||||
("target_type", params.target_type),
|
||||
("access", params.access),
|
||||
("sort_by", params.sort_by),
|
||||
("sort_order", params.sort_order),
|
||||
]
|
||||
@@ -169,6 +177,7 @@ mod tests {
|
||||
admin_user_id: "",
|
||||
target_id: "",
|
||||
target_type: "bulk_job",
|
||||
access: "",
|
||||
sort_by: "createdAt",
|
||||
sort_order: "desc",
|
||||
limit: 50,
|
||||
@@ -176,5 +185,28 @@ mod tests {
|
||||
};
|
||||
let markup = filters_section("/admin", ¶ms).into_string();
|
||||
assert!(markup.contains(r#"<option value="bulk_job" selected>Bulk job</option>"#));
|
||||
assert!(markup.contains(r#"<option value="" selected>All entries</option>"#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn access_filter_survives_form_and_pagination() {
|
||||
let params = AuditLogsParams {
|
||||
query: "",
|
||||
admin_user_id: "",
|
||||
target_id: "1500000000000000001",
|
||||
target_type: "",
|
||||
access: "read",
|
||||
sort_by: "createdAt",
|
||||
sort_order: "desc",
|
||||
limit: 50,
|
||||
current_page: 0,
|
||||
};
|
||||
let markup = filters_section("/admin", ¶ms).into_string();
|
||||
assert!(markup.contains(r#"<select id="access" name="access""#));
|
||||
assert!(markup.contains(r#"<option value="read" selected>Reads only</option>"#));
|
||||
assert_eq!(
|
||||
build_pagination_url("/admin", 1, ¶ms),
|
||||
"/admin/audit-logs?page=1&target_id=1500000000000000001&access=read&sort_by=createdAt&sort_order=desc&limit=50"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
use crate::{
|
||||
api::types::{
|
||||
AppPublicConfigResponse, ExperimentDeliveryConfigResponse, GatewayRolloutConfigResponse,
|
||||
InstanceConfigResponse, InstanceIntegrationsResponse, InstanceMediaResponse,
|
||||
InstancePolicyResponse, InstanceRegistrationResponse, LimitConfigResponse,
|
||||
NoiseSuppressionBackend, PendingRegistrationResponse, RegistrationUrlResponse,
|
||||
SsoConfigResponse, VOICE_NS_MAX_GUILD_OVERRIDES, VOICE_NS_MAX_TARGETED_USERS,
|
||||
VoiceNoiseSuppressionConfigResponse,
|
||||
AppPublicConfigResponse, DOMAIN_MIGRATION_DEFAULT_SALT, DomainMigrationConfigResponse,
|
||||
EXPERIMENT_MAX_TARGETED_USERS, ExperimentDeliveryConfigResponse,
|
||||
GatewayRolloutConfigResponse, InstanceConfigResponse, InstanceIntegrationsResponse,
|
||||
InstanceMediaResponse, InstancePolicyResponse, InstanceRegistrationResponse,
|
||||
LimitConfigResponse, NoiseSuppressionBackend, PUSH_SERVICE_DELIVERY_DEFAULT_SALT,
|
||||
PendingRegistrationResponse, PushServiceDeliveryConfigResponse, RegistrationUrlResponse,
|
||||
SsoConfigResponse, VOICE_NS_MAX_GUILD_OVERRIDES, VoiceNoiseSuppressionConfigResponse,
|
||||
},
|
||||
config::AdminConfig,
|
||||
middleware::auth::AuthContext,
|
||||
@@ -148,6 +149,8 @@ pub fn instance_config_page(
|
||||
html! {
|
||||
(gateway_rollout_section(base, csrf_token, &instance_config.gateway_rollout))
|
||||
(voice_noise_suppression_section(base, csrf_token, &instance_config.voice_noise_suppression))
|
||||
(push_service_delivery_section(base, csrf_token, &instance_config.push_service_delivery))
|
||||
(domain_migration_section(base, csrf_token, &instance_config.domain_migration))
|
||||
(experiment_delivery_section(base, csrf_token, &instance_config.experiment_delivery))
|
||||
@if let Some(limit_config) = limit_config {
|
||||
(limit_config_section(base, limit_config))
|
||||
@@ -848,6 +851,18 @@ fn app_public_config_section(
|
||||
app_public.branding.favicon_url.as_deref().unwrap_or(""),
|
||||
"https://example.com/favicon.ico",
|
||||
))
|
||||
(text_input(
|
||||
"app_status_page_url",
|
||||
"Status page URL",
|
||||
app_public.branding.status_page_url.as_deref().unwrap_or(""),
|
||||
"https://fluxerstatus.com",
|
||||
))
|
||||
(text_input(
|
||||
"app_status_page_incident_history_url",
|
||||
"Status page history URL",
|
||||
app_public.branding.status_page_incident_history_url.as_deref().unwrap_or(""),
|
||||
"https://fluxerstatus.com/history",
|
||||
))
|
||||
}
|
||||
div class="space-y-2" {
|
||||
(checkbox(
|
||||
@@ -1093,12 +1108,12 @@ fn voice_noise_suppression_section(
|
||||
))
|
||||
(entry_count_hint(
|
||||
voice_noise_suppression.included_user_ids.len(),
|
||||
VOICE_NS_MAX_TARGETED_USERS,
|
||||
EXPERIMENT_MAX_TARGETED_USERS,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"One snowflake per line, or comma separated. These users are targeted \
|
||||
regardless of the percentage above. IDs must contain 1 to 20 decimal \
|
||||
digits. Invalid entries prevent the save; blank entries and duplicate \
|
||||
digits. Invalid entries prevent the save. Blank entries and duplicate \
|
||||
IDs are ignored."
|
||||
}
|
||||
}
|
||||
@@ -1113,11 +1128,11 @@ fn voice_noise_suppression_section(
|
||||
))
|
||||
(entry_count_hint(
|
||||
voice_noise_suppression.excluded_user_ids.len(),
|
||||
VOICE_NS_MAX_TARGETED_USERS,
|
||||
EXPERIMENT_MAX_TARGETED_USERS,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Same format. Exclusion wins over both the always-on list and the \
|
||||
percentage, so this is the per-user kill switch."
|
||||
percentage. This is the per-user kill switch."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1145,17 +1160,6 @@ fn voice_noise_suppression_section(
|
||||
}
|
||||
|
||||
h3 class="text-sm font-semibold text-neutral-900" { "Processing" }
|
||||
(checkbox(
|
||||
"voice_ns_stereo_enabled",
|
||||
"true",
|
||||
"Process stereo input instead of downmixing to mono",
|
||||
voice_noise_suppression.stereo_enabled,
|
||||
true,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Costs more CPU on the client. Leave off unless you are testing stereo \
|
||||
capture."
|
||||
}
|
||||
div class="grid grid-cols-1 gap-4 sm:grid-cols-2" {
|
||||
(number_field(
|
||||
"voice_ns_suppression_strength",
|
||||
@@ -1175,6 +1179,248 @@ fn voice_noise_suppression_section(
|
||||
)
|
||||
}
|
||||
|
||||
fn push_service_delivery_section(
|
||||
base: &str,
|
||||
csrf_token: &str,
|
||||
push_service_delivery: &PushServiceDeliveryConfigResponse,
|
||||
) -> Markup {
|
||||
let status = if push_service_delivery.enabled {
|
||||
("Live", BadgeVariant::Success)
|
||||
} else {
|
||||
("Inert", BadgeVariant::Default)
|
||||
};
|
||||
let included_user_ids = push_service_delivery.included_user_ids.join("\n");
|
||||
let excluded_user_ids = push_service_delivery.excluded_user_ids.join("\n");
|
||||
section_card_with_description(
|
||||
"Push Service Delivery",
|
||||
"Routes push notification delivery for the selected accounts through the push service. \
|
||||
Accounts the rollout does not select keep the current path.",
|
||||
html! {
|
||||
form method="post" action={(base) "/instance-config?action=update_push_service_delivery"} {
|
||||
(csrf_input(csrf_token))
|
||||
div class="space-y-6" {
|
||||
div class="flex flex-wrap items-center gap-2" {
|
||||
h3 class="text-sm font-semibold text-neutral-900" { "Master switch" }
|
||||
(badge(status.0, status.1))
|
||||
span class="text-xs text-neutral-500" {
|
||||
"Config version " (push_service_delivery.config_version)
|
||||
}
|
||||
}
|
||||
(checkbox(
|
||||
"push_service_delivery_enabled",
|
||||
"true",
|
||||
"Hand push notifications to the push service",
|
||||
push_service_delivery.enabled,
|
||||
true,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Off is the safe state. With this unchecked every notification keeps the \
|
||||
current delivery path, so the rollout and targeting fields below have no \
|
||||
effect at all."
|
||||
}
|
||||
|
||||
h3 class="text-sm font-semibold text-neutral-900" { "Rollout" }
|
||||
(number_field(
|
||||
"push_service_delivery_rollout_basis_points",
|
||||
"Rollout (basis points)",
|
||||
&push_service_delivery.rollout_basis_points.to_string(),
|
||||
Some(0), Some(10000), "1",
|
||||
Some("Share of users bucketed into the canary, in basis points: 0 is nobody, 100 is 1%, 10000 is everybody."),
|
||||
))
|
||||
div class="flex flex-col gap-2" {
|
||||
(text_input(
|
||||
"push_service_delivery_rollout_salt",
|
||||
"Rollout Salt",
|
||||
&push_service_delivery.rollout_salt,
|
||||
PUSH_SERVICE_DELIVERY_DEFAULT_SALT,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Seeds the bucketing hash. Changing it reshuffles which users fall \
|
||||
inside the percentage above. Leave it alone to keep the current \
|
||||
cohort stable."
|
||||
}
|
||||
}
|
||||
div class="flex flex-col gap-2" {
|
||||
(textarea_input(
|
||||
"push_service_delivery_included_user_ids",
|
||||
"Always-on User IDs",
|
||||
"1500000000000000001\n1500000000000000002",
|
||||
&included_user_ids,
|
||||
4,
|
||||
false,
|
||||
))
|
||||
(entry_count_hint(
|
||||
push_service_delivery.included_user_ids.len(),
|
||||
EXPERIMENT_MAX_TARGETED_USERS,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"One snowflake per line, or comma separated. These users are targeted \
|
||||
regardless of the percentage above. IDs must contain 1 to 20 decimal \
|
||||
digits. Invalid entries prevent the save. Blank entries and duplicate \
|
||||
IDs are ignored."
|
||||
}
|
||||
}
|
||||
div class="flex flex-col gap-2" {
|
||||
(textarea_input(
|
||||
"push_service_delivery_excluded_user_ids",
|
||||
"Never-on User IDs",
|
||||
"1500000000000000003\n1500000000000000004",
|
||||
&excluded_user_ids,
|
||||
4,
|
||||
false,
|
||||
))
|
||||
(entry_count_hint(
|
||||
push_service_delivery.excluded_user_ids.len(),
|
||||
EXPERIMENT_MAX_TARGETED_USERS,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Same format. Exclusion wins over both the always-on list and the \
|
||||
percentage. This is the per-user kill switch."
|
||||
}
|
||||
}
|
||||
|
||||
(form_actions(html! {
|
||||
(submit_button("Save Push Service Delivery Configuration"))
|
||||
}))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn domain_migration_section(
|
||||
base: &str,
|
||||
csrf_token: &str,
|
||||
domain_migration: &DomainMigrationConfigResponse,
|
||||
) -> Markup {
|
||||
let status = if domain_migration.enabled {
|
||||
("Live", BadgeVariant::Success)
|
||||
} else {
|
||||
("Inert", BadgeVariant::Default)
|
||||
};
|
||||
let included_user_ids = domain_migration.included_user_ids.join("\n");
|
||||
let excluded_user_ids = domain_migration.excluded_user_ids.join("\n");
|
||||
section_card_with_description(
|
||||
"Domain Migration",
|
||||
"Moves web clients of the official instance from the legacy web app origin to the new \
|
||||
one. Selected accounts copy their local data across and continue on the new origin. \
|
||||
Clients of other instances read this configuration and ignore it.",
|
||||
html! {
|
||||
form method="post" action={(base) "/instance-config?action=update_domain_migration"} {
|
||||
(csrf_input(csrf_token))
|
||||
div class="space-y-6" {
|
||||
div class="flex flex-wrap items-center gap-2" {
|
||||
h3 class="text-sm font-semibold text-neutral-900" { "Master switch" }
|
||||
(badge(status.0, status.1))
|
||||
span class="text-xs text-neutral-500" {
|
||||
"Config version " (domain_migration.config_version)
|
||||
}
|
||||
}
|
||||
(checkbox(
|
||||
"domain_migration_enabled",
|
||||
"true",
|
||||
"Move selected web clients to the new origin",
|
||||
domain_migration.enabled,
|
||||
true,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Off is the safe state and the kill switch. With this unchecked no client \
|
||||
starts a migration and clients that already migrated stop forwarding the \
|
||||
legacy origin, so the rollout and targeting fields below have no effect at all."
|
||||
}
|
||||
|
||||
h3 class="text-sm font-semibold text-neutral-900" { "Installed apps" }
|
||||
(checkbox(
|
||||
"domain_migration_standalone_forwarding",
|
||||
"true",
|
||||
"Forward installed desktop web apps to the new origin",
|
||||
domain_migration.standalone_forwarding,
|
||||
true,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Leave this off until the manifest scope extension and the association file \
|
||||
are live and verified. While it is off, installed Chromium desktop apps copy \
|
||||
their data across but stay on the legacy origin and offer to install the new \
|
||||
app. Installed mobile and Safari apps never forward either way."
|
||||
}
|
||||
|
||||
h3 class="text-sm font-semibold text-neutral-900" { "Rollout" }
|
||||
(number_field(
|
||||
"domain_migration_rollout_basis_points",
|
||||
"Rollout (basis points)",
|
||||
&domain_migration.rollout_basis_points.to_string(),
|
||||
Some(0), Some(10000), "1",
|
||||
Some("Share of logged-in users bucketed into the migration, in basis points: 0 is nobody, 100 is 1%, 10000 is everybody."),
|
||||
))
|
||||
(number_field(
|
||||
"domain_migration_anonymous_rollout_basis_points",
|
||||
"Anonymous rollout (basis points)",
|
||||
&domain_migration.anonymous_rollout_basis_points.to_string(),
|
||||
Some(0), Some(10000), "1",
|
||||
Some("Share of logged-out devices sent to the new origin, in basis points. Each device is bucketed on its own random ID."),
|
||||
))
|
||||
div class="flex flex-col gap-2" {
|
||||
(text_input(
|
||||
"domain_migration_rollout_salt",
|
||||
"Rollout Salt",
|
||||
&domain_migration.rollout_salt,
|
||||
DOMAIN_MIGRATION_DEFAULT_SALT,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Seeds the bucketing hash for users and devices. Changing it reshuffles \
|
||||
which users and devices fall inside the percentages above. Leave it \
|
||||
alone to keep the current cohort stable."
|
||||
}
|
||||
}
|
||||
div class="flex flex-col gap-2" {
|
||||
(textarea_input(
|
||||
"domain_migration_included_user_ids",
|
||||
"Always-on User IDs",
|
||||
"1500000000000000001\n1500000000000000002",
|
||||
&included_user_ids,
|
||||
4,
|
||||
false,
|
||||
))
|
||||
(entry_count_hint(
|
||||
domain_migration.included_user_ids.len(),
|
||||
EXPERIMENT_MAX_TARGETED_USERS,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"One snowflake per line, or comma separated. These users are targeted \
|
||||
regardless of the percentage above. IDs must contain 1 to 20 decimal \
|
||||
digits. Invalid entries prevent the save. Blank entries and duplicate \
|
||||
IDs are ignored."
|
||||
}
|
||||
}
|
||||
div class="flex flex-col gap-2" {
|
||||
(textarea_input(
|
||||
"domain_migration_excluded_user_ids",
|
||||
"Never-on User IDs",
|
||||
"1500000000000000003\n1500000000000000004",
|
||||
&excluded_user_ids,
|
||||
4,
|
||||
false,
|
||||
))
|
||||
(entry_count_hint(
|
||||
domain_migration.excluded_user_ids.len(),
|
||||
EXPERIMENT_MAX_TARGETED_USERS,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Same format. Exclusion wins over both the always-on list and the \
|
||||
percentage. It stops new migrations only. A user who already moved \
|
||||
stays on the new origin."
|
||||
}
|
||||
}
|
||||
|
||||
(form_actions(html! {
|
||||
(submit_button("Save Domain Migration Configuration"))
|
||||
}))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn experiment_delivery_section(
|
||||
base: &str,
|
||||
csrf_token: &str,
|
||||
@@ -1183,7 +1429,7 @@ fn experiment_delivery_section(
|
||||
section_card_with_description(
|
||||
"Experiment Delivery",
|
||||
"How often every client revalidates its experiment assignments. This is instance-wide \
|
||||
and covers every experiment, not just the one above. Raising the interval sheds \
|
||||
and covers every experiment, not just the ones above. Raising the interval sheds \
|
||||
request volume and makes a change take longer to reach a client. Raising the jitter \
|
||||
spreads a fleet that has synchronised on one tick back out across the interval.",
|
||||
html! {
|
||||
@@ -1818,10 +2064,32 @@ mod tests {
|
||||
assert!(!markup.contains("at the cap"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn domain_migration_section_shows_both_rollouts_and_list_counts() {
|
||||
let domain_migration = DomainMigrationConfigResponse {
|
||||
anonymous_rollout_basis_points: 250,
|
||||
included_user_ids: vec!["1500000000000000001".to_owned()],
|
||||
excluded_user_ids: vec![
|
||||
"1500000000000000002".to_owned(),
|
||||
"1500000000000000003".to_owned(),
|
||||
],
|
||||
..DomainMigrationConfigResponse::default()
|
||||
};
|
||||
let markup = domain_migration_section("/admin", "csrf", &domain_migration).into_string();
|
||||
assert!(markup.contains("action=update_domain_migration"));
|
||||
assert!(markup.contains("domain_migration_enabled"));
|
||||
assert!(markup.contains("name=\"domain_migration_anonymous_rollout_basis_points\""));
|
||||
assert!(markup.contains("value=\"250\""));
|
||||
assert!(markup.contains("name=\"domain_migration_standalone_forwarding\""));
|
||||
assert!(markup.contains("1 of 1000 stored"));
|
||||
assert!(markup.contains("2 of 1000 stored"));
|
||||
assert!(!markup.contains("at the cap"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn voice_noise_suppression_section_flags_a_list_at_its_cap() {
|
||||
let voice_noise_suppression = VoiceNoiseSuppressionConfigResponse {
|
||||
included_user_ids: (0..VOICE_NS_MAX_TARGETED_USERS)
|
||||
included_user_ids: (0..EXPERIMENT_MAX_TARGETED_USERS)
|
||||
.map(|index| index.to_string())
|
||||
.collect(),
|
||||
..VoiceNoiseSuppressionConfigResponse::default()
|
||||
|
||||
@@ -114,7 +114,10 @@ pub fn users_list_page(
|
||||
let content = html! {
|
||||
div class="space-y-6" {
|
||||
(page_header("Users", None))
|
||||
div class="rounded-lg bg-white transition-all border border-neutral-200 p-4" {
|
||||
div class="rounded-lg bg-white transition-all border border-neutral-200 p-3" {
|
||||
p class="mb-1 text-xs text-neutral-500" {
|
||||
"For example, type " span class="font-mono" { "*" } " in to search for all users."
|
||||
}
|
||||
(search_form(base, params))
|
||||
}
|
||||
(results_markup)
|
||||
|
||||
@@ -230,6 +230,7 @@ fn deserialize_audit_logs_response() {
|
||||
"target_type": "user",
|
||||
"target_id": "1130958221824557056",
|
||||
"action": "list_user_sessions",
|
||||
"access": "read",
|
||||
"audit_log_reason": null,
|
||||
"metadata": {"session_count": "3"},
|
||||
"created_at": "2026-05-26T13:21:47.138Z"
|
||||
@@ -243,6 +244,7 @@ fn deserialize_audit_logs_response() {
|
||||
assert_eq!(resp.logs.len(), 1);
|
||||
assert_eq!(resp.logs[0].log_id, "1508822460457747580");
|
||||
assert_eq!(resp.logs[0].action, "list_user_sessions");
|
||||
assert_eq!(resp.logs[0].access.as_deref(), Some("read"));
|
||||
assert_eq!(resp.logs[0].target_type, "user");
|
||||
assert!(resp.logs[0].audit_log_reason.is_none());
|
||||
assert_eq!(resp.logs[0].metadata.get("session_count").unwrap(), "3");
|
||||
@@ -401,13 +403,31 @@ fn deserialize_instance_config_response_with_unknown_keys() {
|
||||
"included_user_ids": [],
|
||||
"excluded_user_ids": [],
|
||||
"guild_overrides": [],
|
||||
"stereo_enabled": false,
|
||||
"suppression_strength": 80,
|
||||
"future_presentation_knob": "verbose",
|
||||
"future_knob": 7,
|
||||
"future_object_knob": {"nested": true},
|
||||
"future_list_knob": ["a", "b"]
|
||||
},
|
||||
"push_service_delivery": {
|
||||
"enabled": true,
|
||||
"config_version": 3,
|
||||
"rollout_basis_points": 5000,
|
||||
"rollout_salt": "push-service-delivery-v1",
|
||||
"included_user_ids": ["1500000000000000002"],
|
||||
"excluded_user_ids": []
|
||||
},
|
||||
"domain_migration": {
|
||||
"enabled": true,
|
||||
"config_version": 2,
|
||||
"rollout_basis_points": 2500,
|
||||
"rollout_salt": "domain-migration-v1",
|
||||
"included_user_ids": ["1500000000000000001"],
|
||||
"excluded_user_ids": [],
|
||||
"future_migration_knob": 9,
|
||||
"anonymous_rollout_basis_points": 100,
|
||||
"standalone_forwarding": true
|
||||
},
|
||||
"experiment_delivery": {"poll_interval_seconds": 300, "poll_jitter_percent": 15},
|
||||
"registration": {
|
||||
"mode": "open",
|
||||
@@ -537,6 +557,13 @@ fn deserialize_instance_config_response_with_unknown_keys() {
|
||||
assert_eq!(resp.voice_noise_suppression.rollout_basis_points, 10000);
|
||||
assert_eq!(*resp.voice_noise_suppression.rollout_salt, "voice-ns-v1");
|
||||
assert_eq!(resp.voice_noise_suppression.enabled_backends.len(), 3);
|
||||
assert!(resp.domain_migration.enabled);
|
||||
assert_eq!(resp.domain_migration.config_version, 2);
|
||||
assert_eq!(resp.domain_migration.rollout_basis_points, 2500);
|
||||
assert_eq!(*resp.domain_migration.rollout_salt, "domain-migration-v1");
|
||||
assert_eq!(resp.domain_migration.included_user_ids.len(), 1);
|
||||
assert_eq!(resp.domain_migration.anonymous_rollout_basis_points, 100);
|
||||
assert!(resp.domain_migration.standalone_forwarding);
|
||||
assert_eq!(resp.experiment_delivery.poll_interval_seconds, 300);
|
||||
assert!(resp.policy.single_community_guild_id.is_none());
|
||||
assert_eq!(resp.policy.services.gif_enabled, Some(true));
|
||||
@@ -547,6 +574,7 @@ fn deserialize_instance_config_response_with_unknown_keys() {
|
||||
.replace("\"future_rollout_knob\": 3,", "")
|
||||
.replace("\"future_presentation_knob\": \"verbose\",", "")
|
||||
.replace("\"future_knob\": 7,", "")
|
||||
.replace("\"future_migration_knob\": 9,", "")
|
||||
.replace("\"future_object_knob\": {\"nested\": true},", "")
|
||||
.replace("\"future_list_knob\": [\"a\", \"b\"],", "")
|
||||
.replace(
|
||||
|
||||
@@ -168,6 +168,39 @@ async fn detail_tab_routes_return_layout_or_fragments_by_route_shape() {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn target_audit_log_tabs_request_write_entries_only() {
|
||||
let app = setup().await;
|
||||
for path in [
|
||||
"/users/1500000000000000001/tabs/audit_logs",
|
||||
"/guilds/1600000000000000001/tabs/audit_logs",
|
||||
] {
|
||||
let fragment = get(&app, path, &[]).await;
|
||||
assert!(fragment.contains("Temp ban"), "{path}\n{fragment}");
|
||||
assert!(!fragment.contains("Get user"), "{path}\n{fragment}");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn audit_log_page_forwards_the_access_filter() {
|
||||
let app = setup().await;
|
||||
let all = get(&app, "/audit-logs", &[]).await;
|
||||
assert!(all.contains("Temp ban"), "{all}");
|
||||
assert!(all.contains("Get user"), "{all}");
|
||||
assert!(
|
||||
all.contains(r#"<option value="" selected>All entries</option>"#),
|
||||
"{all}"
|
||||
);
|
||||
|
||||
let reads = get(&app, "/audit-logs?access=read", &[]).await;
|
||||
assert!(reads.contains("Get user"), "{reads}");
|
||||
assert!(!reads.contains("Temp ban"), "{reads}");
|
||||
assert!(
|
||||
reads.contains(r#"<option value="read" selected>Reads only</option>"#),
|
||||
"{reads}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn report_routes_keep_layout_and_fragment_contract() {
|
||||
let app = setup().await;
|
||||
@@ -432,6 +465,7 @@ async fn mutating_admin_pages_render_usable_csrf_tokens() {
|
||||
"/instance-config?action=update_gateway_rollout",
|
||||
"/instance-config?action=update_sso",
|
||||
"/instance-config?action=update_voice_noise_suppression",
|
||||
"/instance-config?action=update_domain_migration",
|
||||
"/instance-config?action=update_experiment_delivery",
|
||||
][..],
|
||||
),
|
||||
@@ -783,6 +817,9 @@ async fn spawn_mock_api() -> String {
|
||||
|
||||
async fn mock_api(method: Method, uri: Uri) -> Response {
|
||||
let path = uri.path().to_owned();
|
||||
if method == Method::PATCH && path == "/admin/instance/config" {
|
||||
return json_response(instance_config());
|
||||
}
|
||||
match (method, path.as_str()) {
|
||||
(Method::GET, "/admin/users/@me") => json_response(json!({ "user": admin_user() })),
|
||||
(Method::GET, "/admin/api-keys") => json_response(json!([])),
|
||||
@@ -844,6 +881,25 @@ async fn mock_api(method: Method, uri: Uri) -> Response {
|
||||
json_response(instance_config_without_pending_registrations())
|
||||
}
|
||||
(Method::GET, "/admin/limit-config") => json_response(limit_config()),
|
||||
(Method::GET, "/admin/audit-logs") => {
|
||||
let access = uri.query().and_then(|query| {
|
||||
url::form_urlencoded::parse(query.as_bytes())
|
||||
.find(|(key, _)| key == "access")
|
||||
.map(|(_, value)| value.into_owned())
|
||||
});
|
||||
let logs = [
|
||||
audit_log_entry("1900000000000000101", "get_user", "read"),
|
||||
audit_log_entry("1900000000000000102", "temp_ban", "write"),
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|entry| {
|
||||
access
|
||||
.as_deref()
|
||||
.is_none_or(|access| entry.access.to_string() == access)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
json_response(json!({ "total": logs.len(), "logs": logs }))
|
||||
}
|
||||
_ => (StatusCode::NOT_FOUND, Json(json!({ "error": "not found" }))).into_response(),
|
||||
}
|
||||
}
|
||||
@@ -975,6 +1031,32 @@ fn guild_fixtures_match_generated_response_contracts() {
|
||||
assert_eq!(detail.member_count, 12);
|
||||
}
|
||||
|
||||
fn audit_log_entry(
|
||||
log_id: &str,
|
||||
action: &str,
|
||||
access: &str,
|
||||
) -> generated_types::AdminAuditLogResponseSchema {
|
||||
serde_json::from_value(json!({
|
||||
"log_id": log_id,
|
||||
"admin_user_id": "1500000000000000000",
|
||||
"admin_user": null,
|
||||
"target_type": "user",
|
||||
"target_id": "1500000000000000001",
|
||||
"target_user": null,
|
||||
"target_guild": null,
|
||||
"target_channel": null,
|
||||
"related_users": {},
|
||||
"related_guilds": {},
|
||||
"related_channels": {},
|
||||
"action": action,
|
||||
"access": access,
|
||||
"audit_log_reason": null,
|
||||
"metadata": {},
|
||||
"created_at": "2026-09-16T12:00:00.000Z"
|
||||
}))
|
||||
.expect("audit log fixture must match the generated response contract")
|
||||
}
|
||||
|
||||
fn searched_application() -> Value {
|
||||
json!({
|
||||
"id": "1700000000000000001",
|
||||
@@ -1116,9 +1198,18 @@ fn instance_config() -> Value {
|
||||
"included_user_ids": [],
|
||||
"excluded_user_ids": [],
|
||||
"guild_overrides": [],
|
||||
"stereo_enabled": false,
|
||||
"suppression_strength": 80
|
||||
},
|
||||
"domain_migration": {
|
||||
"enabled": false,
|
||||
"config_version": 0,
|
||||
"rollout_basis_points": 0,
|
||||
"rollout_salt": "domain-migration-v1",
|
||||
"included_user_ids": [],
|
||||
"excluded_user_ids": [],
|
||||
"anonymous_rollout_basis_points": 0,
|
||||
"standalone_forwarding": false
|
||||
},
|
||||
"experiment_delivery": {
|
||||
"poll_interval_seconds": 300,
|
||||
"poll_jitter_percent": 15
|
||||
|
||||
+8
-12
@@ -1,11 +1,11 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
ARG BUILD_VERSION
|
||||
|
||||
FROM node:24-bookworm-slim AS base
|
||||
FROM node:26-trixie-slim AS base
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN corepack enable && corepack prepare pnpm@10.29.3 --activate
|
||||
RUN npm install -g pnpm@11.27.0
|
||||
|
||||
FROM base AS deploy
|
||||
|
||||
@@ -23,9 +23,9 @@ COPY . .
|
||||
|
||||
RUN pnpm install --frozen-lockfile
|
||||
RUN pnpm --filter fluxer_api run build
|
||||
RUN pnpm deploy --legacy --filter=fluxer_api --prod --config.allowUnusedPatches=true /out
|
||||
RUN pnpm deploy --legacy --filter=fluxer_api --prod --config.allow-unused-patches=true /out
|
||||
|
||||
FROM node:24-bookworm-slim
|
||||
FROM node:26-trixie-slim
|
||||
|
||||
ARG BUILD_VERSION
|
||||
ARG SOURCE_SHA
|
||||
@@ -45,32 +45,28 @@ LABEL app.fluxer.build-version="${BUILD_VERSION}"
|
||||
|
||||
WORKDIR /usr/src/app/fluxer_api
|
||||
|
||||
RUN echo 'deb http://deb.debian.org/debian bookworm-backports main' > /etc/apt/sources.list.d/backports.list && \
|
||||
apt-get update && apt-get install -y --no-install-recommends \
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
ffmpeg \
|
||||
libimage-exiftool-perl \
|
||||
libwebp7 \
|
||||
libwebpmux3 \
|
||||
libheif1 \
|
||||
libvips42 && \
|
||||
apt-get install -y --no-install-recommends -t bookworm-backports \
|
||||
libheif-plugin-libde265 \
|
||||
libheif-plugin-dav1d && \
|
||||
libheif-plugin-dav1d \
|
||||
libvips42t64 && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN corepack enable && corepack prepare pnpm@10.29.3 --activate
|
||||
RUN npm install -g pnpm@11.27.0
|
||||
|
||||
COPY --from=deploy /out ./
|
||||
COPY --from=deploy /usr/src/app/fluxer_api/dist ./dist
|
||||
COPY --from=deploy /usr/src/app/tsconfigs /usr/src/app/tsconfigs
|
||||
|
||||
RUN rm -rf pkgs && \
|
||||
mkdir -p /usr/src/app/.cache/corepack && \
|
||||
chown -R 65532:65532 /usr/src/app
|
||||
|
||||
ENV HOME=/usr/src/app
|
||||
ENV COREPACK_HOME=/usr/src/app/.cache/corepack
|
||||
ENV NODE_ENV=production
|
||||
ENV NODE_OPTIONS="--enable-source-maps"
|
||||
ENV NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
|
||||
|
||||
@@ -5,19 +5,19 @@
|
||||
"scripts": {
|
||||
"build": "node scripts/build.mjs",
|
||||
"test": "vitest run",
|
||||
"typecheck": "tsgo --noEmit",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "tsx watch --clear-screen=false src/AppEntrypoint.ts",
|
||||
"start": "tsx src/AppEntrypoint.ts",
|
||||
"start:worker": "tsx src/WorkerEntrypoint.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "catalog:",
|
||||
"@atproto/jwk-jose": "catalog:",
|
||||
"@atproto/oauth-client-node": "catalog:",
|
||||
"@aws-sdk/client-s3": "catalog:",
|
||||
"@aws-sdk/lib-storage": "catalog:",
|
||||
"@aws-sdk/s3-request-presigner": "catalog:",
|
||||
"@bluesky-social/jwk-jose": "catalog:",
|
||||
"@bluesky-social/oauth-client-node": "catalog:",
|
||||
"@bufbuild/protobuf": "^2.12.0",
|
||||
"@bufbuild/protobuf": "^2.15.0",
|
||||
"@elastic/elasticsearch": "catalog:",
|
||||
"@fluxer/config": "workspace:*",
|
||||
"@fluxer/constants": "workspace:*",
|
||||
@@ -34,6 +34,8 @@
|
||||
"@hono/node-server": "catalog:",
|
||||
"@messageformat/core": "catalog:",
|
||||
"@messageformat/parser": "catalog:",
|
||||
"@nats-io/jetstream": "catalog:",
|
||||
"@nats-io/transport-node": "catalog:",
|
||||
"@pkgs/cache": "workspace:*",
|
||||
"@pkgs/captcha": "workspace:*",
|
||||
"@pkgs/cassandra": "workspace:*",
|
||||
@@ -71,7 +73,6 @@
|
||||
"lodash": "catalog:",
|
||||
"maxmind": "catalog:",
|
||||
"mime": "catalog:",
|
||||
"nats": "catalog:",
|
||||
"nodemailer": "catalog:",
|
||||
"pg": "catalog:",
|
||||
"pino": "catalog:",
|
||||
@@ -88,10 +89,10 @@
|
||||
"devDependencies": {
|
||||
"@types/archiver": "catalog:",
|
||||
"@types/lodash": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"esbuild": "catalog:",
|
||||
"msw": "catalog:",
|
||||
"typescript": "catalog:ts7",
|
||||
"vitest": "catalog:"
|
||||
},
|
||||
"packageManager": "pnpm@10.29.3"
|
||||
"packageManager": "pnpm@11.27.0"
|
||||
}
|
||||
|
||||
Vendored
+2
-2
@@ -9,14 +9,14 @@
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pkgs/kv_client": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:ts7",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"./*": "./*"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluxer/logger": "workspace:*",
|
||||
@@ -15,6 +15,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
"typescript": "catalog:ts7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
"./*": "./*"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"cassandra-driver": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
"typescript": "catalog:ts7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"./*": "./*"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@elastic/elasticsearch": "catalog:",
|
||||
@@ -15,6 +15,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
"typescript": "catalog:ts7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {AuditLogSearchFilters, SearchableAuditLog} from '@fluxer/schema/src
|
||||
import type {ElasticsearchDistributedLock} from '@pkgs/elasticsearch_search/src/adapters/ElasticsearchIndexAdapter';
|
||||
import {ElasticsearchIndexAdapter} from '@pkgs/elasticsearch_search/src/adapters/ElasticsearchIndexAdapter';
|
||||
import type {ElasticsearchFilter} from '@pkgs/elasticsearch_search/src/ElasticsearchFilterUtils';
|
||||
import {compactFilters, esTermFilter} from '@pkgs/elasticsearch_search/src/ElasticsearchFilterUtils';
|
||||
import {compactFilters, esTermFilter, esTermsFilter} from '@pkgs/elasticsearch_search/src/ElasticsearchFilterUtils';
|
||||
import {ELASTICSEARCH_INDEX_DEFINITIONS} from '@pkgs/elasticsearch_search/src/ElasticsearchIndexDefinitions';
|
||||
|
||||
function buildAuditLogFilters(filters: AuditLogSearchFilters): Array<ElasticsearchFilter | undefined> {
|
||||
@@ -15,6 +15,10 @@ function buildAuditLogFilters(filters: AuditLogSearchFilters): Array<Elasticsear
|
||||
if (filters.targetType) clauses.push(esTermFilter('targetType', filters.targetType));
|
||||
if (filters.targetId) clauses.push(esTermFilter('targetId', filters.targetId));
|
||||
if (filters.action) clauses.push(esTermFilter('action', filters.action));
|
||||
if (filters.actions && filters.actions.length > 0) clauses.push(esTermsFilter('action.keyword', filters.actions));
|
||||
if (filters.excludeActions && filters.excludeActions.length > 0) {
|
||||
clauses.push({bool: {must_not: [esTermsFilter('action.keyword', filters.excludeActions)]}});
|
||||
}
|
||||
return compactFilters(clauses);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluxer/i18n": "workspace:*",
|
||||
@@ -19,8 +19,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@types/nodemailer": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:ts7",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import {createLogger} from '@fluxer/logger/src/Logger';
|
||||
import type {EmailMessage, IEmailProvider} from '@pkgs/email/src/EmailProviderTypes';
|
||||
import nodemailer from 'nodemailer';
|
||||
import nodemailer, {type Transporter} from 'nodemailer';
|
||||
|
||||
const logger = createLogger('@pkgs/email/src/SmtpEmailProvider');
|
||||
|
||||
@@ -18,7 +18,7 @@ interface SmtpEmailConfig {
|
||||
}
|
||||
|
||||
export class SmtpEmailProvider implements IEmailProvider {
|
||||
private readonly transporter: nodemailer.Transporter;
|
||||
private readonly transporter: Transporter;
|
||||
|
||||
constructor(config: SmtpEmailConfig) {
|
||||
this.transporter = nodemailer.createTransport({
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"./*": "./*"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "catalog:",
|
||||
@@ -21,6 +21,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
"typescript": "catalog:ts7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,6 +285,5 @@ export function formatGeoipLocation(result: GeoipResult, locale?: string | null)
|
||||
const localizedCountry = locale && result.countryCode ? countryDisplayName(result.countryCode, locale) : null;
|
||||
const countryLabel = localizedCountry ?? result.countryName ?? result.countryCode;
|
||||
if (countryLabel) parts.push(countryLabel);
|
||||
if (parts.length === 0) return null;
|
||||
return new Intl.ListFormat(locale ?? 'en', {style: 'narrow', type: 'unit'}).format(parts);
|
||||
return parts.length > 0 ? parts.join(', ') : null;
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ interface GeoipRuntimePathOptions {
|
||||
}
|
||||
|
||||
export function parseGeoipSourceConfig(rawValue: string | undefined): GeoipSourceConfig {
|
||||
if (!rawValue || !rawValue.startsWith('s3://')) {
|
||||
if (!rawValue?.startsWith('s3://')) {
|
||||
return createGeoipFilesystemSourceConfig(rawValue);
|
||||
}
|
||||
return parseGeoipS3SourceConfig(rawValue);
|
||||
|
||||
@@ -11,6 +11,8 @@ interface PostgresIpInfoOptions {
|
||||
}
|
||||
|
||||
const VALUE_SEPARATOR = '\u001f';
|
||||
export const IPINFO_CACHE_TTL_SECONDS = 14 * 24 * 60 * 60;
|
||||
export const IPINFO_REQUEST_AUDIT_TTL_SECONDS = 90 * 24 * 60 * 60;
|
||||
|
||||
function getClient(options: PostgresIpInfoOptions): IPostgresClient | null {
|
||||
return options.client ?? options.getClient?.() ?? null;
|
||||
@@ -34,12 +36,9 @@ async function upsertKvRow(
|
||||
partitionKey: string,
|
||||
key: string,
|
||||
row: Record<string, unknown>,
|
||||
ttlSeconds?: number,
|
||||
ttlSeconds: number,
|
||||
): Promise<void> {
|
||||
const expiresAt =
|
||||
ttlSeconds != null && Number.isFinite(ttlSeconds) && ttlSeconds > 0
|
||||
? new Date(Date.now() + ttlSeconds * 1000)
|
||||
: null;
|
||||
const expiresAt = new Date(Date.now() + ttlSeconds * 1000);
|
||||
await client.query(
|
||||
`INSERT INTO ${table(client)} (table_name, partition_key, row_key, row_data, expires_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4::jsonb, $5, now())
|
||||
@@ -77,7 +76,14 @@ export function createPostgresIpInfoCache(options: PostgresIpInfoOptions): IpInf
|
||||
try {
|
||||
const client = getClient(options);
|
||||
if (!client) return;
|
||||
await upsertKvRow(client, 'ipinfo_cache', rowKey([key]), rowKey([key]), {cache_key: key, payload}, ttlSeconds);
|
||||
await upsertKvRow(
|
||||
client,
|
||||
'ipinfo_cache',
|
||||
rowKey([key]),
|
||||
rowKey([key]),
|
||||
{cache_key: key, payload},
|
||||
ttlSeconds != null && Number.isFinite(ttlSeconds) && ttlSeconds > 0 ? ttlSeconds : IPINFO_CACHE_TTL_SECONDS,
|
||||
);
|
||||
} catch (error) {
|
||||
options.onError?.(error, 'ipinfo_cache_set');
|
||||
}
|
||||
@@ -124,6 +130,7 @@ export function createPostgresIpInfoRequestAuditLogger(options: PostgresIpInfoOp
|
||||
is_residential_proxy: event.isResidentialProxy,
|
||||
metadata_json: serializeMetadata(event.metadata),
|
||||
},
|
||||
IPINFO_REQUEST_AUDIT_TTL_SECONDS,
|
||||
);
|
||||
} catch (error) {
|
||||
options.onError?.(error, 'ipinfo_request_audit_record');
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluxer/constants": "workspace:*",
|
||||
@@ -17,8 +17,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"undici-types": "catalog:",
|
||||
"typescript": "catalog:ts7",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from '@pkgs/http_client/src/HttpClientRequestInternals';
|
||||
import type {HttpClientMetrics, HttpClientTelemetry} from '@pkgs/http_client/src/HttpClientTelemetryTypes';
|
||||
import type {
|
||||
FetchDispatcher,
|
||||
HttpClient,
|
||||
HttpClientFactoryOptions,
|
||||
HttpMethod,
|
||||
@@ -26,7 +27,6 @@ import type {
|
||||
StreamResponse,
|
||||
} from '@pkgs/http_client/src/HttpClientTypes';
|
||||
import {HttpError} from '@pkgs/http_client/src/HttpError';
|
||||
import type {Dispatcher} from 'undici-types';
|
||||
|
||||
const DEFAULT_SERVICE_NAME = 'unknown';
|
||||
|
||||
@@ -79,7 +79,7 @@ function createFetchInit(
|
||||
headers: Headers,
|
||||
body: string | undefined,
|
||||
signal: AbortSignal,
|
||||
dispatcher: Dispatcher | undefined,
|
||||
dispatcher: FetchDispatcher | undefined,
|
||||
): RequestInit {
|
||||
return {
|
||||
method,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {HttpClientTelemetry} from '@pkgs/http_client/src/HttpClientTelemetryTypes';
|
||||
import type {Dispatcher} from 'undici-types';
|
||||
|
||||
export type ResponseStream = ReadableStream<Uint8Array> | null;
|
||||
export type FetchDispatcher = NonNullable<RequestInit['dispatcher']>;
|
||||
export type HttpMethod = 'GET' | 'POST' | 'HEAD' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS';
|
||||
export type RequestUrlValidationPhase = 'initial' | 'redirect';
|
||||
|
||||
@@ -14,7 +14,7 @@ export interface RequestUrlValidationContext {
|
||||
}
|
||||
|
||||
export interface RequestUrlPolicy {
|
||||
readonly dispatcher?: Dispatcher;
|
||||
readonly dispatcher?: FetchDispatcher;
|
||||
validate(url: URL, context: RequestUrlValidationContext): Promise<void>;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,13 @@ import dns from 'node:dns';
|
||||
import type {LookupFunction} from 'node:net';
|
||||
import {BlockList, isIP} from 'node:net';
|
||||
import {formatUrlForDiagnostics} from '@pkgs/http_client/src/HttpClientDiagnostics';
|
||||
import type {RequestUrlPolicy, RequestUrlValidationContext} from '@pkgs/http_client/src/HttpClientTypes';
|
||||
import type {
|
||||
FetchDispatcher,
|
||||
RequestUrlPolicy,
|
||||
RequestUrlValidationContext,
|
||||
} from '@pkgs/http_client/src/HttpClientTypes';
|
||||
import {HttpError} from '@pkgs/http_client/src/HttpError';
|
||||
import {Agent} from 'undici';
|
||||
import type {Dispatcher} from 'undici-types';
|
||||
import {Agent, Dispatcher1Wrapper} from 'undici';
|
||||
|
||||
const DEFAULT_DNS_CACHE_TTL_MS = 60000;
|
||||
const DNS_CACHE_MAX_ENTRIES = 10000;
|
||||
@@ -223,7 +226,7 @@ async function defaultLookupHost(hostname: string): Promise<Array<string>> {
|
||||
return addresses.map((addressEntry) => addressEntry.address);
|
||||
}
|
||||
|
||||
function createBlocklistDispatcher(allowPrivateAddresses: boolean): Dispatcher {
|
||||
function createBlocklistDispatcher(allowPrivateAddresses: boolean): FetchDispatcher {
|
||||
const lookup: LookupFunction = (hostname, options, callback) => {
|
||||
dns.lookup(hostname, {...options, all: true, order: options.order ?? 'verbatim'}, (error, addresses) => {
|
||||
if (error) {
|
||||
@@ -246,15 +249,18 @@ function createBlocklistDispatcher(allowPrivateAddresses: boolean): Dispatcher {
|
||||
callback(null, primary.address, primary.family);
|
||||
});
|
||||
};
|
||||
return new Agent({
|
||||
connect: {
|
||||
lookup,
|
||||
},
|
||||
}) as unknown as Dispatcher;
|
||||
return new Dispatcher1Wrapper(
|
||||
new Agent({
|
||||
allowH2: false,
|
||||
connect: {
|
||||
lookup,
|
||||
},
|
||||
}),
|
||||
) as unknown as FetchDispatcher;
|
||||
}
|
||||
|
||||
interface PublicInternetRequestUrlPolicy extends RequestUrlPolicy {
|
||||
readonly dispatcher: Dispatcher;
|
||||
readonly dispatcher: FetchDispatcher;
|
||||
}
|
||||
|
||||
export function createPublicInternetRequestUrlPolicy(
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluxer/constants": "workspace:*",
|
||||
@@ -18,7 +18,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:ts7",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export interface IKVSubscription {
|
||||
}
|
||||
|
||||
export interface KVPurgeBatchResult {
|
||||
urls: Array<string>;
|
||||
entries: Array<string>;
|
||||
tokensConsumed: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -217,7 +217,7 @@ local refillIntervalMs = tonumber(ARGV[5])
|
||||
|
||||
local queueSize = redis.call('SCARD', queueKey)
|
||||
if queueSize == 0 then
|
||||
return '{"urls":[],"tokens":0}'
|
||||
return '{"entries":[],"tokens":0}'
|
||||
end
|
||||
|
||||
local tokens = maxTokens
|
||||
@@ -237,14 +237,14 @@ end
|
||||
local toPop = math.min(maxItems, math.floor(tokens), queueSize)
|
||||
if toPop <= 0 then
|
||||
redis.call('SET', bucketKey, cjson.encode({tokens = tokens, lastRefill = lastRefill}), 'EX', 3600)
|
||||
return '{"urls":[],"tokens":0}'
|
||||
return '{"entries":[],"tokens":0}'
|
||||
end
|
||||
|
||||
local urls = redis.call('SPOP', queueKey, toPop)
|
||||
tokens = tokens - #urls
|
||||
local entries = redis.call('SPOP', queueKey, toPop)
|
||||
tokens = tokens - #entries
|
||||
|
||||
redis.call('SET', bucketKey, cjson.encode({tokens = tokens, lastRefill = lastRefill}), 'EX', 3600)
|
||||
return cjson.encode({urls = urls, tokens = #urls})
|
||||
return cjson.encode({entries = entries, tokens = #entries})
|
||||
`;
|
||||
const CLAIM_BULK_DELETION_SCRIPT = `
|
||||
local score = redis.call('ZSCORE', KEYS[1], ARGV[1])
|
||||
@@ -295,6 +295,7 @@ export class KVClient implements IKVProvider {
|
||||
connectTimeout: this.timeoutMs,
|
||||
commandTimeout: this.timeoutMs,
|
||||
maxRetriesPerRequest: 1,
|
||||
protocol: 2,
|
||||
retryStrategy: createRetryStrategy(),
|
||||
});
|
||||
}
|
||||
@@ -311,6 +312,7 @@ export class KVClient implements IKVProvider {
|
||||
connectTimeout: clusterConfig.timeoutMs,
|
||||
commandTimeout: clusterConfig.timeoutMs,
|
||||
maxRetriesPerRequest: 1,
|
||||
protocol: 2,
|
||||
},
|
||||
scaleReads: 'master',
|
||||
...(hasNatMap ? {natMap} : {}),
|
||||
@@ -902,17 +904,17 @@ function parseRateLimitResult(value: unknown): KVRateLimitResult {
|
||||
function parsePurgeBatchResult(value: unknown, maxItems: number): KVPurgeBatchResult {
|
||||
const command = 'dequeuePurgeBatch';
|
||||
if (!isJsonObject(value)) throw createInvalidResponseError(command, 'a purge batch object');
|
||||
const {urls, tokens} = value;
|
||||
const {entries, tokens} = value;
|
||||
if (
|
||||
!Array.isArray(urls) ||
|
||||
!urls.every((url): url is string => typeof url === 'string') ||
|
||||
!Array.isArray(entries) ||
|
||||
!entries.every((entry): entry is string => typeof entry === 'string') ||
|
||||
!isNonNegativeSafeInteger(tokens) ||
|
||||
tokens !== urls.length ||
|
||||
urls.length > maxItems
|
||||
tokens !== entries.length ||
|
||||
entries.length > maxItems
|
||||
) {
|
||||
throw createInvalidResponseError(command, 'a bounded string array and matching token count');
|
||||
}
|
||||
return {urls, tokensConsumed: tokens};
|
||||
return {entries, tokensConsumed: tokens};
|
||||
}
|
||||
|
||||
function isJsonObject(value: unknown): value is Record<string, unknown> {
|
||||
|
||||
@@ -72,6 +72,7 @@ export class KVSubscription implements IKVSubscription {
|
||||
connectTimeout: this.timeoutMs,
|
||||
commandTimeout: this.timeoutMs,
|
||||
maxRetriesPerRequest: 1,
|
||||
protocol: 2,
|
||||
retryStrategy: createRetryStrategy(),
|
||||
};
|
||||
const connection = this.mode === 'cluster' ? resolveKVClusterConnection(this.url, this.clusterNodes) : null;
|
||||
|
||||
@@ -153,7 +153,7 @@ describe('KVClient script execution', () => {
|
||||
},
|
||||
{
|
||||
name: 'dequeuePurgeBatch',
|
||||
reply: JSON.stringify({urls: ['https://fluxer.test/a.png'], tokens: 1}),
|
||||
reply: JSON.stringify({entries: ['/attachments/1/2/a'], tokens: 1}),
|
||||
keyCount: 2,
|
||||
run: async (client) => client.dequeuePurgeBatch('queue:key', 'bucket:key', 10, 10, 1, 1000),
|
||||
},
|
||||
|
||||
@@ -9,14 +9,14 @@
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluxer/constants": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:ts7",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,10 @@
|
||||
"import": "./src/MediaProxySigner.ts",
|
||||
"types": "./src/MediaProxySigner.ts"
|
||||
},
|
||||
"./src/AttachmentUrlSignature": {
|
||||
"import": "./src/AttachmentUrlSignature.ts",
|
||||
"types": "./src/AttachmentUrlSignature.ts"
|
||||
},
|
||||
"./*": "./*"
|
||||
},
|
||||
"main": "./src/MediaProxyUtils.ts",
|
||||
@@ -31,13 +35,13 @@
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:ts7",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
export const ATTACHMENT_URL_TTL_SECS = 86_400;
|
||||
export const ATTACHMENT_URL_BUCKET_SECS = 43_200;
|
||||
|
||||
export const ORDINARY_USAGE = '';
|
||||
export const DATA_PACKAGE_USAGE = 'dp';
|
||||
|
||||
export type AttachmentUrlUsage = typeof ORDINARY_USAGE | typeof DATA_PACKAGE_USAGE;
|
||||
export type SignatureParameterName = 'ex' | 'is' | 'hm' | 'uc';
|
||||
|
||||
const SIGNATURE_DOMAIN = 'fluxer-attachment-url-v1';
|
||||
const ATTACHMENT_PATH_PREFIX = '/attachments/';
|
||||
const SIGNATURE_PARAMETER_NAMES: ReadonlyArray<SignatureParameterName> = ['ex', 'is', 'hm', 'uc'];
|
||||
const DATA_PACKAGE_EXPIRES = '0';
|
||||
const WINDOW_HEX_LENGTH = 8;
|
||||
const MAX_WINDOW_SECS = 0xff_ff_ff_ff;
|
||||
const LEADING_SLASHES_REGEX = /^\/+/u;
|
||||
const TRAILING_SLASHES_REGEX = /\/+$/u;
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
const strictTextDecoder = new TextDecoder('utf-8', {fatal: true, ignoreBOM: true});
|
||||
|
||||
export interface AttachmentUrlWindow {
|
||||
issued: number;
|
||||
expires: number;
|
||||
}
|
||||
|
||||
export interface SignAttachmentUrlOptions {
|
||||
mediaEndpoint: string;
|
||||
secret: Uint8Array;
|
||||
nowSecs: number;
|
||||
anchorSecs: number;
|
||||
}
|
||||
|
||||
function hexNibble(byte: number): number {
|
||||
if (byte >= 0x30 && byte <= 0x39) return byte - 0x30;
|
||||
if (byte >= 0x41 && byte <= 0x46) return byte - 0x41 + 10;
|
||||
if (byte >= 0x61 && byte <= 0x66) return byte - 0x61 + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
function percentDecodeBytes(value: string, plusAsSpace: boolean): Uint8Array {
|
||||
const bytes = textEncoder.encode(value);
|
||||
const decoded = new Uint8Array(bytes.length);
|
||||
let length = 0;
|
||||
let index = 0;
|
||||
while (index < bytes.length) {
|
||||
const byte = bytes[index] as number;
|
||||
if (byte === 0x25 && index + 2 < bytes.length) {
|
||||
const high = hexNibble(bytes[index + 1] as number);
|
||||
const low = hexNibble(bytes[index + 2] as number);
|
||||
if (high >= 0 && low >= 0) {
|
||||
decoded[length] = (high << 4) | low;
|
||||
length += 1;
|
||||
index += 3;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
decoded[length] = plusAsSpace && byte === 0x2b ? 0x20 : byte;
|
||||
length += 1;
|
||||
index += 1;
|
||||
}
|
||||
return decoded.subarray(0, length);
|
||||
}
|
||||
|
||||
export function percentDecodeStorageKey(path: string): string | null {
|
||||
try {
|
||||
return strictTextDecoder.decode(percentDecodeBytes(path.replace(LEADING_SLASHES_REGEX, ''), false));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function signatureParameterName(name: string): SignatureParameterName | null {
|
||||
const decoded = percentDecodeBytes(name, true);
|
||||
if (decoded.length !== 2) return null;
|
||||
const candidate = String.fromCharCode(decoded[0] as number, decoded[1] as number);
|
||||
return SIGNATURE_PARAMETER_NAMES.find((entry) => entry === candidate) ?? null;
|
||||
}
|
||||
|
||||
export function isSignatureParameterName(name: string): boolean {
|
||||
return signatureParameterName(name) !== null;
|
||||
}
|
||||
|
||||
function isSafeStorageKey(key: string): boolean {
|
||||
if (key.length === 0 || key.startsWith('/')) return false;
|
||||
return key
|
||||
.split('/')
|
||||
.every((component) => component.length > 0 && component !== '.' && component !== '..' && !component.includes('\0'));
|
||||
}
|
||||
|
||||
function firstIndexOf(value: string, characters: ReadonlyArray<string>): number {
|
||||
let found = -1;
|
||||
for (const character of characters) {
|
||||
const index = value.indexOf(character);
|
||||
if (index >= 0 && (found < 0 || index < found)) {
|
||||
found = index;
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
function rawPathFromUrl(url: string): string | null {
|
||||
const schemeIndex = url.indexOf('://');
|
||||
if (schemeIndex < 0) return null;
|
||||
const afterAuthority = url.slice(schemeIndex + 3);
|
||||
const boundary = firstIndexOf(afterAuthority, ['/', '?', '#']);
|
||||
if (boundary < 0 || afterAuthority[boundary] !== '/') return '';
|
||||
const path = afterAuthority.slice(boundary);
|
||||
const queryIndex = firstIndexOf(path, ['?', '#']);
|
||||
return queryIndex < 0 ? path : path.slice(0, queryIndex);
|
||||
}
|
||||
|
||||
function parseWebUrl(value: string): URL | null {
|
||||
try {
|
||||
const parsed = new URL(value);
|
||||
return parsed.protocol === 'http:' || parsed.protocol === 'https:' ? parsed : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function attachmentStorageKeyFromUrl(url: string, mediaEndpoint: string): string | null {
|
||||
const target = parseWebUrl(url);
|
||||
const endpoint = parseWebUrl(mediaEndpoint);
|
||||
if (!target || !endpoint || target.origin !== endpoint.origin) return null;
|
||||
const path = rawPathFromUrl(url);
|
||||
if (path === null) return null;
|
||||
const endpointPath = (rawPathFromUrl(mediaEndpoint) ?? '').replace(TRAILING_SLASHES_REGEX, '');
|
||||
if (!path.startsWith(`${endpointPath}${ATTACHMENT_PATH_PREFIX}`)) return null;
|
||||
const storageKey = percentDecodeStorageKey(path.slice(endpointPath.length));
|
||||
if (storageKey === null || !isSafeStorageKey(storageKey)) return null;
|
||||
return storageKey;
|
||||
}
|
||||
|
||||
interface SplitUrl {
|
||||
base: string;
|
||||
query: string;
|
||||
fragment: string;
|
||||
}
|
||||
|
||||
function splitUrl(url: string): SplitUrl {
|
||||
const fragmentIndex = url.indexOf('#');
|
||||
const head = fragmentIndex < 0 ? url : url.slice(0, fragmentIndex);
|
||||
const fragment = fragmentIndex < 0 ? '' : url.slice(fragmentIndex);
|
||||
const queryIndex = head.indexOf('?');
|
||||
if (queryIndex < 0) return {base: head, query: '', fragment};
|
||||
return {base: head.slice(0, queryIndex), query: head.slice(queryIndex + 1), fragment};
|
||||
}
|
||||
|
||||
function preservedFields(query: string): Array<string> {
|
||||
if (query.length === 0) return [];
|
||||
return query.split('&').filter((field) => {
|
||||
if (field.length === 0 || field === '=') return false;
|
||||
const separator = field.indexOf('=');
|
||||
return !isSignatureParameterName(separator < 0 ? field : field.slice(0, separator));
|
||||
});
|
||||
}
|
||||
|
||||
export function stripAttachmentSignature(url: string): string {
|
||||
const {base, query, fragment} = splitUrl(url);
|
||||
const preserved = preservedFields(query);
|
||||
if (preserved.length === 0) return `${base}${fragment}`;
|
||||
return `${base}?${preserved.join('&')}${fragment}`;
|
||||
}
|
||||
|
||||
export function issueWindow(anchorSecs: number, nowSecs: number): AttachmentUrlWindow {
|
||||
const elapsed = Math.max(0, nowSecs - anchorSecs);
|
||||
const issued = anchorSecs + Math.floor(elapsed / ATTACHMENT_URL_BUCKET_SECS) * ATTACHMENT_URL_BUCKET_SECS;
|
||||
return {issued, expires: issued + ATTACHMENT_URL_TTL_SECS};
|
||||
}
|
||||
|
||||
export function canonicalInput(storageKey: string, exHex: string, isHex: string, usage: AttachmentUrlUsage): string {
|
||||
return `${SIGNATURE_DOMAIN}\n${exHex}\n${isHex}\n${usage}\n${storageKey}`;
|
||||
}
|
||||
|
||||
function windowHex(value: number): string {
|
||||
return value.toString(16).padStart(WINDOW_HEX_LENGTH, '0');
|
||||
}
|
||||
|
||||
function signUsage(url: string, options: SignAttachmentUrlOptions, usage: AttachmentUrlUsage): string {
|
||||
const storageKey = attachmentStorageKeyFromUrl(url, options.mediaEndpoint);
|
||||
if (storageKey === null) return url;
|
||||
const {issued, expires} = issueWindow(options.anchorSecs, options.nowSecs);
|
||||
if (!Number.isSafeInteger(issued) || issued < 0 || expires > MAX_WINDOW_SECS) return url;
|
||||
const isDataPackage = usage === DATA_PACKAGE_USAGE;
|
||||
const exHex = windowHex(isDataPackage ? 0 : expires);
|
||||
const isHex = windowHex(issued);
|
||||
const signature = crypto
|
||||
.createHmac('sha256', options.secret)
|
||||
.update(canonicalInput(storageKey, exHex, isHex, usage))
|
||||
.digest('hex');
|
||||
const signatureFields = isDataPackage
|
||||
? `ex=${DATA_PACKAGE_EXPIRES}&is=${isHex}&hm=${signature}&uc=${DATA_PACKAGE_USAGE}`
|
||||
: `ex=${exHex}&is=${isHex}&hm=${signature}`;
|
||||
const {base, query, fragment} = splitUrl(url);
|
||||
const preserved = preservedFields(query);
|
||||
const fields = preserved.length === 0 ? signatureFields : `${signatureFields}&${preserved.join('&')}`;
|
||||
return `${base}?${fields}${fragment}`;
|
||||
}
|
||||
|
||||
export function signAttachmentUrl(url: string, options: SignAttachmentUrlOptions): string {
|
||||
return signUsage(url, options, ORDINARY_USAGE);
|
||||
}
|
||||
|
||||
export function signDataPackageAttachmentUrl(url: string, options: SignAttachmentUrlOptions): string {
|
||||
return signUsage(url, options, DATA_PACKAGE_USAGE);
|
||||
}
|
||||
@@ -10,13 +10,13 @@
|
||||
"./*": "./*"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"mime": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
"typescript": "catalog:ts7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,13 +7,14 @@
|
||||
"./*": "./*"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"nats": "catalog:"
|
||||
"@nats-io/jetstream": "catalog:",
|
||||
"@nats-io/transport-node": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
"typescript": "catalog:ts7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {NatsConnection} from 'nats';
|
||||
import type {NatsConnection} from '@nats-io/transport-node';
|
||||
|
||||
export interface INatsConnectionManager {
|
||||
connect(): Promise<void>;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {type JetStreamClient, type JetStreamManager, jetstream, jetstreamManager} from '@nats-io/jetstream';
|
||||
import {NatsConnectionManager} from '@pkgs/nats/src/NatsConnectionManager';
|
||||
import type {JetStreamClient, JetStreamManager} from 'nats';
|
||||
|
||||
export class JetStreamConnectionManager extends NatsConnectionManager {
|
||||
getJetStreamClient(): JetStreamClient {
|
||||
return this.getConnection().jetstream();
|
||||
return jetstream(this.getConnection());
|
||||
}
|
||||
|
||||
async getJetStreamManager(): Promise<JetStreamManager> {
|
||||
return this.getConnection().jetstreamManager();
|
||||
return jetstreamManager(this.getConnection());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {connect, DrainingConnectionError, type NatsConnection} from '@nats-io/transport-node';
|
||||
import type {INatsConnectionManager} from '@pkgs/nats/src/INatsConnectionManager';
|
||||
import type {NatsConnectionOptions} from '@pkgs/nats/src/NatsConnectionOptions';
|
||||
import {connect, ErrorCode, type NatsConnection, NatsError} from 'nats';
|
||||
|
||||
const DEFAULT_MAX_RECONNECT_ATTEMPTS = -1;
|
||||
const DEFAULT_RECONNECT_TIME_WAIT_MS = 500;
|
||||
@@ -46,7 +46,7 @@ export class NatsConnectionManager implements INatsConnectionManager {
|
||||
});
|
||||
await this.connectPromise;
|
||||
if (generation !== this.drainGeneration) {
|
||||
throw NatsError.errorForCode(ErrorCode.ConnectionDraining);
|
||||
throw new DrainingConnectionError();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ export class NatsConnectionManager implements INatsConnectionManager {
|
||||
|
||||
private assertNotDraining(): void {
|
||||
if (this.drainPromise !== null) {
|
||||
throw NatsError.errorForCode(ErrorCode.ConnectionDraining);
|
||||
throw new DrainingConnectionError();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"./*": "./*"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"pg": "catalog:"
|
||||
@@ -15,6 +15,6 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@types/pg": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
"typescript": "catalog:ts7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import type {Pool, PoolClient, QueryResult, QueryResultRow} from 'pg';
|
||||
import type {Pool, PoolClient, PoolConfig, QueryResult, QueryResultRow} from 'pg';
|
||||
import pg from 'pg';
|
||||
|
||||
const MAX_DIAGNOSTIC_FIELD_LENGTH = 128;
|
||||
@@ -131,7 +131,7 @@ class PostgresClient implements IPostgresClient {
|
||||
}
|
||||
|
||||
private async openPool(): Promise<void> {
|
||||
const pool = new pg.Pool({
|
||||
const poolConfig: PoolConfig & {scramMaxIterations: number} = {
|
||||
connectionString: this.config.url || undefined,
|
||||
host: this.config.url ? undefined : (this.config.host ?? '127.0.0.1'),
|
||||
port: this.config.url ? undefined : (this.config.port ?? 5432),
|
||||
@@ -140,7 +140,9 @@ class PostgresClient implements IPostgresClient {
|
||||
password: this.config.url ? undefined : (this.config.password ?? 'fluxer'),
|
||||
ssl: this.config.ssl ? {rejectUnauthorized: true, ca: normalizePem(this.config.sslCa)} : undefined,
|
||||
max: this.config.maxConnections ?? 20,
|
||||
});
|
||||
scramMaxIterations: 0,
|
||||
};
|
||||
const pool = new pg.Pool(poolConfig);
|
||||
this.observePoolConnections(pool);
|
||||
try {
|
||||
const client = await pool.connect();
|
||||
|
||||
@@ -7,16 +7,13 @@
|
||||
"./*": "./*"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pkgs/kv_client": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"vitest": "catalog:"
|
||||
"typescript": "catalog:ts7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
interface KVRequiredErrorOptions {
|
||||
serviceName: string;
|
||||
configPath: string;
|
||||
}
|
||||
|
||||
export function throwKVRequiredError(options: KVRequiredErrorOptions): never {
|
||||
const {serviceName, configPath} = options;
|
||||
throw new Error(
|
||||
`${serviceName} requires KV-backed rate limiting. ${configPath} is not set. ` +
|
||||
`internal.kv must be configured for distributed rate limiting.`,
|
||||
);
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {throwKVRequiredError} from '@pkgs/rate_limit/src/KVRequiredError';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
describe('throwKVRequiredError', () => {
|
||||
it('should throw an error with the service name', () => {
|
||||
expect(() =>
|
||||
throwKVRequiredError({
|
||||
serviceName: 'fluxer_api',
|
||||
configPath: 'internal.kv.url',
|
||||
}),
|
||||
).toThrow('fluxer_api requires KV-backed rate limiting');
|
||||
});
|
||||
it('should include the config path in the error message', () => {
|
||||
expect(() =>
|
||||
throwKVRequiredError({
|
||||
serviceName: 'TestService',
|
||||
configPath: 'config.kv.connection_string',
|
||||
}),
|
||||
).toThrow('config.kv.connection_string is not set');
|
||||
});
|
||||
it('should construct complete error message with all parts', () => {
|
||||
let errorMessage = '';
|
||||
try {
|
||||
throwKVRequiredError({
|
||||
serviceName: 'fluxer_admin',
|
||||
configPath: 'admin.kv.endpoint',
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
}
|
||||
expect(errorMessage).toContain('fluxer_admin requires KV-backed rate limiting');
|
||||
expect(errorMessage).toContain('admin.kv.endpoint is not set');
|
||||
expect(errorMessage).toContain('internal.kv must be configured for distributed rate limiting');
|
||||
});
|
||||
it('should always throw (never return)', () => {
|
||||
const fn = () =>
|
||||
throwKVRequiredError({
|
||||
serviceName: 'test',
|
||||
configPath: 'test.path',
|
||||
});
|
||||
expect(fn).toThrow(Error);
|
||||
});
|
||||
it('should handle empty service name', () => {
|
||||
expect(() =>
|
||||
throwKVRequiredError({
|
||||
serviceName: '',
|
||||
configPath: 'internal.kv',
|
||||
}),
|
||||
).toThrow('requires KV-backed rate limiting');
|
||||
});
|
||||
it('should handle empty config path', () => {
|
||||
expect(() =>
|
||||
throwKVRequiredError({
|
||||
serviceName: 'TestService',
|
||||
configPath: '',
|
||||
}),
|
||||
).toThrow('is not set');
|
||||
});
|
||||
});
|
||||
@@ -1,18 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {defineConfig} from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {tsconfigPaths: true},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['**/*.{test,spec}.{ts,tsx}'],
|
||||
exclude: ['node_modules', 'dist'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
exclude: ['**/*.test.tsx', '**/*.spec.tsx', 'node_modules/'],
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -9,7 +9,7 @@
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluxer/constants": "workspace:*",
|
||||
@@ -18,7 +18,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:ts7",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {SmsVerificationUnavailableError} from '@fluxer/errors/src/domains/auth/SmsVerificationUnavailableError';
|
||||
import {createMockLogger} from '@fluxer/logger/src/mock';
|
||||
import {createSmsProvider} from '@pkgs/sms/src/providers/SmsProviderFactory';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
describe('createSmsProvider', () => {
|
||||
it('creates a test provider that accepts the configured code', async () => {
|
||||
const provider = createSmsProvider({
|
||||
mode: 'test',
|
||||
logger: createMockLogger(),
|
||||
verificationCode: '654321',
|
||||
});
|
||||
await expect(provider.startVerification('+15551234567')).resolves.toBeUndefined();
|
||||
await expect(provider.checkVerification('+15551234567', '654321')).resolves.toBe(true);
|
||||
await expect(provider.checkVerification('+15551234567', '123456')).resolves.toBe(false);
|
||||
});
|
||||
it('creates an unavailable provider that throws on verification checks', async () => {
|
||||
const provider = createSmsProvider({
|
||||
mode: 'unavailable',
|
||||
logger: createMockLogger(),
|
||||
});
|
||||
await expect(provider.startVerification('+15551234567')).resolves.toBeUndefined();
|
||||
await expect(provider.checkVerification('+15551234567', '123456')).rejects.toThrow(SmsVerificationUnavailableError);
|
||||
});
|
||||
it('creates a Twilio provider in twilio mode', async () => {
|
||||
const provider = createSmsProvider({
|
||||
mode: 'twilio',
|
||||
config: {
|
||||
accountSid: 'AC123',
|
||||
authToken: 'twilio-secret',
|
||||
verifyServiceSid: 'VA123',
|
||||
},
|
||||
logger: createMockLogger(),
|
||||
fetchFn: async () => new Response(JSON.stringify({status: 'pending'}), {status: 200}),
|
||||
});
|
||||
await expect(provider.startVerification('+15551234567')).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {createMockLogger} from '@fluxer/logger/src/mock';
|
||||
import {TestSmsProvider} from '@pkgs/sms/src/providers/TestSmsProvider';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
describe('TestSmsProvider', () => {
|
||||
describe('startVerification', () => {
|
||||
it('completes without error', async () => {
|
||||
const logger = createMockLogger();
|
||||
const provider = new TestSmsProvider({logger});
|
||||
await expect(provider.startVerification('+15551234567')).resolves.toBeUndefined();
|
||||
});
|
||||
it('supports different phone number formats', async () => {
|
||||
const logger = createMockLogger();
|
||||
const provider = new TestSmsProvider({logger});
|
||||
await expect(provider.startVerification('+14155552671')).resolves.toBeUndefined();
|
||||
await expect(provider.startVerification('+447911123456')).resolves.toBeUndefined();
|
||||
await expect(provider.startVerification('+81312345678')).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
describe('checkVerification', () => {
|
||||
it('returns true for the default valid code', async () => {
|
||||
const logger = createMockLogger();
|
||||
const provider = new TestSmsProvider({logger});
|
||||
await provider.startVerification('+15551234567');
|
||||
const result = await provider.checkVerification('+15551234567', '123456');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
it('returns false for invalid codes', async () => {
|
||||
const logger = createMockLogger();
|
||||
const provider = new TestSmsProvider({logger});
|
||||
await provider.startVerification('+15551234567');
|
||||
expect(await provider.checkVerification('+15551234567', '000000')).toBe(false);
|
||||
expect(await provider.checkVerification('+15551234567', '654321')).toBe(false);
|
||||
expect(await provider.checkVerification('+15551234567', 'abcdef')).toBe(false);
|
||||
expect(await provider.checkVerification('+15551234567', '')).toBe(false);
|
||||
});
|
||||
it('supports custom verification code overrides', async () => {
|
||||
const logger = createMockLogger();
|
||||
const provider = new TestSmsProvider({logger, verificationCode: '654321'});
|
||||
expect(await provider.checkVerification('+15551111111', '123456')).toBe(false);
|
||||
expect(await provider.checkVerification('+15551111111', '654321')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -38,7 +38,7 @@ describe('TwilioSmsProvider', () => {
|
||||
const fetchStub: typeof fetch = async (_input, init) => {
|
||||
capturedRequest = {
|
||||
url: String(_input),
|
||||
authHeader: (init?.headers as Record<string, string>).Authorization,
|
||||
authHeader: (init?.headers as Record<string, string>)?.Authorization,
|
||||
body: init?.body as string,
|
||||
};
|
||||
return new Response(JSON.stringify({success: true}), {status: 200});
|
||||
@@ -65,7 +65,7 @@ describe('TwilioSmsProvider', () => {
|
||||
const fetchStub: typeof fetch = async (_input, init) => {
|
||||
capturedRequest = {
|
||||
url: String(_input),
|
||||
authHeader: (init?.headers as Record<string, string>).Authorization,
|
||||
authHeader: (init?.headers as Record<string, string>)?.Authorization,
|
||||
body: init?.body as string,
|
||||
};
|
||||
return new Response(JSON.stringify({channel: 'auto'}), {status: 200});
|
||||
@@ -232,7 +232,7 @@ describe('TwilioSmsProvider', () => {
|
||||
capturedRequest = {
|
||||
url: String(input),
|
||||
method: init?.method,
|
||||
authHeader: (init?.headers as Record<string, string>).Authorization,
|
||||
authHeader: (init?.headers as Record<string, string>)?.Authorization,
|
||||
};
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"extends": "../../../tsconfigs/package.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node"],
|
||||
"paths": {
|
||||
"@fluxer/*": ["../../../packages/*", "../../../packages/*/src/index.ts"],
|
||||
"@pkgs/*": ["../*"]
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"./*": "./*"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluxer/logger": "workspace:*",
|
||||
@@ -15,6 +15,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
"typescript": "catalog:ts7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"./*": "./*"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluxer/constants": "workspace:*",
|
||||
@@ -60,6 +60,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
"typescript": "catalog:ts7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,10 +45,11 @@ export async function createAPIApp(options: CreateAPIAppOptions): Promise<APIApp
|
||||
configureMiddleware(routes, {
|
||||
logger,
|
||||
nodeEnv: config.nodeEnv,
|
||||
corsOrigins: [config.endpoints.webApp, config.endpoints.marketing],
|
||||
corsOrigins: [...config.endpoints.webAppOrigins, config.endpoints.marketing],
|
||||
trustClientIpHeader: config.proxy.trust_client_ip_header,
|
||||
clientIpHeaderName: config.proxy.client_ip_header,
|
||||
maxInflightRequests: config.maxInflightRequests,
|
||||
torExitBlockingEnabled: config.torExitList.enabled,
|
||||
});
|
||||
routes.onError(AbuseAwareAppErrorHandler);
|
||||
routes.notFound(AppNotFoundHandler);
|
||||
|
||||
@@ -166,3 +166,57 @@ describe('buildAPIConfigFromMaster stripe legacy prices', () => {
|
||||
expect(buildAPIConfigFromMaster(withStripeLegacyPrices(master, undefined)).stripe.legacyPrices).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
function withOptionalOutboundLookups(
|
||||
master: MasterConfig,
|
||||
selfHosted: boolean,
|
||||
overrides: {torExitList?: boolean; breachedPasswordCheck?: boolean} = {},
|
||||
): MasterConfig {
|
||||
return {
|
||||
...master,
|
||||
integrations: {
|
||||
...master.integrations,
|
||||
tor_exit_list: {enabled: overrides.torExitList},
|
||||
breached_password_check: {enabled: overrides.breachedPasswordCheck},
|
||||
},
|
||||
instance: {
|
||||
...master.instance,
|
||||
self_hosted: selfHosted,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('buildAPIConfigFromMaster optional outbound lookups', () => {
|
||||
let master: MasterConfig;
|
||||
beforeAll(async () => {
|
||||
master = await loadConfig();
|
||||
});
|
||||
|
||||
it('keeps both lookups on when the instance is not self-hosted', () => {
|
||||
const config = buildAPIConfigFromMaster(withOptionalOutboundLookups(master, false));
|
||||
expect(config.torExitList.enabled).toBe(true);
|
||||
expect(config.breachedPasswordCheck.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('leaves both lookups off on a self-hosted instance', () => {
|
||||
const config = buildAPIConfigFromMaster(withOptionalOutboundLookups(master, true));
|
||||
expect(config.torExitList.enabled).toBe(false);
|
||||
expect(config.breachedPasswordCheck.enabled).toBe(false);
|
||||
});
|
||||
|
||||
it('lets a self-hosted operator switch each lookup on', () => {
|
||||
const config = buildAPIConfigFromMaster(
|
||||
withOptionalOutboundLookups(master, true, {torExitList: true, breachedPasswordCheck: true}),
|
||||
);
|
||||
expect(config.torExitList.enabled).toBe(true);
|
||||
expect(config.breachedPasswordCheck.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('lets an operator switch each lookup off when the instance is not self-hosted', () => {
|
||||
const config = buildAPIConfigFromMaster(
|
||||
withOptionalOutboundLookups(master, false, {torExitList: false, breachedPasswordCheck: false}),
|
||||
);
|
||||
expect(config.torExitList.enabled).toBe(false);
|
||||
expect(config.breachedPasswordCheck.enabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import type {APIConfig, BlueskyOAuthConfig} from '@app/api/config/APIConfig';
|
||||
import type {WorkerTaskName} from '@app/api/worker/WorkerLaneConfig';
|
||||
import type {MasterConfig} from '@fluxer/config/src/MasterConfig';
|
||||
import {resolveDownloadsProvider} from '@fluxer/config/src/S3DownloadsProvider';
|
||||
import {parseIpAddress} from '@fluxer/ip_utils/src/IpAddress';
|
||||
import {parseGeoipSourceConfig, resolveGeoipRuntimeSourceConfig} from '@pkgs/geoip/src/GeoipStartup';
|
||||
|
||||
@@ -92,18 +91,6 @@ function normalizeIpBanExemptIps(values: Array<string>): Array<string> {
|
||||
return Array.from(normalized);
|
||||
}
|
||||
|
||||
function normalizeCountryCodes(values: Array<string>, configName: string): ReadonlySet<string> {
|
||||
const normalized = new Set<string>();
|
||||
for (const value of values) {
|
||||
const countryCode = value.trim().toUpperCase();
|
||||
if (!/^[A-Z]{2}$/u.test(countryCode)) {
|
||||
throw new Error(`${configName} contains an invalid ISO 3166-1 alpha-2 country code: ${value}`);
|
||||
}
|
||||
normalized.add(countryCode);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function mapPushProviderApps(
|
||||
apps:
|
||||
| Array<{
|
||||
@@ -157,7 +144,6 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
|
||||
const s3Buckets = s3Config.buckets ?? {
|
||||
cdn: '',
|
||||
uploads: '',
|
||||
downloads: '',
|
||||
reports: '',
|
||||
harvests: '',
|
||||
};
|
||||
@@ -174,10 +160,6 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
|
||||
requestTimeoutMs: master.services.api.request_timeout_ms,
|
||||
maxInflightRequests: master.services.api.max_inflight_requests,
|
||||
ipBanExemptIps: normalizeIpBanExemptIps(master.services.api.ip_ban_exempt_ips),
|
||||
desktopGitHubRedirectCountries: normalizeCountryCodes(
|
||||
master.services.api.desktop_github_redirect_countries,
|
||||
'FLUXER_API_DESKTOP_GITHUB_REDIRECT_COUNTRIES',
|
||||
),
|
||||
cassandra: {
|
||||
hosts: cassandraSource?.hosts.join(',') ?? '',
|
||||
port: cassandraSource?.port ?? 9042,
|
||||
@@ -263,6 +245,9 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
|
||||
tokenTtlSecs: uploadRelayConfig.token_ttl_secs,
|
||||
keepDirectCountries: uploadRelayConfig.keep_direct_countries,
|
||||
},
|
||||
attachmentUrls: {
|
||||
secretsBase64: master.services.media_proxy.attachment_urls.secrets_base64,
|
||||
},
|
||||
},
|
||||
geoip: geoipSourceConfig,
|
||||
proxy: {
|
||||
@@ -273,6 +258,7 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
|
||||
apiPublic: master.endpoints.api,
|
||||
apiClient: master.endpoints.api_client,
|
||||
webApp: master.endpoints.app,
|
||||
webAppOrigins: [...new Set([new URL(master.endpoints.app).origin, ...master.services.api.app_origin_aliases])],
|
||||
gateway: master.endpoints.gateway,
|
||||
media: master.endpoints.media,
|
||||
marketing: master.endpoints.marketing,
|
||||
@@ -287,8 +273,6 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
|
||||
donationProxyKey,
|
||||
},
|
||||
hosts: {
|
||||
invite: extractHostname(master.endpoints.invite),
|
||||
gift: extractHostname(master.endpoints.gift),
|
||||
marketing: extractHostname(master.endpoints.marketing),
|
||||
unfurlIgnored: master.services.api.unfurl_ignored_hosts,
|
||||
},
|
||||
@@ -301,7 +285,6 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
|
||||
cacheMinTtlSeconds: master.services.api.embeds.cache_min_ttl_seconds,
|
||||
cacheRespectRemoteTtl: master.services.api.embeds.cache_respect_remote_ttl,
|
||||
},
|
||||
s3Downloads: resolveDownloadsProvider(master),
|
||||
s3: {
|
||||
endpoint: s3Config.endpoint,
|
||||
presignedUrlBase: s3Config.presigned_url_base,
|
||||
@@ -345,6 +328,12 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
|
||||
blocklistFeeds: {
|
||||
enabled: master.integrations.blocklist_feeds.enabled ?? !master.instance.self_hosted,
|
||||
},
|
||||
torExitList: {
|
||||
enabled: master.integrations.tor_exit_list.enabled ?? !master.instance.self_hosted,
|
||||
},
|
||||
breachedPasswordCheck: {
|
||||
enabled: master.integrations.breached_password_check.enabled ?? !master.instance.self_hosted,
|
||||
},
|
||||
captcha: {
|
||||
enabled: master.integrations.captcha.enabled,
|
||||
provider: master.integrations.captcha.provider,
|
||||
@@ -479,6 +468,8 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
|
||||
wordmarkUrl: master.instance.branding.wordmark_url,
|
||||
faviconUrl: master.instance.branding.favicon_url,
|
||||
themeColor: master.instance.branding.theme_color,
|
||||
statusPageUrl: master.instance.branding.status_page_url,
|
||||
statusPageIncidentHistoryUrl: master.instance.branding.status_page_incident_history_url,
|
||||
},
|
||||
setup: {
|
||||
configured: master.instance.setup.configured,
|
||||
@@ -486,6 +477,10 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
|
||||
},
|
||||
abusePolicy: {
|
||||
inboundPhoneCountryCodes: master.instance.abuse_policy.inbound_phone_country_codes,
|
||||
phoneFlagging: {
|
||||
enabled: master.instance.abuse_policy.phone_flagging.enabled,
|
||||
exemptCountryCodes: master.instance.abuse_policy.phone_flagging.exempt_country_codes,
|
||||
},
|
||||
phoneVerification: {
|
||||
inboundRequiredPrefixes: master.instance.abuse_policy.phone_verification.inbound_required_prefixes,
|
||||
},
|
||||
@@ -512,7 +507,6 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
|
||||
validateResponses: resolveValidateResponses(master),
|
||||
},
|
||||
presignedAttachmentUploadsEnabled: master.services.api.presigned_attachment_uploads_enabled ?? false,
|
||||
presignedDownloadsEnabled: master.services.api.presigned_downloads_enabled ?? false,
|
||||
presignedHarvestDownloadsEnabled: master.services.api.presigned_harvest_downloads_enabled ?? true,
|
||||
attachmentDecayEnabled: master.attachment_decay_enabled,
|
||||
deletionGracePeriodHours: master.dev.test_mode_enabled ? 0.01 : master.deletion_grace_period_hours,
|
||||
|
||||
@@ -341,6 +341,7 @@ import {
|
||||
type UsersPendingDeletionRow,
|
||||
} from '@app/api/database/types/UserTypes';
|
||||
import {ATTACHMENT_DECAY_COLUMNS, type AttachmentDecayRow} from '@app/api/types/AttachmentDecayTypes';
|
||||
import {seconds} from 'itty-time';
|
||||
|
||||
export const Users = defineTable<UserRow, 'user_id'>({
|
||||
name: 'users',
|
||||
@@ -499,16 +500,19 @@ export const GuildAuditLogs = defineTable<GuildAuditLogRow, 'guild_id' | 'log_id
|
||||
name: 'guild_audit_logs_v2',
|
||||
columns: GUILD_AUDIT_LOG_COLUMNS,
|
||||
primaryKey: ['guild_id', 'log_id'],
|
||||
defaultTtlSeconds: seconds('45 days'),
|
||||
});
|
||||
export const GuildAuditLogsByUser = defineTable<GuildAuditLogRow, 'guild_id' | 'user_id' | 'log_id'>({
|
||||
name: 'guild_audit_logs_v2_by_user',
|
||||
columns: GUILD_AUDIT_LOG_COLUMNS,
|
||||
primaryKey: ['guild_id', 'user_id', 'log_id'],
|
||||
defaultTtlSeconds: seconds('45 days'),
|
||||
});
|
||||
export const GuildAuditLogsByAction = defineTable<GuildAuditLogRow, 'guild_id' | 'action_type' | 'log_id'>({
|
||||
name: 'guild_audit_logs_v2_by_action',
|
||||
columns: GUILD_AUDIT_LOG_COLUMNS,
|
||||
primaryKey: ['guild_id', 'action_type', 'log_id'],
|
||||
defaultTtlSeconds: seconds('45 days'),
|
||||
});
|
||||
export const GuildAuditLogsByUserAction = defineTable<
|
||||
GuildAuditLogRow,
|
||||
@@ -517,6 +521,7 @@ export const GuildAuditLogsByUserAction = defineTable<
|
||||
name: 'guild_audit_logs_v2_by_user_action',
|
||||
columns: GUILD_AUDIT_LOG_COLUMNS,
|
||||
primaryKey: ['guild_id', 'user_id', 'action_type', 'log_id'],
|
||||
defaultTtlSeconds: seconds('45 days'),
|
||||
});
|
||||
export const GuildMembershipMetadata = defineTable<GuildMembershipMetadataRow, 'guild_id' | 'user_id'>({
|
||||
name: 'guild_membership_metadata',
|
||||
@@ -655,6 +660,7 @@ export const RecentMentions = defineTable<RecentMentionRow, 'user_id' | 'message
|
||||
name: 'recent_mentions',
|
||||
columns: RECENT_MENTION_COLUMNS,
|
||||
primaryKey: ['user_id', 'message_id'],
|
||||
defaultTtlSeconds: seconds('7 days'),
|
||||
});
|
||||
|
||||
interface RecentMentionsByGuildRow {
|
||||
@@ -678,6 +684,7 @@ export const RecentMentionsByGuild = defineTable<RecentMentionsByGuildRow, 'user
|
||||
name: 'recent_mentions_by_guild',
|
||||
columns: RECENT_MENTIONS_BY_GUILD_COLUMNS,
|
||||
primaryKey: ['user_id', 'guild_id', 'message_id'],
|
||||
defaultTtlSeconds: seconds('7 days'),
|
||||
});
|
||||
export const SavedMessages = defineTable<SavedMessageRow, 'user_id' | 'message_id'>({
|
||||
name: 'saved_messages',
|
||||
@@ -688,6 +695,7 @@ export const PushSubscriptions = defineTable<PushSubscriptionRow, 'user_id' | 's
|
||||
name: 'push_subscriptions',
|
||||
columns: PUSH_SUBSCRIPTION_COLUMNS,
|
||||
primaryKey: ['user_id', 'subscription_id'],
|
||||
defaultTtlSeconds: seconds('90 days'),
|
||||
});
|
||||
export const Payments = defineTable<PaymentRow, 'checkout_session_id'>({
|
||||
name: 'payments',
|
||||
@@ -854,11 +862,13 @@ export const EmailVerificationTokens = defineTable<EmailVerificationTokenRow, 't
|
||||
name: 'email_verification_tokens',
|
||||
columns: EMAIL_VERIFICATION_TOKEN_COLUMNS,
|
||||
primaryKey: ['token_', 'user_id'],
|
||||
defaultTtlSeconds: seconds('24 hours'),
|
||||
});
|
||||
export const PasswordResetTokens = defineTable<PasswordResetTokenRow, 'token_' | 'user_id'>({
|
||||
name: 'password_reset_tokens',
|
||||
columns: PASSWORD_RESET_TOKEN_COLUMNS,
|
||||
primaryKey: ['token_', 'user_id'],
|
||||
defaultTtlSeconds: seconds('24 hours'),
|
||||
});
|
||||
export const PasswordResetTokensByUserId = defineTable<
|
||||
{
|
||||
@@ -870,16 +880,19 @@ export const PasswordResetTokensByUserId = defineTable<
|
||||
name: 'password_reset_tokens_by_user_id',
|
||||
columns: ['user_id', 'token_'],
|
||||
primaryKey: ['user_id', 'token_'],
|
||||
defaultTtlSeconds: seconds('24 hours'),
|
||||
});
|
||||
export const EmailRevertTokens = defineTable<EmailRevertTokenRow, 'token_' | 'user_id'>({
|
||||
name: 'email_revert_tokens',
|
||||
columns: EMAIL_REVERT_TOKEN_COLUMNS,
|
||||
primaryKey: ['token_', 'user_id'],
|
||||
defaultTtlSeconds: seconds('48 hours'),
|
||||
});
|
||||
export const PhoneTokens = defineTable<PhoneTokenRow, 'token_'>({
|
||||
name: 'phone_tokens',
|
||||
columns: PHONE_TOKEN_COLUMNS,
|
||||
primaryKey: ['token_'],
|
||||
defaultTtlSeconds: seconds('30 days'),
|
||||
});
|
||||
export const AuthSessions = defineTable<AuthSessionRow, 'session_id_hash'>({
|
||||
name: 'auth_sessions',
|
||||
@@ -901,11 +914,13 @@ export const AuthSessionTombstones = defineTable<AuthSessionTombstoneRow, 'user_
|
||||
name: 'auth_session_tombstones',
|
||||
columns: AUTH_SESSION_TOMBSTONE_COLUMNS,
|
||||
primaryKey: ['user_id', 'session_id_hash'],
|
||||
defaultTtlSeconds: seconds('30 days'),
|
||||
});
|
||||
export const UserCountryHistory = defineTable<UserCountryHistoryRow, 'user_id' | 'country'>({
|
||||
name: 'user_country_history',
|
||||
columns: USER_COUNTRY_HISTORY_COLUMNS,
|
||||
primaryKey: ['user_id', 'country'],
|
||||
defaultTtlSeconds: seconds('365 days'),
|
||||
});
|
||||
export const MfaBackupCodes = defineTable<MfaBackupCodeRow, 'user_id' | 'code'>({
|
||||
name: 'mfa_backup_codes',
|
||||
@@ -932,6 +947,7 @@ export const IpAuthorizationTokens = defineTable<IpAuthorizationTokenRow, 'token
|
||||
name: 'ip_authorization_tokens',
|
||||
columns: IP_AUTHORIZATION_TOKEN_COLUMNS,
|
||||
primaryKey: ['token_', 'user_id'],
|
||||
defaultTtlSeconds: seconds('30 minutes'),
|
||||
});
|
||||
export const AuthorizedIps = defineTable<AuthorizedIpRow, 'user_id' | 'ip'>({
|
||||
name: 'authorized_ips_v2',
|
||||
@@ -1057,26 +1073,31 @@ export const OAuth2AuthorizationCodes = defineTable<OAuth2AuthorizationCodeRow,
|
||||
name: 'oauth2_authorization_codes',
|
||||
columns: OAUTH2_AUTHORIZATION_CODE_COLUMNS,
|
||||
primaryKey: ['code'],
|
||||
defaultTtlSeconds: seconds('10 minutes'),
|
||||
});
|
||||
export const OAuth2AccessTokens = defineTable<OAuth2AccessTokenRow, 'token_'>({
|
||||
name: 'oauth2_access_tokens',
|
||||
columns: OAUTH2_ACCESS_TOKEN_COLUMNS,
|
||||
primaryKey: ['token_'],
|
||||
defaultTtlSeconds: seconds('7 days'),
|
||||
});
|
||||
export const OAuth2AccessTokensByUser = defineTable<OAuth2AccessTokenByUserRow, 'user_id' | 'token_'>({
|
||||
name: 'oauth2_access_tokens_by_user',
|
||||
columns: OAUTH2_ACCESS_TOKENS_BY_USER_COLUMNS,
|
||||
primaryKey: ['user_id', 'token_'],
|
||||
defaultTtlSeconds: seconds('7 days'),
|
||||
});
|
||||
export const OAuth2RefreshTokens = defineTable<OAuth2RefreshTokenRow, 'token_'>({
|
||||
name: 'oauth2_refresh_tokens',
|
||||
columns: OAUTH2_REFRESH_TOKEN_COLUMNS,
|
||||
primaryKey: ['token_'],
|
||||
defaultTtlSeconds: seconds('30 days'),
|
||||
});
|
||||
export const OAuth2RefreshTokensByUser = defineTable<OAuth2RefreshTokenByUserRow, 'user_id' | 'token_'>({
|
||||
name: 'oauth2_refresh_tokens_by_user',
|
||||
columns: OAUTH2_REFRESH_TOKENS_BY_USER_COLUMNS,
|
||||
primaryKey: ['user_id', 'token_'],
|
||||
defaultTtlSeconds: seconds('30 days'),
|
||||
});
|
||||
|
||||
interface WebhooksByChannelRow {
|
||||
@@ -1117,12 +1138,14 @@ export const JobsById = defineTable<JobByIdRow, 'job_id'>({
|
||||
name: 'jobs_by_id',
|
||||
columns: JOB_BY_ID_COLUMNS,
|
||||
primaryKey: ['job_id'],
|
||||
defaultTtlSeconds: seconds('90 days'),
|
||||
});
|
||||
export const JobsByDayBucket = defineTable<JobByDayBucketRow, 'bucket_day' | 'created_at' | 'job_id'>({
|
||||
name: 'jobs_by_day_bucket',
|
||||
columns: JOB_BY_DAY_BUCKET_COLUMNS,
|
||||
primaryKey: ['bucket_day', 'created_at', 'job_id'],
|
||||
partitionKey: ['bucket_day'],
|
||||
defaultTtlSeconds: seconds('90 days'),
|
||||
});
|
||||
export const JobsActive = defineTable<JobActiveRow, 'job_id'>({
|
||||
name: 'jobs_active',
|
||||
@@ -1133,11 +1156,13 @@ export const AttachmentUploadTracesByKey = defineTable<AttachmentUploadTraceByKe
|
||||
name: 'attachment_upload_traces_by_key',
|
||||
columns: ATTACHMENT_UPLOAD_TRACE_BY_KEY_COLUMNS,
|
||||
primaryKey: ['upload_key'],
|
||||
defaultTtlSeconds: seconds('30 days'),
|
||||
});
|
||||
export const AttachmentUploadTracesByAttachment = defineTable<AttachmentUploadTraceByAttachmentRow, 'attachment_id'>({
|
||||
name: 'attachment_upload_traces_by_attachment',
|
||||
columns: ATTACHMENT_UPLOAD_TRACE_BY_ATTACHMENT_COLUMNS,
|
||||
primaryKey: ['attachment_id'],
|
||||
defaultTtlSeconds: seconds('30 days'),
|
||||
});
|
||||
export const NcmecAttachmentSubmissions = defineTable<NcmecAttachmentSubmissionRow, 'attachment_id'>({
|
||||
name: 'ncmec_attachment_submissions',
|
||||
@@ -1154,6 +1179,7 @@ export const RegistrationEventsByIp = defineTable<RegistrationEventByIpRow, 'ip'
|
||||
columns: REGISTRATION_EVENT_BY_IP_COLUMNS,
|
||||
primaryKey: ['ip', 'created_at', 'user_id'],
|
||||
partitionKey: ['ip'],
|
||||
defaultTtlSeconds: seconds('30 days'),
|
||||
});
|
||||
export const RegistrationEventsBySubnet = defineTable<
|
||||
RegistrationEventBySubnetRow,
|
||||
@@ -1164,6 +1190,7 @@ export const RegistrationEventsBySubnet = defineTable<
|
||||
columns: REGISTRATION_EVENT_BY_SUBNET_COLUMNS,
|
||||
primaryKey: ['subnet', 'created_at', 'user_id'],
|
||||
partitionKey: ['subnet'],
|
||||
defaultTtlSeconds: seconds('30 days'),
|
||||
});
|
||||
export const RegistrationEventsByEmailDomain = defineTable<
|
||||
RegistrationEventByEmailDomainRow,
|
||||
@@ -1174,6 +1201,7 @@ export const RegistrationEventsByEmailDomain = defineTable<
|
||||
columns: REGISTRATION_EVENT_BY_EMAIL_DOMAIN_COLUMNS,
|
||||
primaryKey: ['email_domain', 'created_at', 'user_id'],
|
||||
partitionKey: ['email_domain'],
|
||||
defaultTtlSeconds: seconds('30 days'),
|
||||
});
|
||||
export const RegistrationEventsByPlusAddressBase = defineTable<
|
||||
RegistrationEventByPlusAddressBaseRow,
|
||||
@@ -1184,6 +1212,7 @@ export const RegistrationEventsByPlusAddressBase = defineTable<
|
||||
columns: REGISTRATION_EVENT_BY_PLUS_ADDRESS_BASE_COLUMNS,
|
||||
primaryKey: ['plus_address_base', 'created_at', 'user_id'],
|
||||
partitionKey: ['plus_address_base'],
|
||||
defaultTtlSeconds: seconds('30 days'),
|
||||
});
|
||||
export const LatestRiskContextByUser = defineTable<LatestRiskContextByUserRow, 'user_id'>({
|
||||
name: 'latest_risk_context_by_user',
|
||||
@@ -1194,6 +1223,7 @@ export const SuspiciousIps = defineTable<SuspiciousIpRow, 'ip'>({
|
||||
name: 'suspicious_ips',
|
||||
columns: SUSPICIOUS_IP_COLUMNS,
|
||||
primaryKey: ['ip'],
|
||||
defaultTtlSeconds: seconds('180 days'),
|
||||
});
|
||||
export const RiskOutcomesByIp = defineTable<RiskOutcomeByIpRow, 'ip' | 'created_at' | 'user_id' | 'outcome_code', 'ip'>(
|
||||
{
|
||||
@@ -1201,6 +1231,7 @@ export const RiskOutcomesByIp = defineTable<RiskOutcomeByIpRow, 'ip' | 'created_
|
||||
columns: RISK_OUTCOME_BY_IP_COLUMNS,
|
||||
primaryKey: ['ip', 'created_at', 'user_id', 'outcome_code'],
|
||||
partitionKey: ['ip'],
|
||||
defaultTtlSeconds: seconds('180 days'),
|
||||
},
|
||||
);
|
||||
export const RiskOutcomesBySubnet = defineTable<
|
||||
@@ -1212,6 +1243,7 @@ export const RiskOutcomesBySubnet = defineTable<
|
||||
columns: RISK_OUTCOME_BY_SUBNET_COLUMNS,
|
||||
primaryKey: ['subnet', 'created_at', 'user_id', 'outcome_code'],
|
||||
partitionKey: ['subnet'],
|
||||
defaultTtlSeconds: seconds('180 days'),
|
||||
});
|
||||
export const RiskOutcomesByEmailDomain = defineTable<
|
||||
RiskOutcomeByEmailDomainRow,
|
||||
@@ -1222,6 +1254,7 @@ export const RiskOutcomesByEmailDomain = defineTable<
|
||||
columns: RISK_OUTCOME_BY_EMAIL_DOMAIN_COLUMNS,
|
||||
primaryKey: ['email_domain', 'created_at', 'user_id', 'outcome_code'],
|
||||
partitionKey: ['email_domain'],
|
||||
defaultTtlSeconds: seconds('180 days'),
|
||||
});
|
||||
export const RiskOutcomesByAsn = defineTable<
|
||||
RiskOutcomeByAsnRow,
|
||||
@@ -1232,6 +1265,7 @@ export const RiskOutcomesByAsn = defineTable<
|
||||
columns: RISK_OUTCOME_BY_ASN_COLUMNS,
|
||||
primaryKey: ['asn', 'created_at', 'user_id', 'outcome_code'],
|
||||
partitionKey: ['asn'],
|
||||
defaultTtlSeconds: seconds('180 days'),
|
||||
});
|
||||
export const RiskAssessments = defineTable<RiskAssessmentRow, 'assessment_id'>({
|
||||
name: 'risk_assessments',
|
||||
@@ -1248,6 +1282,7 @@ export const InboundSmsChallenges = defineTable<InboundSmsChallengeRow, 'challen
|
||||
name: 'inbound_sms_challenges',
|
||||
columns: INBOUND_SMS_CHALLENGE_COLUMNS,
|
||||
primaryKey: ['challenge_code'],
|
||||
defaultTtlSeconds: seconds('15 minutes'),
|
||||
});
|
||||
export const InboundSmsChallengesByUser = defineTable<
|
||||
InboundSmsChallengeByUserRow,
|
||||
@@ -1258,16 +1293,19 @@ export const InboundSmsChallengesByUser = defineTable<
|
||||
columns: INBOUND_SMS_CHALLENGE_BY_USER_COLUMNS,
|
||||
primaryKey: ['user_id', 'created_at'],
|
||||
partitionKey: ['user_id'],
|
||||
defaultTtlSeconds: seconds('15 minutes'),
|
||||
});
|
||||
export const PhoneLookupCache = defineTable<PhoneLookupCacheRow, 'phone'>({
|
||||
name: 'phone_lookup_cache',
|
||||
columns: PHONE_LOOKUP_CACHE_COLUMNS,
|
||||
primaryKey: ['phone'],
|
||||
defaultTtlSeconds: seconds('7 days'),
|
||||
});
|
||||
export const PhoneVerificationAttempts = defineTable<PhoneVerificationAttemptRow, 'attempt_id'>({
|
||||
name: 'phone_verification_attempts',
|
||||
columns: PHONE_VERIFICATION_ATTEMPT_COLUMNS,
|
||||
primaryKey: ['attempt_id'],
|
||||
defaultTtlSeconds: seconds('90 days'),
|
||||
});
|
||||
export const BillingCustomers = defineTable<BillingCustomerRow, 'provider_id'>({
|
||||
name: 'billing_customers',
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {ValueOf} from '@fluxer/constants/src/ValueOf';
|
||||
import type {AdminAuditAccess} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
||||
|
||||
export const AdminAuditReadActions = {
|
||||
CHECK_BLOCKLIST_ENTRY: 'check_blocklist_entry',
|
||||
GET_ADMIN_API_KEY: 'get_admin_api_key',
|
||||
GET_APPLICATION: 'get_application',
|
||||
GET_ARCHIVE: 'get_archive',
|
||||
GET_ARCHIVE_DOWNLOAD_URL: 'get_archive_download_url',
|
||||
GET_AUDIT_LOG: 'get_audit_log',
|
||||
GET_GATEWAY_STATS: 'get_gateway_stats',
|
||||
GET_GUILD: 'get_guild',
|
||||
GET_INSTANCE_CONFIG: 'get_instance_config',
|
||||
GET_LIMIT_CONFIG: 'get_limit_config',
|
||||
GET_MESSAGE: 'get_message',
|
||||
GET_MESSAGE_SHRED_STATUS: 'get_message_shred_status',
|
||||
GET_REPORT: 'get_report',
|
||||
GET_USER: 'get_user',
|
||||
GET_VOICE_REGION: 'get_voice_region',
|
||||
GET_VOICE_SERVER: 'get_voice_server',
|
||||
GET_VOICE_STATE_COUNTS: 'get_voice_state_counts',
|
||||
LIST_ADMIN_ACLS: 'list_admin_acls',
|
||||
LIST_ADMIN_API_KEYS: 'list_admin_api_keys',
|
||||
LIST_ARCHIVES: 'list_archives',
|
||||
LIST_AUDIT_LOGS: 'list_audit_logs',
|
||||
LIST_BLOCKLIST_ENTRIES: 'list_blocklist_entries',
|
||||
LIST_BLOCKLISTS: 'list_blocklists',
|
||||
LIST_CHANNEL_MESSAGES: 'list_channel_messages',
|
||||
LIST_DISCOVERY_APPLICATIONS: 'list_discovery_applications',
|
||||
LIST_DISCOVERY_CATEGORIES: 'list_discovery_categories',
|
||||
LIST_DISCOVERY_CATEGORY_LISTINGS: 'list_discovery_category_listings',
|
||||
LIST_DISCOVERY_LISTINGS: 'list_discovery_listings',
|
||||
LIST_GUILD_APPLICATIONS: 'list_guild_applications',
|
||||
LIST_GUILD_AUDIT_LOGS: 'list_guild_audit_logs',
|
||||
LIST_GUILD_EMOJIS: 'list_guild_emojis',
|
||||
LIST_GUILD_MEMBERS: 'list_guild_members',
|
||||
LIST_GUILD_MEMORY_STATS: 'list_guild_memory_stats',
|
||||
LIST_GUILD_STICKERS: 'list_guild_stickers',
|
||||
LIST_USER_APPLICATIONS: 'list_user_applications',
|
||||
LIST_USER_CHANGE_LOG: 'list_user_change_log',
|
||||
LIST_USER_DM_CHANNELS: 'list_user_dm_channels',
|
||||
LIST_USER_GUILDS: 'list_user_guilds',
|
||||
LIST_USER_RELATIONSHIPS: 'list_user_relationships',
|
||||
LIST_USER_SESSIONS: 'list_user_sessions',
|
||||
LIST_VOICE_REGIONS: 'list_voice_regions',
|
||||
LIST_VOICE_SERVERS: 'list_voice_servers',
|
||||
LIST_WEBAUTHN_CREDENTIALS: 'list_webauthn_credentials',
|
||||
SEARCH_AUDIT_LOGS: 'search_audit_logs',
|
||||
SEARCH_GUILDS: 'search_guilds',
|
||||
SEARCH_MESSAGES: 'search_messages',
|
||||
SEARCH_REPORTS: 'search_reports',
|
||||
SEARCH_USERS: 'search_users',
|
||||
} as const;
|
||||
|
||||
export type AdminAuditReadAction = ValueOf<typeof AdminAuditReadActions>;
|
||||
|
||||
export const ADMIN_AUDIT_READ_ACTIONS: ReadonlyArray<AdminAuditReadAction> = Object.values(AdminAuditReadActions);
|
||||
|
||||
const READ_ACTION_SET: ReadonlySet<string> = new Set(ADMIN_AUDIT_READ_ACTIONS);
|
||||
|
||||
export function getAdminAuditAccess(action: string): AdminAuditAccess {
|
||||
return READ_ACTION_SET.has(action) ? 'read' : 'write';
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {AdminAuditReadAction} from '@app/api/admin/AdminAuditActions';
|
||||
import type {HonoEnv} from '@app/api/types/HonoEnv';
|
||||
import type {Context} from 'hono';
|
||||
|
||||
type AdminAuditMetadataValue = string | number | bigint | boolean | null | undefined;
|
||||
|
||||
type AdminAuditMetadataInput = Readonly<Record<string, AdminAuditMetadataValue>>;
|
||||
|
||||
interface AdminAuditEntryInput<TAction extends string> {
|
||||
targetType: string;
|
||||
targetId: bigint;
|
||||
action: TAction;
|
||||
metadata?: AdminAuditMetadataInput;
|
||||
}
|
||||
|
||||
const SNOWFLAKE_PATTERN = /^(0|[1-9][0-9]{0,19})$/;
|
||||
|
||||
export function snowflakeOrUndefined(value: string | undefined): string | undefined {
|
||||
return value !== undefined && SNOWFLAKE_PATTERN.test(value) ? value : undefined;
|
||||
}
|
||||
|
||||
function toAdminAuditMetadata(input: AdminAuditMetadataInput = {}): Map<string, string> {
|
||||
const metadata = new Map<string, string>();
|
||||
for (const [key, value] of Object.entries(input)) {
|
||||
if (value === undefined || value === null) continue;
|
||||
metadata.set(key, String(value));
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
async function recordAdminAuditEntry(ctx: Context<HonoEnv>, entry: AdminAuditEntryInput<string>): Promise<void> {
|
||||
await ctx.get('adminService').auditService.createAuditLog({
|
||||
adminUserId: ctx.get('adminUserId'),
|
||||
targetType: entry.targetType,
|
||||
targetId: entry.targetId,
|
||||
action: entry.action,
|
||||
auditLogReason: ctx.get('auditLogReason'),
|
||||
metadata: toAdminAuditMetadata(entry.metadata),
|
||||
});
|
||||
}
|
||||
|
||||
export async function recordAdminRead(
|
||||
ctx: Context<HonoEnv>,
|
||||
entry: AdminAuditEntryInput<AdminAuditReadAction>,
|
||||
): Promise<void> {
|
||||
await recordAdminAuditEntry(ctx, entry);
|
||||
}
|
||||
|
||||
export async function recordAdminWrite(ctx: Context<HonoEnv>, entry: AdminAuditEntryInput<string>): Promise<void> {
|
||||
await recordAdminAuditEntry(ctx, entry);
|
||||
}
|
||||
@@ -2,7 +2,15 @@
|
||||
|
||||
import type {AdminAuditLog, BannedIpEntry, BannedIpKind, IAdminRepository} from '@app/api/admin/IAdminRepository';
|
||||
import {createUserID} from '@app/api/BrandedTypes';
|
||||
import {deleteOneOrMany, fetchMany, fetchOne, upsertOne} from '@app/api/database/CassandraQueryExecution';
|
||||
import {Config} from '@app/api/Config';
|
||||
import {ContentBlocklistCategory} from '@app/api/constants/ContentModeration';
|
||||
import {
|
||||
deleteOneOrMany,
|
||||
executeConditional,
|
||||
fetchMany,
|
||||
fetchOne,
|
||||
upsertOne,
|
||||
} from '@app/api/database/CassandraQueryExecution';
|
||||
import type {
|
||||
AdminAuditLogRow,
|
||||
BannedAvatarHashRow,
|
||||
@@ -282,6 +290,7 @@ export class AdminRepository implements IAdminRepository {
|
||||
}
|
||||
|
||||
async isEmailDomainDisposable(domain: string): Promise<boolean> {
|
||||
if (!Config.blocklistFeeds.enabled) return false;
|
||||
const domainLower = domain.toLowerCase();
|
||||
if (isAccountPolicyContactDomainReputationExempt(domainLower)) return false;
|
||||
const result = await fetchOne<{
|
||||
@@ -395,6 +404,15 @@ export class AdminRepository implements IAdminRepository {
|
||||
await deleteOneOrMany(BannedFileShas.deleteByPk({sha256_hex: sha256Hex.toLowerCase()}));
|
||||
}
|
||||
|
||||
async unbanFeedFileSha(sha256Hex: string): Promise<boolean> {
|
||||
return executeConditional(
|
||||
BannedFileShas.conditionalDeleteByPk(
|
||||
{sha256_hex: sha256Hex.toLowerCase()},
|
||||
{added_by: null, category: ContentBlocklistCategory.MALWARE_BAZAAR},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async loadAllBannedFileShas(): Promise<Array<BannedFileShaRow>> {
|
||||
return fetchMany<BannedFileShaRow>(LOAD_ALL_BANNED_FILE_SHAS_QUERY.bind({}));
|
||||
}
|
||||
|
||||
@@ -109,6 +109,8 @@ export abstract class IAdminRepository {
|
||||
|
||||
abstract unbanFileSha(sha256Hex: string): Promise<void>;
|
||||
|
||||
abstract unbanFeedFileSha(sha256Hex: string): Promise<boolean>;
|
||||
|
||||
abstract loadAllBannedFileShas(): Promise<Array<BannedFileShaRow>>;
|
||||
|
||||
abstract isAvatarHashBanned(hashShort: string): Promise<boolean>;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {AdminAuditReadActions} from '@app/api/admin/AdminAuditActions';
|
||||
import {recordAdminRead, recordAdminWrite} from '@app/api/admin/AdminAuditRecorder';
|
||||
import type {AdminApiKeyView} from '@app/api/admin/services/AdminApiKeyService';
|
||||
import {requireAdminACL} from '@app/api/middleware/AdminMiddleware';
|
||||
import {RateLimitMiddleware} from '@app/api/middleware/RateLimitMiddleware';
|
||||
@@ -62,6 +64,15 @@ export function AdminApiKeyAdminController(app: HonoApp) {
|
||||
expires_at: result.apiKey.expiresAt?.toISOString() ?? null,
|
||||
acls: Array.from(result.apiKey.acls),
|
||||
};
|
||||
await recordAdminWrite(ctx, {
|
||||
targetType: 'admin_api_key',
|
||||
targetId: BigInt(result.apiKey.keyId),
|
||||
action: 'create_admin_api_key',
|
||||
metadata: {
|
||||
acls: response.acls.join(','),
|
||||
expires_in_days: request.expires_in_days,
|
||||
},
|
||||
});
|
||||
return ctx.json(response);
|
||||
},
|
||||
);
|
||||
@@ -84,6 +95,12 @@ export function AdminApiKeyAdminController(app: HonoApp) {
|
||||
const user = ctx.get('user');
|
||||
const keys = await adminApiKeyService.listKeys(user.id);
|
||||
const response: Array<ListAdminApiKeyResponseType> = keys.map(toApiKeyResponse);
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'admin_api_key',
|
||||
targetId: 0n,
|
||||
action: AdminAuditReadActions.LIST_ADMIN_API_KEYS,
|
||||
metadata: {result_count: response.length},
|
||||
});
|
||||
return ctx.json(response);
|
||||
},
|
||||
);
|
||||
@@ -107,6 +124,11 @@ export function AdminApiKeyAdminController(app: HonoApp) {
|
||||
const user = ctx.get('user');
|
||||
const keyId = ctx.req.valid('param').key_id;
|
||||
const key = await adminApiKeyService.getKey(keyId, user.id);
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'admin_api_key',
|
||||
targetId: keyId,
|
||||
action: AdminAuditReadActions.GET_ADMIN_API_KEY,
|
||||
});
|
||||
return ctx.json(toApiKeyResponse(key));
|
||||
},
|
||||
);
|
||||
@@ -131,8 +153,19 @@ export function AdminApiKeyAdminController(app: HonoApp) {
|
||||
const user = ctx.get('user');
|
||||
const adminUserAcls = ctx.get('adminUserAcls');
|
||||
const keyId = ctx.req.valid('param').key_id;
|
||||
const key = await adminApiKeyService.updateKey(keyId, user.id, ctx.req.valid('json'), adminUserAcls);
|
||||
return ctx.json(toApiKeyResponse(key));
|
||||
const request = ctx.req.valid('json');
|
||||
const key = await adminApiKeyService.updateKey(keyId, user.id, request, adminUserAcls);
|
||||
const response = toApiKeyResponse(key);
|
||||
await recordAdminWrite(ctx, {
|
||||
targetType: 'admin_api_key',
|
||||
targetId: keyId,
|
||||
action: 'update_admin_api_key',
|
||||
metadata: {
|
||||
fields: (['name', 'acls'] as const).filter((field) => request[field] !== undefined).join(','),
|
||||
acls: request.acls !== undefined ? response.acls.join(',') : undefined,
|
||||
},
|
||||
});
|
||||
return ctx.json(response);
|
||||
},
|
||||
);
|
||||
app.delete(
|
||||
@@ -155,6 +188,11 @@ export function AdminApiKeyAdminController(app: HonoApp) {
|
||||
const user = ctx.get('user');
|
||||
const keyId = ctx.req.valid('param').key_id;
|
||||
await adminApiKeyService.revokeKey(keyId, user.id);
|
||||
await recordAdminWrite(ctx, {
|
||||
targetType: 'admin_api_key',
|
||||
targetId: keyId,
|
||||
action: 'revoke_admin_api_key',
|
||||
});
|
||||
return ctx.json({success: true}, 200);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {AdminAuditReadActions} from '@app/api/admin/AdminAuditActions';
|
||||
import {recordAdminRead} from '@app/api/admin/AdminAuditRecorder';
|
||||
import {createApplicationID, createGuildID, createUserID} from '@app/api/BrandedTypes';
|
||||
import {requireAdminACL, requireAnyAdminACL} from '@app/api/middleware/AdminMiddleware';
|
||||
import {RateLimitMiddleware} from '@app/api/middleware/RateLimitMiddleware';
|
||||
@@ -50,11 +52,25 @@ export function ApplicationAdminController(app: HonoApp) {
|
||||
}
|
||||
if (guildId != null) {
|
||||
requireRequestAdminACL(ctx.get('adminUserAcls'), AdminACLs.APPLICATION_LOOKUP);
|
||||
return ctx.json(await adminService.applicationService.listGuildApplications(createGuildID(guildId)));
|
||||
const response = await adminService.applicationService.listGuildApplications(createGuildID(guildId));
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'guild',
|
||||
targetId: guildId,
|
||||
action: AdminAuditReadActions.LIST_GUILD_APPLICATIONS,
|
||||
metadata: {application_count: response.applications.length},
|
||||
});
|
||||
return ctx.json(response);
|
||||
}
|
||||
if (ownerId != null) {
|
||||
requireRequestAdminACL(ctx.get('adminUserAcls'), AdminACLs.APPLICATION_LIST_BY_OWNER);
|
||||
return ctx.json(await adminService.applicationService.listUserApplications(createUserID(ownerId)));
|
||||
const response = await adminService.applicationService.listUserApplications(createUserID(ownerId));
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'user',
|
||||
targetId: ownerId,
|
||||
action: AdminAuditReadActions.LIST_USER_APPLICATIONS,
|
||||
metadata: {application_count: response.applications.length},
|
||||
});
|
||||
return ctx.json(response);
|
||||
}
|
||||
throw InputValidationError.create('owner_id', 'One of owner_id and guild_id is required');
|
||||
},
|
||||
@@ -76,7 +92,14 @@ export function ApplicationAdminController(app: HonoApp) {
|
||||
async (ctx) => {
|
||||
const adminService = ctx.get('adminService');
|
||||
const userId = createUserID(ctx.req.valid('param').user_id);
|
||||
return ctx.json(await adminService.applicationService.listUserApplications(userId));
|
||||
const response = await adminService.applicationService.listUserApplications(userId);
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'user',
|
||||
targetId: userId,
|
||||
action: AdminAuditReadActions.LIST_USER_APPLICATIONS,
|
||||
metadata: {application_count: response.applications.length},
|
||||
});
|
||||
return ctx.json(response);
|
||||
},
|
||||
);
|
||||
app.get(
|
||||
@@ -97,7 +120,18 @@ export function ApplicationAdminController(app: HonoApp) {
|
||||
async (ctx) => {
|
||||
const adminService = ctx.get('adminService');
|
||||
const applicationId = createApplicationID(ctx.req.valid('param').application_id);
|
||||
return ctx.json(await adminService.applicationService.lookupApplication(applicationId));
|
||||
const response = await adminService.applicationService.lookupApplication(applicationId);
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'application',
|
||||
targetId: applicationId,
|
||||
action: AdminAuditReadActions.GET_APPLICATION,
|
||||
metadata: {
|
||||
found: response.application !== null,
|
||||
owner_user_id: response.application?.owner_user_id,
|
||||
bot_user_id: response.application?.bot_user_id,
|
||||
},
|
||||
});
|
||||
return ctx.json(response);
|
||||
},
|
||||
);
|
||||
app.patch(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {AdminAuditReadActions} from '@app/api/admin/AdminAuditActions';
|
||||
import {recordAdminRead, recordAdminWrite} from '@app/api/admin/AdminAuditRecorder';
|
||||
import {createGuildID, createUserID} from '@app/api/BrandedTypes';
|
||||
import {requireAdminACL, requireAnyAdminACL} from '@app/api/middleware/AdminMiddleware';
|
||||
import {RateLimitMiddleware} from '@app/api/middleware/RateLimitMiddleware';
|
||||
@@ -66,11 +68,15 @@ export function ArchiveAdminController(app: HonoApp) {
|
||||
async (ctx) => {
|
||||
const adminArchiveService = ctx.get('adminArchiveService');
|
||||
const adminUserId = ctx.get('adminUserId');
|
||||
const result = await adminArchiveService.triggerUserArchive(
|
||||
createUserID(ctx.req.valid('param').user_id),
|
||||
adminUserId,
|
||||
ctx.req.valid('json').include_attachments,
|
||||
);
|
||||
const userId = createUserID(ctx.req.valid('param').user_id);
|
||||
const includeAttachments = ctx.req.valid('json').include_attachments;
|
||||
const result = await adminArchiveService.triggerUserArchive(userId, adminUserId, includeAttachments);
|
||||
await recordAdminWrite(ctx, {
|
||||
targetType: 'user',
|
||||
targetId: userId,
|
||||
action: 'trigger_user_archive',
|
||||
metadata: {archive_id: result.archive_id, include_attachments: includeAttachments},
|
||||
});
|
||||
return ctx.json(result, 200);
|
||||
},
|
||||
);
|
||||
@@ -93,11 +99,15 @@ export function ArchiveAdminController(app: HonoApp) {
|
||||
async (ctx) => {
|
||||
const adminArchiveService = ctx.get('adminArchiveService');
|
||||
const adminUserId = ctx.get('adminUserId');
|
||||
const result = await adminArchiveService.triggerGuildArchive(
|
||||
createGuildID(ctx.req.valid('param').guild_id),
|
||||
adminUserId,
|
||||
ctx.req.valid('json').include_attachments,
|
||||
);
|
||||
const guildId = createGuildID(ctx.req.valid('param').guild_id);
|
||||
const includeAttachments = ctx.req.valid('json').include_attachments;
|
||||
const result = await adminArchiveService.triggerGuildArchive(guildId, adminUserId, includeAttachments);
|
||||
await recordAdminWrite(ctx, {
|
||||
targetType: 'guild',
|
||||
targetId: guildId,
|
||||
action: 'trigger_guild_archive',
|
||||
metadata: {archive_id: result.archive_id, include_attachments: includeAttachments},
|
||||
});
|
||||
return ctx.json(result, 200);
|
||||
},
|
||||
);
|
||||
@@ -120,13 +130,28 @@ export function ArchiveAdminController(app: HonoApp) {
|
||||
const adminArchiveService = ctx.get('adminArchiveService');
|
||||
const adminAcls = ctx.get('adminUserAcls');
|
||||
const query = ctx.req.valid('query');
|
||||
const subjectType = resolveListSubjectType(adminAcls, query.subject_type);
|
||||
const result = await adminArchiveService.listArchives({
|
||||
subjectType: resolveListSubjectType(adminAcls, query.subject_type),
|
||||
subjectType,
|
||||
subjectId: query.subject_id ?? undefined,
|
||||
requestedBy: query.requested_by ?? undefined,
|
||||
limit: query.limit,
|
||||
includeExpired: query.include_expired,
|
||||
});
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'archive',
|
||||
targetId: 0n,
|
||||
action: AdminAuditReadActions.LIST_ARCHIVES,
|
||||
metadata: {
|
||||
subject_type: subjectType,
|
||||
subject_user_id: subjectType === 'user' ? query.subject_id : undefined,
|
||||
subject_guild_id: subjectType === 'guild' ? query.subject_id : undefined,
|
||||
requested_by_user_id: query.requested_by,
|
||||
limit: query.limit,
|
||||
include_expired: query.include_expired,
|
||||
result_count: result.length,
|
||||
},
|
||||
});
|
||||
return ctx.json({archives: result}, 200);
|
||||
},
|
||||
);
|
||||
@@ -151,6 +176,12 @@ export function ArchiveAdminController(app: HonoApp) {
|
||||
const params = ctx.req.valid('param');
|
||||
requireArchiveSubjectAccess(adminAcls, params.subject_type);
|
||||
const archive = await adminArchiveService.getArchive(params.subject_type, params.subject_id, params.archive_id);
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: params.subject_type,
|
||||
targetId: params.subject_id,
|
||||
action: AdminAuditReadActions.GET_ARCHIVE,
|
||||
metadata: {archive_id: params.archive_id, found: archive !== null},
|
||||
});
|
||||
return ctx.json({archive}, 200);
|
||||
},
|
||||
);
|
||||
@@ -179,6 +210,12 @@ export function ArchiveAdminController(app: HonoApp) {
|
||||
params.subject_id,
|
||||
params.archive_id,
|
||||
);
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: params.subject_type,
|
||||
targetId: params.subject_id,
|
||||
action: AdminAuditReadActions.GET_ARCHIVE_DOWNLOAD_URL,
|
||||
metadata: {archive_id: params.archive_id},
|
||||
});
|
||||
return ctx.json(result, 200);
|
||||
},
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user