feat(mcp): подготовить сервер и доступ к приёмке

This commit is contained in:
Denozordec committed 2026-11-03 03:12:03 +07:00
commit bf3d50e1ff
3835 files changed
+537122

No files matched your search

+8
View File
@@ -0,0 +1,8 @@
node_modules/
dist/
*.db
*.db-shm
*.db-wal
.env
storage/backups/
storage/geoip/
+66
View File
@@ -0,0 +1,66 @@
# syntax=docker/dockerfile:1
FROM node:22-bookworm-slim AS deps
RUN apt-get update \
&& apt-get install -y --no-install-recommends python3 make g++ \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Do not set NODE_ENV=production here — npm would omit typescript needed for the build stage.
COPY package.json package-lock.json ./
COPY packages/contracts/package.json packages/contracts/
COPY backend/package.json backend/
# Drop root frontend deps (Next/React/UI) so backend image stays lean.
RUN node -e "\
const fs=require('fs');\
const p=JSON.parse(fs.readFileSync('package.json','utf8'));\
p.dependencies={};\
p.devDependencies={};\
delete p.scripts;\
p.workspaces=['packages/*','backend'];\
fs.writeFileSync('package.json', JSON.stringify(p,null,2)+'\\n');\
"
# Prefer npm ci; if lockfile rejects stripped root package.json, fall back to install.
RUN (npm ci --workspace=@mmapp/contracts --workspace=mikrotik-manager-backend --ignore-scripts \
|| npm install --workspace=@mmapp/contracts --workspace=mikrotik-manager-backend --ignore-scripts) \
&& npm rebuild better-sqlite3
FROM deps AS build
ARG APP_VERSION=dev
ENV APP_VERSION=$APP_VERSION
COPY packages/contracts packages/contracts
COPY backend backend
RUN npm run build -w @mmapp/contracts \
&& npm run build -w mikrotik-manager-backend \
&& npm prune --omit=dev \
# npm may nest workspace deps (e.g. dotenv) under backend/node_modules — keep dir for COPY
&& mkdir -p backend/node_modules
FROM node:22-bookworm-slim AS runner
WORKDIR /app
# Keep system packages independent of application changes and version tags.
RUN apt-get update \
&& apt-get install -y --no-install-recommends gnupg wget ca-certificates \
&& wget -qO- https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor -o /usr/share/keyrings/pgdg.gpg \
&& echo "deb [signed-by=/usr/share/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt bookworm-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends postgresql-client-18 \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir -p /app/data
ARG APP_VERSION=dev
ENV NODE_ENV=production
ENV PORT=8000
ENV DATABASE_PATH=/app/data/mikrotik.db
ENV APP_VERSION=$APP_VERSION
COPY --from=build /app/package.json /app/package-lock.json ./
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/packages/contracts ./packages/contracts
COPY --from=build /app/backend/dist ./backend/dist
COPY --from=build /app/backend/dist-operator ./backend/dist-operator
COPY --from=build /app/backend/package.json ./backend/package.json
COPY --from=build /app/backend/mcp-coverage.json ./backend/mcp-coverage.json
COPY --from=build /app/backend/drizzle ./backend/drizzle
COPY deploy/netflow-agent/install.sh ./deploy/netflow-agent/install.sh
# Nested install from lockfile (dotenv etc.) — ESM resolves from /app/backend/dist → ../node_modules
COPY --from=build /app/backend/node_modules ./backend/node_modules
EXPOSE 8000
CMD ["node", "backend/dist/index.js"]
+15
View File
@@ -0,0 +1,15 @@
import { defineConfig } from "drizzle-kit"
import { config } from "dotenv"
config()
export default defineConfig({
schema: "./src/db/schema.ts",
out: "./drizzle",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL ?? "postgres://mmapp:mmapp@127.0.0.1:5432/mmapp",
},
verbose: true,
strict: true,
})
+626
View File
@@ -0,0 +1,626 @@
-- MikrotikManager PostgreSQL 18 schema (not 1:1 SQLite).
-- Applied once at backend boot via src/db/migrate.ts
CREATE TABLE IF NOT EXISTS schema_migrations (
id TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS servers (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL DEFAULT '',
host TEXT NOT NULL,
port INTEGER NOT NULL DEFAULT 443,
username TEXT NOT NULL DEFAULT 'admin',
password TEXT NOT NULL DEFAULT '',
use_ssl BOOLEAN NOT NULL DEFAULT TRUE,
verify_ssl BOOLEAN NOT NULL DEFAULT FALSE,
type TEXT NOT NULL DEFAULT 'home-router'
CHECK (type IN ('jump-host', 'exit-node', 'home-router')),
site TEXT NOT NULL DEFAULT '',
country TEXT NOT NULL DEFAULT '',
asn TEXT NOT NULL DEFAULT '',
comment TEXT NOT NULL DEFAULT '',
enabled BOOLEAN NOT NULL DEFAULT TRUE,
lan_subnet TEXT NOT NULL DEFAULT '',
wan_uplinks JSONB NOT NULL DEFAULT '[]'::jsonb,
mgmt_tunnel_ip TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS traffic_settings (
id BIGINT PRIMARY KEY CHECK (id = 1),
enabled BOOLEAN NOT NULL DEFAULT TRUE,
interval_sec INTEGER NOT NULL DEFAULT 30,
retention_days INTEGER NOT NULL DEFAULT 14,
last_collected_at TIMESTAMPTZ,
last_duration_ms INTEGER,
last_error TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS servers_api_ping_settings (
id BIGINT PRIMARY KEY CHECK (id = 1),
enabled BOOLEAN NOT NULL DEFAULT FALSE,
interval_sec INTEGER NOT NULL DEFAULT 120,
last_collected_at TIMESTAMPTZ,
last_duration_ms INTEGER,
last_error TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS traffic_flow_settings (
id BIGINT PRIMARY KEY CHECK (id = 1),
enabled BOOLEAN NOT NULL DEFAULT FALSE,
collector_ip TEXT NOT NULL DEFAULT '10.255.254.1',
flow_listen_port INTEGER NOT NULL DEFAULT 4739,
wg_listen_port INTEGER NOT NULL DEFAULT 51821,
prefix TEXT NOT NULL DEFAULT '10.255.254.0/24',
public_endpoint TEXT NOT NULL DEFAULT '',
host_public_key TEXT NOT NULL DEFAULT '',
host_private_key TEXT NOT NULL DEFAULT '',
hub_server_id BIGINT,
retention_hours INTEGER NOT NULL DEFAULT 24,
top_n INTEGER NOT NULL DEFAULT 200,
map_service_min_share_pct DOUBLE PRECISION NOT NULL DEFAULT 5,
last_datagram_at TIMESTAMPTZ,
last_exporter_ip TEXT,
last_error TEXT,
packets_received BIGINT NOT NULL DEFAULT 0,
peers_json JSONB NOT NULL DEFAULT '[]'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS uptime_settings (
id BIGINT PRIMARY KEY CHECK (id = 1),
enabled BOOLEAN NOT NULL DEFAULT TRUE,
resources_enabled BOOLEAN NOT NULL DEFAULT TRUE,
ping_enabled BOOLEAN NOT NULL DEFAULT TRUE,
speed_enabled BOOLEAN NOT NULL DEFAULT TRUE,
interval_sec INTEGER NOT NULL DEFAULT 15,
probe_interval_sec INTEGER NOT NULL DEFAULT 15,
speed_interval_sec INTEGER NOT NULL DEFAULT 60,
retention_days INTEGER NOT NULL DEFAULT 14,
last_collected_at TIMESTAMPTZ,
last_duration_ms INTEGER,
last_error TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS evobgp_settings (
id BIGINT PRIMARY KEY CHECK (id = 1),
base_url TEXT NOT NULL DEFAULT '',
api_key TEXT NOT NULL DEFAULT '',
enabled BOOLEAN NOT NULL DEFAULT FALSE,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS alert_telegram_settings (
id BIGINT PRIMARY KEY CHECK (id = 1),
bot_token TEXT NOT NULL DEFAULT '',
chat_id TEXT NOT NULL DEFAULT '',
message_thread_id INTEGER,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS acme_settings (
id BIGINT PRIMARY KEY CHECK (id = 1),
directory_url TEXT NOT NULL DEFAULT 'https://acme-v02.api.letsencrypt.org/directory',
cloudflare_api_token TEXT NOT NULL DEFAULT '',
default_zone_id TEXT NOT NULL DEFAULT '',
account_private_key TEXT NOT NULL DEFAULT '',
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS certificate_renew_settings (
id BIGINT PRIMARY KEY CHECK (id = 1),
enabled BOOLEAN NOT NULL DEFAULT TRUE,
interval_sec INTEGER NOT NULL DEFAULT 21600,
renew_before_days INTEGER NOT NULL DEFAULT 30,
last_collected_at TIMESTAMPTZ,
last_duration_ms INTEGER,
last_error TEXT,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS backup_schedule_settings (
id BIGINT PRIMARY KEY CHECK (id = 1),
enabled BOOLEAN NOT NULL DEFAULT TRUE,
frequency TEXT NOT NULL DEFAULT 'daily' CHECK (frequency IN ('daily', 'weekly', 'monthly')),
hour INTEGER NOT NULL DEFAULT 3,
minute INTEGER NOT NULL DEFAULT 0,
week_day INTEGER NOT NULL DEFAULT 0,
month_day INTEGER NOT NULL DEFAULT 1,
keep_count INTEGER NOT NULL DEFAULT 7,
format TEXT NOT NULL DEFAULT 'rsc' CHECK (format IN ('rsc', 'backup')),
server_ids_json JSONB NOT NULL DEFAULT '[]'::jsonb,
last_run_at TIMESTAMPTZ,
last_duration_ms INTEGER,
last_error TEXT,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS internet_path_settings (
id BIGINT PRIMARY KEY CHECK (id = 1),
enabled BOOLEAN NOT NULL DEFAULT TRUE,
interval_sec INTEGER NOT NULL DEFAULT 300,
retention_days INTEGER NOT NULL DEFAULT 14,
last_collected_at TIMESTAMPTZ,
last_duration_ms INTEGER,
last_error TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS alert_engine_cursor (
id BIGINT PRIMARY KEY CHECK (id = 1),
last_source_finished_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS data_migration (
id INTEGER PRIMARY KEY CHECK (id = 1),
sqlite_imported_at TIMESTAMPTZ,
sqlite_path TEXT,
sqlite_sha256 TEXT,
report_json JSONB
);
CREATE TABLE IF NOT EXISTS filter_rules (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
sort_order INTEGER NOT NULL DEFAULT 0,
community TEXT NOT NULL,
community_name TEXT,
action TEXT NOT NULL DEFAULT 'route' CHECK (action IN ('route', 'blackhole')),
gateway TEXT NOT NULL DEFAULT '',
gateway_tunnel_id TEXT NOT NULL DEFAULT '',
description TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_filter_rules_server_sort ON filter_rules(server_id, sort_order);
CREATE TABLE IF NOT EXISTS recursive_routes (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
sort_order INTEGER NOT NULL DEFAULT 0,
dst_address TEXT NOT NULL,
gateway TEXT NOT NULL,
distance INTEGER NOT NULL DEFAULT 1,
scope INTEGER,
target_scope INTEGER,
routing_table TEXT NOT NULL DEFAULT 'main',
check_gateway TEXT NOT NULL DEFAULT '',
country TEXT NOT NULL DEFAULT '',
comment TEXT NOT NULL DEFAULT '',
disabled BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_recursive_routes_server_sort ON recursive_routes(server_id, sort_order);
CREATE TABLE IF NOT EXISTS server_snapshots (
id BIGINT GENERATED BY DEFAULT AS IDENTITY,
server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
polled_at TIMESTAMPTZ NOT NULL,
status TEXT NOT NULL CHECK (status IN ('online', 'offline')),
latency_ms DOUBLE PRECISION,
ros_version TEXT,
board_name TEXT,
uptime TEXT,
cpu_load INTEGER,
free_memory BIGINT,
total_memory BIGINT,
identity_name TEXT,
raw_interfaces JSONB,
raw_ip_addresses JSONB,
PRIMARY KEY (id, polled_at)
) PARTITION BY RANGE (polled_at);
CREATE INDEX IF NOT EXISTS idx_server_snapshots_server_time ON server_snapshots(server_id, polled_at);
CREATE INDEX IF NOT EXISTS idx_server_snapshots_polled_brin ON server_snapshots USING BRIN (polled_at);
CREATE TABLE IF NOT EXISTS traffic_samples (
id BIGINT GENERATED BY DEFAULT AS IDENTITY,
server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
interface_name TEXT NOT NULL,
peer_public_key TEXT NOT NULL DEFAULT '',
sampled_at TIMESTAMPTZ NOT NULL,
rx_bytes BIGINT NOT NULL DEFAULT 0,
tx_bytes BIGINT NOT NULL DEFAULT 0,
rx_bps BIGINT NOT NULL DEFAULT 0,
tx_bps BIGINT NOT NULL DEFAULT 0,
running BOOLEAN NOT NULL DEFAULT FALSE,
disabled BOOLEAN NOT NULL DEFAULT FALSE,
PRIMARY KEY (id, sampled_at)
) PARTITION BY RANGE (sampled_at);
CREATE INDEX IF NOT EXISTS idx_traffic_samples_server_time ON traffic_samples(server_id, sampled_at);
CREATE INDEX IF NOT EXISTS idx_traffic_samples_server_iface_time ON traffic_samples(server_id, interface_name, sampled_at);
CREATE INDEX IF NOT EXISTS idx_traffic_samples_sampled_brin ON traffic_samples USING BRIN (sampled_at);
CREATE TABLE IF NOT EXISTS servers_rest_ping_samples (
id BIGINT GENERATED BY DEFAULT AS IDENTITY,
server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
sampled_at TIMESTAMPTZ NOT NULL,
ok BOOLEAN NOT NULL,
latency_ms INTEGER,
error TEXT,
PRIMARY KEY (id, sampled_at)
) PARTITION BY RANGE (sampled_at);
CREATE INDEX IF NOT EXISTS idx_servers_rest_ping_samples_server_id ON servers_rest_ping_samples(server_id, sampled_at);
CREATE INDEX IF NOT EXISTS idx_servers_rest_ping_samples_brin ON servers_rest_ping_samples USING BRIN (sampled_at);
CREATE TABLE IF NOT EXISTS flow_buckets (
server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
bucket_at TIMESTAMPTZ NOT NULL,
src TEXT NOT NULL,
dst TEXT NOT NULL,
proto INTEGER NOT NULL DEFAULT 0,
src_port INTEGER NOT NULL DEFAULT 0,
dst_port INTEGER NOT NULL DEFAULT 0,
bytes BIGINT NOT NULL DEFAULT 0,
packets BIGINT NOT NULL DEFAULT 0,
in_iface TEXT NOT NULL DEFAULT '',
out_iface TEXT NOT NULL DEFAULT '',
next_hop TEXT NOT NULL DEFAULT '',
flow_start_ms BIGINT NOT NULL DEFAULT 0,
flow_end_ms BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (server_id, bucket_at, src, dst, proto, src_port, dst_port, in_iface)
) PARTITION BY RANGE (bucket_at);
CREATE INDEX IF NOT EXISTS idx_flow_buckets_server_time ON flow_buckets(server_id, bucket_at);
CREATE INDEX IF NOT EXISTS idx_flow_buckets_bucket_brin ON flow_buckets USING BRIN (bucket_at);
CREATE TABLE IF NOT EXISTS flow_minute_stats (
server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
bucket_at TIMESTAMPTZ NOT NULL,
bytes BIGINT NOT NULL DEFAULT 0,
packets BIGINT NOT NULL DEFAULT 0,
unique_src INTEGER NOT NULL DEFAULT 0,
unique_dst INTEGER NOT NULL DEFAULT 0,
conversations INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (server_id, bucket_at)
) PARTITION BY RANGE (bucket_at);
CREATE INDEX IF NOT EXISTS idx_flow_minute_stats_brin ON flow_minute_stats USING BRIN (bucket_at);
CREATE TABLE IF NOT EXISTS flow_minute_dims (
server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
bucket_at TIMESTAMPTZ NOT NULL,
dim TEXT NOT NULL,
key TEXT NOT NULL,
bytes BIGINT NOT NULL DEFAULT 0,
packets BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (server_id, bucket_at, dim, key)
) PARTITION BY RANGE (bucket_at);
CREATE INDEX IF NOT EXISTS idx_flow_minute_dims_time ON flow_minute_dims(bucket_at, dim);
CREATE INDEX IF NOT EXISTS idx_flow_minute_dims_brin ON flow_minute_dims USING BRIN (bucket_at);
CREATE TABLE IF NOT EXISTS flow_daily_dims (
server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
day DATE NOT NULL,
dim TEXT NOT NULL,
key TEXT NOT NULL,
bytes BIGINT NOT NULL DEFAULT 0,
packets BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (server_id, day, dim, key)
) PARTITION BY RANGE (day);
CREATE INDEX IF NOT EXISTS idx_flow_daily_dims_day ON flow_daily_dims(day, dim);
CREATE TABLE IF NOT EXISTS flow_ip_meta (
prefix TEXT PRIMARY KEY,
asn INTEGER NOT NULL DEFAULT 0,
country TEXT NOT NULL DEFAULT '',
lat DOUBLE PRECISION,
lng DOUBLE PRECISION,
holder TEXT NOT NULL DEFAULT '',
ok INTEGER NOT NULL DEFAULT 1,
fetched_at TIMESTAMPTZ NOT NULL
);
CREATE TABLE IF NOT EXISTS flow_asn_meta (
asn INTEGER PRIMARY KEY,
holder TEXT NOT NULL DEFAULT '',
fetched_at TIMESTAMPTZ NOT NULL
);
CREATE TABLE IF NOT EXISTS uptime_probes (
id TEXT PRIMARY KEY,
src_server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
src_interface TEXT NOT NULL DEFAULT '',
name TEXT NOT NULL,
target TEXT NOT NULL,
probe_filter TEXT NOT NULL DEFAULT '—',
enabled BOOLEAN NOT NULL DEFAULT TRUE,
interval_sec INTEGER NOT NULL DEFAULT 0,
show_on_dashboard BOOLEAN NOT NULL DEFAULT FALSE,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_uptime_probes_server_sort ON uptime_probes(src_server_id, sort_order);
CREATE TABLE IF NOT EXISTS uptime_probe_samples (
id BIGINT GENERATED BY DEFAULT AS IDENTITY,
probe_id TEXT NOT NULL REFERENCES uptime_probes(id) ON DELETE CASCADE,
sampled_at TIMESTAMPTZ NOT NULL,
rtt_ms INTEGER,
loss_pct INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'down' CHECK (status IN ('up', 'warn', 'down')),
PRIMARY KEY (id, sampled_at)
) PARTITION BY RANGE (sampled_at);
CREATE INDEX IF NOT EXISTS idx_uptime_probe_samples_probe_time ON uptime_probe_samples(probe_id, sampled_at);
CREATE INDEX IF NOT EXISTS idx_uptime_probe_samples_brin ON uptime_probe_samples USING BRIN (sampled_at);
CREATE TABLE IF NOT EXISTS uptime_resource_samples (
id BIGINT GENERATED BY DEFAULT AS IDENTITY,
server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
sampled_at TIMESTAMPTZ NOT NULL,
status TEXT NOT NULL DEFAULT 'offline' CHECK (status IN ('online', 'offline')),
cpu_load INTEGER NOT NULL DEFAULT 0,
free_memory BIGINT NOT NULL DEFAULT 0,
total_memory BIGINT NOT NULL DEFAULT 0,
free_hdd_space BIGINT NOT NULL DEFAULT 0,
total_hdd_space BIGINT NOT NULL DEFAULT 0,
uptime_seconds BIGINT NOT NULL DEFAULT 0,
board_name TEXT NOT NULL DEFAULT '',
ros_version TEXT NOT NULL DEFAULT '',
PRIMARY KEY (id, sampled_at)
) PARTITION BY RANGE (sampled_at);
CREATE INDEX IF NOT EXISTS idx_uptime_resource_samples_server_time ON uptime_resource_samples(server_id, sampled_at);
CREATE INDEX IF NOT EXISTS idx_uptime_resource_samples_brin ON uptime_resource_samples USING BRIN (sampled_at);
CREATE TABLE IF NOT EXISTS uptime_speed_probes (
id TEXT PRIMARY KEY,
src_server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
dst_server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
src_interface TEXT NOT NULL DEFAULT '',
dst_interface TEXT NOT NULL DEFAULT '',
protocol TEXT NOT NULL DEFAULT 'tcp' CHECK (protocol IN ('tcp', 'udp')),
direction TEXT NOT NULL DEFAULT 'both' CHECK (direction IN ('transmit', 'receive', 'both')),
duration_sec INTEGER NOT NULL DEFAULT 10,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
last_run_at TIMESTAMPTZ,
last_tx_avg_mbps DOUBLE PRECISION,
last_rx_avg_mbps DOUBLE PRECISION,
last_status TEXT CHECK (last_status IN ('done', 'error')),
last_error TEXT,
last_ping_rtt_ms INTEGER,
last_ping_loss_pct INTEGER,
last_ping_at TIMESTAMPTZ,
last_ping_error TEXT,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_uptime_speed_probes_src_sort ON uptime_speed_probes(src_server_id, sort_order);
CREATE TABLE IF NOT EXISTS uptime_speed_test_runs (
id TEXT PRIMARY KEY,
probe_id TEXT,
src_server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
dst_server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
src_interface TEXT NOT NULL DEFAULT '',
dst_interface TEXT NOT NULL DEFAULT '',
src_address TEXT,
dst_address TEXT,
src_interface_address TEXT,
dst_interface_address TEXT,
protocol TEXT NOT NULL DEFAULT 'tcp' CHECK (protocol IN ('tcp', 'udp')),
direction TEXT NOT NULL DEFAULT 'both' CHECK (direction IN ('transmit', 'receive', 'both')),
duration_sec INTEGER NOT NULL DEFAULT 10,
tx_avg_mbps DOUBLE PRECISION,
rx_avg_mbps DOUBLE PRECISION,
ping_rtt_ms INTEGER,
ping_loss_pct INTEGER,
ping_error TEXT,
status TEXT NOT NULL DEFAULT 'done' CHECK (status IN ('done', 'error')),
error TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_uptime_speed_test_runs_created_at ON uptime_speed_test_runs(created_at);
CREATE TABLE IF NOT EXISTS certificate_issue_jobs (
id TEXT PRIMARY KEY,
status TEXT NOT NULL DEFAULT 'queued' CHECK (status IN ('queued', 'running', 'done', 'failed')),
step TEXT NOT NULL DEFAULT 'queued',
source TEXT NOT NULL DEFAULT 'manual' CHECK (source IN ('manual', 'scheduler')),
server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
cert_name TEXT NOT NULL,
domain_names JSONB NOT NULL,
key_type TEXT NOT NULL DEFAULT 'rsa2048',
trust_store TEXT NOT NULL DEFAULT 'www,api',
requested_at TIMESTAMPTZ NOT NULL DEFAULT now(),
started_at TIMESTAMPTZ,
finished_at TIMESTAMPTZ,
error TEXT
);
CREATE TABLE IF NOT EXISTS backup_entries (
id TEXT PRIMARY KEY,
server_id BIGINT REFERENCES servers(id) ON DELETE SET NULL,
server_name TEXT NOT NULL,
filename TEXT NOT NULL,
size_bytes BIGINT NOT NULL,
kind TEXT NOT NULL DEFAULT 'manual' CHECK (kind IN ('manual', 'auto')),
notes TEXT,
created_at TIMESTAMPTZ NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_backup_entries_filename ON backup_entries(filename);
CREATE INDEX IF NOT EXISTS idx_backup_entries_server_created ON backup_entries(server_id, created_at);
CREATE TABLE IF NOT EXISTS alert_groups (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
combine_mode TEXT NOT NULL DEFAULT 'any' CHECK (combine_mode IN ('any', 'all')),
enabled BOOLEAN NOT NULL DEFAULT TRUE,
cooldown_override TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS alert_rules (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
type TEXT NOT NULL,
target TEXT NOT NULL,
condition TEXT NOT NULL,
severity TEXT NOT NULL CHECK (severity IN ('critical', 'warning', 'info')),
enabled BOOLEAN NOT NULL DEFAULT TRUE,
cooldown TEXT NOT NULL DEFAULT '5м',
rule_chat_id TEXT NOT NULL DEFAULT '',
confirm_stability_sec INTEGER,
recovery_mode TEXT NOT NULL DEFAULT 'always' CHECK (recovery_mode IN ('always', 'never', 'conditional')),
recovery_stability_sec INTEGER,
group_id TEXT REFERENCES alert_groups(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS alert_rule_targets (
id TEXT PRIMARY KEY,
rule_id TEXT NOT NULL REFERENCES alert_rules(id) ON DELETE CASCADE,
target TEXT NOT NULL,
sort_index INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_alert_rule_targets_rule ON alert_rule_targets(rule_id);
CREATE TABLE IF NOT EXISTS alert_rule_conditions (
id TEXT PRIMARY KEY,
rule_id TEXT NOT NULL REFERENCES alert_rules(id) ON DELETE CASCADE,
condition_line TEXT NOT NULL,
sort_index INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_alert_rule_conditions_rule ON alert_rule_conditions(rule_id);
CREATE TABLE IF NOT EXISTS alert_engine_state (
scope_key TEXT PRIMARY KEY,
last_fired_at TEXT NOT NULL DEFAULT '',
last_payload_hash TEXT
);
CREATE TABLE IF NOT EXISTS alert_engine_prev_live (
kind TEXT PRIMARY KEY CHECK (kind IN ('gre', 'bgp')),
payload_json JSONB NOT NULL DEFAULT '{}'::jsonb,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS alert_engine_confirm_pending (
rule_id TEXT PRIMARY KEY,
payload_hash TEXT NOT NULL,
since_at TIMESTAMPTZ NOT NULL
);
CREATE TABLE IF NOT EXISTS alert_history (
id TEXT PRIMARY KEY,
rule_id TEXT,
group_id TEXT,
rule_name TEXT NOT NULL,
severity TEXT NOT NULL CHECK (severity IN ('critical', 'warning', 'info')),
message TEXT NOT NULL,
sent_ok BOOLEAN NOT NULL DEFAULT TRUE,
fired_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_alert_history_fired_at ON alert_history(fired_at);
CREATE INDEX IF NOT EXISTS idx_alert_history_rule_id ON alert_history(rule_id);
CREATE TABLE IF NOT EXISTS alert_outbox (
id TEXT PRIMARY KEY,
dedupe_key TEXT NOT NULL,
channel TEXT NOT NULL DEFAULT 'telegram' CHECK (channel IN ('telegram')),
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'sent', 'failed')),
retry_count INTEGER NOT NULL DEFAULT 0,
max_retries INTEGER NOT NULL DEFAULT 3,
next_attempt_at TIMESTAMPTZ NOT NULL,
payload_json JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
sent_at TIMESTAMPTZ,
last_error TEXT
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_alert_outbox_dedupe ON alert_outbox(dedupe_key);
CREATE INDEX IF NOT EXISTS idx_alert_outbox_status_next_attempt ON alert_outbox(status, next_attempt_at);
CREATE INDEX IF NOT EXISTS idx_alert_outbox_pending_next ON alert_outbox(next_attempt_at) WHERE status = 'pending';
CREATE TABLE IF NOT EXISTS scheduler_runs (
id TEXT PRIMARY KEY,
job_key TEXT NOT NULL,
started_at TIMESTAMPTZ NOT NULL,
finished_at TIMESTAMPTZ NOT NULL,
status TEXT NOT NULL CHECK (status IN ('ok', 'error')),
error TEXT,
duration_ms INTEGER NOT NULL DEFAULT 0,
result_json JSONB
);
CREATE INDEX IF NOT EXISTS idx_scheduler_runs_job_time ON scheduler_runs(job_key, started_at);
CREATE TABLE IF NOT EXISTS events (
id TEXT PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL,
level TEXT NOT NULL CHECK (level IN ('critical', 'warning', 'info')),
event_type TEXT NOT NULL,
source_module TEXT NOT NULL,
title TEXT NOT NULL,
message TEXT NOT NULL,
entity_type TEXT,
entity_id TEXT,
payload_json JSONB
);
CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_events_level_created_at ON events(level, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_events_source_created_at ON events(source_module, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_events_event_type_created_at ON events(event_type, created_at DESC);
CREATE TABLE IF NOT EXISTS app_users (
id TEXT PRIMARY KEY,
name TEXT NOT NULL DEFAULT '',
login TEXT NOT NULL UNIQUE,
email TEXT NOT NULL DEFAULT '',
role TEXT NOT NULL DEFAULT 'viewer' CHECK (role IN ('admin', 'operator', 'viewer')),
active BOOLEAN NOT NULL DEFAULT TRUE,
avatar TEXT NOT NULL DEFAULT '',
last_seen TIMESTAMPTZ,
sections_json JSONB NOT NULL DEFAULT '[]'::jsonb,
servers_json JSONB NOT NULL DEFAULT '[]'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS user_interface_bindings (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES app_users(id) ON DELETE CASCADE,
server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
interface_name TEXT NOT NULL,
interface_type TEXT NOT NULL DEFAULT 'other' CHECK (interface_type IN ('ether', 'gre', 'wg', 'other')),
peer_public_key TEXT NOT NULL DEFAULT '',
peer_name TEXT NOT NULL DEFAULT '',
comment TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (server_id, interface_name, peer_public_key)
);
CREATE INDEX IF NOT EXISTS idx_user_iface_bind_user ON user_interface_bindings(user_id);
CREATE TABLE IF NOT EXISTS internet_path_snapshots (
id BIGINT GENERATED BY DEFAULT AS IDENTITY,
sampled_at TIMESTAMPTZ NOT NULL,
payload_json JSONB NOT NULL,
PRIMARY KEY (id, sampled_at)
) PARTITION BY RANGE (sampled_at);
CREATE INDEX IF NOT EXISTS idx_internet_path_snapshots_sampled ON internet_path_snapshots(sampled_at);
CREATE INDEX IF NOT EXISTS idx_internet_path_snapshots_brin ON internet_path_snapshots USING BRIN (sampled_at);
INSERT INTO traffic_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
INSERT INTO traffic_flow_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
INSERT INTO uptime_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
INSERT INTO evobgp_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
INSERT INTO servers_api_ping_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
INSERT INTO internet_path_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
INSERT INTO alert_telegram_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
INSERT INTO acme_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
INSERT INTO certificate_renew_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
INSERT INTO backup_schedule_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
INSERT INTO alert_engine_cursor (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
INSERT INTO data_migration (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
+620
View File
@@ -0,0 +1,620 @@
-- Compact PostgreSQL 18 schema: wipe all app tables (not schema_migrations),
-- recreate with smaller time-series rows. SQLite is re-imported after this.
DO $$
DECLARE
r record;
BEGIN
FOR r IN
SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
AND tablename <> 'schema_migrations'
LOOP
EXECUTE format('DROP TABLE IF EXISTS public.%I CASCADE', r.tablename);
END LOOP;
END $$;
CREATE TABLE IF NOT EXISTS schema_migrations (
id TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS servers (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL DEFAULT '',
host TEXT NOT NULL,
port INTEGER NOT NULL DEFAULT 443,
username TEXT NOT NULL DEFAULT 'admin',
password TEXT NOT NULL DEFAULT '',
use_ssl BOOLEAN NOT NULL DEFAULT TRUE,
verify_ssl BOOLEAN NOT NULL DEFAULT FALSE,
type TEXT NOT NULL DEFAULT 'home-router'
CHECK (type IN ('jump-host', 'exit-node', 'home-router')),
site TEXT NOT NULL DEFAULT '',
country TEXT NOT NULL DEFAULT '',
asn TEXT NOT NULL DEFAULT '',
comment TEXT NOT NULL DEFAULT '',
enabled BOOLEAN NOT NULL DEFAULT TRUE,
lan_subnet TEXT NOT NULL DEFAULT '',
wan_uplinks JSONB COMPRESSION lz4 NOT NULL DEFAULT '[]'::jsonb,
mgmt_tunnel_ip TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS traffic_settings (
id BIGINT PRIMARY KEY CHECK (id = 1),
enabled BOOLEAN NOT NULL DEFAULT TRUE,
interval_sec INTEGER NOT NULL DEFAULT 30,
retention_days INTEGER NOT NULL DEFAULT 14,
last_collected_at TIMESTAMPTZ,
last_duration_ms INTEGER,
last_error TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS servers_api_ping_settings (
id BIGINT PRIMARY KEY CHECK (id = 1),
enabled BOOLEAN NOT NULL DEFAULT FALSE,
interval_sec INTEGER NOT NULL DEFAULT 120,
last_collected_at TIMESTAMPTZ,
last_duration_ms INTEGER,
last_error TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS traffic_flow_settings (
id BIGINT PRIMARY KEY CHECK (id = 1),
enabled BOOLEAN NOT NULL DEFAULT FALSE,
collector_ip TEXT NOT NULL DEFAULT '10.255.254.1',
flow_listen_port INTEGER NOT NULL DEFAULT 4739,
wg_listen_port INTEGER NOT NULL DEFAULT 51821,
prefix TEXT NOT NULL DEFAULT '10.255.254.0/24',
public_endpoint TEXT NOT NULL DEFAULT '',
host_public_key TEXT NOT NULL DEFAULT '',
host_private_key TEXT NOT NULL DEFAULT '',
hub_server_id BIGINT,
retention_hours INTEGER NOT NULL DEFAULT 24,
top_n INTEGER NOT NULL DEFAULT 200,
map_service_min_share_pct DOUBLE PRECISION NOT NULL DEFAULT 5,
last_datagram_at TIMESTAMPTZ,
last_exporter_ip TEXT,
last_error TEXT,
packets_received BIGINT NOT NULL DEFAULT 0,
peers_json JSONB COMPRESSION lz4 NOT NULL DEFAULT '[]'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS uptime_settings (
id BIGINT PRIMARY KEY CHECK (id = 1),
enabled BOOLEAN NOT NULL DEFAULT TRUE,
resources_enabled BOOLEAN NOT NULL DEFAULT TRUE,
ping_enabled BOOLEAN NOT NULL DEFAULT TRUE,
speed_enabled BOOLEAN NOT NULL DEFAULT TRUE,
interval_sec INTEGER NOT NULL DEFAULT 15,
probe_interval_sec INTEGER NOT NULL DEFAULT 15,
speed_interval_sec INTEGER NOT NULL DEFAULT 60,
retention_days INTEGER NOT NULL DEFAULT 14,
last_collected_at TIMESTAMPTZ,
last_duration_ms INTEGER,
last_error TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS evobgp_settings (
id BIGINT PRIMARY KEY CHECK (id = 1),
base_url TEXT NOT NULL DEFAULT '',
api_key TEXT NOT NULL DEFAULT '',
enabled BOOLEAN NOT NULL DEFAULT FALSE,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS alert_telegram_settings (
id BIGINT PRIMARY KEY CHECK (id = 1),
bot_token TEXT NOT NULL DEFAULT '',
chat_id TEXT NOT NULL DEFAULT '',
message_thread_id INTEGER,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS acme_settings (
id BIGINT PRIMARY KEY CHECK (id = 1),
directory_url TEXT NOT NULL DEFAULT 'https://acme-v02.api.letsencrypt.org/directory',
cloudflare_api_token TEXT NOT NULL DEFAULT '',
default_zone_id TEXT NOT NULL DEFAULT '',
account_private_key TEXT NOT NULL DEFAULT '',
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS certificate_renew_settings (
id BIGINT PRIMARY KEY CHECK (id = 1),
enabled BOOLEAN NOT NULL DEFAULT TRUE,
interval_sec INTEGER NOT NULL DEFAULT 21600,
renew_before_days INTEGER NOT NULL DEFAULT 30,
last_collected_at TIMESTAMPTZ,
last_duration_ms INTEGER,
last_error TEXT,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS backup_schedule_settings (
id BIGINT PRIMARY KEY CHECK (id = 1),
enabled BOOLEAN NOT NULL DEFAULT TRUE,
frequency TEXT NOT NULL DEFAULT 'daily' CHECK (frequency IN ('daily', 'weekly', 'monthly')),
hour INTEGER NOT NULL DEFAULT 3,
minute INTEGER NOT NULL DEFAULT 0,
week_day INTEGER NOT NULL DEFAULT 0,
month_day INTEGER NOT NULL DEFAULT 1,
keep_count INTEGER NOT NULL DEFAULT 7,
format TEXT NOT NULL DEFAULT 'rsc' CHECK (format IN ('rsc', 'backup')),
server_ids_json JSONB COMPRESSION lz4 NOT NULL DEFAULT '[]'::jsonb,
last_run_at TIMESTAMPTZ,
last_duration_ms INTEGER,
last_error TEXT,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS internet_path_settings (
id BIGINT PRIMARY KEY CHECK (id = 1),
enabled BOOLEAN NOT NULL DEFAULT TRUE,
interval_sec INTEGER NOT NULL DEFAULT 300,
retention_days INTEGER NOT NULL DEFAULT 14,
last_collected_at TIMESTAMPTZ,
last_duration_ms INTEGER,
last_error TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS alert_engine_cursor (
id BIGINT PRIMARY KEY CHECK (id = 1),
last_source_finished_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS data_migration (
id INTEGER PRIMARY KEY CHECK (id = 1),
sqlite_imported_at TIMESTAMPTZ,
sqlite_path TEXT,
sqlite_sha256 TEXT,
report_json JSONB COMPRESSION lz4
);
CREATE TABLE IF NOT EXISTS filter_rules (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
sort_order INTEGER NOT NULL DEFAULT 0,
community TEXT NOT NULL,
community_name TEXT,
action TEXT NOT NULL DEFAULT 'route' CHECK (action IN ('route', 'blackhole')),
gateway TEXT NOT NULL DEFAULT '',
gateway_tunnel_id TEXT NOT NULL DEFAULT '',
description TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_filter_rules_server_sort ON filter_rules(server_id, sort_order);
CREATE TABLE IF NOT EXISTS recursive_routes (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
sort_order INTEGER NOT NULL DEFAULT 0,
dst_address TEXT NOT NULL,
gateway TEXT NOT NULL,
distance INTEGER NOT NULL DEFAULT 1,
scope INTEGER,
target_scope INTEGER,
routing_table TEXT NOT NULL DEFAULT 'main',
check_gateway TEXT NOT NULL DEFAULT '',
country TEXT NOT NULL DEFAULT '',
comment TEXT NOT NULL DEFAULT '',
disabled BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_recursive_routes_server_sort ON recursive_routes(server_id, sort_order);
CREATE TABLE IF NOT EXISTS server_snapshots (
id BIGINT GENERATED BY DEFAULT AS IDENTITY,
server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
polled_at TIMESTAMPTZ NOT NULL,
status TEXT NOT NULL CHECK (status IN ('online', 'offline')),
latency_ms DOUBLE PRECISION,
ros_version TEXT,
board_name TEXT,
uptime TEXT,
cpu_load INTEGER,
free_memory BIGINT,
total_memory BIGINT,
identity_name TEXT,
raw_interfaces JSONB COMPRESSION lz4,
raw_ip_addresses JSONB COMPRESSION lz4,
PRIMARY KEY (id, polled_at)
) PARTITION BY RANGE (polled_at);
CREATE INDEX IF NOT EXISTS idx_server_snapshots_server_time ON server_snapshots(server_id, polled_at);
CREATE TABLE IF NOT EXISTS traffic_samples (
server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
interface_name TEXT NOT NULL,
peer_public_key TEXT NOT NULL DEFAULT '',
sampled_at TIMESTAMPTZ NOT NULL,
rx_bytes BIGINT NOT NULL DEFAULT 0,
tx_bytes BIGINT NOT NULL DEFAULT 0,
rx_bps BIGINT NOT NULL DEFAULT 0,
tx_bps BIGINT NOT NULL DEFAULT 0,
flags SMALLINT NOT NULL DEFAULT 0,
PRIMARY KEY (server_id, sampled_at, interface_name, peer_public_key)
) PARTITION BY RANGE (sampled_at);
CREATE INDEX IF NOT EXISTS idx_traffic_samples_server_iface_time ON traffic_samples(server_id, interface_name, sampled_at);
CREATE TABLE IF NOT EXISTS servers_rest_ping_samples (
server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
sampled_at TIMESTAMPTZ NOT NULL,
ok BOOLEAN NOT NULL,
latency_ms INTEGER,
error TEXT,
PRIMARY KEY (server_id, sampled_at)
) PARTITION BY RANGE (sampled_at);
CREATE TABLE IF NOT EXISTS flow_buckets (
server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
bucket_at TIMESTAMPTZ NOT NULL,
src INET NOT NULL,
dst INET NOT NULL,
proto SMALLINT NOT NULL DEFAULT 0,
src_port INTEGER NOT NULL DEFAULT 0,
dst_port INTEGER NOT NULL DEFAULT 0,
bytes BIGINT NOT NULL DEFAULT 0,
packets BIGINT NOT NULL DEFAULT 0,
in_iface TEXT NOT NULL DEFAULT '',
out_iface TEXT NOT NULL DEFAULT '',
next_hop INET,
flow_start_ms BIGINT NOT NULL DEFAULT 0,
flow_end_ms BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (server_id, bucket_at, src, dst, proto, src_port, dst_port, in_iface)
) PARTITION BY RANGE (bucket_at);
CREATE INDEX IF NOT EXISTS idx_flow_buckets_server_time ON flow_buckets(server_id, bucket_at);
CREATE TABLE IF NOT EXISTS flow_minute_stats (
server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
bucket_at TIMESTAMPTZ NOT NULL,
bytes BIGINT NOT NULL DEFAULT 0,
packets BIGINT NOT NULL DEFAULT 0,
unique_src INTEGER NOT NULL DEFAULT 0,
unique_dst INTEGER NOT NULL DEFAULT 0,
conversations INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (server_id, bucket_at)
) PARTITION BY RANGE (bucket_at);
CREATE TABLE IF NOT EXISTS flow_minute_dims (
server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
bucket_at TIMESTAMPTZ NOT NULL,
dim TEXT NOT NULL,
key TEXT NOT NULL,
bytes BIGINT NOT NULL DEFAULT 0,
packets BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (server_id, bucket_at, dim, key)
) PARTITION BY RANGE (bucket_at);
CREATE INDEX IF NOT EXISTS idx_flow_minute_dims_time ON flow_minute_dims(bucket_at, dim);
CREATE TABLE IF NOT EXISTS flow_daily_dims (
server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
day DATE NOT NULL,
dim TEXT NOT NULL,
key TEXT NOT NULL,
bytes BIGINT NOT NULL DEFAULT 0,
packets BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (server_id, day, dim, key)
) PARTITION BY RANGE (day);
CREATE INDEX IF NOT EXISTS idx_flow_daily_dims_day ON flow_daily_dims(day, dim);
CREATE TABLE IF NOT EXISTS flow_ip_meta (
prefix TEXT PRIMARY KEY,
asn INTEGER NOT NULL DEFAULT 0,
country TEXT NOT NULL DEFAULT '',
lat DOUBLE PRECISION,
lng DOUBLE PRECISION,
holder TEXT NOT NULL DEFAULT '',
ok INTEGER NOT NULL DEFAULT 1,
fetched_at TIMESTAMPTZ NOT NULL
);
CREATE TABLE IF NOT EXISTS flow_asn_meta (
asn INTEGER PRIMARY KEY,
holder TEXT NOT NULL DEFAULT '',
fetched_at TIMESTAMPTZ NOT NULL
);
CREATE TABLE IF NOT EXISTS uptime_probes (
id TEXT PRIMARY KEY,
src_server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
src_interface TEXT NOT NULL DEFAULT '',
name TEXT NOT NULL,
target TEXT NOT NULL,
probe_filter TEXT NOT NULL DEFAULT '—',
enabled BOOLEAN NOT NULL DEFAULT TRUE,
interval_sec INTEGER NOT NULL DEFAULT 0,
show_on_dashboard BOOLEAN NOT NULL DEFAULT FALSE,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_uptime_probes_server_sort ON uptime_probes(src_server_id, sort_order);
CREATE TABLE IF NOT EXISTS uptime_probe_samples (
probe_id TEXT NOT NULL REFERENCES uptime_probes(id) ON DELETE CASCADE,
sampled_at TIMESTAMPTZ NOT NULL,
rtt_ms INTEGER,
loss_pct INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'down' CHECK (status IN ('up', 'warn', 'down')),
PRIMARY KEY (probe_id, sampled_at)
) PARTITION BY RANGE (sampled_at);
CREATE TABLE IF NOT EXISTS uptime_resource_samples (
server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
sampled_at TIMESTAMPTZ NOT NULL,
status TEXT NOT NULL DEFAULT 'offline' CHECK (status IN ('online', 'offline')),
cpu_load INTEGER NOT NULL DEFAULT 0,
free_memory BIGINT NOT NULL DEFAULT 0,
total_memory BIGINT NOT NULL DEFAULT 0,
free_hdd_space BIGINT NOT NULL DEFAULT 0,
total_hdd_space BIGINT NOT NULL DEFAULT 0,
uptime_seconds BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (server_id, sampled_at)
) PARTITION BY RANGE (sampled_at);
CREATE TABLE IF NOT EXISTS uptime_speed_probes (
id TEXT PRIMARY KEY,
src_server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
dst_server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
src_interface TEXT NOT NULL DEFAULT '',
dst_interface TEXT NOT NULL DEFAULT '',
protocol TEXT NOT NULL DEFAULT 'tcp' CHECK (protocol IN ('tcp', 'udp')),
direction TEXT NOT NULL DEFAULT 'both' CHECK (direction IN ('transmit', 'receive', 'both')),
duration_sec INTEGER NOT NULL DEFAULT 10,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
last_run_at TIMESTAMPTZ,
last_tx_avg_mbps DOUBLE PRECISION,
last_rx_avg_mbps DOUBLE PRECISION,
last_status TEXT CHECK (last_status IN ('done', 'error')),
last_error TEXT,
last_ping_rtt_ms INTEGER,
last_ping_loss_pct INTEGER,
last_ping_at TIMESTAMPTZ,
last_ping_error TEXT,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_uptime_speed_probes_src_sort ON uptime_speed_probes(src_server_id, sort_order);
CREATE TABLE IF NOT EXISTS uptime_speed_test_runs (
id TEXT PRIMARY KEY,
probe_id TEXT,
src_server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
dst_server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
src_interface TEXT NOT NULL DEFAULT '',
dst_interface TEXT NOT NULL DEFAULT '',
src_address TEXT,
dst_address TEXT,
src_interface_address TEXT,
dst_interface_address TEXT,
protocol TEXT NOT NULL DEFAULT 'tcp' CHECK (protocol IN ('tcp', 'udp')),
direction TEXT NOT NULL DEFAULT 'both' CHECK (direction IN ('transmit', 'receive', 'both')),
duration_sec INTEGER NOT NULL DEFAULT 10,
tx_avg_mbps DOUBLE PRECISION,
rx_avg_mbps DOUBLE PRECISION,
ping_rtt_ms INTEGER,
ping_loss_pct INTEGER,
ping_error TEXT,
status TEXT NOT NULL DEFAULT 'done' CHECK (status IN ('done', 'error')),
error TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_uptime_speed_test_runs_created_at ON uptime_speed_test_runs(created_at);
CREATE TABLE IF NOT EXISTS certificate_issue_jobs (
id TEXT PRIMARY KEY,
status TEXT NOT NULL DEFAULT 'queued' CHECK (status IN ('queued', 'running', 'done', 'failed')),
step TEXT NOT NULL DEFAULT 'queued',
source TEXT NOT NULL DEFAULT 'manual' CHECK (source IN ('manual', 'scheduler')),
server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
cert_name TEXT NOT NULL,
domain_names JSONB COMPRESSION lz4 NOT NULL,
key_type TEXT NOT NULL DEFAULT 'rsa2048',
trust_store TEXT NOT NULL DEFAULT 'www,api',
requested_at TIMESTAMPTZ NOT NULL DEFAULT now(),
started_at TIMESTAMPTZ,
finished_at TIMESTAMPTZ,
error TEXT
);
CREATE TABLE IF NOT EXISTS backup_entries (
id TEXT PRIMARY KEY,
server_id BIGINT REFERENCES servers(id) ON DELETE SET NULL,
server_name TEXT NOT NULL,
filename TEXT NOT NULL,
size_bytes BIGINT NOT NULL,
kind TEXT NOT NULL DEFAULT 'manual' CHECK (kind IN ('manual', 'auto')),
notes TEXT,
created_at TIMESTAMPTZ NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_backup_entries_filename ON backup_entries(filename);
CREATE INDEX IF NOT EXISTS idx_backup_entries_server_created ON backup_entries(server_id, created_at);
CREATE TABLE IF NOT EXISTS alert_groups (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
combine_mode TEXT NOT NULL DEFAULT 'any' CHECK (combine_mode IN ('any', 'all')),
enabled BOOLEAN NOT NULL DEFAULT TRUE,
cooldown_override TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS alert_rules (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
type TEXT NOT NULL,
target TEXT NOT NULL,
condition TEXT NOT NULL,
severity TEXT NOT NULL CHECK (severity IN ('critical', 'warning', 'info')),
enabled BOOLEAN NOT NULL DEFAULT TRUE,
cooldown TEXT NOT NULL DEFAULT '5м',
rule_chat_id TEXT NOT NULL DEFAULT '',
confirm_stability_sec INTEGER,
recovery_mode TEXT NOT NULL DEFAULT 'always' CHECK (recovery_mode IN ('always', 'never', 'conditional')),
recovery_stability_sec INTEGER,
group_id TEXT REFERENCES alert_groups(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS alert_rule_targets (
id TEXT PRIMARY KEY,
rule_id TEXT NOT NULL REFERENCES alert_rules(id) ON DELETE CASCADE,
target TEXT NOT NULL,
sort_index INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_alert_rule_targets_rule ON alert_rule_targets(rule_id);
CREATE TABLE IF NOT EXISTS alert_rule_conditions (
id TEXT PRIMARY KEY,
rule_id TEXT NOT NULL REFERENCES alert_rules(id) ON DELETE CASCADE,
condition_line TEXT NOT NULL,
sort_index INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_alert_rule_conditions_rule ON alert_rule_conditions(rule_id);
CREATE TABLE IF NOT EXISTS alert_engine_state (
scope_key TEXT PRIMARY KEY,
last_fired_at TEXT NOT NULL DEFAULT '',
last_payload_hash TEXT
);
CREATE TABLE IF NOT EXISTS alert_engine_prev_live (
kind TEXT PRIMARY KEY CHECK (kind IN ('gre', 'bgp')),
payload_json JSONB COMPRESSION lz4 NOT NULL DEFAULT '{}'::jsonb,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS alert_engine_confirm_pending (
rule_id TEXT PRIMARY KEY,
payload_hash TEXT NOT NULL,
since_at TIMESTAMPTZ NOT NULL
);
CREATE TABLE IF NOT EXISTS alert_history (
id TEXT PRIMARY KEY,
rule_id TEXT,
group_id TEXT,
rule_name TEXT NOT NULL,
severity TEXT NOT NULL CHECK (severity IN ('critical', 'warning', 'info')),
message TEXT NOT NULL,
sent_ok BOOLEAN NOT NULL DEFAULT TRUE,
fired_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_alert_history_fired_at ON alert_history(fired_at);
CREATE INDEX IF NOT EXISTS idx_alert_history_rule_id ON alert_history(rule_id);
CREATE TABLE IF NOT EXISTS alert_outbox (
id TEXT PRIMARY KEY,
dedupe_key TEXT NOT NULL,
channel TEXT NOT NULL DEFAULT 'telegram' CHECK (channel IN ('telegram')),
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'sent', 'failed')),
retry_count INTEGER NOT NULL DEFAULT 0,
max_retries INTEGER NOT NULL DEFAULT 3,
next_attempt_at TIMESTAMPTZ NOT NULL,
payload_json JSONB COMPRESSION lz4 NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
sent_at TIMESTAMPTZ,
last_error TEXT
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_alert_outbox_dedupe ON alert_outbox(dedupe_key);
CREATE INDEX IF NOT EXISTS idx_alert_outbox_status_next_attempt ON alert_outbox(status, next_attempt_at);
CREATE INDEX IF NOT EXISTS idx_alert_outbox_pending_next ON alert_outbox(next_attempt_at) WHERE status = 'pending';
CREATE TABLE IF NOT EXISTS scheduler_runs (
id TEXT PRIMARY KEY,
job_key TEXT NOT NULL,
started_at TIMESTAMPTZ NOT NULL,
finished_at TIMESTAMPTZ NOT NULL,
status TEXT NOT NULL CHECK (status IN ('ok', 'error')),
error TEXT,
duration_ms INTEGER NOT NULL DEFAULT 0,
result_json JSONB COMPRESSION lz4
);
CREATE INDEX IF NOT EXISTS idx_scheduler_runs_job_time ON scheduler_runs(job_key, started_at);
CREATE TABLE IF NOT EXISTS events (
id TEXT PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL,
level TEXT NOT NULL CHECK (level IN ('critical', 'warning', 'info')),
event_type TEXT NOT NULL,
source_module TEXT NOT NULL,
title TEXT NOT NULL,
message TEXT NOT NULL,
entity_type TEXT,
entity_id TEXT,
payload_json JSONB COMPRESSION lz4
);
CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_events_level_created_at ON events(level, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_events_source_created_at ON events(source_module, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_events_event_type_created_at ON events(event_type, created_at DESC);
CREATE TABLE IF NOT EXISTS app_users (
id TEXT PRIMARY KEY,
name TEXT NOT NULL DEFAULT '',
login TEXT NOT NULL UNIQUE,
email TEXT NOT NULL DEFAULT '',
role TEXT NOT NULL DEFAULT 'viewer' CHECK (role IN ('admin', 'operator', 'viewer')),
active BOOLEAN NOT NULL DEFAULT TRUE,
avatar TEXT NOT NULL DEFAULT '',
last_seen TIMESTAMPTZ,
sections_json JSONB COMPRESSION lz4 NOT NULL DEFAULT '[]'::jsonb,
servers_json JSONB COMPRESSION lz4 NOT NULL DEFAULT '[]'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS user_interface_bindings (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES app_users(id) ON DELETE CASCADE,
server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
interface_name TEXT NOT NULL,
interface_type TEXT NOT NULL DEFAULT 'other' CHECK (interface_type IN ('ether', 'gre', 'wg', 'other')),
peer_public_key TEXT NOT NULL DEFAULT '',
peer_name TEXT NOT NULL DEFAULT '',
comment TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (server_id, interface_name, peer_public_key)
);
CREATE INDEX IF NOT EXISTS idx_user_iface_bind_user ON user_interface_bindings(user_id);
CREATE TABLE IF NOT EXISTS internet_path_snapshots (
id BIGINT GENERATED BY DEFAULT AS IDENTITY,
sampled_at TIMESTAMPTZ NOT NULL,
payload_json JSONB COMPRESSION lz4 NOT NULL,
PRIMARY KEY (id, sampled_at)
) PARTITION BY RANGE (sampled_at);
CREATE INDEX IF NOT EXISTS idx_internet_path_snapshots_sampled ON internet_path_snapshots(sampled_at);
INSERT INTO traffic_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
INSERT INTO traffic_flow_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
INSERT INTO uptime_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
INSERT INTO evobgp_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
INSERT INTO servers_api_ping_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
INSERT INTO internet_path_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
INSERT INTO alert_telegram_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
INSERT INTO acme_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
INSERT INTO certificate_renew_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
INSERT INTO backup_schedule_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
INSERT INTO alert_engine_cursor (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
INSERT INTO data_migration (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
+44
View File
@@ -0,0 +1,44 @@
-- S3-compatible storage for RouterOS backups
CREATE TABLE IF NOT EXISTS backup_storage_settings (
id BIGINT PRIMARY KEY CHECK (id = 1),
provider TEXT NOT NULL DEFAULT 'local' CHECK (provider IN ('local', 's3')),
s3_endpoint TEXT NOT NULL DEFAULT '',
s3_region TEXT NOT NULL DEFAULT 'us-east-1',
s3_bucket TEXT NOT NULL DEFAULT '',
s3_prefix TEXT NOT NULL DEFAULT 'mikrotik',
s3_access_key_id TEXT NOT NULL DEFAULT '',
s3_secret_access_key TEXT NOT NULL DEFAULT '',
s3_force_path_style BOOLEAN NOT NULL DEFAULT TRUE,
keep_local_copy BOOLEAN NOT NULL DEFAULT TRUE,
last_test_at TIMESTAMPTZ,
last_test_error TEXT,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
INSERT INTO backup_storage_settings (id)
VALUES (1)
ON CONFLICT (id) DO NOTHING;
ALTER TABLE backup_entries
ADD COLUMN IF NOT EXISTS storage TEXT NOT NULL DEFAULT 'local';
ALTER TABLE backup_entries
ADD COLUMN IF NOT EXISTS s3_key TEXT;
ALTER TABLE backup_entries
ADD COLUMN IF NOT EXISTS s3_etag TEXT;
ALTER TABLE backup_entries
ADD COLUMN IF NOT EXISTS upload_error TEXT;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'backup_entries_storage_check'
) THEN
ALTER TABLE backup_entries
ADD CONSTRAINT backup_entries_storage_check
CHECK (storage IN ('local', 's3', 'both'));
END IF;
END $$;
+19
View File
@@ -0,0 +1,19 @@
-- GeoLite2 mmdb (страна/ASN для netflow): настройки автообновления зеркала P3TERX
CREATE TABLE IF NOT EXISTS geoip_settings (
id BIGINT PRIMARY KEY CHECK (id = 1),
enabled BOOLEAN NOT NULL DEFAULT TRUE,
update_interval_sec INTEGER NOT NULL DEFAULT 604800,
last_check_at TIMESTAMPTZ,
last_success_at TIMESTAMPTZ,
last_error TEXT,
country_build_at TIMESTAMPTZ,
asn_build_at TIMESTAMPTZ,
etags_json JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
INSERT INTO geoip_settings (id)
VALUES (1)
ON CONFLICT (id) DO NOTHING;
+28
View File
@@ -0,0 +1,28 @@
-- Statistics cube: hour + daily facts (server × iface × country × service × ASN).
-- Compact types. FILLFACTOR/autovacuum нельзя на partitioned parent (PG 42809) —
-- задаются на листовых партициях в ensurePartitionFor.
-- Retention: DROP partitions only (see PARTITION_SPECS).
CREATE TABLE IF NOT EXISTS flow_hour_facts (
server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
bucket_at TIMESTAMPTZ NOT NULL,
iface TEXT NOT NULL,
country CHAR(2) NOT NULL,
service TEXT NOT NULL,
asn INTEGER NOT NULL,
bytes BIGINT NOT NULL DEFAULT 0,
packets BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (server_id, bucket_at, iface, country, service, asn)
) PARTITION BY RANGE (bucket_at);
CREATE TABLE IF NOT EXISTS flow_daily_facts (
server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
day DATE NOT NULL,
iface TEXT NOT NULL,
country CHAR(2) NOT NULL,
service TEXT NOT NULL,
asn INTEGER NOT NULL,
bytes BIGINT NOT NULL DEFAULT 0,
packets BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (server_id, day, iface, country, service, asn)
) PARTITION BY RANGE (day);
+16
View File
@@ -0,0 +1,16 @@
-- История desired/actual снапшотов managed-секций (фильтры, рекурсивные маршруты).
-- Retention — prune в сервисе (последние 50 на пару server+section).
CREATE TABLE IF NOT EXISTS config_revisions (
id TEXT PRIMARY KEY,
server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
section TEXT NOT NULL,
source TEXT NOT NULL,
fingerprint TEXT NOT NULL,
payload JSONB NOT NULL DEFAULT '[]'::jsonb,
note TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_config_revisions_server_section_created
ON config_revisions (server_id, section, created_at DESC);
@@ -0,0 +1,7 @@
-- IPFIX postNAT (IANA 225/226) + postNAPT ports (IANA 227/228) from MikroTik Traffic Flow.
-- Needed to rebuild facts with the same internet dest as the network map.
ALTER TABLE flow_buckets ADD COLUMN IF NOT EXISTS nat_src INET;
ALTER TABLE flow_buckets ADD COLUMN IF NOT EXISTS nat_dst INET;
ALTER TABLE flow_buckets ADD COLUMN IF NOT EXISTS nat_src_port INTEGER NOT NULL DEFAULT 0;
ALTER TABLE flow_buckets ADD COLUMN IF NOT EXISTS nat_dst_port INTEGER NOT NULL DEFAULT 0;
@@ -0,0 +1,6 @@
-- Захват перед POST и удержание неизвестных исходов. Исторические строки не меняются.
BEGIN;
ALTER TABLE alert_outbox DROP CONSTRAINT IF EXISTS alert_outbox_status_check;
ALTER TABLE alert_outbox ADD CONSTRAINT alert_outbox_status_check
CHECK (status IN ('pending', 'processing', 'unknown', 'sent', 'failed'));
COMMIT;
@@ -0,0 +1,4 @@
BEGIN;
ALTER TABLE alert_outbox ADD COLUMN IF NOT EXISTS resolution_version integer NOT NULL DEFAULT 0;
ALTER TABLE alert_outbox ADD CONSTRAINT alert_outbox_resolution_version_check CHECK (resolution_version >= 0);
COMMIT;
@@ -0,0 +1,3 @@
-- A failed command is an attempt, not a measured packet-loss percentage.
ALTER TABLE uptime_probe_samples ADD COLUMN IF NOT EXISTS measurement_known BOOLEAN NOT NULL DEFAULT true;
ALTER TABLE uptime_probe_samples ADD COLUMN IF NOT EXISTS observation_error TEXT;
@@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS traffic_collection_state (
server_id BIGINT PRIMARY KEY REFERENCES servers(id) ON DELETE CASCADE,
server_updated_at TIMESTAMPTZ NOT NULL,
last_attempt_at TIMESTAMPTZ NOT NULL,
last_succeeded_at TIMESTAMPTZ,
status TEXT NOT NULL CHECK(status IN ('pending', 'ok', 'error')),
error TEXT
);
@@ -0,0 +1 @@
ALTER TABLE traffic_samples ADD COLUMN IF NOT EXISTS native_counter_identity TEXT;
@@ -0,0 +1,4 @@
-- Retention runs after each scheduler receipt; avoid scanning the complete history.
-- On a busy existing installation, pre-create this same index CONCURRENTLY
-- outside a transaction before deploying (deploy/PRODUCTION-RETENTION-INDEX.md).
CREATE INDEX IF NOT EXISTS idx_scheduler_runs_finished_at ON scheduler_runs (finished_at);
+26
View File
@@ -0,0 +1,26 @@
CREATE TABLE IF NOT EXISTS router_log_settings (
id INTEGER PRIMARY KEY CHECK (id = 1), enabled BOOLEAN NOT NULL DEFAULT false,
interval_sec INTEGER NOT NULL DEFAULT 10 CHECK (interval_sec BETWEEN 5 AND 3600),
retention_days INTEGER NOT NULL DEFAULT 14 CHECK (retention_days BETWEEN 1 AND 365)
);
INSERT INTO router_log_settings(id) VALUES (1) ON CONFLICT DO NOTHING;
-- No foreign key: collected history survives device deletion.
CREATE TABLE IF NOT EXISTS router_log_collection_state (
server_id INTEGER PRIMARY KEY, server_name TEXT NOT NULL, host TEXT NOT NULL,
generation INTEGER NOT NULL DEFAULT 0, snapshot JSONB NOT NULL DEFAULT '[]',
last_collected_at TIMESTAMPTZ, last_error TEXT, gap_at TIMESTAMPTZ,
next_poll_at TIMESTAMPTZ, failures INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS router_log_entries (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, server_id INTEGER NOT NULL,
server_name TEXT NOT NULL, host TEXT NOT NULL, router_id TEXT NOT NULL, raw_time TEXT NOT NULL,
event_at TIMESTAMPTZ, received_at TIMESTAMPTZ NOT NULL,
effective_at TIMESTAMPTZ NOT NULL, topics TEXT[] NOT NULL, level TEXT NOT NULL,
message TEXT NOT NULL, generation INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS router_logs_server_time ON router_log_entries(server_id, effective_at DESC, id DESC);
CREATE INDEX IF NOT EXISTS router_logs_time ON router_log_entries(effective_at DESC, id DESC);
CREATE INDEX IF NOT EXISTS router_logs_level_time ON router_log_entries(level, effective_at DESC);
CREATE INDEX IF NOT EXISTS router_logs_topics ON router_log_entries USING gin(topics);
CREATE INDEX IF NOT EXISTS router_logs_received ON router_log_entries(received_at);
CREATE INDEX IF NOT EXISTS router_logs_server_cursor ON router_log_entries(server_id, id);
+101
View File
@@ -0,0 +1,101 @@
BEGIN;
CREATE TABLE IF NOT EXISTS mcp_settings (
id integer PRIMARY KEY CHECK(id=1), enabled boolean NOT NULL DEFAULT false,
requests_retention_days integer NOT NULL DEFAULT 30 CHECK(requests_retention_days BETWEEN 1 AND 365),
audit_retention_days integer NOT NULL DEFAULT 180 CHECK(audit_retention_days BETWEEN 30 AND 3650),
updated_at timestamptz NOT NULL DEFAULT now()
);
INSERT INTO mcp_settings(id) VALUES(1) ON CONFLICT DO NOTHING;
CREATE TABLE IF NOT EXISTS mcp_connections (
id uuid PRIMARY KEY, name text NOT NULL, owner_id text NOT NULL, owner_permissions jsonb NOT NULL,
policy jsonb NOT NULL, generation text NOT NULL, created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(), last_used_at timestamptz
);
CREATE TABLE IF NOT EXISTS mcp_credentials (
id uuid PRIMARY KEY, connection_id uuid NOT NULL REFERENCES mcp_connections(id) ON DELETE CASCADE,
digest text NOT NULL UNIQUE, kind text NOT NULL CHECK(kind IN ('bearer','access','refresh')),
expires_at timestamptz NOT NULL, revoked_at timestamptz, created_at timestamptz NOT NULL DEFAULT now(),
family_id uuid, client_id text, audience text
);
CREATE INDEX IF NOT EXISTS mcp_credentials_connection ON mcp_credentials(connection_id);
CREATE TABLE IF NOT EXISTS mcp_audit (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, actor_id text NOT NULL, action text NOT NULL,
connection_id uuid, payload jsonb NOT NULL DEFAULT '{}', created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS mcp_audit_time ON mcp_audit(created_at,id);
CREATE TABLE IF NOT EXISTS mcp_requests (
id uuid PRIMARY KEY, connection_id uuid REFERENCES mcp_connections(id) ON DELETE SET NULL,
actor_id text, tool text NOT NULL, server_ids bigint[] NOT NULL DEFAULT '{}',
status text NOT NULL, input jsonb NOT NULL DEFAULT '{}', result jsonb, error_code text,
duration_ms integer, created_at timestamptz NOT NULL DEFAULT now(), finished_at timestamptz
);
CREATE INDEX IF NOT EXISTS mcp_requests_connection_time ON mcp_requests(connection_id,created_at,id);
CREATE INDEX IF NOT EXISTS mcp_requests_server ON mcp_requests USING gin(server_ids);
CREATE TABLE IF NOT EXISTS mcp_protections (
id uuid PRIMARY KEY, object_ref jsonb NOT NULL, reason text NOT NULL DEFAULT '',
created_by text NOT NULL, created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS mcp_plans (
id uuid PRIMARY KEY, connection_id uuid NOT NULL REFERENCES mcp_connections(id), operation_id text NOT NULL,
operation_version integer NOT NULL, policy_version integer NOT NULL, generation text NOT NULL,
payload jsonb NOT NULL, evidence jsonb NOT NULL, state text NOT NULL, expires_at timestamptz NOT NULL,
approved_by text, approved_at timestamptz, created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS mcp_plans_connection ON mcp_plans(connection_id,created_at);
CREATE TABLE IF NOT EXISTS mcp_operations (
id uuid PRIMARY KEY, plan_id uuid NOT NULL UNIQUE REFERENCES mcp_plans(id),
connection_id uuid NOT NULL REFERENCES mcp_connections(id), idempotency_key text NOT NULL,
state text NOT NULL DEFAULT 'pending', verification text NOT NULL DEFAULT 'pending',
result jsonb, error_code text, created_at timestamptz NOT NULL DEFAULT now(), finished_at timestamptz,
UNIQUE(connection_id,idempotency_key)
);
CREATE TABLE IF NOT EXISTS mcp_operation_steps (
operation_id uuid NOT NULL REFERENCES mcp_operations(id), step_index integer NOT NULL,
server_id bigint, state text NOT NULL, before_state jsonb, intended_state jsonb, result jsonb,
created_at timestamptz NOT NULL DEFAULT now(), finished_at timestamptz,
PRIMARY KEY(operation_id,step_index)
);
CREATE TABLE IF NOT EXISTS mcp_budget_buckets (
connection_id uuid NOT NULL REFERENCES mcp_connections(id) ON DELETE CASCADE,
kind text NOT NULL, bucket timestamptz NOT NULL, used integer NOT NULL CHECK(used>=0),
PRIMARY KEY(connection_id,kind,bucket)
);
CREATE TABLE IF NOT EXISTS mcp_read_leases (
id uuid PRIMARY KEY, connection_id uuid NOT NULL REFERENCES mcp_connections(id) ON DELETE CASCADE,
expires_at timestamptz NOT NULL
);
CREATE TABLE IF NOT EXISTS mcp_grants (
id uuid PRIMARY KEY, connection_id uuid NOT NULL REFERENCES mcp_connections(id) ON DELETE CASCADE,
payload jsonb NOT NULL, revoked_at timestamptz
);
CREATE TABLE IF NOT EXISTS mcp_secrets (
id uuid PRIMARY KEY, purpose text NOT NULL, server_id bigint, ciphertext text NOT NULL,
created_by text NOT NULL, created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS mcp_artifacts (
id uuid PRIMARY KEY, connection_id uuid NOT NULL REFERENCES mcp_connections(id),
filename text NOT NULL, media_type text NOT NULL, ciphertext text NOT NULL,
expires_at timestamptz NOT NULL, created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS mcp_oauth_clients (
id text PRIMARY KEY, name text NOT NULL, redirect_uris jsonb NOT NULL, enabled boolean NOT NULL DEFAULT true,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS mcp_oauth_requests (
id uuid PRIMARY KEY, client_id text NOT NULL REFERENCES mcp_oauth_clients(id),
redirect_uri text NOT NULL, challenge text NOT NULL, state text, resource text NOT NULL,
scope text NOT NULL, expires_at timestamptz NOT NULL, consumed_at timestamptz,
connection_id uuid REFERENCES mcp_connections(id), code_digest text UNIQUE
);
CREATE TABLE IF NOT EXISTS mcp_routing_profiles (
id uuid PRIMARY KEY, connection_id uuid NOT NULL REFERENCES mcp_connections(id),
version integer NOT NULL DEFAULT 1, config jsonb NOT NULL, state jsonb NOT NULL DEFAULT '{}',
enabled boolean NOT NULL DEFAULT false, grant_expires_at timestamptz NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS mcp_drift (
id uuid PRIMARY KEY, operation_id uuid NOT NULL REFERENCES mcp_operations(id),
server_id bigint, expected jsonb NOT NULL, observed jsonb NOT NULL,
acknowledged_at timestamptz, detected_at timestamptz NOT NULL DEFAULT now()
);
COMMIT;
@@ -0,0 +1,12 @@
BEGIN;
CREATE TABLE IF NOT EXISTS mcp_routing_observations (
id uuid PRIMARY KEY, src_server_id bigint NOT NULL, dst_server_id bigint NOT NULL,
src_interface text NOT NULL, dst_interface text NOT NULL, family text NOT NULL CHECK(family IN ('ipv4','ipv6')),
routing_table text NOT NULL DEFAULT 'main', ping_ms double precision, loss_pct double precision,
dl_mbps double precision, ul_mbps double precision, ping_at timestamptz, bandwidth_at timestamptz,
context_verified boolean NOT NULL DEFAULT false, source text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(), CHECK(ping_ms IS NULL OR ping_ms>=0),
CHECK(loss_pct IS NULL OR loss_pct BETWEEN 0 AND 100), CHECK(dl_mbps IS NULL OR dl_mbps>=0), CHECK(ul_mbps IS NULL OR ul_mbps>=0)
);
CREATE INDEX IF NOT EXISTS mcp_routing_observations_leg ON mcp_routing_observations(src_server_id,dst_server_id,src_interface,family,routing_table,created_at DESC);
COMMIT;
+7
View File
@@ -0,0 +1,7 @@
BEGIN;
ALTER TABLE mcp_routing_profiles ADD COLUMN IF NOT EXISTS credential_id uuid REFERENCES mcp_credentials(id);
ALTER TABLE mcp_routing_profiles ADD COLUMN IF NOT EXISTS generation text;
ALTER TABLE mcp_routing_profiles ADD COLUMN IF NOT EXISTS policy_version integer;
CREATE INDEX IF NOT EXISTS mcp_routing_profiles_enabled ON mcp_routing_profiles(updated_at) WHERE enabled;
CREATE UNIQUE INDEX IF NOT EXISTS mcp_drift_open_step ON mcp_drift(operation_id,(expected->>'stepIndex')) WHERE acknowledged_at IS NULL;
COMMIT;
@@ -0,0 +1,3 @@
BEGIN;
ALTER TABLE mcp_connections ADD COLUMN IF NOT EXISTS owner_valid_until timestamptz NOT NULL DEFAULT now();
COMMIT;
+7
View File
@@ -0,0 +1,7 @@
BEGIN;
CREATE TABLE IF NOT EXISTS mcp_operation_checkpoints (
operation_id uuid NOT NULL REFERENCES mcp_operations(id), step_index integer NOT NULL,
before_sealed text NOT NULL, after_sealed text, created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY(operation_id,step_index)
);
COMMIT;
+8
View File
@@ -0,0 +1,8 @@
BEGIN;
CREATE TABLE IF NOT EXISTS mcp_management_channels (
id uuid PRIMARY KEY, server_id bigint NOT NULL UNIQUE REFERENCES servers(id) ON DELETE CASCADE,
config_sealed text NOT NULL, identity_hash text NOT NULL, attestation text NOT NULL,
created_by text NOT NULL, generation text NOT NULL, expires_at timestamptz NOT NULL,
verified_at timestamptz NOT NULL DEFAULT now(), revoked_at timestamptz
);
COMMIT;
+8
View File
@@ -0,0 +1,8 @@
BEGIN;
CREATE TABLE IF NOT EXISTS mcp_path_checks (
id uuid PRIMARY KEY, connection_id uuid NOT NULL REFERENCES mcp_connections(id),
profile_hash text NOT NULL, candidate_id text NOT NULL, status text NOT NULL CHECK(status IN ('passed','failed','unknown')),
evidence jsonb NOT NULL, created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS mcp_path_checks_latest ON mcp_path_checks(connection_id,profile_hash,candidate_id,created_at DESC);
COMMIT;
@@ -0,0 +1,7 @@
BEGIN;
CREATE TABLE IF NOT EXISTS mcp_recovery_intents (
operation_id uuid PRIMARY KEY REFERENCES mcp_operations(id), state text NOT NULL,
authority_profile_id uuid NOT NULL REFERENCES mcp_routing_profiles(id),
created_at timestamptz NOT NULL DEFAULT now(), finished_at timestamptz
);
COMMIT;
@@ -0,0 +1,6 @@
BEGIN;
CREATE TABLE IF NOT EXISTS mcp_unauthorized_buckets (
bucket timestamptz NOT NULL, client_hash text NOT NULL, reason text NOT NULL,
attempts integer NOT NULL DEFAULT 1, PRIMARY KEY(bucket,client_hash,reason)
);
COMMIT;
@@ -0,0 +1,4 @@
BEGIN;
ALTER TABLE user_interface_bindings DROP CONSTRAINT IF EXISTS user_interface_bindings_interface_type_check;
ALTER TABLE user_interface_bindings ADD CONSTRAINT user_interface_bindings_interface_type_check CHECK(interface_type IN ('ether','bridge','vlan','vxlan','gre','wg','ipsec','other'));
COMMIT;
@@ -0,0 +1,11 @@
BEGIN;
CREATE TABLE IF NOT EXISTS mcp_owner_revocations (
owner_id text PRIMARY KEY, invalid_before timestamptz NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS mcp_portal_events (
id uuid PRIMARY KEY, owner_id text NOT NULL, payload_hash text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS mcp_portal_events_time ON mcp_portal_events(created_at);
COMMIT;
@@ -0,0 +1,3 @@
BEGIN;
ALTER TABLE mcp_connections ADD COLUMN IF NOT EXISTS owner_claims_issued_at timestamptz NOT NULL DEFAULT to_timestamp(0);
COMMIT;
+8
View File
@@ -0,0 +1,8 @@
CREATE INDEX IF NOT EXISTS mcp_audit_handshake_cooldown
ON mcp_audit(connection_id, created_at DESC)
WHERE action = 'connection.handshake.start';
CREATE INDEX IF NOT EXISTS mcp_credentials_readiness_expired
ON mcp_credentials(connection_id, expires_at)
WHERE client_id = 'mcp-readiness-probe';
CREATE INDEX IF NOT EXISTS mcp_grants_credential
ON mcp_grants(connection_id, (payload->>'credentialId'));
@@ -0,0 +1,2 @@
ALTER TABLE mcp_artifacts ADD COLUMN IF NOT EXISTS access_scope jsonb;
CREATE INDEX IF NOT EXISTS mcp_artifacts_expiry ON mcp_artifacts(expires_at);
@@ -0,0 +1,23 @@
BEGIN;
ALTER TABLE servers ADD COLUMN IF NOT EXISTS routing_endpoint_epoch uuid NOT NULL DEFAULT gen_random_uuid();
CREATE OR REPLACE FUNCTION mm_routing_endpoint_epoch() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF ROW(NEW.host,NEW.port,NEW.username,NEW.password,NEW.use_ssl,NEW.verify_ssl,NEW.enabled)
IS DISTINCT FROM ROW(OLD.host,OLD.port,OLD.username,OLD.password,OLD.use_ssl,OLD.verify_ssl,OLD.enabled) THEN
NEW.routing_endpoint_epoch := gen_random_uuid();
ELSE
NEW.routing_endpoint_epoch := OLD.routing_endpoint_epoch;
END IF;
RETURN NEW;
END $$;
DROP TRIGGER IF EXISTS mm_routing_endpoint_epoch ON servers;
CREATE TRIGGER mm_routing_endpoint_epoch BEFORE UPDATE ON servers FOR EACH ROW EXECUTE FUNCTION mm_routing_endpoint_epoch();
ALTER TABLE mcp_routing_observations ADD COLUMN IF NOT EXISTS source_proof text;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname='mcp_routing_observation_source_proof' AND conrelid='mcp_routing_observations'::regclass) THEN
ALTER TABLE mcp_routing_observations ADD CONSTRAINT mcp_routing_observation_source_proof CHECK(source_proof IS NULL OR source_proof ~ '^[a-f0-9]{64}$');
END IF;
END $$;
-- Existing measurements stay readable but unsigned. Never attest old samples
-- using today's endpoint: that would invent capture-time provenance.
COMMIT;
@@ -0,0 +1,6 @@
BEGIN;
-- Service-owned exact read-back must survive a subsequent network probe failure.
-- Generic after_sealed remains an array of resource snapshots.
ALTER TABLE mcp_operation_checkpoints ADD COLUMN IF NOT EXISTS configuration_sealed text;
ALTER TABLE mcp_operation_checkpoints ADD COLUMN IF NOT EXISTS configuration_proof text;
COMMIT;
@@ -0,0 +1,24 @@
BEGIN;
CREATE TABLE IF NOT EXISTS mcp_scoped_recovery_intents (
source_operation_id uuid NOT NULL REFERENCES mcp_operations(id),
source_step_index integer NOT NULL CHECK(source_step_index>=0),
source_receipt_id uuid NOT NULL,
source_configuration_proof text NOT NULL,
recovery_receipt_id uuid NOT NULL UNIQUE,
recovery_operation_id uuid REFERENCES mcp_operations(id),
mode text NOT NULL CHECK(mode IN ('manual','reserved')),
authority_profile_id uuid REFERENCES mcp_routing_profiles(id),
authority_profile_version integer,
intent_sealed text NOT NULL,
intent_proof text NOT NULL,
state text NOT NULL CHECK(state IN ('intent','verified','unknown')),
deadline timestamptz NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
finished_at timestamptz,
verified_sealed text,
verified_proof text,
PRIMARY KEY(source_operation_id,source_step_index),
CHECK(mode='manual' OR authority_profile_id IS NOT NULL AND authority_profile_version IS NOT NULL),
CHECK((state='verified')=(verified_sealed IS NOT NULL AND verified_proof IS NOT NULL))
);
COMMIT;
@@ -0,0 +1,10 @@
-- Immutable commit evidence shared by ingestion and REST/MCP fact rebuild.
-- No raw flow payload, IP addresses, credentials or notification contents belong here.
CREATE TABLE IF NOT EXISTS flow_write_receipts (
batch_id uuid PRIMARY KEY,
kind text NOT NULL CHECK (kind IN ('ingest', 'facts', 'rebuild')),
source_proof text NOT NULL CHECK (source_proof ~ '^[a-f0-9]{64}$'),
result jsonb NOT NULL CHECK (jsonb_typeof(result) = 'object' AND octet_length(result::text) <= 65536),
committed_at timestamptz NOT NULL DEFAULT now()
);
-- Receipts are intentionally not pruned: an owned unknown batch may still need them.
@@ -0,0 +1,11 @@
-- A claim survives unknown results and is never recycled for another attempt.
CREATE TABLE IF NOT EXISTS flow_settings_intents (
receipt_id uuid NOT NULL,
phase text NOT NULL CHECK (phase IN ('configuration', 'listener')),
request_proof text NOT NULL,
state text NOT NULL DEFAULT 'claimed' CHECK (state IN ('claimed', 'completed')),
result jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (receipt_id, phase)
);
INSERT INTO traffic_flow_settings(id) VALUES(1) ON CONFLICT DO NOTHING;
@@ -0,0 +1,19 @@
-- Precondition receipts only. No executor capability is granted by this table.
CREATE TABLE IF NOT EXISTS vrf_production_tickets (
ticket_id uuid PRIMARY KEY,
plan_id uuid NOT NULL UNIQUE REFERENCES mcp_plans(id),
connection_id uuid NOT NULL REFERENCES mcp_connections(id),
server_id bigint NOT NULL REFERENCES servers(id),
owner_id text NOT NULL,
action_id text NOT NULL CHECK (action_id IN ('ros.vrf.create','ros.vrf.update','ros.vrf.delete')),
action_version integer NOT NULL CHECK (action_version > 0),
source_proof text NOT NULL CHECK (source_proof ~ '^[a-f0-9]{64}$'),
digest text NOT NULL CHECK (digest ~ '^[a-f0-9]{64}$'),
ciphertext text NOT NULL,
state text NOT NULL DEFAULT 'issued' CHECK (state IN ('issued','claimed')),
issued_at timestamptz NOT NULL,
expires_at timestamptz NOT NULL,
claimed_at timestamptz,
CHECK (expires_at > issued_at AND expires_at <= issued_at + interval '5 minutes'),
CHECK ((state='issued' AND claimed_at IS NULL) OR (state='claimed' AND claimed_at IS NOT NULL))
);
@@ -0,0 +1,67 @@
-- Durable diagnostic attempts; this migration registers no MCP action.
BEGIN;
CREATE TABLE server_probe_executions (
receipt_id uuid PRIMARY KEY, binding_proof text NOT NULL CHECK(binding_proof ~ '^[a-f0-9]{64}$'),
identity_kind text NOT NULL CHECK(identity_kind IN ('mcp','rest')),
execution_id uuid NOT NULL, actor_id text NOT NULL CHECK(length(actor_id) BETWEEN 1 AND 200),
authentication text NOT NULL CHECK(authentication IN ('mcp','jwt','local')),
mcp_connection_id uuid REFERENCES mcp_connections(id), mcp_credential_id uuid REFERENCES mcp_credentials(id), request_id text,
authority_issued_at bigint NOT NULL CHECK(authority_issued_at>=0), authority_expires_at timestamptz NOT NULL,
source_server_id integer NOT NULL CHECK(source_server_id>0), destination_server_id integer CHECK(destination_server_id>0),
tool text NOT NULL CHECK(tool IN ('ping','traceroute','bandwidth','dns','route','mtu')),
state text NOT NULL DEFAULT 'claimed' CHECK(state IN ('claimed','completed','failed','cancelled')),
attempted_stages integer CHECK(attempted_stages BETWEEN 0 AND 11), completed_stages integer CHECK(completed_stages BETWEEN 0 AND attempted_stages),
receipt_ciphertext text CHECK(octet_length(receipt_ciphertext)<=262144), receipt_proof text CHECK(receipt_proof ~ '^[a-f0-9]{64}$'),
created_at timestamptz NOT NULL DEFAULT clock_timestamp(), finished_at timestamptz,
UNIQUE(identity_kind,execution_id),
CHECK((identity_kind='mcp' AND authentication='mcp' AND mcp_connection_id IS NOT NULL AND mcp_credential_id IS NOT NULL AND request_id IS NULL)
OR (identity_kind='rest' AND authentication IN ('jwt','local') AND mcp_connection_id IS NULL AND mcp_credential_id IS NULL AND request_id IS NOT NULL AND length(request_id) BETWEEN 1 AND 200)),
CHECK((tool='bandwidth')=(destination_server_id IS NOT NULL)),
CHECK((state='claimed' AND receipt_ciphertext IS NULL AND receipt_proof IS NULL AND finished_at IS NULL AND attempted_stages IS NULL AND completed_stages IS NULL)
OR (state<>'claimed' AND receipt_ciphertext IS NOT NULL AND receipt_proof IS NOT NULL AND finished_at IS NOT NULL AND attempted_stages IS NOT NULL AND completed_stages IS NOT NULL))
);
CREATE TABLE server_probe_execution_stages (
receipt_id uuid NOT NULL REFERENCES server_probe_executions(receipt_id), stage_index integer NOT NULL CHECK(stage_index BETWEEN 1 AND 11),
kind text NOT NULL CHECK(kind IN ('source-dns-A','native-ping','native-traceroute','native-bandwidth','native-route-read',
'manager-dns-A','manager-dns-AAAA','manager-dns-MX','manager-dns-NS','manager-dns-TXT','manager-dns-CNAME','manager-dns-PTR',
'native-mtu-1500','native-mtu-1492','native-mtu-1480','native-mtu-1476','native-mtu-1472','native-mtu-1468','native-mtu-1400','native-mtu-1280','native-mtu-1024','native-mtu-576')),
timeout_ms integer NOT NULL CHECK(timeout_ms BETWEEN 1 AND 120000),
state text NOT NULL DEFAULT 'intent' CHECK(state IN ('intent','received')),
outcome text CHECK(outcome IN ('received','source-unavailable')),
created_at timestamptz NOT NULL DEFAULT clock_timestamp(), received_at timestamptz,
PRIMARY KEY(receipt_id,stage_index),
CHECK((state='intent' AND outcome IS NULL AND received_at IS NULL) OR (state='received' AND outcome IS NOT NULL AND received_at IS NOT NULL))
);
CREATE TABLE server_probe_execution_quarantines (
receipt_id uuid PRIMARY KEY, binding_proof text NOT NULL CHECK(binding_proof ~ '^[a-f0-9]{64}$'),
identity_kind text NOT NULL CHECK(identity_kind IN ('mcp','rest')), execution_id uuid NOT NULL,
identity_proof text NOT NULL CHECK(identity_proof ~ '^[a-f0-9]{64}$'),
error_code text NOT NULL CHECK(error_code ~ '^[A-Z_]{1,64}$'),
attempted_stages integer NOT NULL CHECK(attempted_stages BETWEEN 0 AND 11), completed_stages integer NOT NULL CHECK(completed_stages BETWEEN 0 AND attempted_stages),
created_at timestamptz NOT NULL DEFAULT clock_timestamp(), UNIQUE(identity_kind,execution_id)
);
CREATE FUNCTION server_probe_execution_immutable() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN
IF TG_OP='DELETE' THEN RAISE EXCEPTION 'probe execution journal is append only'; END IF;
IF OLD.state<>'claimed' OR (to_jsonb(OLD)-ARRAY['state','attempted_stages','completed_stages','receipt_ciphertext','receipt_proof','finished_at']) IS DISTINCT FROM
(to_jsonb(NEW)-ARRAY['state','attempted_stages','completed_stages','receipt_ciphertext','receipt_proof','finished_at']) OR NEW.state='claimed'
THEN RAISE EXCEPTION 'probe execution identity or terminal receipt is immutable'; END IF;
IF EXISTS(SELECT 1 FROM server_probe_execution_quarantines WHERE receipt_id=OLD.receipt_id) THEN RAISE EXCEPTION 'probe execution quarantined'; END IF;
RETURN NEW; END $$;
CREATE TRIGGER server_probe_execution_immutable BEFORE UPDATE OR DELETE ON server_probe_executions FOR EACH ROW EXECUTE FUNCTION server_probe_execution_immutable();
CREATE FUNCTION server_probe_stage_immutable() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN
IF TG_OP='DELETE' THEN RAISE EXCEPTION 'probe stage journal is append only'; END IF;
PERFORM receipt_id FROM server_probe_executions WHERE receipt_id=NEW.receipt_id AND state='claimed' FOR UPDATE;
IF NOT FOUND OR EXISTS(SELECT 1 FROM server_probe_execution_quarantines WHERE receipt_id=NEW.receipt_id) THEN RAISE EXCEPTION 'probe attempt is not executable'; END IF;
IF TG_OP='INSERT' AND (NEW.stage_index<>(SELECT COALESCE(max(stage_index),0)+1 FROM server_probe_execution_stages WHERE receipt_id=NEW.receipt_id)
OR EXISTS(SELECT 1 FROM server_probe_execution_stages WHERE receipt_id=NEW.receipt_id AND state<>'received'))
THEN RAISE EXCEPTION 'probe stages must be serial and contiguous'; END IF;
IF TG_OP='UPDATE' AND (OLD.state<>'intent' OR NEW.state<>'received' OR (to_jsonb(OLD)-ARRAY['state','outcome','received_at']) IS DISTINCT FROM (to_jsonb(NEW)-ARRAY['state','outcome','received_at']))
THEN RAISE EXCEPTION 'probe stage terminal state is immutable'; END IF;
RETURN NEW; END $$;
CREATE TRIGGER server_probe_stage_immutable BEFORE INSERT OR UPDATE OR DELETE ON server_probe_execution_stages FOR EACH ROW EXECUTE FUNCTION server_probe_stage_immutable();
CREATE FUNCTION server_probe_quarantine_immutable() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'probe quarantine is immutable'; END $$;
CREATE TRIGGER server_probe_quarantine_immutable BEFORE UPDATE OR DELETE ON server_probe_execution_quarantines FOR EACH ROW EXECUTE FUNCTION server_probe_quarantine_immutable();
INSERT INTO schema_migrations(id) VALUES ('0035_server_probe_journal') ON CONFLICT (id) DO NOTHING;
COMMIT;
@@ -0,0 +1,14 @@
-- Durable stop requests for opt-in diagnostics, not configuration rollback.
BEGIN;
CREATE TABLE mcp_diagnostic_cancellations (
operation_id uuid PRIMARY KEY REFERENCES mcp_operations(id),
connection_id uuid NOT NULL REFERENCES mcp_connections(id),
actor_id text NOT NULL CHECK (length(actor_id) BETWEEN 1 AND 200),
created_at timestamptz NOT NULL DEFAULT clock_timestamp()
);
CREATE FUNCTION mcp_diagnostic_cancellation_immutable() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN RAISE EXCEPTION 'diagnostic cancellation request is immutable'; END $$;
CREATE TRIGGER mcp_diagnostic_cancellation_immutable BEFORE UPDATE OR DELETE ON mcp_diagnostic_cancellations
FOR EACH ROW EXECUTE FUNCTION mcp_diagnostic_cancellation_immutable();
INSERT INTO schema_migrations(id) VALUES ('0036_diagnostic_cancellations') ON CONFLICT (id) DO NOTHING;
COMMIT;
@@ -0,0 +1,226 @@
-- Fresh-only enrollment of empty diagnostic tables. No historical backfill,
-- source-generation claim, public registration or automatic cleanup is granted.
BEGIN;
LOCK TABLE server_probe_executions,server_probe_execution_stages,server_probe_execution_quarantines,mcp_diagnostic_cancellations IN ACCESS EXCLUSIVE MODE;
DO $$ BEGIN
IF EXISTS(SELECT 1 FROM server_probe_executions)
OR EXISTS(SELECT 1 FROM server_probe_execution_stages)
OR EXISTS(SELECT 1 FROM server_probe_execution_quarantines)
OR EXISTS(SELECT 1 FROM mcp_diagnostic_cancellations)
THEN RAISE EXCEPTION 'nonempty legacy diagnostic history prevents fresh storage enrollment'; END IF;
IF EXISTS(SELECT 1 FROM pg_catalog.pg_class WHERE relname IN
('server_probe_storage_usage','server_probe_storage_admissions','server_probe_execution_payloads'))
THEN RAISE EXCEPTION 'unmarked diagnostic storage relation prevents fresh storage enrollment'; END IF;
IF NOT EXISTS(SELECT 1 FROM mcp_settings WHERE id=1 AND requests_retention_days BETWEEN 1 AND 365)
THEN RAISE EXCEPTION 'authoritative probe retention setting unavailable'; END IF;
END $$;
CREATE TABLE server_probe_storage_usage (
scope text PRIMARY KEY CHECK(scope='global' OR (scope LIKE 'owner:%' AND length(scope) BETWEEN 7 AND 206)),
admissions bigint NOT NULL DEFAULT 0 CHECK(admissions>=0),
reserved_files bigint NOT NULL DEFAULT 0 CHECK(reserved_files>=0 AND reserved_files<=admissions),
reserved_bytes bigint NOT NULL DEFAULT 0 CHECK(reserved_bytes>=0),
CHECK(reserved_bytes=admissions*32768+reserved_files*262144)
);
CREATE TABLE server_probe_storage_admissions (
receipt_id uuid PRIMARY KEY,
owner_id text NOT NULL CHECK(length(owner_id) BETWEEN 1 AND 200),
identity_kind text NOT NULL CHECK(identity_kind IN ('mcp','rest')), execution_id uuid NOT NULL,
binding_proof text NOT NULL CHECK(binding_proof ~ '^[a-f0-9]{64}$'),
authority_expires_at timestamptz NOT NULL,
claim_token uuid, claim_attempted_at timestamptz,
purged_at timestamptz, cancelled_at timestamptz, created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
retention_days integer NOT NULL CHECK(retention_days BETWEEN 1 AND 365),
safety_ms integer NOT NULL DEFAULT 60000 CHECK(safety_ms BETWEEN 60000 AND 86400000),
gc_not_before timestamptz NOT NULL DEFAULT 'infinity',
CHECK(gc_not_before>=authority_expires_at+make_interval(days=>retention_days)+safety_ms*interval '1 millisecond'),
UNIQUE(identity_kind,execution_id),
CHECK(claim_token IS NULL OR (claim_attempted_at IS NULL AND purged_at IS NULL AND cancelled_at IS NULL)),
CHECK(cancelled_at IS NULL OR (claim_token IS NULL AND claim_attempted_at IS NOT NULL))
);
CREATE INDEX server_probe_storage_admissions_owner_age ON server_probe_storage_admissions(owner_id,authority_expires_at,receipt_id);
CREATE TABLE server_probe_execution_payloads (
receipt_id uuid PRIMARY KEY REFERENCES server_probe_executions(receipt_id),
receipt_ciphertext text NOT NULL CHECK(octet_length(receipt_ciphertext) BETWEEN 1 AND 262144),
receipt_proof text NOT NULL CHECK(receipt_proof ~ '^[a-f0-9]{64}$')
);
INSERT INTO server_probe_storage_usage(scope,admissions,reserved_files,reserved_bytes) VALUES('global',0,0,0);
-- PostgreSQL drops table CHECKs dependent on this column together with it.
-- The locked empty-table guard precedes this column removal; no data is copied.
ALTER TABLE server_probe_executions DROP COLUMN receipt_ciphertext;
ALTER TABLE server_probe_executions ADD CONSTRAINT server_probe_storage_terminal_state CHECK(
(state='claimed' AND receipt_proof IS NULL AND finished_at IS NULL AND attempted_stages IS NULL AND completed_stages IS NULL)
OR(state<>'claimed' AND receipt_proof IS NOT NULL AND finished_at IS NOT NULL AND attempted_stages IS NOT NULL AND completed_stages IS NOT NULL));
-- Predicate only: it performs no DELETE. Every guarded exception below must
-- independently satisfy the persisted lifetime, actual current retention and
-- original terminal operation. The GUC alone never grants cleanup authority.
CREATE FUNCTION server_probe_storage_gc_allowed(id uuid) RETURNS boolean LANGUAGE plpgsql SET TimeZone='UTC' AS $$
DECLARE a server_probe_storage_admissions%ROWTYPE; activity timestamptz; current_retention integer; terminal_at timestamptz;
BEGIN
IF current_setting('mm.probe_storage_gc',true) IS DISTINCT FROM 'on' THEN RETURN false; END IF;
PERFORM pg_advisory_xact_lock(hashtextextended('server-probe-storage-v2',0));
SELECT * INTO a FROM server_probe_storage_admissions WHERE receipt_id=id FOR UPDATE;
IF NOT FOUND OR a.gc_not_before>=clock_timestamp() OR a.authority_expires_at>=clock_timestamp() THEN RETURN false; END IF;
SELECT settings.requests_retention_days INTO current_retention FROM mcp_settings settings WHERE settings.id=1;
IF current_retention IS NULL OR current_retention NOT BETWEEN 1 AND 365 THEN RETURN false; END IF;
terminal_at:=a.created_at;
IF a.identity_kind='mcp' THEN
SELECT GREATEST(o.finished_at,p.expires_at,COALESCE(cancellation.created_at,o.finished_at)) INTO terminal_at
FROM mcp_operations o JOIN mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id
JOIN mcp_connections owner_connection ON owner_connection.id=o.connection_id
LEFT JOIN mcp_diagnostic_cancellations cancellation ON cancellation.operation_id=o.id
WHERE o.id=a.execution_id AND owner_connection.owner_id=a.owner_id AND p.operation_id='servers.probes.run'
AND o.state IN ('applied','failed','unknown','rejected','partial','cancelled') AND o.finished_at IS NOT NULL
AND (cancellation.operation_id IS NULL OR(cancellation.connection_id=o.connection_id AND cancellation.actor_id=a.owner_id))
FOR UPDATE OF o,p,owner_connection;
IF NOT FOUND OR terminal_at IS NULL THEN RETURN false; END IF;
END IF;
SELECT GREATEST(a.created_at,a.authority_expires_at,terminal_at,COALESCE(a.cancelled_at,a.created_at),
COALESCE((SELECT finished_at FROM server_probe_executions WHERE receipt_id=id),a.created_at),
COALESCE((SELECT created_at FROM server_probe_execution_quarantines WHERE receipt_id=id),a.created_at),
COALESCE((SELECT max(GREATEST(created_at,COALESCE(received_at,created_at))) FROM server_probe_execution_stages WHERE receipt_id=id),a.created_at)) INTO activity;
RETURN activity+make_interval(days=>GREATEST(a.retention_days,current_retention))+a.safety_ms*interval '1 millisecond'<clock_timestamp();
END $$;
CREATE FUNCTION server_probe_storage_admission_guard() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE current_retention integer; activity timestamptz;
BEGIN
PERFORM pg_advisory_xact_lock(hashtextextended('server-probe-storage-v2',0));
IF TG_OP='DELETE' THEN
IF server_probe_storage_gc_allowed(OLD.receipt_id) THEN RETURN OLD; END IF;
RAISE EXCEPTION 'probe admission lifetime has not elapsed';
END IF;
SELECT requests_retention_days INTO current_retention FROM mcp_settings WHERE id=1;
IF current_retention IS NULL OR current_retention NOT BETWEEN 1 AND 365 THEN RAISE EXCEPTION 'probe retention unavailable'; END IF;
IF TG_OP='INSERT' THEN
IF NEW.purged_at IS NOT NULL OR (NEW.cancelled_at IS NULL AND
(NEW.authority_expires_at<=clock_timestamp() OR NEW.claim_token IS NULL OR NEW.claim_attempted_at IS NOT NULL))
OR (NEW.cancelled_at IS NOT NULL AND (NEW.claim_token IS NOT NULL OR NEW.claim_attempted_at IS NULL))
THEN RAISE EXCEPTION 'invalid fresh probe admission'; END IF;
IF NEW.cancelled_at IS NOT NULL THEN NEW.cancelled_at:=clock_timestamp(); NEW.claim_attempted_at:=NEW.cancelled_at; END IF;
NEW.retention_days:=current_retention; RETURN NEW;
END IF;
IF (to_jsonb(OLD)-ARRAY['claim_token','claim_attempted_at','purged_at','cancelled_at']) IS DISTINCT FROM
(to_jsonb(NEW)-ARRAY['claim_token','claim_attempted_at','purged_at','cancelled_at'])
THEN RAISE EXCEPTION 'immutable probe admission identity'; END IF;
IF OLD.cancelled_at IS DISTINCT FROM NEW.cancelled_at THEN
IF OLD.cancelled_at IS NULL AND NEW.cancelled_at IS NOT NULL AND NEW.claim_token IS NULL
AND NEW.purged_at IS NOT DISTINCT FROM OLD.purged_at AND NEW.claim_attempted_at IS NOT NULL
AND (OLD.claim_attempted_at IS NULL OR NEW.claim_attempted_at IS NOT DISTINCT FROM OLD.claim_attempted_at)
THEN NEW.cancelled_at:=clock_timestamp(); IF OLD.claim_attempted_at IS NULL THEN NEW.claim_attempted_at:=NEW.cancelled_at; END IF; RETURN NEW; END IF;
RAISE EXCEPTION 'probe cancellation is irreversible';
END IF;
IF OLD.purged_at IS NULL AND NEW.purged_at IS NULL AND OLD.claim_token IS NOT NULL AND NEW.claim_token IS NULL
AND OLD.cancelled_at IS NULL AND OLD.claim_attempted_at IS NULL AND NEW.claim_attempted_at IS NOT NULL AND OLD.authority_expires_at>clock_timestamp()
THEN RETURN NEW; END IF;
IF OLD.purged_at IS NULL AND NEW.purged_at IS NOT NULL AND NEW.claim_token IS NULL
AND NEW.claim_attempted_at IS NOT DISTINCT FROM OLD.claim_attempted_at THEN
SELECT GREATEST(OLD.authority_expires_at,COALESCE(e.finished_at,OLD.authority_expires_at),COALESCE(q.created_at,OLD.authority_expires_at)) INTO activity
FROM (SELECT 1) x LEFT JOIN server_probe_executions e ON e.receipt_id=OLD.receipt_id
LEFT JOIN server_probe_execution_quarantines q ON q.receipt_id=OLD.receipt_id;
IF activity<clock_timestamp()-make_interval(days=>GREATEST(OLD.retention_days,current_retention)) THEN RETURN NEW; END IF;
END IF;
RAISE EXCEPTION 'probe admission transition refused';
END $$;
CREATE TRIGGER server_probe_storage_admission_guard BEFORE INSERT OR UPDATE OR DELETE ON server_probe_storage_admissions
FOR EACH ROW EXECUTE FUNCTION server_probe_storage_admission_guard();
CREATE FUNCTION server_probe_storage_anchor_guard() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE a server_probe_storage_admissions%ROWTYPE;
BEGIN
PERFORM pg_advisory_xact_lock(hashtextextended('server-probe-storage-v2',0));
SELECT * INTO a FROM server_probe_storage_admissions WHERE receipt_id=NEW.receipt_id FOR UPDATE;
IF NOT FOUND OR a.owner_id<>NEW.actor_id OR a.identity_kind<>NEW.identity_kind OR a.execution_id<>NEW.execution_id
OR a.binding_proof<>NEW.binding_proof OR a.authority_expires_at<>NEW.authority_expires_at
OR a.purged_at IS NOT NULL OR a.cancelled_at IS NOT NULL OR a.claim_token IS NOT NULL OR a.claim_attempted_at IS NULL OR a.authority_expires_at<=clock_timestamp()
THEN RAISE EXCEPTION 'probe anchor has no live consumed admission'; END IF;
RETURN NEW;
END $$;
CREATE TRIGGER server_probe_storage_anchor_guard BEFORE INSERT OR UPDATE ON server_probe_executions
FOR EACH ROW EXECUTE FUNCTION server_probe_storage_anchor_guard();
CREATE OR REPLACE FUNCTION server_probe_execution_immutable() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN
IF TG_OP='DELETE' THEN
IF server_probe_storage_gc_allowed(OLD.receipt_id) THEN RETURN OLD; END IF;
RAISE EXCEPTION 'probe execution lifetime has not elapsed';
END IF;
IF OLD.state<>'claimed' OR (to_jsonb(OLD)-ARRAY['state','attempted_stages','completed_stages','receipt_proof','finished_at']) IS DISTINCT FROM
(to_jsonb(NEW)-ARRAY['state','attempted_stages','completed_stages','receipt_proof','finished_at']) OR NEW.state='claimed'
THEN RAISE EXCEPTION 'probe execution identity or terminal receipt is immutable'; END IF;
IF EXISTS(SELECT 1 FROM server_probe_execution_quarantines WHERE receipt_id=OLD.receipt_id)
THEN RAISE EXCEPTION 'probe execution quarantined'; END IF;
RETURN NEW; END $$;
CREATE FUNCTION server_probe_storage_stage_guard() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN
PERFORM pg_advisory_xact_lock(hashtextextended('server-probe-storage-v2',0));
PERFORM receipt_id FROM server_probe_storage_admissions WHERE receipt_id=NEW.receipt_id AND purged_at IS NULL AND cancelled_at IS NULL
AND claim_token IS NULL AND claim_attempted_at IS NOT NULL AND authority_expires_at>clock_timestamp() FOR UPDATE;
IF NOT FOUND THEN RAISE EXCEPTION 'probe stage has no live admission'; END IF;
RETURN NEW; END $$;
CREATE TRIGGER server_probe_storage_stage_guard BEFORE INSERT OR UPDATE ON server_probe_execution_stages
FOR EACH ROW EXECUTE FUNCTION server_probe_storage_stage_guard();
CREATE FUNCTION server_probe_storage_quarantine_guard() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN
PERFORM pg_advisory_xact_lock(hashtextextended('server-probe-storage-v2',0));
PERFORM receipt_id FROM server_probe_storage_admissions WHERE receipt_id=NEW.receipt_id AND identity_kind=NEW.identity_kind
AND execution_id=NEW.execution_id AND binding_proof=NEW.binding_proof FOR UPDATE;
IF NOT FOUND THEN RAISE EXCEPTION 'probe quarantine has no committed matching admission'; END IF;
RETURN NEW; END $$;
CREATE TRIGGER server_probe_storage_quarantine_guard BEFORE INSERT ON server_probe_execution_quarantines
FOR EACH ROW EXECUTE FUNCTION server_probe_storage_quarantine_guard();
CREATE FUNCTION server_probe_storage_payload_guard() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE a server_probe_storage_admissions%ROWTYPE; e server_probe_executions%ROWTYPE;
BEGIN
PERFORM pg_advisory_xact_lock(hashtextextended('server-probe-storage-v2',0));
IF TG_OP='UPDATE' THEN RAISE EXCEPTION 'probe payload is immutable'; END IF;
IF TG_OP='DELETE' THEN
PERFORM receipt_id FROM server_probe_storage_admissions WHERE receipt_id=OLD.receipt_id AND purged_at IS NOT NULL FOR UPDATE;
IF NOT FOUND AND NOT server_probe_storage_gc_allowed(OLD.receipt_id) THEN RAISE EXCEPTION 'probe payload is not purged'; END IF;
RETURN OLD;
END IF;
SELECT * INTO a FROM server_probe_storage_admissions WHERE receipt_id=NEW.receipt_id FOR UPDATE;
IF NOT FOUND OR a.purged_at IS NOT NULL OR a.cancelled_at IS NOT NULL OR a.claim_token IS NOT NULL OR a.claim_attempted_at IS NULL OR a.authority_expires_at<=clock_timestamp()
THEN RAISE EXCEPTION 'probe payload has no live admission'; END IF;
SELECT * INTO e FROM server_probe_executions WHERE receipt_id=NEW.receipt_id FOR UPDATE;
IF NOT FOUND OR e.state='claimed' OR e.receipt_proof<>NEW.receipt_proof OR e.actor_id<>a.owner_id OR e.binding_proof<>a.binding_proof
OR e.identity_kind<>a.identity_kind OR e.execution_id<>a.execution_id OR EXISTS(SELECT 1 FROM server_probe_execution_quarantines q
WHERE q.receipt_id=NEW.receipt_id OR(q.identity_kind=a.identity_kind AND q.execution_id=a.execution_id))
THEN RAISE EXCEPTION 'probe payload terminal identity mismatch or quarantine'; END IF;
RETURN NEW;
END $$;
CREATE TRIGGER server_probe_storage_payload_guard BEFORE INSERT OR UPDATE OR DELETE ON server_probe_execution_payloads
FOR EACH ROW EXECUTE FUNCTION server_probe_storage_payload_guard();
-- Preserve every original INSERT/UPDATE rule; only the already-validated
-- lifetime predicate can admit DELETE; no cleanup is scheduled by migration.
CREATE OR REPLACE FUNCTION server_probe_stage_immutable() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN
IF TG_OP='DELETE' THEN
IF server_probe_storage_gc_allowed(OLD.receipt_id) THEN RETURN OLD; END IF;
RAISE EXCEPTION 'probe stage lifetime has not elapsed';
END IF;
PERFORM receipt_id FROM server_probe_executions WHERE receipt_id=NEW.receipt_id AND state='claimed' FOR UPDATE;
IF NOT FOUND OR EXISTS(SELECT 1 FROM server_probe_execution_quarantines WHERE receipt_id=NEW.receipt_id) THEN RAISE EXCEPTION 'probe attempt is not executable'; END IF;
IF TG_OP='INSERT' AND (NEW.stage_index<>(SELECT COALESCE(max(stage_index),0)+1 FROM server_probe_execution_stages WHERE receipt_id=NEW.receipt_id)
OR EXISTS(SELECT 1 FROM server_probe_execution_stages WHERE receipt_id=NEW.receipt_id AND state<>'received'))
THEN RAISE EXCEPTION 'probe stages must be serial and contiguous'; END IF;
IF TG_OP='UPDATE' AND (OLD.state<>'intent' OR NEW.state<>'received' OR (to_jsonb(OLD)-ARRAY['state','outcome','received_at']) IS DISTINCT FROM (to_jsonb(NEW)-ARRAY['state','outcome','received_at']))
THEN RAISE EXCEPTION 'probe stage terminal state is immutable'; END IF;
RETURN NEW; END $$;
CREATE OR REPLACE FUNCTION server_probe_quarantine_immutable() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN
IF TG_OP='DELETE' AND server_probe_storage_gc_allowed(OLD.receipt_id) THEN RETURN OLD; END IF;
RAISE EXCEPTION 'probe quarantine remains immutable'; END $$;
CREATE OR REPLACE FUNCTION mcp_diagnostic_cancellation_immutable() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE original_receipt uuid;
BEGIN
IF TG_OP='DELETE' THEN
SELECT a.receipt_id INTO original_receipt FROM server_probe_storage_admissions a
JOIN mcp_operations o ON o.id=a.execution_id AND o.connection_id=OLD.connection_id
WHERE a.identity_kind='mcp' AND a.execution_id=OLD.operation_id AND a.owner_id=OLD.actor_id;
IF FOUND AND server_probe_storage_gc_allowed(original_receipt) THEN RETURN OLD; END IF;
END IF;
RAISE EXCEPTION 'diagnostic cancellation lifetime has not elapsed';
END $$;
INSERT INTO schema_migrations(id) VALUES ('0037_server_probe_storage_v2') ON CONFLICT (id) DO NOTHING;
COMMIT;
@@ -0,0 +1,59 @@
-- Initial poll journal and adopted partition-writer backstop.
-- Mixed old replicas MUST be drained before activation: triggers alone run
-- after relation access and cannot replace pre-DML shared writer admission.
BEGIN;
DO $$ BEGIN
PERFORM pg_catalog.pg_advisory_xact_lock(8723110::bigint);
IF EXISTS(SELECT 1 FROM pg_catalog.pg_class WHERE relname=ANY(ARRAY['server_poll_executions','server_poll_intents','server_poll_acks','server_poll_receipts','server_poll_quarantines']))
OR EXISTS(SELECT 1 FROM pg_catalog.pg_proc WHERE proname=ANY(ARRAY['server_poll_journal_immutable','server_partition_writer_guard']))
OR EXISTS(SELECT 1 FROM pg_catalog.pg_trigger WHERE tgname='server_partition_writer_guard')
THEN RAISE EXCEPTION 'unmarked poll journal or partition guard requires explicit reconciliation'; END IF;
END $$;
CREATE TABLE server_poll_executions (
receipt_id uuid PRIMARY KEY,identity_kind text NOT NULL CHECK(identity_kind IN ('mcp','rest')),execution_id uuid NOT NULL,
binding_proof text NOT NULL CHECK(binding_proof ~ '^[a-f0-9]{64}$'),context_ciphertext text NOT NULL CHECK(octet_length(context_ciphertext)<=2097152),
context_proof text NOT NULL CHECK(context_proof ~ '^[a-f0-9]{64}$'),created_at timestamptz NOT NULL DEFAULT clock_timestamp(),UNIQUE(identity_kind,execution_id));
CREATE TABLE server_poll_intents (
receipt_id uuid REFERENCES server_poll_executions(receipt_id),stage text NOT NULL CHECK(stage ~ '^(native\.(identity|resource|interfaces|addresses)|publish\.[0-9]{1,4})$'),
before_proof text NOT NULL CHECK(before_proof ~ '^[a-f0-9]{64}$'),effect_proof text NOT NULL CHECK(effect_proof ~ '^[a-f0-9]{64}$'),
intent_proof text NOT NULL CHECK(intent_proof ~ '^[a-f0-9]{64}$'),created_at timestamptz NOT NULL DEFAULT clock_timestamp(),PRIMARY KEY(receipt_id,stage));
CREATE TABLE server_poll_acks (
receipt_id uuid NOT NULL,stage text NOT NULL,result_proof text NOT NULL CHECK(result_proof ~ '^[a-f0-9]{64}$'),outcome text NOT NULL CHECK(outcome IN ('confirmed','read-failed')),
ack_proof text NOT NULL CHECK(ack_proof ~ '^[a-f0-9]{64}$'),created_at timestamptz NOT NULL DEFAULT clock_timestamp(),PRIMARY KEY(receipt_id,stage),FOREIGN KEY(receipt_id,stage) REFERENCES server_poll_intents(receipt_id,stage),CHECK(stage LIKE 'native.%' OR outcome='confirmed'));
CREATE TABLE server_poll_receipts (
receipt_id uuid PRIMARY KEY REFERENCES server_poll_executions(receipt_id),receipt_ciphertext text NOT NULL CHECK(octet_length(receipt_ciphertext)<=262144),receipt_proof text NOT NULL CHECK(receipt_proof ~ '^[a-f0-9]{64}$'),created_at timestamptz NOT NULL DEFAULT clock_timestamp());
CREATE TABLE server_poll_quarantines (
receipt_id uuid PRIMARY KEY,identity_kind text NOT NULL CHECK(identity_kind IN ('mcp','rest')),execution_id uuid NOT NULL,context_proof text NOT NULL CHECK(context_proof ~ '^[a-f0-9]{64}$'),created_at timestamptz NOT NULL DEFAULT clock_timestamp(),UNIQUE(identity_kind,execution_id));
CREATE FUNCTION server_poll_journal_immutable() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'server poll journal is append only'; END $$;
CREATE TRIGGER server_poll_execution_immutable BEFORE UPDATE OR DELETE ON server_poll_executions FOR EACH ROW EXECUTE FUNCTION server_poll_journal_immutable();
CREATE TRIGGER server_poll_intent_immutable BEFORE UPDATE OR DELETE ON server_poll_intents FOR EACH ROW EXECUTE FUNCTION server_poll_journal_immutable();
CREATE TRIGGER server_poll_ack_immutable BEFORE UPDATE OR DELETE ON server_poll_acks FOR EACH ROW EXECUTE FUNCTION server_poll_journal_immutable();
CREATE TRIGGER server_poll_receipt_immutable BEFORE UPDATE OR DELETE ON server_poll_receipts FOR EACH ROW EXECUTE FUNCTION server_poll_journal_immutable();
CREATE TRIGGER server_poll_quarantine_immutable BEFORE UPDATE OR DELETE ON server_poll_quarantines FOR EACH ROW EXECUTE FUNCTION server_poll_journal_immutable();
CREATE FUNCTION public.server_partition_writer_guard() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN PERFORM pg_advisory_xact_lock_shared(8723110::bigint); IF TG_OP='DELETE' THEN RETURN OLD; END IF; RETURN NEW; END; $$;
CREATE TRIGGER server_partition_writer_guard BEFORE INSERT OR UPDATE OR DELETE ON public."flow_buckets" FOR EACH ROW EXECUTE FUNCTION public.server_partition_writer_guard();
ALTER TABLE public."flow_buckets" ENABLE ALWAYS TRIGGER server_partition_writer_guard;
CREATE TRIGGER server_partition_writer_guard BEFORE INSERT OR UPDATE OR DELETE ON public."flow_minute_stats" FOR EACH ROW EXECUTE FUNCTION public.server_partition_writer_guard();
ALTER TABLE public."flow_minute_stats" ENABLE ALWAYS TRIGGER server_partition_writer_guard;
CREATE TRIGGER server_partition_writer_guard BEFORE INSERT OR UPDATE OR DELETE ON public."flow_minute_dims" FOR EACH ROW EXECUTE FUNCTION public.server_partition_writer_guard();
ALTER TABLE public."flow_minute_dims" ENABLE ALWAYS TRIGGER server_partition_writer_guard;
CREATE TRIGGER server_partition_writer_guard BEFORE INSERT OR UPDATE OR DELETE ON public."flow_daily_dims" FOR EACH ROW EXECUTE FUNCTION public.server_partition_writer_guard();
ALTER TABLE public."flow_daily_dims" ENABLE ALWAYS TRIGGER server_partition_writer_guard;
CREATE TRIGGER server_partition_writer_guard BEFORE INSERT OR UPDATE OR DELETE ON public."flow_hour_facts" FOR EACH ROW EXECUTE FUNCTION public.server_partition_writer_guard();
ALTER TABLE public."flow_hour_facts" ENABLE ALWAYS TRIGGER server_partition_writer_guard;
CREATE TRIGGER server_partition_writer_guard BEFORE INSERT OR UPDATE OR DELETE ON public."flow_daily_facts" FOR EACH ROW EXECUTE FUNCTION public.server_partition_writer_guard();
ALTER TABLE public."flow_daily_facts" ENABLE ALWAYS TRIGGER server_partition_writer_guard;
CREATE TRIGGER server_partition_writer_guard BEFORE INSERT OR UPDATE OR DELETE ON public."traffic_samples" FOR EACH ROW EXECUTE FUNCTION public.server_partition_writer_guard();
ALTER TABLE public."traffic_samples" ENABLE ALWAYS TRIGGER server_partition_writer_guard;
CREATE TRIGGER server_partition_writer_guard BEFORE INSERT OR UPDATE OR DELETE ON public."servers_rest_ping_samples" FOR EACH ROW EXECUTE FUNCTION public.server_partition_writer_guard();
ALTER TABLE public."servers_rest_ping_samples" ENABLE ALWAYS TRIGGER server_partition_writer_guard;
CREATE TRIGGER server_partition_writer_guard BEFORE INSERT OR UPDATE OR DELETE ON public."uptime_probe_samples" FOR EACH ROW EXECUTE FUNCTION public.server_partition_writer_guard();
ALTER TABLE public."uptime_probe_samples" ENABLE ALWAYS TRIGGER server_partition_writer_guard;
CREATE TRIGGER server_partition_writer_guard BEFORE INSERT OR UPDATE OR DELETE ON public."uptime_resource_samples" FOR EACH ROW EXECUTE FUNCTION public.server_partition_writer_guard();
ALTER TABLE public."uptime_resource_samples" ENABLE ALWAYS TRIGGER server_partition_writer_guard;
CREATE TRIGGER server_partition_writer_guard BEFORE INSERT OR UPDATE OR DELETE ON public."server_snapshots" FOR EACH ROW EXECUTE FUNCTION public.server_partition_writer_guard();
ALTER TABLE public."server_snapshots" ENABLE ALWAYS TRIGGER server_partition_writer_guard;
CREATE TRIGGER server_partition_writer_guard BEFORE INSERT OR UPDATE OR DELETE ON public."internet_path_snapshots" FOR EACH ROW EXECUTE FUNCTION public.server_partition_writer_guard();
ALTER TABLE public."internet_path_snapshots" ENABLE ALWAYS TRIGGER server_partition_writer_guard;
INSERT INTO schema_migrations(id) VALUES ('0038_server_poll_journal_and_writer_guards') ON CONFLICT(id) DO NOTHING;
COMMIT;
@@ -0,0 +1,175 @@
-- Fresh-only enrollment of the reviewed bounded poll journal. No historical backfill.
-- Fixed logical quota; physical heap/index/WAL and vacuum remain independent.
-- Requires the empty 0038 poll journal. No scheduling or public activation.
BEGIN;
LOCK TABLE server_poll_executions,server_poll_intents,server_poll_acks,server_poll_receipts,server_poll_quarantines IN ACCESS EXCLUSIVE MODE;
DO $$ BEGIN
IF EXISTS(SELECT 1 FROM server_poll_executions) OR EXISTS(SELECT 1 FROM server_poll_intents)
OR EXISTS(SELECT 1 FROM server_poll_acks) OR EXISTS(SELECT 1 FROM server_poll_receipts)
OR EXISTS(SELECT 1 FROM server_poll_quarantines)
THEN RAISE EXCEPTION 'nonempty poll journal needs explicit reconciliation'; END IF;
IF NOT EXISTS(SELECT 1 FROM mcp_settings WHERE id=1 AND requests_retention_days BETWEEN 1 AND 365)
THEN RAISE EXCEPTION 'poll retention unavailable'; END IF;
IF EXISTS(SELECT 1 FROM pg_catalog.pg_class WHERE relname IN('server_poll_storage_usage','server_poll_storage_admissions'))
OR EXISTS(SELECT 1 FROM pg_catalog.pg_proc WHERE proname IN('server_poll_storage_gc_allowed','server_poll_storage_admission_guard','server_poll_storage_row_guard','server_poll_storage_row_meter'))
OR EXISTS(SELECT 1 FROM pg_catalog.pg_trigger WHERE tgname LIKE 'server_poll_storage_%')
THEN RAISE EXCEPTION 'unmarked poll storage requires explicit reconciliation'; END IF;
END $$;
CREATE TABLE server_poll_storage_usage (
scope text PRIMARY KEY CHECK(scope='global' OR(scope LIKE 'owner:%' AND length(scope) BETWEEN 7 AND 206)),
admissions bigint NOT NULL DEFAULT 0 CHECK(admissions>=0 AND admissions<=128),
reserved_rows bigint NOT NULL DEFAULT 0 CHECK(reserved_rows=admissions*4019),
reserved_bytes bigint NOT NULL DEFAULT 0 CHECK(reserved_bytes=admissions*8421376),
CHECK(scope='global' OR admissions<=8)
);
CREATE TABLE server_poll_storage_admissions (
receipt_id uuid PRIMARY KEY, identity_kind text NOT NULL CHECK(identity_kind IN('mcp','rest')),
execution_id uuid NOT NULL, connection_id uuid, owner_id text NOT NULL CHECK(length(owner_id) BETWEEN 1 AND 200 AND btrim(owner_id)=owner_id AND owner_id !~ '[[:cntrl:]]'),
binding_proof text NOT NULL CHECK(binding_proof ~ '^[a-f0-9]{64}$'), context_proof text NOT NULL CHECK(context_proof ~ '^[a-f0-9]{64}$'),
authority_issued_at timestamptz NOT NULL CHECK(isfinite(authority_issued_at)),
authority_expires_at timestamptz NOT NULL CHECK(isfinite(authority_expires_at) AND authority_expires_at>authority_issued_at),
max_stages integer NOT NULL CHECK(max_stages BETWEEN 8 AND 2008),
claim_token uuid, claim_attempted_at timestamptz, created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
retention_days integer NOT NULL CHECK(retention_days BETWEEN 1 AND 365),
safety_ms integer NOT NULL DEFAULT 60000 CHECK(safety_ms=60000),
gc_not_before timestamptz NOT NULL CHECK(isfinite(gc_not_before)),
used_rows integer NOT NULL DEFAULT 0 CHECK(used_rows BETWEEN 0 AND 4019 AND used_rows<=max_stages*2+3),
used_bytes bigint NOT NULL DEFAULT 0 CHECK(used_bytes BETWEEN 0 AND 8388608),
UNIQUE(identity_kind,execution_id),
CHECK((identity_kind='mcp' AND connection_id IS NOT NULL) OR(identity_kind='rest' AND connection_id IS NULL)),
CHECK((claim_token IS NOT NULL AND claim_attempted_at IS NULL) OR(claim_token IS NULL AND claim_attempted_at IS NOT NULL)),
CHECK(gc_not_before>=authority_expires_at+make_interval(days=>retention_days)+interval '60 seconds')
);
CREATE INDEX server_poll_storage_expiry ON server_poll_storage_admissions(gc_not_before,receipt_id);
INSERT INTO server_poll_storage_usage(scope) VALUES('global');
-- Every guard is installed in this same empty-enrollment transaction;
-- no historical admission or journal identity is inferred or imported.
CREATE FUNCTION server_poll_storage_gc_allowed(id uuid) RETURNS boolean LANGUAGE plpgsql SET TimeZone='UTC' AS $$
DECLARE a server_poll_storage_admissions%ROWTYPE; retention integer; activity timestamptz; terminal_at timestamptz;
BEGIN
IF current_setting('mm.poll_storage_gc',true) IS DISTINCT FROM 'on' THEN RETURN false; END IF;
PERFORM pg_advisory_xact_lock(hashtextextended('server-poll-storage-v1',0));
SELECT * INTO a FROM server_poll_storage_admissions WHERE receipt_id=id FOR UPDATE;
IF NOT FOUND OR a.authority_expires_at>=clock_timestamp() OR a.gc_not_before>=clock_timestamp() THEN RETURN false; END IF;
SELECT requests_retention_days INTO retention FROM mcp_settings WHERE mcp_settings.id=1 FOR SHARE;
IF retention IS NULL OR retention NOT BETWEEN 1 AND 365 THEN RETURN false; END IF;
terminal_at:=a.created_at;
IF a.identity_kind='mcp' THEN
SELECT GREATEST(o.finished_at,p.expires_at) INTO terminal_at FROM mcp_operations o
JOIN mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id
JOIN mcp_connections c ON c.id=o.connection_id
WHERE o.id=a.execution_id AND o.connection_id=a.connection_id AND c.owner_id=a.owner_id AND p.operation_id='servers.poll'
AND o.state IN('applied','failed','unknown','rejected','partial','cancelled') AND o.finished_at IS NOT NULL
FOR UPDATE OF o,p,c;
IF NOT FOUND OR terminal_at IS NULL THEN RETURN false; END IF;
END IF;
SELECT GREATEST(a.authority_expires_at,a.created_at,COALESCE(a.claim_attempted_at,a.created_at),terminal_at,
COALESCE((SELECT created_at FROM server_poll_executions WHERE receipt_id=id),a.created_at),
COALESCE((SELECT max(created_at) FROM server_poll_intents WHERE receipt_id=id),a.created_at),
COALESCE((SELECT max(created_at) FROM server_poll_acks WHERE receipt_id=id),a.created_at),
COALESCE((SELECT created_at FROM server_poll_receipts WHERE receipt_id=id),a.created_at),
COALESCE((SELECT created_at FROM server_poll_quarantines WHERE receipt_id=id),a.created_at)) INTO activity;
RETURN activity+make_interval(days=>GREATEST(a.retention_days,retention))+a.safety_ms*interval '1 millisecond'<clock_timestamp();
END $$;
CREATE FUNCTION server_poll_storage_admission_guard() RETURNS trigger LANGUAGE plpgsql SET TimeZone='UTC' AS $$
DECLARE retention integer; charged integer;
BEGIN
PERFORM pg_advisory_xact_lock(hashtextextended('server-poll-storage-v1',0));
IF TG_OP='DELETE' THEN
IF NOT server_poll_storage_gc_allowed(OLD.receipt_id) OR EXISTS(SELECT 1 FROM server_poll_executions WHERE receipt_id=OLD.receipt_id)
OR EXISTS(SELECT 1 FROM server_poll_intents WHERE receipt_id=OLD.receipt_id) OR EXISTS(SELECT 1 FROM server_poll_acks WHERE receipt_id=OLD.receipt_id)
OR EXISTS(SELECT 1 FROM server_poll_receipts WHERE receipt_id=OLD.receipt_id) OR EXISTS(SELECT 1 FROM server_poll_quarantines WHERE receipt_id=OLD.receipt_id)
THEN RAISE EXCEPTION 'poll lifetime or children prevent admission deletion'; END IF;
UPDATE server_poll_storage_usage SET admissions=admissions-1,reserved_rows=reserved_rows-4019,reserved_bytes=reserved_bytes-8421376
WHERE scope IN('global','owner:'||OLD.owner_id) AND admissions>=1 AND reserved_rows>=4019 AND reserved_bytes>=8421376;
GET DIAGNOSTICS charged=ROW_COUNT;
IF charged<>2 THEN RAISE EXCEPTION 'poll quota refund mismatch'; END IF;
RETURN OLD;
END IF;
IF TG_OP='UPDATE' THEN
IF(to_jsonb(OLD)-ARRAY['claim_token','claim_attempted_at','used_rows','used_bytes']) IS DISTINCT FROM
(to_jsonb(NEW)-ARRAY['claim_token','claim_attempted_at','used_rows','used_bytes']) THEN RAISE EXCEPTION 'poll admission identity is immutable'; END IF;
IF OLD.claim_token IS NOT NULL AND NEW.claim_token IS NULL AND OLD.claim_attempted_at IS NULL AND NEW.claim_attempted_at IS NOT NULL
AND NEW.used_rows=OLD.used_rows AND NEW.used_bytes=OLD.used_bytes AND OLD.authority_expires_at>clock_timestamp()
THEN NEW.claim_attempted_at:=clock_timestamp(); RETURN NEW; END IF;
IF OLD.claim_token IS NOT DISTINCT FROM NEW.claim_token AND OLD.claim_attempted_at IS NOT DISTINCT FROM NEW.claim_attempted_at
AND NEW.used_rows=OLD.used_rows+1 AND NEW.used_bytes>OLD.used_bytes THEN RETURN NEW; END IF;
RAISE EXCEPTION 'poll admission cannot refresh, reopen or refund itself';
END IF;
SELECT requests_retention_days INTO retention FROM mcp_settings WHERE id=1;
IF retention IS NULL OR retention NOT BETWEEN 1 AND 365 OR NEW.authority_issued_at>clock_timestamp()
OR NEW.authority_expires_at<=clock_timestamp() OR NEW.claim_token IS NULL OR NEW.claim_attempted_at IS NOT NULL
OR NEW.used_rows<>0 OR NEW.used_bytes<>0
THEN RAISE EXCEPTION 'new poll admission needs original live authority'; END IF;
IF EXISTS(SELECT 1 FROM server_poll_storage_admissions WHERE receipt_id=NEW.receipt_id OR(identity_kind=NEW.identity_kind AND execution_id=NEW.execution_id))
THEN RAISE EXCEPTION 'poll identity already spent'; END IF;
NEW.created_at:=clock_timestamp(); NEW.retention_days:=retention; NEW.safety_ms:=60000;
NEW.gc_not_before:=NEW.authority_expires_at+make_interval(days=>retention)+interval '60 seconds';
INSERT INTO server_poll_storage_usage(scope) VALUES('owner:'||NEW.owner_id) ON CONFLICT DO NOTHING;
IF(SELECT count(*) FROM server_poll_storage_usage WHERE scope IN('global','owner:'||NEW.owner_id)
AND admissions<CASE WHEN scope='global' THEN 128 ELSE 8 END)<>2 THEN RAISE EXCEPTION 'poll admission quota exhausted'; END IF;
UPDATE server_poll_storage_usage SET admissions=admissions+1,reserved_rows=reserved_rows+4019,reserved_bytes=reserved_bytes+8421376
WHERE scope IN('global','owner:'||NEW.owner_id);
GET DIAGNOSTICS charged=ROW_COUNT; IF charged<>2 THEN RAISE EXCEPTION 'poll admission quota unavailable'; END IF;
RETURN NEW;
END $$;
CREATE TRIGGER server_poll_storage_admission_guard BEFORE INSERT OR UPDATE OR DELETE ON server_poll_storage_admissions
FOR EACH ROW EXECUTE FUNCTION server_poll_storage_admission_guard();
CREATE FUNCTION server_poll_storage_row_guard() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE a server_poll_storage_admissions%ROWTYPE; n integer;
BEGIN
PERFORM pg_advisory_xact_lock(hashtextextended('server-poll-storage-v1',0));
SELECT * INTO a FROM server_poll_storage_admissions WHERE receipt_id=NEW.receipt_id FOR UPDATE;
IF NOT FOUND THEN RAISE EXCEPTION 'poll journal needs a committed admission'; END IF;
IF TG_TABLE_NAME='server_poll_quarantines' THEN
IF NEW.identity_kind<>a.identity_kind OR NEW.execution_id<>a.execution_id OR NEW.context_proof<>a.context_proof
THEN RAISE EXCEPTION 'poll quarantine admission mismatch'; END IF;
ELSE
IF a.claim_token IS NOT NULL OR a.claim_attempted_at IS NULL OR a.authority_expires_at<=clock_timestamp()
THEN RAISE EXCEPTION 'poll admission is not consumed and live'; END IF;
END IF;
IF TG_TABLE_NAME='server_poll_executions' THEN
IF NEW.identity_kind<>a.identity_kind OR NEW.execution_id<>a.execution_id OR NEW.context_proof<>a.context_proof OR NEW.binding_proof<>a.binding_proof
THEN RAISE EXCEPTION 'poll anchor admission mismatch'; END IF;
ELSIF TG_TABLE_NAME IN('server_poll_intents','server_poll_acks') THEN
IF NEW.stage LIKE 'publish.%' AND substring(NEW.stage FROM 9)::integer>=a.max_stages-4 THEN RAISE EXCEPTION 'poll stage exceeds private reservation'; END IF;
IF TG_TABLE_NAME='server_poll_intents' THEN SELECT count(*) INTO n FROM server_poll_intents WHERE receipt_id=NEW.receipt_id;
ELSE SELECT count(*) INTO n FROM server_poll_acks WHERE receipt_id=NEW.receipt_id; END IF;
IF n>=a.max_stages THEN RAISE EXCEPTION 'poll stage row budget exhausted'; END IF;
END IF;
IF TG_TABLE_NAME<>'server_poll_quarantines' AND EXISTS(SELECT 1 FROM server_poll_quarantines WHERE receipt_id=NEW.receipt_id)
THEN RAISE EXCEPTION 'poll identity quarantined'; END IF;
NEW.created_at:=clock_timestamp();
RETURN NEW;
END $$;
-- Meter actual inserts, not BEFORE attempts: ON CONFLICT DO NOTHING must not
-- spend another row/byte allowance for a marker which was never inserted.
CREATE FUNCTION server_poll_storage_row_meter() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE cost bigint; n integer;
BEGIN
PERFORM pg_advisory_xact_lock(hashtextextended('server-poll-storage-v1',0));
cost:=pg_column_size(NEW);
UPDATE server_poll_storage_admissions SET used_rows=used_rows+1,used_bytes=used_bytes+cost
WHERE receipt_id=NEW.receipt_id AND used_rows<max_stages*2+3 AND used_bytes<=8388608-cost;
GET DIAGNOSTICS n=ROW_COUNT; IF n<>1 THEN RAISE EXCEPTION 'poll journal byte or row budget exhausted'; END IF;
RETURN NEW;
END $$;
CREATE TRIGGER server_poll_storage_anchor_guard BEFORE INSERT ON server_poll_executions FOR EACH ROW EXECUTE FUNCTION server_poll_storage_row_guard();
CREATE TRIGGER server_poll_storage_intent_guard BEFORE INSERT ON server_poll_intents FOR EACH ROW EXECUTE FUNCTION server_poll_storage_row_guard();
CREATE TRIGGER server_poll_storage_ack_guard BEFORE INSERT ON server_poll_acks FOR EACH ROW EXECUTE FUNCTION server_poll_storage_row_guard();
CREATE TRIGGER server_poll_storage_receipt_guard BEFORE INSERT ON server_poll_receipts FOR EACH ROW EXECUTE FUNCTION server_poll_storage_row_guard();
CREATE TRIGGER server_poll_storage_quarantine_guard BEFORE INSERT ON server_poll_quarantines FOR EACH ROW EXECUTE FUNCTION server_poll_storage_row_guard();
CREATE TRIGGER server_poll_storage_anchor_meter AFTER INSERT ON server_poll_executions FOR EACH ROW EXECUTE FUNCTION server_poll_storage_row_meter();
CREATE TRIGGER server_poll_storage_intent_meter AFTER INSERT ON server_poll_intents FOR EACH ROW EXECUTE FUNCTION server_poll_storage_row_meter();
CREATE TRIGGER server_poll_storage_ack_meter AFTER INSERT ON server_poll_acks FOR EACH ROW EXECUTE FUNCTION server_poll_storage_row_meter();
CREATE TRIGGER server_poll_storage_receipt_meter AFTER INSERT ON server_poll_receipts FOR EACH ROW EXECUTE FUNCTION server_poll_storage_row_meter();
CREATE TRIGGER server_poll_storage_quarantine_meter AFTER INSERT ON server_poll_quarantines FOR EACH ROW EXECUTE FUNCTION server_poll_storage_row_meter();
CREATE OR REPLACE FUNCTION server_poll_journal_immutable() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN
IF TG_OP='DELETE' AND server_poll_storage_gc_allowed(OLD.receipt_id) THEN RETURN OLD; END IF;
RAISE EXCEPTION 'server poll journal is immutable before its full lifetime elapses';
END $$;
INSERT INTO schema_migrations(id) VALUES ('0039_server_poll_storage') ON CONFLICT(id) DO NOTHING;
COMMIT;
@@ -0,0 +1,56 @@
-- Additive, inactive reserved recovery ledger. Existing manual evidence is retained.
-- Legacy reserved rows cannot prove an original two-write debit: never backfill them.
BEGIN;
LOCK TABLE public.mcp_scoped_recovery_intents IN ACCESS EXCLUSIVE MODE;
DO $$ BEGIN
IF EXISTS(SELECT 1 FROM public.mcp_scoped_recovery_intents WHERE mode='reserved')
THEN RAISE EXCEPTION 'legacy reserved recovery needs explicit reconciliation'; END IF;
IF EXISTS(SELECT 1 FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid=c.relnamespace
WHERE n.nspname='public' AND c.relname IN('mcp_routing_reservations','mcp_routing_reservation_slots','mcp_routing_reservation_slots_window'))
OR EXISTS(SELECT 1 FROM pg_catalog.pg_attribute a WHERE a.attrelid='public.mcp_scoped_recovery_intents'::regclass
AND NOT a.attisdropped AND a.attname IN('reservation_id','slot_phase','inverse_intent_sealed','inverse_intent_proof','inverse_configuration_sealed','inverse_configuration_proof'))
OR EXISTS(SELECT 1 FROM pg_catalog.pg_constraint c WHERE c.conrelid='public.mcp_scoped_recovery_intents'::regclass
AND c.conname IN('scoped_reservation_slot_shape','reserved_inverse_evidence_pairs'))
THEN RAISE EXCEPTION 'unmarked routing reservation objects need explicit reconciliation'; END IF;
END $$;
CREATE TABLE public.mcp_routing_reservations (
id uuid PRIMARY KEY,
source_plan_id uuid NOT NULL REFERENCES public.mcp_plans(id),
source_operation_id uuid NOT NULL REFERENCES public.mcp_operations(id),
source_step_index integer NOT NULL CHECK(source_step_index=0),
profile_id uuid NOT NULL REFERENCES public.mcp_routing_profiles(id),
connection_id uuid NOT NULL REFERENCES public.mcp_connections(id),
binding_sealed text NOT NULL,binding_proof text NOT NULL,
state_sealed text NOT NULL,state_proof text NOT NULL,revision integer NOT NULL CHECK(revision>=1),
deadline timestamptz NOT NULL,created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE(source_operation_id,source_step_index)
);
CREATE TABLE public.mcp_routing_reservation_slots (
reservation_id uuid NOT NULL REFERENCES public.mcp_routing_reservations(id),
profile_id uuid NOT NULL REFERENCES public.mcp_routing_profiles(id),
lane text NOT NULL CHECK(lane IN ('forward','inverse')),
state text NOT NULL CHECK(state IN ('held','spent','proven-unused','unknown')),
created_at timestamptz NOT NULL DEFAULT now(),PRIMARY KEY(reservation_id,lane)
);
CREATE INDEX mcp_routing_reservation_slots_window ON public.mcp_routing_reservation_slots(profile_id,created_at);
ALTER TABLE public.mcp_scoped_recovery_intents ADD COLUMN reservation_id uuid REFERENCES public.mcp_routing_reservations(id);
ALTER TABLE public.mcp_scoped_recovery_intents ADD COLUMN slot_phase text NOT NULL DEFAULT 'recovery-intent';
ALTER TABLE public.mcp_scoped_recovery_intents ALTER COLUMN source_configuration_proof DROP NOT NULL;
ALTER TABLE public.mcp_scoped_recovery_intents ADD COLUMN inverse_intent_sealed text;
ALTER TABLE public.mcp_scoped_recovery_intents ADD COLUMN inverse_intent_proof text;
ALTER TABLE public.mcp_scoped_recovery_intents ADD COLUMN inverse_configuration_sealed text;
ALTER TABLE public.mcp_scoped_recovery_intents ADD COLUMN inverse_configuration_proof text;
ALTER TABLE public.mcp_scoped_recovery_intents ADD CONSTRAINT scoped_reservation_slot_shape CHECK (
(mode='manual' AND reservation_id IS NULL AND slot_phase='recovery-intent' AND source_configuration_proof IS NOT NULL)
OR (mode='reserved' AND reservation_id IS NOT NULL AND
((slot_phase='reservation-held' AND source_configuration_proof IS NULL)
OR (slot_phase IN ('reservation-bound','reserved-attempt') AND source_configuration_proof IS NOT NULL)))
);
ALTER TABLE public.mcp_scoped_recovery_intents ADD CONSTRAINT reserved_inverse_evidence_pairs CHECK (
(inverse_intent_sealed IS NULL)=(inverse_intent_proof IS NULL)
AND (inverse_configuration_sealed IS NULL)=(inverse_configuration_proof IS NULL)
AND (inverse_configuration_sealed IS NULL OR inverse_intent_sealed IS NOT NULL)
AND (inverse_intent_sealed IS NULL OR mode='reserved')
);
INSERT INTO public.schema_migrations(id) VALUES ('0040_mcp_routing_reservations') ON CONFLICT(id) DO NOTHING;
COMMIT;
@@ -0,0 +1,333 @@
-- Additive terminal poll archives. Existing 0040 full journals are retained.
-- No compaction, retention run, source backfill or worker activation occurs here.
BEGIN;
DO $$ BEGIN
PERFORM pg_advisory_xact_lock(hashtextextended('server-poll-storage-v1',0));
IF EXISTS(SELECT 1 FROM pg_class WHERE relname='server_poll_archives')
OR EXISTS(SELECT 1 FROM pg_attribute WHERE attrelid='public.server_poll_storage_admissions'::regclass AND NOT attisdropped AND attname IN('storage_state','compact_at','archive_bytes','original_activity_at','compaction_claim','compaction_claimed_at','compaction_claim_xid'))
OR EXISTS(SELECT 1 FROM pg_attribute WHERE attrelid='public.server_poll_storage_usage'::regclass AND NOT attisdropped AND attname IN('compacted_admissions','archive_bytes'))
OR EXISTS(SELECT 1 FROM pg_proc WHERE proname IN('server_poll_storage_source_payload','server_poll_storage_compact_allowed','server_poll_storage_accounting_valid','server_poll_archive_insert_guard','server_poll_archive_insert_finish','server_poll_archive_immutable','server_poll_archive_final_guard','server_poll_compacted_row_guard'))
OR EXISTS(SELECT 1 FROM pg_trigger WHERE tgname IN('server_poll_archive_insert_guard','server_poll_archive_insert_finish','server_poll_archive_immutable','server_poll_archive_final_guard','server_poll_admission_final_guard','server_poll_compact_final_guard','server_poll_compacted_anchor_guard','server_poll_compacted_intent_guard','server_poll_compacted_ack_guard','server_poll_compacted_receipt_guard','server_poll_compacted_quarantine_guard'))
THEN RAISE EXCEPTION 'unmarked poll archive objects require explicit reconciliation'; END IF;
END $$;
LOCK TABLE server_poll_storage_usage,server_poll_storage_admissions,server_poll_executions,server_poll_intents,server_poll_acks,server_poll_receipts,server_poll_quarantines IN ACCESS EXCLUSIVE MODE;
DO $$ BEGIN
IF (SELECT count(*) FROM server_poll_storage_usage WHERE scope='global')<>1
OR EXISTS(SELECT 1 FROM server_poll_storage_usage u WHERE
u.admissions<>(SELECT count(*) FROM server_poll_storage_admissions a WHERE u.scope='global' OR u.scope='owner:'||a.owner_id)
OR u.reserved_rows<>u.admissions*4019 OR u.reserved_bytes<>u.admissions*8421376)
OR EXISTS(SELECT 1 FROM server_poll_storage_admissions a WHERE NOT EXISTS(SELECT 1 FROM server_poll_storage_usage u WHERE u.scope='owner:'||a.owner_id))
THEN RAISE EXCEPTION 'existing poll accounting requires explicit reconciliation'; END IF;
IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conrelid='public.server_poll_storage_usage'::regclass AND conname='server_poll_storage_usage_check'
AND pg_get_constraintdef(oid)='CHECK ((reserved_rows = (admissions * 4019)))')
OR NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conrelid='public.server_poll_storage_usage'::regclass AND conname='server_poll_storage_usage_check1'
AND pg_get_constraintdef(oid)='CHECK ((reserved_bytes = (admissions * 8421376)))')
THEN RAISE EXCEPTION 'original poll accounting constraints require explicit reconciliation'; END IF;
END $$;
ALTER TABLE server_poll_storage_admissions
ADD COLUMN storage_state text NOT NULL DEFAULT 'full' CHECK(storage_state IN('full','compacted')),
ADD COLUMN compact_at timestamptz,
ADD COLUMN archive_bytes integer,
ADD COLUMN original_activity_at timestamptz,
ADD COLUMN compaction_claim uuid,
ADD COLUMN compaction_claimed_at timestamptz,
ADD COLUMN compaction_claim_xid xid8,
ADD CONSTRAINT server_poll_compaction_claim_shape CHECK(
(compaction_claim IS NULL AND compaction_claimed_at IS NULL AND compaction_claim_xid IS NULL)
OR(compaction_claim IS NOT NULL AND compaction_claimed_at IS NOT NULL AND compaction_claim_xid IS NOT NULL AND isfinite(compaction_claimed_at))),
ADD CONSTRAINT server_poll_storage_compact_shape CHECK(
(storage_state='full' AND compact_at IS NULL AND archive_bytes IS NULL AND original_activity_at IS NULL)
OR(storage_state='compacted' AND identity_kind='mcp' AND claim_token IS NULL AND claim_attempted_at IS NOT NULL
AND compaction_claim IS NOT NULL AND compaction_claimed_at IS NOT NULL AND compaction_claim_xid IS NOT NULL
AND compact_at IS NOT NULL AND archive_bytes IS NOT NULL AND original_activity_at IS NOT NULL
AND isfinite(compact_at) AND archive_bytes BETWEEN 1 AND 131072 AND isfinite(original_activity_at)
AND original_activity_at>=created_at AND compact_at>authority_expires_at+safety_ms*interval '1 millisecond'));
ALTER TABLE server_poll_storage_usage
DROP CONSTRAINT server_poll_storage_usage_check,
DROP CONSTRAINT server_poll_storage_usage_check1,
ADD COLUMN compacted_admissions bigint NOT NULL DEFAULT 0 CHECK(compacted_admissions>=0),
ADD COLUMN archive_bytes bigint NOT NULL DEFAULT 0 CHECK(archive_bytes>=0),
ADD CONSTRAINT server_poll_storage_rows_charge CHECK(reserved_rows=admissions*4019+compacted_admissions*2
AND reserved_rows<=CASE WHEN scope='global' THEN 514432 ELSE 32152 END),
ADD CONSTRAINT server_poll_storage_bytes_charge CHECK(reserved_bytes=admissions*8421376+compacted_admissions*32768+archive_bytes
AND reserved_bytes<=CASE WHEN scope='global' THEN 1077936128 ELSE 67371008 END),
ADD CONSTRAINT server_poll_storage_archive_charge CHECK(archive_bytes>=compacted_admissions AND archive_bytes<=compacted_admissions*131072);
CREATE TABLE server_poll_archives (
receipt_id uuid PRIMARY KEY REFERENCES server_poll_storage_admissions(receipt_id),version smallint NOT NULL CHECK(version=1),
execution_id uuid NOT NULL UNIQUE,connection_id uuid NOT NULL,owner_id text NOT NULL,
binding_proof text NOT NULL CHECK(binding_proof ~ '^[a-f0-9]{64}$'),context_proof text NOT NULL CHECK(context_proof ~ '^[a-f0-9]{64}$'),
source_digest text NOT NULL CHECK(source_digest ~ '^[a-f0-9]{64}$'),source_sha256 text NOT NULL CHECK(source_sha256 ~ '^[a-f0-9]{64}$'),
terminal_digest text NOT NULL CHECK(terminal_digest ~ '^[a-f0-9]{64}$'),
original_activity_at timestamptz NOT NULL CHECK(isfinite(original_activity_at)),original_expires_at timestamptz NOT NULL CHECK(isfinite(original_expires_at)),
decoded_bytes integer NOT NULL CHECK(decoded_bytes BETWEEN 1 AND 16777216),compressed_bytes integer NOT NULL CHECK(compressed_bytes BETWEEN 1 AND 98048),
ciphertext text NOT NULL,ciphertext_bytes integer NOT NULL CHECK(ciphertext_bytes BETWEEN 1 AND 131072 AND ciphertext_bytes=octet_length(ciphertext)),
archive_proof text NOT NULL CHECK(archive_proof ~ '^[a-f0-9]{64}$'),
created_at timestamptz NOT NULL,compaction_xid xid8 NOT NULL
);
-- SQL checks structure/ownership/current rows. The fixed private producer and
-- historical reader additionally authenticate every existing keyed proof and
-- the archive MAC; SQL never receives an encryption/HMAC key or boolean grant.
CREATE FUNCTION server_poll_storage_compact_allowed(id uuid) RETURNS boolean LANGUAGE plpgsql SET TimeZone='UTC' SET search_path=pg_catalog,public AS $$
DECLARE a public.server_poll_storage_admissions%ROWTYPE; o public.mcp_operations%ROWTYPE; p public.mcp_plans%ROWTYPE; s public.mcp_operation_steps%ROWTYPE; n integer; pubs integer; activity timestamptz;
BEGIN
PERFORM pg_advisory_xact_lock(hashtextextended('server-poll-storage-v1',0));
SELECT * INTO a FROM public.server_poll_storage_admissions WHERE receipt_id=id FOR UPDATE;
IF NOT FOUND OR a.storage_state<>'full' OR a.identity_kind<>'mcp' OR a.claim_token IS NOT NULL OR a.claim_attempted_at IS NULL
OR a.authority_expires_at+a.safety_ms*interval '1 millisecond'>=clock_timestamp()
OR EXISTS(SELECT 1 FROM public.server_poll_quarantines WHERE receipt_id=id OR(identity_kind=a.identity_kind AND execution_id=a.execution_id))
THEN RETURN false; END IF;
IF NOT pg_try_advisory_xact_lock(hashtextextended('mcp-operation:'||a.execution_id::text,0)) THEN RETURN false; END IF;
IF NOT pg_try_advisory_xact_lock(hashtextextended('server-poll-execution-v1:mcp:'||a.execution_id::text,0)) THEN RETURN false; END IF;
IF NOT pg_try_advisory_xact_lock(hashtextextended('server-poll-receipt-v1:'||a.receipt_id::text,0)) THEN RETURN false; END IF;
BEGIN
SELECT * INTO o FROM public.mcp_operations WHERE mcp_operations.id=a.execution_id AND connection_id=a.connection_id FOR UPDATE NOWAIT;
IF NOT FOUND OR o.state<>'applied' OR o.verification<>'passed' OR o.error_code IS NOT NULL OR o.finished_at IS NULL THEN RETURN false; END IF;
SELECT * INTO p FROM public.mcp_plans WHERE mcp_plans.id=o.plan_id AND connection_id=a.connection_id FOR UPDATE NOWAIT;
IF NOT FOUND OR p.operation_id<>'servers.poll' OR p.operation_version<>1 OR p.state<>'completed' OR p.approved_by IS NULL OR p.approved_at IS NULL
OR p.created_at>p.approved_at OR p.approved_at>o.created_at OR o.created_at>=LEAST(p.expires_at,a.authority_expires_at) THEN RETURN false; END IF;
SELECT * INTO s FROM public.mcp_operation_steps WHERE operation_id=o.id AND step_index=0 FOR UPDATE NOWAIT;
IF NOT FOUND OR s.state<>'verified' OR s.server_id IS NOT NULL OR s.result IS NULL OR s.finished_at IS NULL
OR(SELECT count(*) FROM public.mcp_operation_steps WHERE operation_id=o.id)<>1
OR NOT EXISTS(SELECT 1 FROM public.mcp_connections c WHERE c.id=a.connection_id AND c.owner_id=a.owner_id)
OR s.intended_state->>'kind' IS DISTINCT FROM 'service' OR s.intended_state->>'service' IS DISTINCT FROM 'servers.poll'
OR s.intended_state#>>'{input,receiptId}' IS DISTINCT FROM id::text
OR s.result->>'operationId' IS DISTINCT FROM o.id::text OR s.result->>'receiptId' IS DISTINCT FROM id::text
OR s.result->'nativeReads' IS DISTINCT FROM '4'::jsonb
OR s.result->'complete' IS DISTINCT FROM 'true'::jsonb OR s.result->'maintenanceComplete' IS DISTINCT FROM 'true'::jsonb
OR s.result->'configurationVerified' IS DISTINCT FROM 'true'::jsonb OR s.result->'automaticReplay' IS DISTINCT FROM 'false'::jsonb
THEN RETURN false; END IF;
EXCEPTION WHEN lock_not_available THEN RETURN false; END;
IF NOT EXISTS(SELECT 1 FROM public.server_poll_executions e WHERE e.receipt_id=id AND e.identity_kind=a.identity_kind AND e.execution_id=a.execution_id AND e.binding_proof=a.binding_proof AND e.context_proof=a.context_proof)
OR NOT EXISTS(SELECT 1 FROM public.server_poll_receipts r WHERE r.receipt_id=id AND r.created_at<a.authority_expires_at) THEN RETURN false; END IF;
SELECT count(*) INTO n FROM public.server_poll_intents WHERE receipt_id=id;
SELECT count(*) INTO pubs FROM public.server_poll_intents WHERE receipt_id=id AND stage LIKE 'publish.%';
IF n<>pubs+4 OR pubs NOT IN(a.max_stages-6,a.max_stages-4) OR a.used_rows<>2*n+2
OR(SELECT count(*) FROM public.server_poll_acks WHERE receipt_id=id)<>n
OR s.result->'confirmedPublications' IS DISTINCT FROM to_jsonb(pubs)
OR s.result->'completedPublications' IS DISTINCT FROM to_jsonb(pubs)
OR EXISTS(SELECT 1 FROM public.server_poll_acks WHERE receipt_id=id AND outcome<>'confirmed')
OR EXISTS(SELECT 1 FROM public.server_poll_acks WHERE receipt_id=id AND created_at>=a.authority_expires_at)
OR EXISTS(SELECT 1 FROM unnest(ARRAY['native.identity','native.resource','native.interfaces','native.addresses']) wanted(stage)
WHERE NOT EXISTS(SELECT 1 FROM public.server_poll_intents i JOIN public.server_poll_acks k USING(receipt_id,stage) WHERE i.receipt_id=id AND i.stage=wanted.stage AND k.outcome='confirmed'))
OR EXISTS(SELECT 1 FROM generate_series(0,pubs-1) wanted(stage) WHERE NOT EXISTS(SELECT 1 FROM public.server_poll_intents i JOIN public.server_poll_acks k USING(receipt_id,stage) WHERE i.receipt_id=id AND i.stage='publish.'||wanted.stage::text AND k.outcome='confirmed'))
THEN RETURN false; END IF;
SELECT GREATEST(a.authority_expires_at,a.created_at,a.claim_attempted_at,o.finished_at,p.expires_at,s.finished_at,
(SELECT created_at FROM public.server_poll_executions WHERE receipt_id=id),(SELECT max(created_at) FROM public.server_poll_intents WHERE receipt_id=id),
(SELECT max(created_at) FROM public.server_poll_acks WHERE receipt_id=id),(SELECT created_at FROM public.server_poll_receipts WHERE receipt_id=id)) INTO activity;
RETURN isfinite(activity) AND activity+a.safety_ms*interval '1 millisecond'<clock_timestamp();
END $$;
CREATE FUNCTION server_poll_storage_source_payload(receipt_uuid uuid) RETURNS jsonb LANGUAGE plpgsql SET TimeZone='UTC' SET search_path=pg_catalog,public AS $$
DECLARE payload jsonb;
BEGIN
IF NOT public.server_poll_storage_compact_allowed(receipt_uuid) THEN RETURN NULL; END IF;
SELECT jsonb_build_object('admission',to_jsonb(a),'journal',jsonb_build_object(
'execution',(SELECT to_jsonb(e) FROM public.server_poll_executions e WHERE e.receipt_id=receipt_uuid),
'intents',(SELECT COALESCE(jsonb_agg(to_jsonb(i) ORDER BY i.stage),'[]'::jsonb) FROM public.server_poll_intents i WHERE i.receipt_id=receipt_uuid),
'acks',(SELECT COALESCE(jsonb_agg(to_jsonb(k) ORDER BY k.stage),'[]'::jsonb) FROM public.server_poll_acks k WHERE k.receipt_id=receipt_uuid),
'receipt',(SELECT to_jsonb(r) FROM public.server_poll_receipts r WHERE r.receipt_id=receipt_uuid),
'quarantines',(SELECT COALESCE(jsonb_agg(to_jsonb(q) ORDER BY q.created_at,q.receipt_id),'[]'::jsonb) FROM public.server_poll_quarantines q WHERE q.receipt_id=receipt_uuid)),
'terminal',jsonb_build_object('operation',to_jsonb(o),'plan',to_jsonb(p),'step',to_jsonb(s))) INTO payload
FROM public.server_poll_storage_admissions a JOIN public.mcp_operations o ON o.id=a.execution_id AND o.connection_id=a.connection_id
JOIN public.mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id JOIN public.mcp_operation_steps s ON s.operation_id=o.id AND s.step_index=0
WHERE a.receipt_id=receipt_uuid;
IF octet_length(payload::text)>16777216 THEN RETURN NULL; END IF;
RETURN payload;
END $$;
CREATE FUNCTION server_poll_storage_accounting_valid() RETURNS boolean LANGUAGE sql SET search_path=pg_catalog,public AS $$
WITH charges AS (
SELECT owner_id,CASE WHEN storage_state='full' THEN 1 ELSE 0 END::bigint AS heavy,
CASE WHEN storage_state='compacted' THEN 1 ELSE 0 END::bigint AS compact,
COALESCE(archive_bytes,0)::bigint AS archives,
CASE WHEN storage_state='full' THEN 4019 ELSE 2 END::bigint AS rows,
CASE WHEN storage_state='full' THEN 8421376 ELSE 32768+archive_bytes END::bigint AS bytes
FROM public.server_poll_storage_admissions
), expected AS (
SELECT 'global' AS scope,COALESCE(sum(heavy),0) AS heavy,COALESCE(sum(compact),0) AS compact,COALESCE(sum(archives),0) AS archives,COALESCE(sum(rows),0) AS rows,COALESCE(sum(bytes),0) AS bytes FROM charges
UNION ALL SELECT 'owner:'||owner_id,sum(heavy),sum(compact),sum(archives),sum(rows),sum(bytes) FROM charges GROUP BY owner_id
) SELECT NOT EXISTS(SELECT 1 FROM public.server_poll_storage_usage u FULL JOIN expected e USING(scope)
WHERE u.scope IS NULL OR u.admissions<>COALESCE(e.heavy,0) OR u.compacted_admissions<>COALESCE(e.compact,0)
OR u.archive_bytes<>COALESCE(e.archives,0) OR u.reserved_rows<>COALESCE(e.rows,0) OR u.reserved_bytes<>COALESCE(e.bytes,0))
AND NOT EXISTS(SELECT 1 FROM public.server_poll_storage_admissions a LEFT JOIN public.server_poll_archives r USING(receipt_id)
WHERE (a.storage_state='full' AND r.receipt_id IS NOT NULL) OR(a.storage_state='compacted' AND(r.receipt_id IS NULL OR a.archive_bytes<>r.ciphertext_bytes)))
$$;
CREATE FUNCTION server_poll_archive_insert_guard() RETURNS trigger LANGUAGE plpgsql SET TimeZone='UTC' SET search_path=pg_catalog,public AS $$
DECLARE a public.server_poll_storage_admissions%ROWTYPE; payload jsonb; activity timestamptz;
BEGIN
payload:=public.server_poll_storage_source_payload(NEW.receipt_id);
IF payload IS NULL OR NOT public.server_poll_storage_accounting_valid() THEN RAISE EXCEPTION 'poll source is not eligible for terminal archive'; END IF;
SELECT * INTO a FROM public.server_poll_storage_admissions WHERE receipt_id=NEW.receipt_id FOR UPDATE;
SELECT GREATEST(a.authority_expires_at,a.created_at,a.claim_attempted_at,(payload#>>'{terminal,operation,finished_at}')::timestamptz,
(payload#>>'{terminal,plan,expires_at}')::timestamptz,(payload#>>'{terminal,step,finished_at}')::timestamptz,
(payload#>>'{journal,execution,created_at}')::timestamptz,(payload#>>'{journal,receipt,created_at}')::timestamptz,
(SELECT max((v->>'created_at')::timestamptz) FROM jsonb_array_elements(payload#>'{journal,intents}') v),
(SELECT max((v->>'created_at')::timestamptz) FROM jsonb_array_elements(payload#>'{journal,acks}') v)) INTO activity;
IF a.compaction_claim IS NULL OR a.compaction_claimed_at IS NULL OR NOT isfinite(a.compaction_claimed_at)
OR a.compaction_claim_xid IS NULL OR a.compaction_claim_xid=pg_current_xact_id()
OR NEW.version<>1 OR NEW.execution_id<>a.execution_id OR NEW.connection_id<>a.connection_id OR NEW.owner_id<>a.owner_id
OR NEW.binding_proof<>a.binding_proof OR NEW.context_proof<>a.context_proof OR NEW.original_expires_at<>a.authority_expires_at
OR NEW.original_activity_at<>activity OR NEW.decoded_bytes<>octet_length(payload::text)
OR NEW.source_sha256<>encode(sha256(convert_to(payload::text,'UTF8')),'hex')
OR NEW.ciphertext_bytes<>octet_length(NEW.ciphertext) OR NEW.ciphertext_bytes>131072
OR NEW.ciphertext !~ '^[A-Za-z0-9+/]+={0,2}$'
OR replace(encode(decode(NEW.ciphertext,'base64'),'base64'),E'\n','')<>NEW.ciphertext
OR octet_length(decode(NEW.ciphertext,'base64'))<>NEW.compressed_bytes+28
THEN RAISE EXCEPTION 'poll archive does not match the entire immutable source'; END IF;
NEW.created_at:=clock_timestamp(); NEW.compaction_xid:=pg_current_xact_id();
RETURN NEW;
END $$;
CREATE TRIGGER server_poll_archive_insert_guard BEFORE INSERT ON server_poll_archives FOR EACH ROW EXECUTE FUNCTION server_poll_archive_insert_guard();
CREATE OR REPLACE FUNCTION server_poll_journal_immutable() RETURNS trigger LANGUAGE plpgsql SET search_path=pg_catalog,public AS $$ BEGIN
IF TG_OP='DELETE' AND EXISTS(SELECT 1 FROM public.server_poll_archives r JOIN public.server_poll_storage_admissions a USING(receipt_id)
WHERE r.receipt_id=OLD.receipt_id AND r.compaction_xid=pg_current_xact_id() AND a.storage_state='full') THEN RETURN OLD; END IF;
IF TG_OP='DELETE' AND public.server_poll_storage_gc_allowed(OLD.receipt_id) THEN RETURN OLD; END IF;
RAISE EXCEPTION 'server poll journal is immutable outside complete archive or original lifetime GC';
END $$;
CREATE OR REPLACE FUNCTION server_poll_storage_admission_guard() RETURNS trigger LANGUAGE plpgsql SET TimeZone='UTC' SET search_path=pg_catalog,public AS $$
DECLARE retention integer; charged integer; r public.server_poll_archives%ROWTYPE;
BEGIN
PERFORM pg_advisory_xact_lock(hashtextextended('server-poll-storage-v1',0));
IF TG_OP='DELETE' THEN
IF NOT public.server_poll_storage_gc_allowed(OLD.receipt_id) OR EXISTS(SELECT 1 FROM public.server_poll_executions WHERE receipt_id=OLD.receipt_id)
OR EXISTS(SELECT 1 FROM public.server_poll_intents WHERE receipt_id=OLD.receipt_id) OR EXISTS(SELECT 1 FROM public.server_poll_acks WHERE receipt_id=OLD.receipt_id)
OR EXISTS(SELECT 1 FROM public.server_poll_receipts WHERE receipt_id=OLD.receipt_id) OR EXISTS(SELECT 1 FROM public.server_poll_quarantines WHERE receipt_id=OLD.receipt_id)
OR EXISTS(SELECT 1 FROM public.server_poll_archives WHERE receipt_id=OLD.receipt_id) THEN RAISE EXCEPTION 'poll lifetime or children prevent admission deletion'; END IF;
UPDATE public.server_poll_storage_usage SET admissions=admissions-CASE WHEN OLD.storage_state='full' THEN 1 ELSE 0 END,
compacted_admissions=compacted_admissions-CASE WHEN OLD.storage_state='compacted' THEN 1 ELSE 0 END,
archive_bytes=archive_bytes-COALESCE(OLD.archive_bytes,0),
reserved_rows=reserved_rows-CASE WHEN OLD.storage_state='full' THEN 4019 ELSE 2 END,
reserved_bytes=reserved_bytes-CASE WHEN OLD.storage_state='full' THEN 8421376 ELSE 32768+OLD.archive_bytes END
WHERE scope IN('global','owner:'||OLD.owner_id);
GET DIAGNOSTICS charged=ROW_COUNT; IF charged<>2 THEN RAISE EXCEPTION 'poll quota refund mismatch'; END IF; RETURN OLD;
END IF;
IF TG_OP='UPDATE' THEN
-- This durable one-shot marker is written and ACKed in its OWN transaction
-- before archive construction. A committed/unknown claim never resets and
-- the worker never retries destructive work against an already claimed full
-- source. Claim time is maintenance metadata, not renewed execution activity.
IF OLD.storage_state='full' AND NEW.storage_state='full' AND OLD.compaction_claim IS NULL AND OLD.compaction_claimed_at IS NULL AND OLD.compaction_claim_xid IS NULL
AND NEW.compaction_claim IS NOT NULL AND NEW.compaction_claim_xid IS NULL
AND(to_jsonb(OLD)-ARRAY['compaction_claim','compaction_claimed_at','compaction_claim_xid'])=(to_jsonb(NEW)-ARRAY['compaction_claim','compaction_claimed_at','compaction_claim_xid'])
AND public.server_poll_storage_compact_allowed(OLD.receipt_id)
THEN NEW.compaction_claimed_at:=clock_timestamp(); NEW.compaction_claim_xid:=pg_current_xact_id(); RETURN NEW; END IF;
IF(to_jsonb(OLD)-ARRAY['claim_token','claim_attempted_at','used_rows','used_bytes','storage_state','compact_at','archive_bytes','original_activity_at']) IS DISTINCT FROM
(to_jsonb(NEW)-ARRAY['claim_token','claim_attempted_at','used_rows','used_bytes','storage_state','compact_at','archive_bytes','original_activity_at']) THEN RAISE EXCEPTION 'poll admission identity is immutable'; END IF;
IF OLD.storage_state<>'full' THEN RAISE EXCEPTION 'compacted poll identity cannot reopen or change'; END IF;
IF NEW.storage_state='compacted' THEN
SELECT * INTO r FROM public.server_poll_archives WHERE receipt_id=OLD.receipt_id AND compaction_xid=pg_current_xact_id();
IF NOT FOUND OR NEW.used_rows<>OLD.used_rows OR NEW.used_bytes<>OLD.used_bytes OR NEW.claim_token IS DISTINCT FROM OLD.claim_token
OR NEW.claim_attempted_at IS DISTINCT FROM OLD.claim_attempted_at OR NEW.archive_bytes<>r.ciphertext_bytes OR NEW.original_activity_at<>r.original_activity_at
OR EXISTS(SELECT 1 FROM public.server_poll_executions WHERE receipt_id=OLD.receipt_id)
OR EXISTS(SELECT 1 FROM public.server_poll_intents WHERE receipt_id=OLD.receipt_id) OR EXISTS(SELECT 1 FROM public.server_poll_acks WHERE receipt_id=OLD.receipt_id)
OR EXISTS(SELECT 1 FROM public.server_poll_receipts WHERE receipt_id=OLD.receipt_id) OR EXISTS(SELECT 1 FROM public.server_poll_quarantines WHERE receipt_id=OLD.receipt_id)
THEN RAISE EXCEPTION 'poll compaction requires the complete current-transaction archive'; END IF;
NEW.compact_at:=r.created_at;
UPDATE public.server_poll_storage_usage SET admissions=admissions-1,compacted_admissions=compacted_admissions+1,
archive_bytes=archive_bytes+r.ciphertext_bytes,reserved_rows=reserved_rows-4019+2,reserved_bytes=reserved_bytes-8421376+32768+r.ciphertext_bytes
WHERE scope IN('global','owner:'||OLD.owner_id) AND admissions>=1;
GET DIAGNOSTICS charged=ROW_COUNT; IF charged<>2 THEN RAISE EXCEPTION 'poll compaction charge mismatch'; END IF;
RETURN NEW;
END IF;
IF NEW.compact_at IS NOT NULL OR NEW.archive_bytes IS NOT NULL OR NEW.original_activity_at IS NOT NULL THEN RAISE EXCEPTION 'full poll cannot carry archive metadata'; END IF;
IF OLD.claim_token IS NOT NULL AND NEW.claim_token IS NULL AND OLD.claim_attempted_at IS NULL AND NEW.claim_attempted_at IS NOT NULL
AND NEW.used_rows=OLD.used_rows AND NEW.used_bytes=OLD.used_bytes AND OLD.authority_expires_at>clock_timestamp()
THEN NEW.claim_attempted_at:=clock_timestamp(); RETURN NEW; END IF;
IF OLD.claim_token IS NOT DISTINCT FROM NEW.claim_token AND OLD.claim_attempted_at IS NOT DISTINCT FROM NEW.claim_attempted_at
AND NEW.used_rows=OLD.used_rows+1 AND NEW.used_bytes>OLD.used_bytes THEN RETURN NEW; END IF;
RAISE EXCEPTION 'poll admission cannot refresh, reopen or refund itself';
END IF;
IF NEW.storage_state<>'full' OR NEW.compact_at IS NOT NULL OR NEW.archive_bytes IS NOT NULL OR NEW.original_activity_at IS NOT NULL
OR NEW.compaction_claim IS NOT NULL OR NEW.compaction_claimed_at IS NOT NULL OR NEW.compaction_claim_xid IS NOT NULL
OR NOT public.server_poll_storage_accounting_valid() THEN RAISE EXCEPTION 'new poll requires full reservation and exact ledger'; END IF;
SELECT requests_retention_days INTO retention FROM public.mcp_settings WHERE id=1;
IF retention IS NULL OR retention NOT BETWEEN 1 AND 365 OR NEW.authority_issued_at>clock_timestamp() OR NEW.authority_expires_at<=clock_timestamp()
OR NEW.claim_token IS NULL OR NEW.claim_attempted_at IS NOT NULL OR NEW.used_rows<>0 OR NEW.used_bytes<>0 THEN RAISE EXCEPTION 'new poll admission needs original live authority'; END IF;
IF EXISTS(SELECT 1 FROM public.server_poll_storage_admissions WHERE receipt_id=NEW.receipt_id OR(identity_kind=NEW.identity_kind AND execution_id=NEW.execution_id)) THEN RAISE EXCEPTION 'poll identity already spent'; END IF;
NEW.created_at:=clock_timestamp(); NEW.retention_days:=retention; NEW.safety_ms:=60000;
NEW.gc_not_before:=NEW.authority_expires_at+make_interval(days=>retention)+interval '60 seconds';
INSERT INTO public.server_poll_storage_usage(scope) VALUES('owner:'||NEW.owner_id) ON CONFLICT DO NOTHING;
IF(SELECT count(*) FROM public.server_poll_storage_usage WHERE scope IN('global','owner:'||NEW.owner_id)
AND admissions<CASE WHEN scope='global' THEN 128 ELSE 8 END
AND reserved_rows<=CASE WHEN scope='global' THEN 514432 ELSE 32152 END-4019
AND reserved_bytes<=CASE WHEN scope='global' THEN 1077936128 ELSE 67371008 END-8421376)<>2 THEN RAISE EXCEPTION 'poll admission quota exhausted'; END IF;
UPDATE public.server_poll_storage_usage SET admissions=admissions+1,reserved_rows=reserved_rows+4019,reserved_bytes=reserved_bytes+8421376 WHERE scope IN('global','owner:'||NEW.owner_id);
GET DIAGNOSTICS charged=ROW_COUNT; IF charged<>2 THEN RAISE EXCEPTION 'poll admission quota unavailable'; END IF;
RETURN NEW;
END $$;
CREATE FUNCTION server_poll_archive_insert_finish() RETURNS trigger LANGUAGE plpgsql SET search_path=pg_catalog,public AS $$ BEGIN
DELETE FROM public.server_poll_acks WHERE receipt_id=NEW.receipt_id;
DELETE FROM public.server_poll_intents WHERE receipt_id=NEW.receipt_id;
DELETE FROM public.server_poll_receipts WHERE receipt_id=NEW.receipt_id;
DELETE FROM public.server_poll_executions WHERE receipt_id=NEW.receipt_id;
UPDATE public.server_poll_storage_admissions SET storage_state='compacted',compact_at=NEW.created_at,archive_bytes=NEW.ciphertext_bytes,original_activity_at=NEW.original_activity_at WHERE receipt_id=NEW.receipt_id AND storage_state='full';
IF NOT FOUND THEN RAISE EXCEPTION 'poll admission did not transition with archive'; END IF;
RETURN NEW;
END $$;
CREATE TRIGGER server_poll_archive_insert_finish AFTER INSERT ON server_poll_archives FOR EACH ROW EXECUTE FUNCTION server_poll_archive_insert_finish();
CREATE OR REPLACE FUNCTION server_poll_storage_gc_allowed(id uuid) RETURNS boolean LANGUAGE plpgsql SET TimeZone='UTC' SET search_path=pg_catalog,public AS $$
DECLARE a public.server_poll_storage_admissions%ROWTYPE; retention integer; activity timestamptz; terminal_at timestamptz;
BEGIN
IF current_setting('mm.poll_storage_gc',true) IS DISTINCT FROM 'on' THEN RETURN false; END IF;
PERFORM pg_advisory_xact_lock(hashtextextended('server-poll-storage-v1',0));
SELECT * INTO a FROM public.server_poll_storage_admissions WHERE receipt_id=id FOR UPDATE;
IF NOT FOUND OR a.authority_expires_at>=clock_timestamp() OR a.gc_not_before>=clock_timestamp() THEN RETURN false; END IF;
SELECT requests_retention_days INTO retention FROM public.mcp_settings WHERE mcp_settings.id=1 FOR SHARE;
IF retention IS NULL OR retention NOT BETWEEN 1 AND 365 THEN RETURN false; END IF;
terminal_at:=a.created_at;
IF a.identity_kind='mcp' THEN
IF NOT pg_try_advisory_xact_lock(hashtextextended('mcp-operation:'||a.execution_id::text,0)) THEN RETURN false; END IF;
BEGIN
SELECT GREATEST(o.finished_at,p.expires_at) INTO terminal_at FROM public.mcp_operations o
JOIN public.mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id JOIN public.mcp_connections c ON c.id=o.connection_id
WHERE o.id=a.execution_id AND o.connection_id=a.connection_id AND c.owner_id=a.owner_id AND p.operation_id='servers.poll'
AND o.state IN('applied','failed','unknown','rejected','partial','cancelled') AND o.finished_at IS NOT NULL FOR UPDATE OF o,p NOWAIT;
IF NOT FOUND OR terminal_at IS NULL THEN RETURN false; END IF;
EXCEPTION WHEN lock_not_available THEN RETURN false; END;
END IF;
IF a.storage_state='compacted' THEN activity:=GREATEST(a.original_activity_at,a.authority_expires_at,terminal_at);
ELSE
SELECT GREATEST(a.authority_expires_at,a.created_at,COALESCE(a.claim_attempted_at,a.created_at),terminal_at,
COALESCE((SELECT created_at FROM public.server_poll_executions WHERE receipt_id=id),a.created_at),
COALESCE((SELECT max(created_at) FROM public.server_poll_intents WHERE receipt_id=id),a.created_at),
COALESCE((SELECT max(created_at) FROM public.server_poll_acks WHERE receipt_id=id),a.created_at),
COALESCE((SELECT created_at FROM public.server_poll_receipts WHERE receipt_id=id),a.created_at),
COALESCE((SELECT created_at FROM public.server_poll_quarantines WHERE receipt_id=id),a.created_at)) INTO activity;
END IF;
RETURN isfinite(activity) AND activity+make_interval(days=>GREATEST(a.retention_days,retention))+a.safety_ms*interval '1 millisecond'<clock_timestamp();
END $$;
CREATE FUNCTION server_poll_archive_immutable() RETURNS trigger LANGUAGE plpgsql SET search_path=pg_catalog,public AS $$ BEGIN
IF TG_OP='DELETE' AND public.server_poll_storage_gc_allowed(OLD.receipt_id) THEN RETURN OLD; END IF;
RAISE EXCEPTION 'poll archive is immutable until original full retention GC';
END $$;
CREATE TRIGGER server_poll_archive_immutable BEFORE UPDATE OR DELETE ON server_poll_archives FOR EACH ROW EXECUTE FUNCTION server_poll_archive_immutable();
CREATE FUNCTION server_poll_archive_final_guard() RETURNS trigger LANGUAGE plpgsql SET search_path=pg_catalog,public AS $$
DECLARE id uuid; a public.server_poll_storage_admissions%ROWTYPE;
BEGIN
id:=CASE WHEN TG_OP='DELETE' THEN OLD.receipt_id ELSE NEW.receipt_id END;
SELECT * INTO a FROM public.server_poll_storage_admissions WHERE receipt_id=id;
IF FOUND AND a.storage_state='compacted' AND(EXISTS(SELECT 1 FROM public.server_poll_executions WHERE receipt_id=id)
OR EXISTS(SELECT 1 FROM public.server_poll_intents WHERE receipt_id=id) OR EXISTS(SELECT 1 FROM public.server_poll_acks WHERE receipt_id=id)
OR EXISTS(SELECT 1 FROM public.server_poll_receipts WHERE receipt_id=id) OR EXISTS(SELECT 1 FROM public.server_poll_quarantines WHERE receipt_id=id))
THEN RAISE EXCEPTION 'compacted poll retained incomplete original children'; END IF;
IF NOT public.server_poll_storage_accounting_valid() THEN RAISE EXCEPTION 'poll final archive or exact ledger mismatch'; END IF;
RETURN NULL;
END $$;
CREATE CONSTRAINT TRIGGER server_poll_archive_final_guard AFTER INSERT OR DELETE ON server_poll_archives DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION server_poll_archive_final_guard();
CREATE CONSTRAINT TRIGGER server_poll_admission_final_guard AFTER INSERT OR DELETE ON server_poll_storage_admissions DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION server_poll_archive_final_guard();
CREATE CONSTRAINT TRIGGER server_poll_compact_final_guard AFTER UPDATE ON server_poll_storage_admissions DEFERRABLE INITIALLY DEFERRED FOR EACH ROW WHEN(OLD.storage_state IS DISTINCT FROM NEW.storage_state) EXECUTE FUNCTION server_poll_archive_final_guard();
-- Existing row guard additionally refuses compacted tombstones, including late
-- quarantine attempts; the original meter and immutable identity stay intact.
CREATE FUNCTION server_poll_compacted_row_guard() RETURNS trigger LANGUAGE plpgsql SET search_path=pg_catalog,public AS $$ BEGIN
PERFORM pg_advisory_xact_lock(hashtextextended('server-poll-storage-v1',0));
IF EXISTS(SELECT 1 FROM public.server_poll_storage_admissions WHERE receipt_id=NEW.receipt_id AND storage_state='compacted') THEN RAISE EXCEPTION 'compacted poll journal cannot reopen'; END IF;
RETURN NEW;
END $$;
CREATE TRIGGER server_poll_compacted_anchor_guard BEFORE INSERT ON server_poll_executions FOR EACH ROW EXECUTE FUNCTION server_poll_compacted_row_guard();
CREATE TRIGGER server_poll_compacted_intent_guard BEFORE INSERT ON server_poll_intents FOR EACH ROW EXECUTE FUNCTION server_poll_compacted_row_guard();
CREATE TRIGGER server_poll_compacted_ack_guard BEFORE INSERT ON server_poll_acks FOR EACH ROW EXECUTE FUNCTION server_poll_compacted_row_guard();
CREATE TRIGGER server_poll_compacted_receipt_guard BEFORE INSERT ON server_poll_receipts FOR EACH ROW EXECUTE FUNCTION server_poll_compacted_row_guard();
CREATE TRIGGER server_poll_compacted_quarantine_guard BEFORE INSERT ON server_poll_quarantines FOR EACH ROW EXECUTE FUNCTION server_poll_compacted_row_guard();
INSERT INTO schema_migrations(id) VALUES('0041_server_poll_terminal_archives') ON CONFLICT(id) DO NOTHING;
COMMIT;
@@ -0,0 +1,49 @@
-- Approval-time configuration identity. DDL and its marker commit atomically.
BEGIN;
-- Existing rows retain all fields; the new epoch invalidates old complete proofs.
-- This schema addition does not activate the delayed-approval execution protocol.
LOCK TABLE public.traffic_settings, public.uptime_settings,
public.internet_path_settings IN ACCESS EXCLUSIVE MODE;
ALTER TABLE public.traffic_settings ADD COLUMN poll_configuration_epoch uuid NOT NULL DEFAULT pg_catalog.gen_random_uuid();
ALTER TABLE public.uptime_settings ADD COLUMN poll_configuration_epoch uuid NOT NULL DEFAULT pg_catalog.gen_random_uuid();
ALTER TABLE public.internet_path_settings ADD COLUMN poll_configuration_epoch uuid NOT NULL DEFAULT pg_catalog.gen_random_uuid();
CREATE FUNCTION public.server_poll_settings_epoch_guard() RETURNS trigger
LANGUAGE plpgsql SET search_path = pg_catalog AS $epoch$
BEGIN
IF TG_TABLE_SCHEMA <> 'public' OR TG_TABLE_NAME NOT IN
('traffic_settings', 'uptime_settings', 'internet_path_settings')
OR TG_LEVEL <> 'ROW' OR TG_WHEN <> 'BEFORE' THEN
RAISE EXCEPTION 'unexpected poll settings epoch trigger target';
END IF;
IF TG_OP = 'INSERT' THEN
-- Never accept an epoch supplied by an importer or application caller.
NEW.poll_configuration_epoch := pg_catalog.gen_random_uuid();
ELSIF TG_OP = 'UPDATE' THEN
-- Every unknown/future field is configuration by default. Only these four
-- current collector fields are excluded; created_at stays significant.
IF (pg_catalog.to_jsonb(NEW) - ARRAY['poll_configuration_epoch',
'last_collected_at', 'last_duration_ms', 'last_error', 'updated_at'])
IS DISTINCT FROM
(pg_catalog.to_jsonb(OLD) - ARRAY['poll_configuration_epoch',
'last_collected_at', 'last_duration_ms', 'last_error', 'updated_at']) THEN
NEW.poll_configuration_epoch := pg_catalog.gen_random_uuid();
ELSE
NEW.poll_configuration_epoch := OLD.poll_configuration_epoch;
END IF;
ELSE
RAISE EXCEPTION 'unexpected poll settings epoch trigger operation';
END IF;
RETURN NEW;
END;
$epoch$;
CREATE TRIGGER server_poll_settings_epoch BEFORE INSERT OR UPDATE ON public.traffic_settings
FOR EACH ROW EXECUTE FUNCTION public.server_poll_settings_epoch_guard();
CREATE TRIGGER server_poll_settings_epoch BEFORE INSERT OR UPDATE ON public.uptime_settings
FOR EACH ROW EXECUTE FUNCTION public.server_poll_settings_epoch_guard();
CREATE TRIGGER server_poll_settings_epoch BEFORE INSERT OR UPDATE ON public.internet_path_settings
FOR EACH ROW EXECUTE FUNCTION public.server_poll_settings_epoch_guard();
ALTER TABLE public.traffic_settings ENABLE ALWAYS TRIGGER server_poll_settings_epoch;
ALTER TABLE public.uptime_settings ENABLE ALWAYS TRIGGER server_poll_settings_epoch;
ALTER TABLE public.internet_path_settings ENABLE ALWAYS TRIGGER server_poll_settings_epoch;
INSERT INTO schema_migrations(id) VALUES('0042_server_poll_settings_epoch') ON CONFLICT(id) DO NOTHING;
COMMIT;
@@ -0,0 +1,62 @@
-- Durable persistent-forward charge; no execution authority is issued.
-- Reuses the accepted B2.a immutable-parent charging rule, narrowed to ONE
-- forward charge. Existing 0040 two-slot reservations/0031 remain untouched.
-- This SQL cannot authenticate a binding or grant execution authority.
BEGIN;
DO $$ BEGIN
IF to_regclass('public.mcp_routing_forward_charges') IS NOT NULL
THEN RAISE EXCEPTION 'unmarked forward charge storage requires explicit reconciliation'; END IF;
END $$;
CREATE TABLE public.mcp_routing_forward_charges (
-- Logical historical identities, intentionally NOT lifetime FKs. INSERT is
-- checked against the actual current source below; later source/profile
-- retention cannot erase this charge or be blocked forever by old charges.
source_operation_id uuid PRIMARY KEY,
source_plan_id uuid NOT NULL,
source_step_index integer NOT NULL CHECK(source_step_index=0),
profile_id uuid NOT NULL,
connection_id uuid NOT NULL,
charge_units integer NOT NULL DEFAULT 1 CHECK(charge_units=1),
binding_sealed text NOT NULL CHECK(octet_length(binding_sealed) BETWEEN 1 AND 262144),
binding_proof text NOT NULL CHECK(binding_proof ~ '^[a-f0-9]{64}$'),
created_at timestamptz NOT NULL
);
CREATE INDEX mcp_routing_forward_charges_window
ON public.mcp_routing_forward_charges(profile_id,created_at);
CREATE INDEX mcp_routing_forward_charges_retention
ON public.mcp_routing_forward_charges(created_at,source_operation_id);
-- The immutable parent IS the charged unit. No mutable status/child can drop it
-- from the hourly projection after reject/unknown, human save or restart.
CREATE FUNCTION public.mcp_routing_forward_charge_source() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF NOT EXISTS(
SELECT 1 FROM public.mcp_operations o
JOIN public.mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id
JOIN public.mcp_routing_profiles r ON r.id=NEW.profile_id AND r.connection_id=o.connection_id
WHERE o.id=NEW.source_operation_id AND p.id=NEW.source_plan_id
AND o.connection_id=NEW.connection_id AND o.state='pending' AND p.state='executing'
AND p.operation_id='routing.community.switch' AND p.operation_version=7
) THEN RAISE EXCEPTION 'forward charge differs from actual pending admission source'; END IF;
RETURN NEW;
END $$;
CREATE TRIGGER mcp_routing_forward_charge_source BEFORE INSERT ON public.mcp_routing_forward_charges
FOR EACH ROW EXECUTE FUNCTION public.mcp_routing_forward_charge_source();
CREATE FUNCTION public.mcp_routing_forward_charge_immutable() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE retention integer;
BEGIN
IF TG_OP<>'DELETE' THEN RAISE EXCEPTION 'immutable forward charge; rewrite/refund forbidden'; END IF;
SELECT requests_retention_days INTO retention FROM public.mcp_settings WHERE id=1 FOR SHARE;
IF retention IS NULL OR retention NOT BETWEEN 1 AND 365
OR OLD.created_at>clock_timestamp()-interval '1 hour'
OR OLD.created_at>=clock_timestamp()-make_interval(days=>retention)
OR EXISTS(SELECT 1 FROM public.mcp_operations WHERE id=OLD.source_operation_id AND state IN('pending','running'))
THEN RAISE EXCEPTION 'forward charge is still retained or unresolved'; END IF;
RETURN OLD;
END $$;
CREATE TRIGGER mcp_routing_forward_charge_immutable BEFORE UPDATE OR DELETE ON public.mcp_routing_forward_charges
FOR EACH ROW EXECUTE FUNCTION public.mcp_routing_forward_charge_immutable();
CREATE TRIGGER mcp_routing_forward_charge_no_truncate BEFORE TRUNCATE ON public.mcp_routing_forward_charges
FOR EACH STATEMENT EXECUTE FUNCTION public.mcp_routing_forward_charge_immutable();
-- DDL and exact file marker commit atomically. No SECURITY DEFINER or grants.
INSERT INTO public.schema_migrations (id) VALUES ('0043_mcp_routing_forward_charges') ON CONFLICT (id) DO NOTHING;
COMMIT;
@@ -0,0 +1,93 @@
-- Additive durable scoped manual inverse-one charge. No execution grant.
-- Existing 0043 is immutable. Logical charge identities outlive family FKs.
BEGIN;
DO $$ BEGIN
IF to_regclass('public.mcp_routing_manual_inverse_charges') IS NOT NULL
THEN RAISE EXCEPTION 'unmarked manual inverse storage requires reconciliation'; END IF;
END $$;
CREATE TABLE public.mcp_routing_manual_inverse_charges (
recovery_operation_id uuid PRIMARY KEY,
recovery_plan_id uuid NOT NULL,
source_operation_id uuid NOT NULL,
source_plan_id uuid NOT NULL,
source_step_index integer NOT NULL CHECK(source_step_index=0),
profile_id uuid NOT NULL,
profile_version integer NOT NULL CHECK(profile_version>0),
connection_id uuid NOT NULL,
owner_id text NOT NULL CHECK(octet_length(owner_id) BETWEEN 1 AND 1000),
charge_units integer NOT NULL CHECK(charge_units=1),
binding_sealed text NOT NULL CHECK(octet_length(binding_sealed) BETWEEN 1 AND 262144),
binding_proof text NOT NULL CHECK(binding_proof ~ '^[a-f0-9]{64}$'),
created_at timestamptz NOT NULL,
CONSTRAINT mcp_manual_inverse_source_step_unique UNIQUE(source_operation_id,source_step_index)
);
CREATE INDEX mcp_routing_manual_inverse_charges_window
ON public.mcp_routing_manual_inverse_charges(profile_id,created_at);
CREATE INDEX mcp_routing_manual_inverse_charges_retention
ON public.mcp_routing_manual_inverse_charges(created_at,recovery_operation_id);
CREATE INDEX mcp_scoped_recovery_manual_history
ON public.mcp_scoped_recovery_intents(mode,source_operation_id,source_step_index)
WHERE mode='manual';
CREATE FUNCTION public.mcp_routing_manual_inverse_charge_source() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF NOT EXISTS(
SELECT 1 FROM public.mcp_operations o
JOIN public.mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id
JOIN public.mcp_routing_profiles r ON r.id=NEW.profile_id AND r.connection_id=o.connection_id
JOIN public.mcp_operations source ON source.id=NEW.source_operation_id AND source.connection_id=o.connection_id
JOIN public.mcp_plans sp ON sp.id=source.plan_id AND sp.connection_id=source.connection_id
JOIN public.mcp_connections c ON c.id=o.connection_id
WHERE o.id=NEW.recovery_operation_id AND p.id=NEW.recovery_plan_id
AND o.connection_id=NEW.connection_id AND c.owner_id=NEW.owner_id
AND r.version=NEW.profile_version AND o.state='pending' AND p.state='executing'
AND p.operation_id='operations.recover' AND p.operation_version=7
AND sp.id=NEW.source_plan_id AND sp.operation_id='routing.community.switch' AND sp.operation_version=7
AND source.state IN('applied','partial')
) THEN RAISE EXCEPTION 'manual inverse charge differs from pending admission'; END IF;
RETURN NEW;
END $$;
CREATE TRIGGER mcp_routing_manual_inverse_charge_source BEFORE INSERT ON public.mcp_routing_manual_inverse_charges
FOR EACH ROW EXECUTE FUNCTION public.mcp_routing_manual_inverse_charge_source();
CREATE FUNCTION public.mcp_routing_manual_inverse_charge_immutable() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE retention integer;
BEGIN
IF TG_OP<>'DELETE' THEN RAISE EXCEPTION 'immutable manual inverse charge; rewrite/refund forbidden'; END IF;
SELECT requests_retention_days INTO retention FROM public.mcp_settings WHERE id=1 FOR SHARE;
IF retention IS NULL OR retention NOT BETWEEN 1 AND 365
OR OLD.created_at>clock_timestamp()-interval '1 hour'
OR OLD.created_at>=clock_timestamp()-make_interval(days=>retention)
OR EXISTS(SELECT 1 FROM public.mcp_operations WHERE id=OLD.recovery_operation_id AND state IN('pending','running'))
THEN RAISE EXCEPTION 'manual inverse charge still retained or unresolved'; END IF;
RETURN OLD;
END $$;
CREATE TRIGGER mcp_routing_manual_inverse_charge_immutable BEFORE UPDATE OR DELETE ON public.mcp_routing_manual_inverse_charges
FOR EACH ROW EXECUTE FUNCTION public.mcp_routing_manual_inverse_charge_immutable();
CREATE TRIGGER mcp_routing_manual_inverse_charge_no_truncate BEFORE TRUNCATE ON public.mcp_routing_manual_inverse_charges
FOR EACH STATEMENT EXECUTE FUNCTION public.mcp_routing_manual_inverse_charge_immutable();
-- This is a version/identity fence, never authentication of encrypted source.
-- Old binaries cannot create a fresh v6 manual intent after this DDL commits.
-- Already-claimed old work requires the normal drained-writer cutover.
CREATE FUNCTION public.mcp_scoped_recovery_manual_version() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF NEW.mode='manual' AND NOT pg_try_advisory_xact_lock(hashtextextended('mcp-runner-admission',0))
THEN RAISE EXCEPTION 'manual intent admission snapshot busy'; END IF;
IF NEW.mode='manual' AND NOT EXISTS(
SELECT 1 FROM public.mcp_operations o
JOIN public.mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id
JOIN public.mcp_operation_steps s ON s.operation_id=o.id AND s.step_index=0
JOIN public.mcp_operations source ON source.id=NEW.source_operation_id AND source.connection_id=o.connection_id
WHERE o.id=NEW.recovery_operation_id AND o.state='running'
AND p.state='executing' AND p.operation_id='operations.recover' AND p.operation_version=7
AND NEW.source_step_index=0 AND s.state='intent'
AND s.intended_state->>'kind'='service'
AND s.intended_state->>'service'='routing.community.scoped.recover'
AND s.intended_state->'input'->>'mode'='manual'
AND s.intended_state->'input'->>'sourceOperationId'=NEW.source_operation_id::text
AND s.intended_state->'input'->>'receiptId'=NEW.recovery_receipt_id::text
) THEN RAISE EXCEPTION 'manual inverse requires current exact running recovery'; END IF;
RETURN NEW;
END $$;
CREATE TRIGGER mcp_scoped_recovery_manual_version BEFORE INSERT ON public.mcp_scoped_recovery_intents
FOR EACH ROW EXECUTE FUNCTION public.mcp_scoped_recovery_manual_version();
INSERT INTO public.schema_migrations(id) VALUES('0044_mcp_routing_manual_inverse_charges') ON CONFLICT(id) DO NOTHING;
COMMIT;
+276
View File
@@ -0,0 +1,276 @@
{
"version": "6",
"dialect": "sqlite",
"id": "0b2f7dde-98dc-44e9-84a9-fd04e004299d",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"server_snapshots": {
"name": "server_snapshots",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"server_id": {
"name": "server_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"polled_at": {
"name": "polled_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"latency_ms": {
"name": "latency_ms",
"type": "real",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"ros_version": {
"name": "ros_version",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"board_name": {
"name": "board_name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"uptime": {
"name": "uptime",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"cpu_load": {
"name": "cpu_load",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"free_memory": {
"name": "free_memory",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"total_memory": {
"name": "total_memory",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"identity_name": {
"name": "identity_name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"raw_interfaces": {
"name": "raw_interfaces",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"raw_ip_addresses": {
"name": "raw_ip_addresses",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"server_snapshots_server_id_servers_id_fk": {
"name": "server_snapshots_server_id_servers_id_fk",
"tableFrom": "server_snapshots",
"tableTo": "servers",
"columnsFrom": [
"server_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"servers": {
"name": "servers",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "''"
},
"host": {
"name": "host",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"port": {
"name": "port",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 443
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'admin'"
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "''"
},
"use_ssl": {
"name": "use_ssl",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"verify_ssl": {
"name": "verify_ssl",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'home-router'"
},
"site": {
"name": "site",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "''"
},
"country": {
"name": "country",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "''"
},
"asn": {
"name": "asn",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "''"
},
"comment": {
"name": "comment",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "''"
},
"enabled": {
"name": "enabled",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(datetime('now'))"
},
"updated_at": {
"name": "updated_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(datetime('now'))"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1777572014210,
"tag": "0000_postgresql",
"breakpoints": true
}
]
}
File diff suppressed because it is too large. Load diff
+208
View File
@@ -0,0 +1,208 @@
{
"name": "mikrotik-manager-backend",
"version": "1.0.0",
"description": "MikroTik Manager backend — Fastify + Drizzle + PostgreSQL",
"type": "module",
"scripts": {
"test:mcp:module-completion": "tsx src/services/ipsec-server-init-model.test.ts && tsx src/modules/mcp/ipsec-server-init-actions.test.ts && tsx src/services/wireguard-revision-restore.test.ts && tsx src/modules/mcp/vrf-ticket-runtime.test.ts && tsx src/services/ipsec-restore.test.ts && tsx src/services/ipsec-pool-dependencies.test.ts && tsx src/services/vrf-production-transition.test.ts && tsx src/services/vrf-production-action-binding.test.ts && tsx src/services/ipsec-revision-restore-model.test.ts && tsx src/modules/mcp/ipsec-revision-actions.test.ts && tsx src/modules/mcp/service-native-progress.test.ts && tsx src/services/ipsec-revision-restore.test.ts && tsx src/services/ipsec-revision-secret.test.ts",
"test:mcp:module-completion:pg": "node scripts/test-mcp-postgres.mjs --module-completion --restore",
"test:mcp:pg": "node scripts/test-mcp-postgres.mjs",
"test:mcp:external-process:pg": "node scripts/mcp-external-process/run-owned.mjs",
"test:traffic-flow:ownership": "tsx src/services/traffic-flow-batch-proof.test.ts && tsx src/services/traffic-flow-owned-batch.test.ts && tsx src/services/traffic-flow-owned-writer.test.ts && tsx src/services/traffic-flow-write-transaction.test.ts && tsx src/services/traffic-flow-writer-exclusion.test.ts && tsx src/services/traffic-flow-drain-protocol.test.ts && tsx src/services/traffic-flow-facts-rebuild-execution.test.ts && tsx src/services/traffic-flow-facts-rebuild-source.test.ts && tsx src/services/traffic-flow-facts-rebuild-classifier.test.ts && tsx src/services/traffic-flow-facts-rebuild-service.test.ts && tsx src/services/traffic-flow-ripe-prune-owned.test.ts && tsx src/services/traffic-flow-engine-owned.test.ts && tsx src/routes/traffic-flow-rebuild.test.ts && tsx src/services/traffic-flow-settings-execution.test.ts",
"test:traffic-flow:rebuild:pg": "node scripts/test-mcp-postgres.mjs --flow-rebuild",
"test:traffic-flow:pg": "node scripts/test-mcp-postgres.mjs --flow-writes",
"test:traffic-flow:quality:pg": "node scripts/test-mcp-postgres.mjs --flow-quality",
"test:mcp:pg:all": "node scripts/test-mcp-postgres.mjs --all",
"mcp:stdio": "node dist/scripts/mcp-stdio.js",
"mcp:stdio:dev": "tsx src/scripts/mcp-stdio.ts",
"test:logs": "tsx src/services/router-logs-export.test.ts && tsx src/services/router-logs-model.test.ts && tsx src/plugins/auth.logs.test.ts && tsx src/services/mikrotik-logs.test.ts",
"test:logs:pg": "tsx src/services/router-logs-sql.test.ts",
"test:sidebar-counts": "tsx src/services/sidebar-counts.test.ts && tsx src/services/complete-count.test.ts && tsx src/routes/sidebar-counts.test.ts",
"dev": "tsx watch src/index.ts",
"build": "npm run build -w @mmapp/contracts && tsc && npm run build:database-operator",
"build:database-operator": "node scripts/build-database-operator.mjs",
"test:alerts-telegram": "tsx src/services/alerts-outbox-retry.test.ts && tsx src/services/alerts-outbox-overview.test.ts && tsx src/services/alert-engine/outbox-execution.test.ts && tsx src/services/alerts-telegram-delivery.test.ts && tsx src/services/alerts-revision.test.ts && tsx src/services/alerts-overview.test.ts && tsx ../packages/contracts/src/alerts-normalization.test.ts && tsx src/services/alerts-config-execution.test.ts && tsx src/services/alerts-telegram-settings.test.ts && tsx src/services/alerts-telegram-send.test.ts && tsx src/routes/alerts.test.ts && tsx src/plugins/auth.alerts.test.ts",
"test:alert-engine": "tsx src/services/alert-engine/execution-access.test.ts && tsx src/services/alert-engine/server-signal-identity.test.ts && tsx src/services/alert-engine/collector-source-signals.test.ts && tsx src/services/alert-engine/monitoring-history.test.ts && tsx src/services/alert-engine/groups.test.ts && tsx src/services/alert-engine/evaluate-rule.test.ts && tsx src/services/gre-tunnels-live.test.ts && tsx src/services/bgp-peers-live.test.ts && tsx src/services/gre-bgp-snapshot-collector.test.ts && tsx src/services/gre-bgp-snapshot-history.test.ts && tsx src/services/gre-bgp-snapshot-sql.test.ts && tsx src/services/alert-engine/collector-source-sql.test.ts && tsx src/services/alert-engine/confirm-stability.test.ts",
"start": "node dist/index.js",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio",
"db:migrate-from-sqlite": "tsx src/scripts/migrate-sqlite-to-pg.ts",
"facts:rebuild": "tsx src/scripts/rebuild-flow-facts.ts",
"test:mcp:vrf-cleanup": "tsx src/test/chr-vrf-companion-cleanup-proof.test.ts && tsx src/test/chr-vrf-companion-disabled-cleanup-proof.test.ts && tsx src/test/chr-vrf-companion-transfer-cleanup-proof.test.ts",
"test:probes": "npm run test:speed-workflow && tsx src/services/manual-data-collection-execution.test.ts && tsx src/routes/manual-data-collection.test.ts && tsx src/services/network-host-resolution.test.ts && tsx src/routes/probes.test.ts && tsx src/routes/probes.cancel.test.ts && tsx src/services/ping-measurement.test.ts && tsx src/services/uptime-probe-observations.test.ts && tsx src/services/ping-measurement-sql.test.ts",
"test:exec": "tsx src/services/mikrotik-logs.test.ts && tsx src/services/terminal-diagnostics.test.ts && tsx src/routes/exec.test.ts && tsx src/plugins/auth.exec.test.ts",
"test:auth": "tsx --test src/modules/mcp/routing-manual-inverse-history.test.ts && tsx src/db/application-writer-profile-registry-structure.test.ts && tsx src/db/application-writer-profile-registry-read.test.ts && tsx src/db/application-writer-profile-registry-read-parity.test.ts && tsx src/db/application-writer-profile-format.test.ts && tsx src/plugins/auth-path.test.ts && tsx src/lib/permissions.test.ts && tsx src/plugins/auth-provenance.test.ts && tsx src/services/bandwidth-rest-speed-original.test.ts && tsx src/plugins/auth.smoke.test.ts && tsx src/plugins/auth.ospf-optimizer.test.ts && npm run test:mcp",
"test:mcp": "tsx --test src/services/server-poll-error-provenance.test.ts && tsx --test src/modules/mcp/plan-expiry.test.ts src/modules/mcp/result-budget.test.ts src/modules/mcp/result-delivery.test.ts src/modules/mcp/wire-admission.test.ts src/modules/mcp/flow-envelope-allowance.test.ts src/modules/mcp/sdk-terminal-budget.test.ts src/modules/mcp/inflight-owner.test.ts && npm run test:database-source-binding && tsx --test src/modules/mcp/integration-fixture-expiry.test.ts && npm run test:mcp:operation-origin && npm run test:mcp:service-context && npm run test:mcp:operation-locks && tsx --test src/modules/mcp/routing-community-public.test.ts src/modules/mcp/management-transport.test.ts src/modules/mcp/routing-community-context-model.test.ts src/modules/mcp/routing-community-context-native.test.ts ../packages/contracts/src/mcp-routing.test.ts && tsx src/modules/mcp/human-artifact-receipts.test.ts && tsx src/modules/mcp/chr-overlay-readiness.test.ts && tsx src/modules/mcp/chr-recovery-source.test.ts && tsx src/modules/mcp/chr-native-failure.test.ts && tsx src/modules/mcp/ospf-owner-graph.test.ts && tsx src/modules/mcp/routing-measurement-address.test.ts && tsx src/modules/mcp/routing-observation-source.test.ts && tsx src/services/traffic-flow-talkers-model.test.ts && tsx src/services/bgp-prefix-model.test.ts && tsx src/services/vrf-members.test.ts && tsx src/services/routing-gateway-reference.test.ts && tsx src/services/bgp-export-model.test.ts && tsx src/services/traffic-history-model.test.ts && tsx src/services/internet-path-snapshot.test.ts && tsx src/services/internet-path-observation.test.ts && tsx src/modules/mcp/coverage.test.ts && tsx src/modules/mcp/policy.test.ts && tsx src/modules/mcp/events-batch-model.test.ts && tsx src/modules/mcp/operation-revisions.test.ts && tsx src/modules/mcp/native-create-identity.test.ts && tsx src/modules/mcp/ros-step-verification.test.ts && tsx src/modules/mcp/ros-prefix.test.ts && tsx src/modules/mcp/native-conservation-http.test.ts && tsx src/modules/mcp/security.test.ts && tsx src/modules/mcp/state-file-publish.test.ts && tsx src/modules/mcp/routing-model.test.ts && tsx src/modules/mcp/routing-observations-model.test.ts && tsx src/modules/mcp/ospf-health.test.ts && tsx src/modules/mcp/ecmp-preflight.test.ts && tsx src/modules/mcp/recursive-recovery.test.ts && tsx src/modules/mcp/control-path-model.test.ts && tsx src/modules/mcp/routing-community-scoped-model.test.ts && tsx src/modules/mcp/routing-community-scoped-native.test.ts && tsx src/modules/mcp/path-verification.test.ts && tsx src/modules/mcp/database-maintenance.test.ts && tsx src/routes/mcp.test.ts && tsx ../lib/auth-mcp.test.ts && tsx --test src/modules/mcp/routing-controller-fence.test.ts && tsx src/test/chr-vrf-companion-proof.test.ts && tsx src/test/chr-vrf-companion-telemetry.test.ts && tsx src/test/chr-vrf-companion-address-space.test.ts && tsx src/modules/mcp/flow-facts-rebuild-actions.test.ts && tsx src/modules/mcp/ipsec-peer-create-dependencies.test.ts && tsx src/services/ipsec-certificate-update-model.test.ts && tsx src/modules/mcp/ipsec-native-dispatch.test.ts && tsx src/modules/mcp/ipsec-user-update-actions.test.ts && tsx src/modules/mcp/ipsec-server-delete-actions.test.ts && tsx src/services/ipsec-proof-telemetry.test.ts && tsx src/services/vrf-production-admission.test.ts && tsx src/modules/mcp/flow-host-keys-actions.test.ts && tsx src/services/vrf-production-read.test.ts && tsx src/modules/mcp/ipsec-user-create-actions.test.ts && tsx src/modules/mcp/flow-settings-actions.test.ts && tsx src/services/vrf-production-ticket-store.test.ts && npm run test:mcp:routing-baseline",
"test:wireguard": "tsx src/services/mikrotik-response-bounds.test.ts && tsx src/services/wireguard-interface-update.test.ts && tsx src/services/wireguard-peer-update.test.ts && npx tsx src/services/wireguard-config.test.ts && npx tsx src/services/wireguard-ipv6.test.ts && npx tsx --test src/services/wireguard-import.test.ts src/services/wireguard-import-apply.test.ts src/services/wireguard-import-revision.test.ts src/services/wireguard-delete.test.ts && npx tsx src/routes/wireguard-import.test.ts && tsx src/services/wireguard-interface-create.test.ts && tsx src/services/wireguard-peer-create.test.ts && tsx src/services/wireguard-import-plan.test.ts && tsx src/services/wireguard-import-execution.test.ts && tsx src/modules/mcp/wireguard-import-actions.test.ts && tsx --test src/services/wireguard-export.test.ts",
"test:traffic-rate": "tsx src/services/traffic-rate.test.ts && tsx src/services/traffic-observation.test.ts && tsx src/services/traffic-freshness.test.ts && tsx src/services/traffic-live.test.ts && tsx src/services/traffic-observation-sql.test.ts && tsx src/services/traffic-history-sql.test.ts && tsx src/services/traffic-live-sql.test.ts && tsx src/services/traffic-volume-sql.test.ts && tsx src/services/traffic-binding-status.test.ts && tsx src/services/traffic-status-sql.test.ts && tsx src/services/traffic-gap.test.ts && tsx src/services/traffic-identity.test.ts && tsx src/services/traffic-membership.test.ts && tsx src/services/traffic-aggregate-peak.test.ts && tsx src/services/traffic-users-cache.test.ts && tsx src/services/traffic-collection-lock.test.ts && tsx src/services/traffic-concurrency-sql.test.ts",
"test:traffic-flow": "tsx --test src/services/traffic-flow-heartbeat.test.ts src/services/traffic-flow-heartbeat-wiring.test.ts src/services/traffic-flow-heartbeat-receiver.test.ts src/services/traffic-flow-ingest-quality.test.ts src/services/traffic-flow-quality-contract.test.ts && tsx src/services/traffic-flow-catalog-read.test.ts && tsx src/routes/traffic-flow-query.test.ts && tsx src/services/traffic-flow-month.test.ts && tsx src/services/traffic-flow-parse.test.ts && tsx src/services/traffic-flow-ipv6-pipeline.test.ts && tsx src/services/traffic-flow-map-exporter.test.ts && tsx src/services/traffic-flow-ifaces.test.ts && tsx src/services/traffic-flow-ifindex.test.ts && tsx src/services/traffic-flow-dedup.test.ts && tsx src/services/traffic-flow-planes.test.ts && tsx src/services/traffic-flow-ip.test.ts && tsx src/services/traffic-flow-dest.test.ts && tsx src/services/traffic-flow-classify.test.ts && tsx src/services/traffic-flow-ripe.test.ts && tsx src/services/traffic-flow-brands.test.ts && tsx src/services/traffic-flow-ingest.test.ts && tsx src/services/traffic-flow-analytics.test.ts && tsx src/services/traffic-flow-map-hops.test.ts && tsx src/services/traffic-flow-purge.test.ts && tsx src/services/geoip-download.test.ts && tsx src/services/traffic-flow-geoip.test.ts && tsx src/services/traffic-flow-fact-accumulator.test.ts && tsx src/services/traffic-flow-facts.test.ts && tsx src/services/traffic-flow-facts-filter.test.ts && tsx src/services/traffic-flow-facts-rebuild.test.ts && tsx src/services/statistics-aggregate.test.ts && tsx src/services/geoip-manual-update.test.ts && tsx src/services/traffic-flow-overlay-model.test.ts && npm run test:traffic-flow:ownership && tsx src/services/traffic-flow-worker-platform.test.ts && tsx src/services/traffic-flow-template-cache.test.ts && tsx --test src/services/traffic-flow-template-lifetime.test.ts && tsx src/services/traffic-flow-protocol-scope.test.ts && npm run test:traffic-flow:read-budget && npm run test:traffic-flow:resource-profile",
"test:users": "tsx ../packages/contracts/src/users.test.ts && tsx src/modules/users/iface-type.test.ts && tsx src/modules/users/bindings.test.ts && tsx src/modules/users/interface-inventory.test.ts && tsx src/routes/users-catalog.test.ts",
"test:pg": "tsx src/db/pg-pool.test.ts && tsx src/db/sql-bind.test.ts && tsx src/db/sqlite-json.test.ts && tsx src/db/traffic-flags.test.ts && tsx src/db/pg-schema.test.ts && tsx src/db/scheduler-retention-index.test.ts && tsx src/services/config-revisions.test.ts",
"test:recursive-routes": "tsx src/services/recursive-route-identity.test.ts && tsx src/routes/recursive-routes.test.ts && tsx src/services/recursive-route-catalog.test.ts && tsx src/services/recursive-effective-properties.test.ts && tsx src/services/recursive-apply.test.ts",
"test:config-sync": "tsx src/services/owned-routing-policy.test.ts && tsx src/services/routing-filter-body.test.ts && tsx src/services/bgp-community-policy-model.test.ts && tsx src/services/bgp-connection-read.test.ts && tsx src/services/bgp-filter-binding-model.test.ts && tsx src/services/filter-rules-write-model.test.ts && tsx src/services/filter-rules-mutation.test.ts && tsx src/utils/server-id.test.ts && tsx src/services/config-apply-plan.test.ts && tsx src/services/entity-snapshots.test.ts && tsx src/services/gre-native-identity.test.ts && tsx src/services/gre-ipv6.test.ts && tsx src/routes/gre-ipv6-validation.test.ts && tsx src/services/network-disabled-server.test.ts",
"test:ros-file": "tsx src/services/mikrotik-get-cancel.test.ts && tsx src/services/ros-file-contents.test.ts && tsx src/services/mikrotik-response.test.ts && tsx src/services/mikrotik-owned-file.test.ts && tsx --test src/services/mikrotik-owned-file-zeroization.test.ts",
"test:scheduler": "tsx src/services/scheduler-running.test.ts && tsx src/services/scheduler-recorded-run.test.ts && tsx src/routes/scheduler.test.ts",
"test:backups": "tsx src/modules/mcp/backup-storage-actions.test.ts && tsx src/services/backup-job-read.test.ts && tsx src/services/backup-local-file-state.test.ts && tsx src/services/router-config-export.test.ts && tsx src/services/backup-scheduler-run.test.ts && tsx src/services/backup-restore.test.ts && tsx src/services/backup-deletion.test.ts && tsx src/services/backup-dto.test.ts && tsx src/services/backup-storage-update.test.ts && tsx src/services/backup-job-execution.test.ts && tsx src/routes/backups-operations.test.ts && tsx src/services/s3-backup-client.test.ts && tsx src/services/backup-content-bounds.test.ts",
"test:ipsec": "tsx src/services/ipsec-config.test.ts && tsx src/services/ipsec-state.test.ts && tsx src/routes/ipsec-history.test.ts && tsx src/services/ipsec-restore.test.ts && tsx src/services/ipsec-operation.test.ts && tsx src/routes/ipsec-mutation.test.ts && tsx src/routes/ipsec-nat.test.ts && tsx src/routes/ipsec-group.test.ts && tsx src/services/ipsec-peer-delete.test.ts && tsx src/modules/mcp/ipsec-peer-delete-actions.test.ts && tsx src/services/ipsec-user-delete.test.ts && tsx src/modules/mcp/ipsec-user-delete-actions.test.ts && tsx src/services/ipsec-certificate-delete.test.ts && tsx src/modules/mcp/ipsec-certificate-delete-actions.test.ts && tsx src/services/ipsec-peer-update.test.ts && tsx src/modules/mcp/ipsec-peer-update-actions.test.ts && tsx src/services/ipsec-user-update-model.test.ts && tsx src/modules/mcp/ipsec-user-update-actions.test.ts && tsx src/services/ipsec-server-delete-model.test.ts && tsx src/modules/mcp/ipsec-server-delete-actions.test.ts && tsx src/services/ipsec-proof-telemetry.test.ts && tsx src/services/ipsec-user-create-model.test.ts",
"test:live-maps": "tsx src/services/gre-revision-restore.test.ts && tsx src/services/gre-native-mutation.test.ts && tsx src/services/gre-mutation.test.ts && tsx src/services/filter-rules-observed.test.ts && tsx src/services/firewall-revision-restore.test.ts && tsx src/services/firewall-address-write.test.ts && tsx src/services/firewall-rule-write.test.ts && tsx src/services/gre-observed-read.test.ts && tsx src/services/network-observed-read.test.ts && tsx src/services/firewall-rule-move.test.ts && tsx src/services/ospf-state-read.test.ts && tsx src/modules/mcp/readiness-http.test.ts && tsx src/services/probe-interface-model.test.ts && tsx src/services/wan-runtime-model.test.ts && tsx src/services/gre-keepalive.test.ts && tsx src/services/gre-read.test.ts && tsx src/services/ospf-route-parse.test.ts && tsx src/services/vxlan-live.test.ts && tsx src/routes/vxlan.test.ts && tsx src/services/containers-live.test.ts && tsx src/services/firewall-poll.test.ts && tsx src/services/gre-catalog-poll.test.ts && tsx src/routes/filters-gre-catalog.test.ts && tsx src/routes/bgp-catalog.test.ts && tsx src/services/bgp-catalog-parse.test.ts && tsx src/routes/ospf-catalog.test.ts && tsx src/services/ospf-observed-values.test.ts && tsx src/routes/ospf-legacy.test.ts",
"test": "node scripts/test.mjs",
"test:evolution": "tsx src/services/evobgp-catalog.test.ts && tsx src/services/evobgp-client.test.ts && tsx src/routes/evobgp-catalog.test.ts && tsx src/routes/evobgp-settings.test.ts && tsx src/plugins/auth.evolution-settings.test.ts",
"test:netflow-purge": "tsx src/db/operation-lock.test.ts && tsx src/services/traffic-flow-purge-execution.test.ts && tsx src/services/flow-purge-partitions.test.ts && tsx --test src/services/traffic-flow-worker-lifecycle.test.ts && tsx src/routes/traffic-flow-purge.test.ts",
"test:netflow-settings": "tsx src/services/traffic-flow-host-file-builders.test.ts && tsx src/services/traffic-flow-key-execution.test.ts && tsx src/routes/traffic-flow-settings.test.ts && tsx src/plugins/auth.netflow-settings.test.ts && tsx src/services/traffic-flow-agent-install.test.ts && tsx src/services/traffic-flow-agent-client.test.ts && tsx src/services/traffic-flow-operation-queue.test.ts && tsx src/routes/traffic-flow-host.test.ts && tsx src/services/traffic-flow-settings-execution.test.ts && tsx src/modules/mcp/flow-host-keys-actions.test.ts && tsx src/services/traffic-flow-lifecycle-fence.test.ts && tsx src/services/traffic-flow-settings-store.test.ts && tsx src/services/traffic-flow-settings-service.test.ts && tsx src/modules/mcp/flow-settings-actions.test.ts",
"test:geoip": "tsx src/services/geoip-download.test.ts && tsx src/services/traffic-flow-geoip.test.ts && tsx src/plugins/auth.geoip.test.ts",
"test:ospf-optimizer": "tsx src/services/ospf-cost-optimization.test.ts && tsx src/modules/mcp/ospf-legacy-source.test.ts && tsx src/services/ospf-legacy-optimization.test.ts && tsx src/services/ospf-template-flags.test.ts && tsx src/services/ospf-route-proof.test.ts && tsx src/services/ospf-link-catalog.test.ts && tsx src/services/ospf-identity-catalog.test.ts && tsx src/services/ospf-template-cost-write.test.ts && tsx src/services/ospf-template-cost-http.test.ts && tsx src/services/ospf-optimizer-identity.test.ts && tsx src/services/ospf-optimizer-metrics.test.ts && tsx src/services/ospf-optimizer-plan.test.ts && tsx src/services/ospf-effective-disabled.test.ts && tsx src/routes/ospf-optimizer.test.ts",
"test:containers": "tsx src/services/containers-live.test.ts && tsx src/services/containers-state.test.ts && tsx src/services/container-removal.test.ts && tsx src/routes/containers-state.test.ts",
"test:certificates": "tsx src/services/certificate-installation.test.ts && tsx src/services/certificate-renew-collector.test.ts && tsx src/services/certificate-issue-execution.test.ts && tsx src/services/certificate-issue-request.test.ts && tsx src/routes/certificates-issue.test.ts && tsx src/services/certificate-issue-dto.test.ts && tsx src/services/certificate-parse.test.ts && tsx src/services/certificates-state.test.ts && tsx src/routes/certificates-state.test.ts && tsx src/services/acme-settings-test.test.ts && tsx src/services/acme-settings-update-model.test.ts && tsx src/modules/mcp/acme-settings-actions.test.ts",
"test:system-database": "tsx src/services/bounded-database-dump.test.ts && tsx src/services/database-dump-connection.test.ts && tsx src/services/system-database-partition-restore.test.ts && tsx src/services/system-database-cache.test.ts && tsx src/services/database-background-work.test.ts && tsx src/services/database-request-gate.test.ts && tsx src/services/traffic-flow-worker-shutdown.test.ts && tsx src/services/system-database-restore-execution.test.ts && tsx src/services/system-database-operation.test.ts && tsx src/services/system-database-schema-readiness.test.ts && tsx src/routes/system-database.test.ts && tsx src/plugins/auth.system-database.test.ts && tsx src/services/database-control-archive-boundary.test.ts && tsx src/db/database-schema-control-model.test.ts && tsx src/db/database-schema-control.test.ts && tsx src/db/controlled-migrations.test.ts && tsx src/db/controlled-schema-verification.test.ts && tsx --test src/db/migration-data-inventory.test.ts",
"test:all": "npm run test:logs && npm run test:probes && npm run test:exec && npm run test:alert-engine && npm run test:alerts-telegram && npm run test:auth && npm run test:wireguard && npm run test:ipsec && npm run test:traffic-rate && npm run test:traffic-flow && npm run test:users && npm run test:pg && npm run test:sidebar-counts && npm run test:backups && npm run test:scheduler && npm run test:live-maps && npm run test:ospf-optimizer && npm run test:recursive-routes && npm run test:config-sync && npm run test:ros-file && npm run test:containers && npm run test:certificates && npm run test:netflow-settings && npm run test:netflow-purge && npm run test:evolution && npm run test:system-database && npm run test:server-privacy && npm run test:mcp:module-completion && npm run test:mcp:loopback-agent && npm run test:server-probe-management && npm run test:server-poll-management && npm run test:mcp:routing-authority && npm run test:mcp:native-readers && npm run test:mcp:chr-acquisition && npm run test:mcp:shared-context && npm run test:mcp:telemetry-configuration",
"test:server-privacy": "tsx ../packages/contracts/src/servers.test.ts && tsx src/routes/server-secrets-sql.test.ts && tsx --test src/services/server-connection-test.test.ts",
"test:mcp:flow-rebuild:pg": "node scripts/test-mcp-postgres.mjs --flow-rebuild-mcp",
"test:mcp:flow-host-keys:pg": "node scripts/test-mcp-postgres.mjs --flow-host-keys",
"test:mcp:loopback-agent": "tsx src/test/loopback-http-agent.test.ts",
"test:mcp:pending-foundations": "tsx src/services/server-poll-execution.test.ts && tsx src/services/server-probe-execution.test.ts && tsx src/services/ipsec-existing-certificate-export.test.ts && tsx src/modules/mcp/ipsec-existing-certificate-export-actions.test.ts && tsx src/modules/mcp/ipsec-existing-certificate-export-production.test.ts && tsx src/services/ipsec-existing-certificate-export-rest.test.ts && tsx src/services/ipsec-existing-certificate-export-rest-production.test.ts && tsx src/services/ipsec-existing-certificate-export-rest-journal.test.ts && tsx src/services/ipsec-existing-certificate-export-rest-http.test.ts && tsx src/services/ipsec-existing-certificate-export-artifact.test.ts && tsx src/modules/mcp/ipsec-existing-certificate-export-registration.test.ts",
"test:mcp:ipsec-create:pg": "node scripts/test-mcp-postgres.mjs --module-completion --only=ipsec-create --restore",
"test:mcp:ipsec-export-journal:pg": "node scripts/test-mcp-postgres.mjs --ipsec-export-journal --restore",
"test:mcp:ipsec-export:http:pg": "node scripts/test-mcp-postgres.mjs --ipsec-export-http --restore",
"test:mcp:diagnostic-cancel-host:pg": "node scripts/test-mcp-postgres.mjs --diagnostic-cancel-host --restore",
"test:server-probe-management": "tsx src/services/server-probe-authority.test.ts && tsx src/services/server-probe-public-result.test.ts && tsx src/services/server-probe-execution.test.ts && tsx src/services/server-probe-rest-contract.test.ts && tsx src/services/server-probe-rest-execution.test.ts && tsx src/services/server-probe-journal.test.ts && tsx src/services/server-probe-journal-history.test.ts && tsx src/modules/mcp/server-probe-actions.test.ts && tsx src/modules/mcp/diagnostic-cancellation.test.ts && tsx src/modules/mcp/service-diagnostic-progress.test.ts && tsx src/services/mikrotik-get-admission.test.ts && tsx src/services/server-probe-storage-journal-adapter.test.ts && tsx --test src/services/server-probe-storage-history.test.ts && npm run test:server-probe-storage-models",
"test:server-poll-http:pg": "node scripts/test-server-poll-http-owned.mjs",
"test:server-poll-approval:pg": "node scripts/test-server-poll-approval-owned.mjs",
"test:server-poll-atomic:pg": "node scripts/test-server-poll-atomic-owned.mjs",
"test:source-capture-adopted:pg": "node scripts/test-source-capture-adopted-postgres.mjs",
"test:server-poll-settings-epoch:pg": "node scripts/test-server-poll-settings-epoch-owned.mjs",
"test:server-poll-management": "tsx --test src/services/server-poll-maintenance-planner.test.ts src/services/server-poll-maintenance-groups.test.ts && tsx src/services/server-poll-execution.test.ts && tsx src/services/server-poll-production.test.ts && tsx src/services/server-poll-journal.test.ts && tsx src/services/server-poll-authority.test.ts && tsx src/services/server-poll-rest.test.ts && tsx src/services/server-partition-maintenance-lease.test.ts && npm run test:server-poll-storage && npm run test:partition-writers && tsx --test src/services/server-poll-history-verification.test.ts src/services/server-poll-terminal-source.test.ts src/services/server-poll-terminal-archive.test.ts src/services/server-poll-archive-accounting.test.ts src/services/server-poll-terminal-compaction.test.ts src/services/server-poll-history-reader.test.ts src/modules/mcp/server-poll-actions.test.ts src/modules/mcp/server-poll-production.test.ts src/modules/mcp/server-poll-registration.test.ts && tsx src/modules/mcp/effective-connection-source.test.ts && tsx src/services/server-poll-cost-diagnostics.test.ts && tsx src/services/server-poll-journal-snapshot.test.ts && tsx src/modules/mcp/principal-security-source.test.ts && tsx --test src/modules/mcp/poll-runtime-policy-source.test.ts src/modules/mcp/poll-policy-coordinator.test.ts && tsx src/services/server-poll-http.test.ts && tsx --test src/services/server-poll-approval-context.test.ts",
"test:server-poll-storage": "tsx src/services/server-poll-capacity.test.ts && tsx src/services/server-poll-storage-model.test.ts && tsx src/services/server-poll-storage.test.ts && tsx src/services/server-poll-storage-journal.test.ts && tsx src/services/server-poll-storage-retention.test.ts",
"test:bandwidth-admission:pg": "node scripts/test-mcp-postgres.mjs --bandwidth-admission --restore",
"test:bandwidth-control-source:pg": "node scripts/test-mcp-postgres.mjs --bandwidth-control-source --restore",
"test:bandwidth-control-source": "tsx src/services/bandwidth-control-role.test.ts && tsx src/services/bandwidth-control-catalog.test.ts && tsx --test src/services/bandwidth-control-identity.test.ts",
"bandwidth:control:seal": "tsx src/scripts/seal-bandwidth-control-source.ts",
"test:bandwidth-node-observation": "tsx src/services/bandwidth-node-observation.test.ts",
"test:bandwidth-peers": "tsx src/services/bandwidth-peer-observation.test.ts && tsx src/services/bandwidth-peer-network-observation.test.ts && tsx src/services/bandwidth-peer-direct-observation.test.ts && tsx src/services/bandwidth-operator-enrollment.test.ts && tsx --test --test-concurrency=1 src/services/bandwidth-control-registry-frame.test.ts src/services/bandwidth-control-registry-identity.test.ts src/services/bandwidth-node-identity.test.ts src/services/bandwidth-node-registry-batch.test.ts src/services/bandwidth-operator-docker-stop.test.ts src/services/bandwidth-operator-writer-window.test.ts src/services/bandwidth-operator-writer-composition.test.ts src/services/bandwidth-operator-registry.test.ts src/services/bandwidth-external-node-registry.test.ts src/services/bandwidth-external-node-registry-catalog.test.ts src/services/bandwidth-external-node-registry-role.test.ts src/modules/mcp/routing-bandwidth-original.test.ts src/services/bandwidth-peer-identity.test.ts src/services/bandwidth-peer-resolver.test.ts && node --experimental-test-module-mocks --import tsx --test --test-concurrency=1 src/services/bandwidth-node-proof-batch.test.ts src/services/bandwidth-node-key-effect.test.ts src/services/bandwidth-control-file.test.ts src/services/bandwidth-node-bootstrap-v2.test.ts src/services/bandwidth-peer-fence.test.ts src/services/bandwidth-operator-public-files.test.ts src/services/bandwidth-registry-combined.test.ts && npm run test:bandwidth-public-metadata && npm run test:bandwidth-public-registry && npm run test:bandwidth-peer-metadata && npm run test:bandwidth-guarded-peer-sources",
"test:bandwidth-peers:pg": "node scripts/test-mcp-postgres.mjs --bandwidth-external-registry",
"test:bandwidth-rest-origin:pg": "node scripts/test-mcp-postgres.mjs --rest-speed-original",
"test:peer-speed-workflow:pg": "node scripts/test-mcp-postgres.mjs --bandwidth-speed-workflow",
"test:scheduled-speed-original:pg": "node scripts/test-mcp-postgres.mjs --scheduled-speed-original",
"test:operator-docker:pg": "node scripts/test-mcp-postgres.mjs --operator-docker-sql",
"test:bandwidth-public-metadata": "node --experimental-test-module-mocks --import tsx --test --test-concurrency=1 src/services/bandwidth-control-public-metadata.test.ts src/services/bandwidth-node-public-metadata.test.ts",
"test:bandwidth-public-registry:pg": "node scripts/test-mcp-postgres.mjs --public-registry-metadata",
"test:bandwidth-public-registry": "node --experimental-test-module-mocks --import tsx --test --test-concurrency=1 src/services/bandwidth-control-public-registry.test.ts src/services/bandwidth-node-public-registry.test.ts",
"test:bandwidth-peer-metadata": "node --experimental-test-module-mocks --import tsx --test --test-concurrency=1 src/services/bandwidth-peer-metadata.test.ts src/services/bandwidth-peer-metadata-source.test.ts src/services/bandwidth-operator-metadata.test.ts",
"test:operator-peer-metadata:pg": "node scripts/test-mcp-postgres.mjs --operator-peer-metadata",
"test:bandwidth-guarded-peer-sources": "node --experimental-test-module-mocks --import tsx --test --test-concurrency=1 src/services/server-probe-storage-connection.test.ts src/services/bandwidth-operator-peer-source-guard.test.ts src/services/bandwidth-external-peer-source.test.ts src/services/bandwidth-managed-peer-source.test.ts src/services/bandwidth-operator-peer-source-composition.test.ts",
"test:guarded-peer-sources:pg": "node scripts/test-mcp-postgres.mjs --external-peer-metadata",
"test:mcp:operation-origin": "node --experimental-test-module-mocks --import tsx --test src/modules/mcp/operation-execution-origin.test.ts",
"test:mcp:operation-origin:pg": "node scripts/test-mcp-postgres.mjs --mcp-operation-origin",
"test:mcp:speed-original": "node --import tsx --test src/modules/mcp/speed-bandwidth-original.test.ts",
"test:mcp:speed-original:pg": "node scripts/test-mcp-postgres.mjs --mcp-speed-original",
"test:router-configuration-guard:pg": "node scripts/test-mcp-postgres.mjs --router-configuration-guard",
"test:router-configuration-writer": "node --experimental-test-module-mocks --import tsx --test --test-concurrency=1 src/services/router-configuration-writer-context.test.ts src/services/router-configuration-native-transport.test.ts",
"test:scheduled-speed-original": "node --experimental-test-module-mocks --import tsx src/services/bandwidth-scheduled-speed-original.test.ts",
"test:peer-speed-workflow": "node --experimental-test-module-mocks --import tsx --test src/services/bandwidth-speed-run-identity.test.ts src/services/bandwidth-speed-workflow.test.ts",
"test:bandwidth-node-registry:pg": "node scripts/test-mcp-postgres.mjs --bandwidth-node-registry --restore",
"test:mcp:poll-terminal-claim-loss:pg": "node scripts/test-mcp-postgres.mjs --poll-terminal-archive-claim-loss --restore",
"test:mcp:poll-terminal-history-cold:pg": "node scripts/test-mcp-postgres.mjs --poll-terminal-archive-history-cold --restore",
"test:mcp:poll-terminal-history:pg": "node scripts/test-mcp-postgres.mjs --poll-terminal-archive-history --restore",
"test:mcp:poll-terminal-archive-lost-ack:pg": "node scripts/test-mcp-postgres.mjs --poll-terminal-archive-lost-ack --restore",
"test:mcp:poll-terminal-archive:pg": "node scripts/test-mcp-postgres.mjs --poll-terminal-archive --restore",
"test:mcp:poll-archive-schema:pg": "node scripts/test-mcp-postgres.mjs --poll-archive-schema",
"test:mcp:telemetry-configuration:pg": "node scripts/test-mcp-postgres.mjs --telemetry-settings-configuration",
"test:mcp:telemetry-configuration": "tsx --test src/modules/mcp/telemetry-settings-configuration.test.ts",
"test:mcp:routing-schema:pg": "node scripts/test-mcp-postgres.mjs --routing-reservation-schema",
"test:mcp:reserved-step": "tsx --test src/modules/mcp/routing-reserved-step.test.ts",
"test:mcp:routing-forward-rounds:pg": "node scripts/test-mcp-postgres.mjs --routing-network-forward-rounds",
"test:flow-active-services:pg": "node scripts/test-mcp-postgres.mjs --flow-active-services",
"test:flow-active-rebuild:pg": "node scripts/test-mcp-postgres.mjs --flow-active-rebuild",
"test:flow-worker-ipc:pg": "node scripts/test-mcp-postgres.mjs --flow-worker-ipc",
"test:manual-uptime:pg": "node scripts/test-mcp-postgres.mjs --manual-uptime",
"test:manual-ping-group:pg": "node scripts/test-mcp-postgres.mjs --manual-ping-group",
"test:manual-traffic:pg": "node scripts/test-mcp-postgres.mjs --manual-traffic",
"test:manual-remaining": "node --experimental-test-module-mocks --import tsx --test --test-concurrency=1 src/modules/mcp/manual-uptime-actions.test.ts src/modules/mcp/manual-ping-group-actions.test.ts src/modules/mcp/manual-traffic-actions.test.ts src/services/manual-data-collection-execution.test.ts src/routes/manual-data-collection.test.ts",
"test:manual-api-ping:pg": "node scripts/test-mcp-postgres.mjs --manual-api-ping",
"test:manual-api-ping": "node --experimental-test-module-mocks --import tsx --test src/modules/mcp/manual-api-ping-actions.test.ts src/services/manual-data-collection-execution.test.ts src/routes/manual-data-collection.test.ts",
"test:simple-speed:pg": "node scripts/test-mcp-postgres.mjs --simple-speed",
"test:simple-speed:chr": "node scripts/test-mcp-postgres.mjs --simple-speed-chr",
"test:simple-speed": "node --experimental-test-module-mocks --import tsx --test src/modules/mcp/speed-test-actions.test.ts src/services/uptime-speed-service.test.ts && node --import tsx --test src/routes/speed-test.test.ts src/services/speed-run-persistence.test.ts src/services/btest-node-locks.test.ts src/services/bandwidth-test-status.test.ts src/services/bandwidth-test-rates.test.ts",
"test:bandwidth-node-key-effect": "node --experimental-test-module-mocks --import tsx --test --test-concurrency=1 src/services/bandwidth-node-key-effect.test.ts src/services/bandwidth-node-proof-batch.test.ts",
"test:bandwidth-node-source": "tsx src/services/bandwidth-node-registry-role.test.ts && tsx src/services/bandwidth-node-registry-catalog.test.ts",
"test:bandwidth-node-source:pg": "node scripts/test-mcp-postgres.mjs --bandwidth-node-source --restore",
"test:mcp:service-context": "tsx --test src/services/config-mcp-service-execution.test.ts src/modules/mcp/temporary-grant-origin.test.ts src/modules/mcp/routing-measurement-origin.test.ts",
"test:mcp:service-context:pg": "node scripts/test-mcp-postgres.mjs --service-execution-origin",
"test:mcp:operation-locks": "tsx --test src/services/operation-config-lock-lease.test.ts && npm run test:scheduler-automatic",
"test:mcp:operation-locks:pg": "node scripts/test-mcp-postgres.mjs --operation-locks",
"test:scheduler-automatic": "node --test src/services/scheduler-automatic-root.test.mjs src/services/scheduler-automatic-authority.test.mjs",
"test:speed-workflow": "npm run test:router-configuration-writer && npm run test:mcp:speed-original && npm run test:scheduled-speed-original && npm run test:peer-speed-workflow && npm run test:bandwidth-node-observation && npm run test:bandwidth-native-permit && tsx src/services/bandwidth-admission.test.ts && tsx src/services/btest-node-locks.test.ts && tsx src/services/mikrotik-speed-admission.test.ts && tsx src/services/bandwidth-test-status.test.ts && tsx src/services/bandwidth-test-rates.test.ts && tsx src/services/speed-run-persistence.test.ts && tsx src/routes/speed-test.test.ts && tsx src/routes/speed-probes-write.test.ts",
"test:bandwidth-native-dispatch": "node --experimental-test-module-mocks --import tsx --test --test-concurrency=1 src/services/mikrotik-managed-source.test.ts src/services/bandwidth-native-dispatch-composite.test.ts src/services/bandwidth-peer-execution-host-fence.test.ts src/services/bandwidth-native-dispatch-source.test.ts src/services/bandwidth-native-dispatch.test.ts src/services/bandwidth-native-permit.test.ts src/services/bandwidth-admission.test.ts src/services/mikrotik-speed-admission.test.ts",
"test:bandwidth-native-permit": "tsx src/services/bandwidth-native-permit.test.ts",
"test:bandwidth-native-permit:pg": "node scripts/test-mcp-postgres.mjs --bandwidth-control-source --restore",
"test:server-poll-mcp": "tsx src/services/server-poll-boundary.test.ts && tsx src/modules/mcp/server-poll-actions.test.ts && tsx src/modules/mcp/server-poll-production.test.ts && tsx src/modules/mcp/server-poll-registration.test.ts",
"test:server-poll-storage:pg": "node scripts/test-mcp-postgres.mjs --server-poll-storage --restore",
"test:server-poll-storage-fresh:pg": "node scripts/test-mcp-postgres.mjs --server-poll-storage-fresh --restore",
"test:partition-writers": "tsx src/services/partition-write-transaction.test.ts && tsx src/services/collector-partition-writes.test.ts && tsx src/db/partitions-writer.test.ts && tsx src/db/migrate-partition-admission.test.ts && tsx src/db/migration-manifest.test.ts && tsx src/db/sqlite-import-partition-admission.test.ts && tsx src/services/system-database-partition-restore.test.ts && tsx src/services/traffic-flow-write-transaction.test.ts && tsx src/services/traffic-flow-facts-rebuild-cli.test.ts",
"test:mcp:routing-authority": "tsx --test src/modules/mcp/routing-switch-ledger-contract.test.ts && tsx src/modules/mcp/routing-profile-authority.test.ts && tsx src/modules/mcp/routing-recovery-authority.test.ts && tsx src/modules/mcp/routing-reservation.test.ts && tsx src/modules/mcp/routing-community-reserved-source.test.ts && tsx src/modules/mcp/routing-network-verifier-model.test.ts && tsx --test src/test/committed-scoped-checkpoint-observer.test.ts && node --test src/modules/mcp/routing-forward-transport-charge.test.cjs",
"test:mcp:native-readers": "tsx src/modules/mcp/routing-network-verifier-model.test.ts && tsx src/modules/mcp/routing-network-native-parser.test.ts && tsx src/modules/mcp/routing-network-native-session.test.ts && tsx src/db/migration-schema-inventory.test.ts && tsx src/modules/mcp/routing-network-observation-assembly.test.ts && tsx src/modules/mcp/routing-network-configuration-bridge.test.ts",
"test:mcp:native-readers:pg": "node scripts/test-mcp-postgres.mjs --routing-network-native --restore",
"test:mcp:schema-inventory:pg": "node scripts/test-mcp-postgres.mjs --migration-schema-inventory --restore",
"test:mcp:native-tls": "tsx src/modules/mcp/routing-network-native-tls.test.ts",
"test:mcp:schema-control:pg": "node scripts/test-mcp-postgres.mjs --database-schema-control --restore",
"test:mcp:chr-acquisition": "node --test scripts/acceptance/chr-recursive-observation.test.mjs",
"test:mcp:controlled-migrations:pg": "node scripts/test-mcp-postgres.mjs --controlled-migrations --restore",
"test:mcp:shared-context": "tsx --test src/modules/mcp/routing-network-scoped-context.test.ts",
"test:mcp:controlled-reference:pg": "node scripts/test-mcp-postgres.mjs --controlled-schema-reference --restore",
"test:server-probe-storage-fresh:pg": "node scripts/test-mcp-postgres.mjs --probe-storage-fresh --restore",
"test:server-poll-durable:pg": "node scripts/test-mcp-postgres.mjs --server-poll-durable --restore",
"test:server-probe-legacy:pg": "node scripts/test-mcp-postgres.mjs --probe-legacy-orphans",
"test:server-probe-storage:pg": "node scripts/test-mcp-postgres.mjs --probe-storage-journal",
"test:server-probe-history": "tsx --test src/services/server-probe-storage-history.test.ts",
"test:server-probe-history:pg": "node scripts/test-mcp-postgres.mjs --probe-storage-history",
"test:server-probe-storage-models": "tsx src/services/server-probe-journal-maintenance.test.ts && tsx src/modules/mcp/diagnostic-cancellation-source.test.ts && tsx src/services/server-probe-storage-source.test.ts && tsx src/services/server-probe-storage-connection.test.ts && tsx src/modules/mcp/server-probe-storage-host.test.ts && tsx src/services/server-probe-storage-rest.test.ts && tsx src/services/server-probe-storage-metadata.test.ts && tsx src/services/server-probe-legacy-verifier.test.ts && tsx src/services/server-probe-legacy-snapshot.test.ts && tsx src/services/server-probe-legacy-inspection.test.ts && tsx src/db/database-source-observation.test.ts && tsx src/services/server-probe-retention.test.ts && tsx src/modules/mcp/server-probe-registration.test.ts",
"test:mcp:migration-data:pg": "node scripts/test-mcp-postgres.mjs --migration-data-inventory --restore",
"test:mcp:poll-memory:pg": "npm run build -w @mmapp/contracts && node scripts/acceptance/build-poll-memory.mjs && node scripts/test-mcp-postgres.mjs --poll-memory",
"test:mcp:poll-memory-bootstrap:pg": "npm run build -w @mmapp/contracts && node scripts/acceptance/build-poll-memory.mjs && node scripts/test-mcp-postgres.mjs --poll-memory-bootstrap",
"test:mcp:poll-memory-cost:pg": "npm run build -w @mmapp/contracts && node scripts/acceptance/build-poll-memory.mjs && node scripts/test-mcp-postgres.mjs --poll-memory-cost",
"test:mcp:poll-journal-snapshot:pg": "node scripts/test-mcp-postgres.mjs --poll-journal-snapshot",
"test:mcp:principal-security-source:pg": "node scripts/test-mcp-postgres.mjs --principal-security-source",
"test:traffic-flow:read-budget": "tsx --test src/services/traffic-flow-read-budget.test.ts src/modules/mcp/flow-read-quality.test.ts && node scripts/test-flow-read-adapters.mjs",
"test:traffic-flow:read-http:pg": "node scripts/test-flow-read-http-owned.mjs",
"test:traffic-flow:read-source:pg": "tsx scripts/test-flow-read-sql-owned.mts",
"test:mcp:routing-baseline": "tsx --test src/modules/mcp/routing-controller-active-writer.test.ts src/modules/mcp/routing-controller-fence.test.ts src/modules/mcp/routing-community-public.test.ts src/modules/mcp/routing-controller-overlap.test.ts",
"test:mcp:routing-baseline:pg": "node scripts/test-routing-controller-baseline.mjs",
"test:mcp:routing-baseline-inverse:pg": "node scripts/test-routing-controller-baseline.mjs --default-scoped",
"test:sqlite-import-control:pg": "node scripts/test-sqlite-import-control-postgres.mjs",
"test:traffic-flow:resource-profile": "tsx --test src/services/traffic-flow-read-resource-profile.test.ts && node scripts/test-flow-read-profile-startup.mjs",
"test:traffic-flow:settings-read-profiles:pg": "node scripts/test-flow-settings-read-profiles-owned.mjs",
"test:database-source-binding": "node scripts/test-database-source-binding.mjs"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.888.0",
"@fastify/cors": "^11.2.0",
"@fastify/jwt": "^10.2.2",
"@fastify/type-provider-zod": "^1.0.0",
"@mmapp/contracts": "1.0.0",
"@modelcontextprotocol/client": "2.3.1",
"@modelcontextprotocol/fastify": "2.0.1",
"@modelcontextprotocol/node": "2.1.1",
"@modelcontextprotocol/server": "2.3.1",
"acme-client": "^5.4.0",
"better-sqlite3": "^12.9.0",
"dotenv": "^16.4.7",
"drizzle-orm": "^0.45.2",
"fastify": "^5.8.5",
"fastify-plugin": "^5.1.0",
"maxmind": "^5.0.7",
"pg": "^8.23.0",
"undici": "^8.1.0",
"zod": "^4.4.1"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"@types/node": "^22.15.3",
"@types/pg": "^8.23.1",
"drizzle-kit": "^0.31.10",
"embedded-postgres": "18.4.0-beta.17",
"jose": "^6.2.11",
"pino-pretty": "^13.1.3",
"tsx": "^4.19.3",
"typescript": "^5.8.3"
}
}
@@ -0,0 +1,16 @@
import fs from 'node:fs';import assert from 'node:assert/strict';import {chromium,expect} from '@playwright/test';
const privatePath=process.env.MMAPP_ACME_FIXTURE||'tmp/acme250-private.json',p=JSON.parse(fs.readFileSync(privatePath)),login=JSON.parse(fs.readFileSync('tmp/portal195-private.json'));
assert(/^mmt250-[a-f0-9]{8}\.pngs\.top$/.test(p.fqdn));assert(!p.jobId,'Never submit an unknown or existing issue job again');
const report={date:'2026-10-14',block:'12.5.250',fqdn:p.fqdn,certName:p.certName,serverId:p.serverId,realPortalLogin:true,noMocks:true,issuePosts:0,pageErrors:[]};
const browser=await chromium.launch({channel:'msedge',headless:true});
try{
const page=await browser.newPage({viewport:{width:1440,height:1000}});page.on('pageerror',e=>report.pageErrors.push(e.name));page.on('request',r=>{if(new URL(r.url()).pathname==='/api/certificates/issue'&&r.method()==='POST'){const b=r.postDataJSON();assert.equal(String(b.serverId),p.serverId);assert.equal(b.certName,p.certName);assert.deepEqual(b.domainNames,[p.fqdn]);p.jobId=b.requestId;assert(p.jobId);report.issuePosts++;report.jobId=p.jobId;fs.writeFileSync(privatePath,JSON.stringify(p))}});
await page.goto('http://localhost:3000/certificates?server=2');await page.getByLabel('Email',{exact:true}).fill(login.email);await page.getByLabel('Пароль',{exact:true}).fill(login.password);await page.getByRole('button',{name:'Войти',exact:true}).click();await expect(page.getByRole('navigation',{name:'Путь страницы'})).toContainText('Сертификаты',{timeout:30000});
const settings=await page.evaluate(async({token,zoneId})=>{const headers={Authorization:'Bearer '+localStorage.getItem('mmapp_token'),'Content-Type':'application/json'};const before=await fetch('/api/certificates/acme-settings',{headers});if(!before.ok)throw Error('Settings read failed');const old=await before.json();if(old.tokenConfigured)throw Error('Existing CF token must not be overwritten');const test=await fetch('/api/certificates/acme-settings/test',{method:'POST',headers,body:JSON.stringify({cloudflareApiToken:token})});const tested=await test.json();if(!test.ok||tested.ok!==true)return{testStatus:test.status,testOk:false,message:tested.message};const put=await fetch('/api/certificates/acme-settings',{method:'PUT',headers,body:JSON.stringify({directoryUrl:'https://acme-v02.api.letsencrypt.org/directory',defaultZoneId:zoneId,cloudflareApiToken:token})});const configured=await put.json();if(!put.ok||!configured.tokenConfigured)throw Error('Settings save failed');return{testStatus:test.status,testOk:true,saveStatus:put.status,tokenConfigured:configured.tokenConfigured,directoryUrl:configured.directoryUrl}}, {token:p.token,zoneId:p.zoneId});
report.settings=settings;assert.equal(settings.testOk,true);
await page.getByRole('button',{name:'Выпустить сертификат',exact:true}).click();const dialog=page.getByRole('dialog',{name:'Выпуск сертификата'});await dialog.getByRole('combobox',{name:'Сервер выпуска'}).selectOption(p.serverId);await dialog.getByRole('textbox',{name:'Имя сертификата',exact:true}).fill(p.certName);await dialog.getByRole('button',{name:'Далее',exact:true}).click();await dialog.getByRole('textbox',{name:'Основной домен',exact:true}).fill(p.fqdn);await dialog.getByRole('button',{name:'Далее',exact:true}).click();await dialog.getByRole('button',{name:'Далее',exact:true}).click();await expect(dialog).toContainText(p.fqdn);
const response=page.waitForResponse(r=>new URL(r.url()).pathname==='/api/certificates/issue'&&r.request().method()==='POST');await dialog.getByRole('button',{name:'Выпустить',exact:true}).click();const accepted=await response;assert.equal(accepted.status(),200);assert.equal((await accepted.json()).jobId,p.jobId);report.accepted=true;fs.writeFileSync('docs/ux-ui/acceptance/acme250-live-ui.json',JSON.stringify(report,null,2)+'\n');console.log('One ACME issue accepted for '+p.fqdn);
await expect.poll(()=>page.evaluate(async(jobId)=>{const r=await fetch('/api/certificates/issue/'+jobId,{headers:{Authorization:'Bearer '+localStorage.getItem('mmapp_token')}});if(!r.ok)return'unknown';return(await r.json()).status},p.jobId),{timeout:180000,intervals:[2000]}).toMatch(/^(done|failed)$/);
report.job=await page.evaluate(async(jobId)=>{const r=await fetch('/api/certificates/issue/'+jobId,{headers:{Authorization:'Bearer '+localStorage.getItem('mmapp_token')}});const b=await r.json();return{id:b.id,status:b.status,step:b.step,error:b.error}},p.jobId);assert.equal(report.issuePosts,1);assert.equal(report.job.status,'done');await expect(dialog.getByRole('status')).toContainText('Завершена',{timeout:15000});assert.deepEqual(report.pageErrors,[]);report.uiTerminalConfirmed=true;
fs.writeFileSync('docs/ux-ui/acceptance/acme250-live-ui.json',JSON.stringify(report,null,2)+'\n');console.log('ACME UI job done; one issue POST, JS errors0');
}catch(e){report.failure={name:e.name};fs.writeFileSync('docs/ux-ui/acceptance/acme250-live-ui.json',JSON.stringify(report,null,2)+'\n');console.error('ACME test incomplete: '+e.name);process.exitCode=1}finally{await browser.close()}
@@ -0,0 +1,71 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import tls from 'node:tls';
import { X509Certificate } from 'node:crypto';
import { pathToFileURL } from 'node:url';
const fingerprint = value => {
assert.equal(typeof value, 'string', 'Fingerprint is required');
const normalized = value.replaceAll(':', '').toLowerCase();
assert(/^[a-f0-9]{64}$/.test(normalized), 'Expected SHA-256 fingerprint');
return normalized;
};
export function compareRenewal(before, after) {
for (const sample of [before, after]) {
assert(/^mmt253-[a-f0-9]{8}\.pngs\.top$/.test(sample.fqdn), 'Only the prepared exact test domain is permitted');
assert.equal(sample.remoteAddress, '104.165.244.7');
assert.equal(sample.authorized, true);
assert.equal(sample.hostnameMatched, true);
assert.equal(sample.nativePrivateKey, true);
assert.equal(sample.nativeName, sample.certName);
assert.equal(sample.certName, sample.fqdn.replace('.pngs.top', ''));
assert.equal(fingerprint(sample.nativeFingerprint256), fingerprint(sample.fingerprint256), 'Native leaf must match served TLS');
assert(Number.isFinite(Date.parse(sample.validUntil)), 'Expiry is required');
assert(Number.isFinite(Date.parse(sample.sampledAt)), 'Sample time is required');
assert(Date.parse(sample.validUntil) > Date.parse(sample.sampledAt), 'Certificate is expired');
}
assert.equal(after.fqdn, before.fqdn);
assert.equal(after.certName, before.certName);
assert.notEqual(fingerprint(after.fingerprint256), fingerprint(before.fingerprint256), 'Old certificate is still being served');
assert(Date.parse(after.sampledAt) >= Date.parse(before.sampledAt));
assert(Date.parse(after.validUntil) > Date.parse(after.sampledAt), 'New certificate is expired');
// A second issuance on the same day need not extend expiry by a full day.
return { fingerprintChanged: true, nativeAndTlsMatch: true, trustedHostnameMatch: true };
}
async function sample(fixture) {
assert(/^mmt253-[a-f0-9]{8}\.pngs\.top$/.test(fixture.fqdn));
assert.equal(fixture.certName, fixture.fqdn.replace('.pngs.top', ''));
assert.equal(fixture.nativePrivateKey, true);
fingerprint(fixture.nativeFingerprint256);
return await new Promise((resolve, reject) => {
const socket = tls.connect({ host: '104.165.244.7', port: 443, servername: fixture.fqdn, rejectUnauthorized: true });
socket.setTimeout(15000, () => socket.destroy(new Error('TLS acceptance timeout')));
socket.once('error', reject);
socket.once('secureConnect', () => {
try {
assert.equal(socket.authorized, true);
const cert = new X509Certificate(socket.getPeerCertificate().raw);
assert(cert.checkHost(fixture.fqdn), 'Hostname mismatch');
assert(Date.parse(cert.validTo) > Date.now(), 'Certificate is expired');
assert.equal(fingerprint(cert.fingerprint256), fingerprint(fixture.nativeFingerprint256), 'Native leaf does not match TLS');
resolve({ sampledAt: new Date().toISOString(), fqdn: fixture.fqdn, certName: fixture.certName,
remoteAddress: socket.remoteAddress, authorized: socket.authorized, hostnameMatched: true,
protocol: socket.getProtocol(), fingerprint256: cert.fingerprint256,
validFrom: cert.validFrom, validUntil: cert.validTo,
nativeName: fixture.certName, nativePrivateKey: true, nativeFingerprint256: fixture.nativeFingerprint256 });
} catch (error) { reject(error); } finally { socket.end(); }
});
});
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const [mode, input, output] = process.argv.slice(2);
assert(input && output && ['sample', 'compare'].includes(mode), 'Usage: sample native-summary.json output.json | compare samples.json output.json');
assert(!fs.existsSync(output), 'Never overwrite acceptance evidence');
const data = JSON.parse(fs.readFileSync(input, 'utf8'));
const result = mode === 'sample' ? await sample(data) : compareRenewal(data.before, data.after);
fs.writeFileSync(output, JSON.stringify(result, null, 2) + '\n', { flag: 'wx' });
console.log(mode === 'sample' ? 'Trusted TLS/native fingerprint sample saved' : 'Renewal fingerprint comparison passed');
}
@@ -0,0 +1,13 @@
import assert from 'node:assert/strict';
import { compareRenewal } from './acme-renew-tls.mjs';
const before = { fqdn: 'mmt253-a1b2c3d4.pngs.top', certName: 'mmt253-a1b2c3d4', nativeName: 'mmt253-a1b2c3d4',
remoteAddress: '104.165.244.7', authorized: true, hostnameMatched: true, nativePrivateKey: true,
fingerprint256: 'a'.repeat(64), nativeFingerprint256: 'a'.repeat(64), sampledAt: '2026-10-14T12:00:00Z', validUntil: '2027-01-12T12:00:00Z' };
const after = { ...before, fingerprint256: 'b'.repeat(64), nativeFingerprint256: 'b'.repeat(64), sampledAt: '2026-10-14T12:01:00Z' };
assert.equal(compareRenewal(before, after).fingerprintChanged, true);
for (const patch of [{ fingerprint256: before.fingerprint256, nativeFingerprint256: before.nativeFingerprint256 },
{ nativeFingerprint256: before.nativeFingerprint256 }, { authorized: false }, { hostnameMatched: false },
{ nativePrivateKey: false }, { nativeName: 'another' }, { fqdn: 'other.pngs.top' },
{ remoteAddress: '194.226.121.150' }, { validUntil: 'bad' }, { validUntil: '2026-10-13T12:00:00Z' },
{ sampledAt: '2026-10-14T11:00:00Z' }]) assert.throws(() => compareRenewal(before, { ...after, ...patch }));
console.log('Renewal acceptance comparison: changed leaf passes; 11 false-success cases rejected. No network or CA calls.');
@@ -0,0 +1,19 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import {createServer} from 'node:http'
import {randomBytes} from 'node:crypto'
const database=process.env.MMAPP_OWN_SQL_DB,url=new URL(process.env.DATABASE_URL)
assert(/^mm_accept_166_\d+$/.test(database??''));assert.equal(url.pathname,'/'+database);assert.equal(url.username,database)
process.env.AUTH_REQUIRED='true';process.env.AUTH_JWT_SECRET=randomBytes(32).toString('hex');process.env.AUTH_ISSUER='https://own211.invalid';process.env.AUTH_PORTAL_URL='https://own211.invalid'
const {pool,closePool}=await import('/app/backend/dist/db/index.js'),{initDatabase}=await import('/app/backend/dist/db/bootstrap.js'),{buildApp}=await import('/app/backend/dist/index.js')
assert.equal((await pool.query('SELECT current_database() AS name')).rows[0].name,database);assert(Object.values((await pool.query('SELECT rolsuper,rolcreatedb,rolcreaterole,rolbypassrls FROM pg_roles WHERE rolname=current_user')).rows[0]).every(v=>v===false))
const psk='own211-'+randomBytes(24).toString('base64url'),baseline=false,cases=[];let app,serverId,nativeWrites=0,nativeCalls=0
const native=createServer((req,res)=>{nativeCalls++;if(req.method!=='GET')nativeWrites++;res.setHeader('content-type','application/json');res.end(JSON.stringify(req.url.endsWith('/interface/gre')?[{'.id':'*1',name:'own211-encrypted','local-address':'192.0.2.1','remote-address':'192.0.2.2','ipsec-secret':psk,mtu:'1476',keepalive:'10s,3',disabled:'false',running:'true','allow-fast-path':'false'},{'.id':'*2',name:'own211-plain','remote-address':'192.0.2.3','ipsec-secret':'',disabled:'false',running:'true'}]:[]))});await new Promise(r=>native.listen(0,'127.0.0.1',r))
const report={date:'2026-10-13',block:'12.5.211',database,baseline,cases,scope:'Production compiled canonical auth/RBAC using own restricted PostgreSQL, local native HTTP fixture and synthetic JWT/PSK',passed:false}
try{await initDatabase();serverId=Number((await pool.query("INSERT INTO servers(name,host,port,use_ssl,enabled,username,password) VALUES('own211-native','127.0.0.1',$1,false,true,'fixture','fixture') RETURNING id",[native.address().port])).rows[0].id);app=await buildApp({logger:false,startScheduler:false});await app.ready()
for(const path of ['/api/servers','/api/%73ervers','/%61pi/servers','/%61%70%69/servers','/api/gre/tunnels','/%61pi/gre/tunnels'])for(const method of ['GET','HEAD']){const before=nativeCalls;const r=await app.inject({method,url:path});assert.equal(r.statusCode,401);assert.equal(nativeCalls,before);cases.push({path,method,role:'anonymous',status:401})}
for(const path of ['/api/gre/tunnels','/api/%67re/tunnels','/%61pi/gre/tunnels'])for(const role of ['dashboard','reader','writer']){const permissions=role==='dashboard'?['mm:dashboard:read']:role==='reader'?['mm:network:read']:['mm:network:write'],token=app.jwt.sign({sub:'own211-'+role,apps:['mm'],permissions,iss:process.env.AUTH_ISSUER},{expiresIn:'5m'});const r=await app.inject({method:'GET',url:path+'?serverId='+serverId,headers:{authorization:'Bearer '+token}});assert.equal(r.statusCode,role==='dashboard'?403:200);if(role!=='dashboard')assert.equal(r.json().tunnels.find(t=>t.name==='own211-encrypted').ipsec.secret,role==='writer'?psk:'');cases.push({path,method:'GET',role,status:r.statusCode,privateMaterialStored:false});if(role!=='writer'){const patch=await app.inject({method:'PATCH',url:path,headers:{authorization:'Bearer '+token},payload:{serverId:String(serverId),rosId:'*1',comment:'not applied'}});assert.equal(patch.statusCode,403);cases.push({path,method:'PATCH',role,status:403})}}
for(const path of ['/api/auth/config','/%61pi/auth/config']){assert.equal((await app.inject({url:path})).statusCode,200);cases.push({path,role:'anonymous',status:200,publicConfig:true})}
assert.equal(nativeWrites,0);report.nativeWrites=0;report.passed=true
}catch(e){report.errorName=e.name;fs.writeFileSync('/app/backend/acceptance/gre211-private.log',String(e.stack??e),{mode:0o600})}finally{if(app)await app.close();native.closeAllConnections();await new Promise(r=>native.close(r));await closePool();fs.writeFileSync('/app/backend/acceptance/auth211-production-result.json',JSON.stringify(report,null,2));console.log(JSON.stringify(report))}
if(!report.passed)process.exitCode=1
@@ -0,0 +1,3 @@
import assert from'node:assert/strict';import fs from'node:fs';import{createRequire}from'node:module';import{createServer}from'node:http';
const database=process.env.MMAPP_OWN_SQL_DB;assert(/^mm_accept_166_\d+$/.test(database));const url=new URL(process.env.DATABASE_URL);assert.equal(url.pathname,'/'+database);assert.equal(url.username,database);fs.mkdirSync('/tmp/'+database,{recursive:true});process.chdir('/tmp/'+database);
const require=createRequire('/app/backend/package.json'),Fastify=require('fastify'),{validatorCompiler,serializerCompiler}=require('@fastify/type-provider-zod');const{pool,closePool,db}=await import('/app/backend/dist/db/index.js');const{initDatabase}=await import('/app/backend/dist/db/bootstrap.js');const role=(await pool.query('SELECT rolsuper,rolcreatedb,rolcreaterole,rolbypassrls FROM pg_roles WHERE rolname=current_user')).rows[0];assert(Object.values(role).every(v=>v===false));await initDatabase();const{createBackupsRoutes}=await import('/app/backend/dist/routes/backups.js');const{servers}=await import('/app/backend/dist/db/schema.js');const{databaseBackgroundWork}=await import('/app/backend/dist/services/database-background-work.js');let nativeRequests=0;const native=createServer((_req,res)=>{nativeRequests++;res.statusCode=500;res.end('{}')});await new Promise(r=>native.listen(0,'127.0.0.1',r));const[server]=await db.insert(servers).values({name:'owned-availability237',host:'127.0.0.1',port:native.address().port,useSsl:false,username:'fixture',password:'fixture',enabled:true,type:'exit-node'}).returning();fs.mkdirSync('storage/backups',{recursive:true});fs.writeFileSync('storage/backups/available237.rsc','# harmless own fixture\n');fs.mkdirSync('storage/backups/directory237.rsc');const fixtures=[['available237','available237.rsc','local',null],['missing237','missing237.rsc','local',null],['both237','both237.rsc','both','own-test/both237.rsc'],['remote237','remote237.rsc','s3','own-test/remote237.rsc'],['directory237','directory237.rsc','local',null]];for(const[id,filename,storage,key]of fixtures)await pool.query('INSERT INTO backup_entries(id,server_id,server_name,filename,size_bytes,kind,storage,s3_key,created_at) VALUES($1,$2,$3,$4,1,$5,$6,$7,now())',[id,server.id,'fixture',filename,'manual',storage,key]);const app=Fastify({logger:false});app.setValidatorCompiler(validatorCompiler);app.setSerializerCompiler(serializerCompiler);await app.register(createBackupsRoutes(),{prefix:'/api'});await app.ready();const report={date:'2026-10-14',block:'12.5.237',passed:false,checks:[],externalS3Contacts:0,chrContacts:0};try{let r=await app.inject({method:'GET',url:'/api/backups'});assert.equal(r.statusCode,200);const statuses=Object.fromEntries(r.json().map(r=>[r.id,r.localFileStatus]));assert.deepEqual(statuses,{available237:'available',missing237:'missing',both237:'missing',remote237:'not-stored',directory237:'unknown'});report.checks.push({listStates:statuses});r=await app.inject({method:'GET',url:'/api/backups/available237/download'});assert.equal(r.statusCode,200);assert.equal(r.body,'# harmless own fixture\n');report.checks.push({availableDownload:200});r=await app.inject({method:'GET',url:'/api/backups/missing237/download'});assert.equal(r.statusCode,404);report.checks.push({missingDownload:404});r=await app.inject({method:'POST',url:'/api/backups/missing237/restore'});assert.equal(r.statusCode,400);assert.equal(r.json().outcome,'not-applied');assert.match(r.json().error,/Локальный файл/);assert.equal(nativeRequests,0);report.checks.push({missingRestore:400,outcome:'not-applied',nativeRequests:0});r=await app.inject({method:'GET',url:'/api/backups'});assert.equal(r.json().length,5);report.checks.push({failurePreservesCatalog:true});fs.writeFileSync('storage/backups/missing237.rsc','# recreated fixture\n');r=await app.inject({method:'DELETE',url:'/api/backups/missing237?recordOnly=true'});assert.equal(r.statusCode,409);assert(fs.existsSync('storage/backups/missing237.rsc'));assert.equal((await pool.query("SELECT count(*)::int AS n FROM backup_entries WHERE id='missing237'")).rows[0].n,1);report.checks.push({recreatedFilePreserved:409});fs.unlinkSync('storage/backups/missing237.rsc');r=await app.inject({method:'DELETE',url:'/api/backups/missing237?recordOnly=true'});assert.equal(r.statusCode,204);assert.equal(r.headers['x-backup-delete-mode'],'record-only');report.checks.push({missingRecordDeletion:204,mode:'record-only'});report.passed=true}catch(e){report.failure={name:e.name}}finally{await databaseBackgroundWork.drain();await app.close();await new Promise(r=>native.close(r));await closePool();fs.writeFileSync('/checks/backup-availability237.json',JSON.stringify(report,null,2)+'\n');console.log(JSON.stringify(report));if(!report.passed)process.exitCode=1}
@@ -0,0 +1,3 @@
import assert from 'node:assert/strict';import fs from 'node:fs';import{randomUUID}from'node:crypto';import{createServer}from'node:http';import{createRequire}from'node:module';
const database=process.env.MMAPP_OWN_SQL_DB;assert(/^mm_accept_166_\d+$/.test(database));const url=new URL(process.env.DATABASE_URL);assert.equal(url.pathname,'/'+database);assert.equal(url.username,database);fs.mkdirSync('/tmp/'+database,{recursive:true});process.chdir('/tmp/'+database);
const require=createRequire('/app/backend/package.json'),Fastify=require('fastify'),{validatorCompiler,serializerCompiler}=require('@fastify/type-provider-zod');const{pool,closePool,db}=await import('/app/backend/dist/db/index.js');const{initDatabase}=await import('/app/backend/dist/db/bootstrap.js');assert.equal((await pool.query('SELECT current_database() AS name')).rows[0].name,database);const role=(await pool.query('SELECT rolsuper,rolcreatedb,rolcreaterole,rolbypassrls FROM pg_roles WHERE rolname=current_user')).rows[0];assert(Object.values(role).every(v=>v===false));await initDatabase();const{createBackupsRoutes}=await import('/app/backend/dist/routes/backups.js');const{servers}=await import('/app/backend/dist/db/schema.js');const{databaseBackgroundWork}=await import('/app/backend/dist/services/database-background-work.js');let nativeRequests=0;const native=createServer((_req,res)=>{nativeRequests++;res.statusCode=500;res.end('{}')});await new Promise(r=>native.listen(0,'127.0.0.1',r));const[server]=await db.insert(servers).values({name:'owned-legacy234',host:'127.0.0.1',port:native.address().port,useSsl:false,username:'fixture',password:'fixture',enabled:true,type:'exit-node'}).returning();const filename='legacy234-'+randomUUID()+'.rsc',file='storage/backups/'+filename;fs.mkdirSync('storage/backups',{recursive:true});await pool.query("INSERT INTO backup_entries(id,server_id,server_name,filename,size_bytes,kind,storage,created_at) VALUES('owned-legacy234',$1,'fixture',$2,1,'manual','local',now())",[server.id,filename]);const app=Fastify({logger:false});app.setValidatorCompiler(validatorCompiler);app.setSerializerCompiler(serializerCompiler);await app.register(createBackupsRoutes(),{prefix:'/api'});await app.ready();const report={date:'2026-10-14',block:'12.5.234',checks:[],passed:false,chrContacts:0,externalS3Contacts:0};try{for(const prefix of['','\uFEFF\r\n ']){const content=prefix+'# synthetic export generated by MikrotikManager\n/system identity\nset name=legacy-fixture\n';fs.writeFileSync(file,content);const download=await app.inject({method:'GET',url:'/api/backups/owned-legacy234/download'});assert.equal(download.statusCode,200);assert.equal(download.body,content);const restore=await app.inject({method:'POST',url:'/api/backups/owned-legacy234/restore'});assert.equal(restore.statusCode,400);assert.equal(restore.json().outcome,'not-applied');assert.match(restore.json().error,/частичным экспортом/);assert.equal(fs.readFileSync(file,'utf8'),content);assert.equal(nativeRequests,0);assert.equal((await pool.query("SELECT count(*)::int AS n FROM backup_entries WHERE id='owned-legacy234'")).rows[0].n,1);report.checks.push({bomAndWhitespace:Boolean(prefix),downloadStatus:200,restoreStatus:400,outcome:'not-applied',fileAndCatalogPreserved:true,nativeRequests:0})}report.passed=true}catch(e){report.failure={name:e.name}}finally{await databaseBackgroundWork.drain();const deleted=await app.inject({method:'DELETE',url:'/api/backups/owned-legacy234'});assert.equal(deleted.statusCode,204);assert(!fs.existsSync(file));assert.equal((await pool.query('SELECT count(*)::int AS n FROM backup_entries')).rows[0].n,0);report.cleanup={catalogRows:0,localFiles:0};await app.close();await new Promise(r=>native.close(r));await closePool();fs.writeFileSync('/checks/legacy-restore234.json',JSON.stringify(report,null,2)+'\n');console.log(JSON.stringify(report));if(!report.passed)process.exitCode=1}
@@ -0,0 +1,5 @@
import assert from 'node:assert/strict';import fs from 'node:fs';import{createRequire}from'node:module';
const database=process.env.MMAPP_OWN_SQL_DB;assert(/^mm_accept_166_\d+$/.test(database));const url=new URL(process.env.DATABASE_URL);assert.equal(url.pathname,'/'+database);assert.equal(url.username,database);fs.mkdirSync('/tmp/'+database,{recursive:true});process.chdir('/tmp/'+database);
const require=createRequire('/app/backend/package.json'),Fastify=require('fastify'),{validatorCompiler,serializerCompiler}=require('@fastify/type-provider-zod');const{pool,closePool}=await import('/app/backend/dist/db/index.js');const{initDatabase}=await import('/app/backend/dist/db/bootstrap.js');assert.equal((await pool.query('SELECT current_database() AS name')).rows[0].name,database);const role=(await pool.query('SELECT rolsuper,rolcreatedb,rolcreaterole,rolbypassrls FROM pg_roles WHERE rolname=current_user')).rows[0];assert(Object.values(role).every(v=>v===false));await initDatabase();const{createBackupsRoutes}=await import('/app/backend/dist/routes/backups.js');const{databaseBackgroundWork}=await import('/app/backend/dist/services/database-background-work.js');const baseline=process.env.MMAPP_ORIGIN_BASELINE==='true',report={date:'2026-10-14',block:'12.5.233',baseline,checks:[],externalS3Contacts:0,chrContacts:0,passed:false};const app=Fastify({logger:false});app.setValidatorCompiler(validatorCompiler);app.setSerializerCompiler(serializerCompiler);await app.register(createBackupsRoutes(),{prefix:'/api'});await app.ready();const config={provider:'s3',s3Endpoint:'https://s3.invalid',s3Region:'test-region',s3Bucket:'origin-bucket',s3Prefix:'owned-test',s3AccessKeyId:'fixture',s3SecretAccessKey:'fixture',s3ForcePathStyle:false,keepLocalCopy:true};async function api(method,path,payload,status=200){const r=await app.inject({method,url:'/api'+path,...(payload?{payload}:{})});assert.equal(r.statusCode,status,method+' '+path);return r}
async function insert(){await pool.query("INSERT INTO backup_entries(id,server_id,server_name,filename,size_bytes,kind,storage,s3_key,created_at) VALUES('owned-origin',NULL,'fixture','fixture.rsc',1,'manual','s3','owned-test/fixture.rsc',now())")}
try{await api('PUT','/backups/storage',config);await insert();for(const patch of[{s3Bucket:'other-bucket'},{s3Endpoint:'https://other.invalid'},{s3Region:'other'},{s3ForcePathStyle:true},{provider:'local'}]){const before=(await api('GET','/backups/storage')).json();await api('PUT','/backups/storage',patch,baseline?200:409);const after=(await api('GET','/backups/storage')).json();if(!baseline)assert.deepEqual(after,before);else await api('PUT','/backups/storage',config);report.checks.push({change:Object.keys(patch)[0],status:baseline?200:409,settingsUnchanged:!baseline})}for(const patch of[{s3Prefix:'new-prefix'},{s3SecretAccessKey:'rotated-fixture'},{keepLocalCopy:false}]){await api('PUT','/backups/storage',patch);report.checks.push({allowed:Object.keys(patch)[0],status:200})}await pool.query("DELETE FROM backup_entries WHERE id='owned-origin'");await api('PUT','/backups/storage',{s3Bucket:'empty-catalog-bucket'});report.checks.push({emptyCatalogChangeAllowed:true});if(!baseline){const{withBackupStorageOperation}=await import('/app/backend/dist/services/backup-storage-operation.js');let release,started;const active=new Promise(r=>started=r),wait=new Promise(r=>release=r);const creator=withBackupStorageOperation(async()=>{started();await wait;await insert()});await active;let settled=false;const update=api('PUT','/backups/storage',{s3Bucket:'racing-bucket'},409).then(()=>settled=true);await new Promise(r=>setTimeout(r,20));await update;assert.equal(settled,true);release();await creator;assert.equal((await api('GET','/backups/storage')).json().s3Bucket,'empty-catalog-bucket');report.checks.push({inFlightUpdateRejectedBeforeCatalogPublication:true});const{readBackupContent,getBackupById}=await import('/app/backend/dist/services/backup-service.js');const stale=await getBackupById('owned-origin');let releaseDelete;const deletion=withBackupStorageOperation(async()=>{await new Promise(r=>releaseDelete=r);await pool.query("DELETE FROM backup_entries WHERE id='owned-origin'")});const download=readBackupContent(stale);const rejected=assert.rejects(download,/Бэкап не найден/);await Promise.resolve();releaseDelete();await deletion;await rejected;report.checks.push({staleDownloadRejectsBeforeS3AfterDeletion:true})}report.passed=true}catch(e){report.failure={name:e.name}}finally{await pool.query("DELETE FROM backup_entries WHERE id='owned-origin'");await databaseBackgroundWork.drain();await app.close();await closePool();fs.writeFileSync('/checks/storage-origin233.json',JSON.stringify(report,null,2)+'\n');console.log(JSON.stringify(report));if(!report.passed)process.exitCode=1}
@@ -0,0 +1,34 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import {createServer} from 'node:http'
import {randomBytes} from 'node:crypto'
const database=process.env.MMAPP_OWN_SQL_DB,url=new URL(process.env.DATABASE_URL)
assert(/^mm_accept_166_\d+$/.test(database??''));assert.equal(url.pathname,'/'+database);assert.equal(url.username,database)
process.env.AUTH_REQUIRED='true';process.env.AUTH_JWT_SECRET=randomBytes(32).toString('hex');process.env.AUTH_ISSUER='https://own216.invalid';process.env.AUTH_PORTAL_URL='https://own216.invalid'
const {pool,closePool}=await import('/app/backend/dist/db/index.js'),{initDatabase}=await import('/app/backend/dist/db/bootstrap.js'),{buildApp}=await import('/app/backend/dist/index.js'),{fetchBgpSessionsForAlerts}=await import('/app/backend/dist/services/bgp-peers-live.js'),{collectGreBgpSnapshotOnce}=await import('/app/backend/dist/services/gre-bgp-snapshot-collector.js')
assert.equal((await pool.query('SELECT current_database() AS name')).rows[0].name,database);assert(Object.values((await pool.query('SELECT rolsuper,rolcreatedb,rolcreaterole,rolbypassrls FROM pg_roles WHERE rolname=current_user')).rows[0]).every(v=>v===false))
let phase='populated',app,nativeCalls=0,nativeWrites=0,disabledCalls=0;const cases=[]
const fixture=label=>createServer((req,res)=>{nativeCalls++;if(label==='disabled')disabledCalls++;if(req.method!=='GET')nativeWrites++;res.setHeader('content-type','application/json');if(phase==='failed'||(phase==='partial'&&label==='second')){res.statusCode=401;res.end(JSON.stringify({error:401,message:'own216 fixture denied'}));return}const rows=phase==='populated'?[{'.id':'*1',name:'own216-peer',state:'established'}]:[];res.end(JSON.stringify(rows))})
const natives=['first','second','disabled'].map(fixture);for(const n of natives)await new Promise(r=>n.listen(0,'127.0.0.1',r))
const baseline=process.env.MMAPP_BASELINE==='true';const report={date:'2026-10-14',block:'12.5.216',baseline,database,cases,scope:'Compiled production routes/auth, isolated PostgreSQL and RouterOS HTTP fixture; real CHR unchanged',passed:false}
try{
await initDatabase();const ids=[];for(let i=0;i<natives.length;i++)ids.push(Number((await pool.query("INSERT INTO servers(name,host,port,use_ssl,enabled,username,password,type) VALUES($1,'127.0.0.1',$2,false,$3,'fixture','fixture',$4) RETURNING id",['own216-'+i,natives[i].address().port,i<2,i===2?'home-router':'exit-node'])).rows[0].id))
app=await buildApp({logger:false,startScheduler:false});await app.ready();const token=app.jwt.sign({sub:'own216-reader',apps:['mm'],permissions:['mm:bgp:read'],iss:process.env.AUTH_ISSUER},{expiresIn:'5m'}),headers={authorization:'Bearer '+token}
const wrongToken=app.jwt.sign({sub:'own216-other-reader',apps:['mm'],permissions:['mm:network:read'],iss:process.env.AUTH_ISSUER},{expiresIn:'5m'});for(const [name,h,status] of [['anonymous',{},401],['wrong permission',{authorization:'Bearer '+wrongToken},403]]){const before=nativeCalls,r=await app.inject({method:'GET',url:'/api/bgp/sessions',headers:h});assert.equal(r.statusCode,status);assert.equal(nativeCalls,before);cases.push({phase:name,endpoint:'bgp/sessions',status,nativeCalls:0})}
for(phase of ['populated','empty','partial','failed','disabled']){
if(phase==='disabled')await pool.query('UPDATE servers SET enabled=false')
const hasFailure=['partial','failed'].includes(phase),expected=phase==='partial'?[ids[1]]:phase==='failed'?ids.slice(0,2):[]
for(const endpoint of ['bgp/sessions','bgp/catalog']){
const before=nativeCalls,r=await app.inject({method:'GET',url:'/api/'+endpoint,headers}),body=r.json(),isCatalog=endpoint==='bgp/catalog'
assert.equal(r.statusCode,!baseline&&hasFailure&&!isCatalog?503:200)
if(isCatalog||(!baseline&&hasFailure)){assert.deepEqual(body.failures.map(f=>f.serverId),expected);if(isCatalog)assert.equal(body.sessions.length,phase==='populated'?2:0)}else{assert(Array.isArray(body));assert.equal(body.length,phase==='populated'?2:0)}
if(phase==='disabled')assert.equal(nativeCalls,before)
cases.push({phase,endpoint,status:r.statusCode,failures:body.failures?.length??0,nativeCalls:nativeCalls-before})
}
const snapshot=await collectGreBgpSnapshotOnce({fetchGre:async()=>[{targetLabel:'own216-GRE-control',status:'up'}],fetchBgp:fetchBgpSessionsForAlerts})
assert.equal(snapshot.greWritten,1);assert.equal(snapshot.bgpWritten,phase==='populated'?2:0);assert.equal(snapshot.errors.length,!baseline&&hasFailure?1:0)
if(snapshot.errors.length)assert.match(snapshot.errors[0],/BGP: Не опрошены устройства BGP/)
cases.push({phase,endpoint:'compiled snapshot collector',bgpWritten:snapshot.bgpWritten,greWritten:1,sourceErrors:snapshot.errors.length})
}
assert.equal(nativeWrites,0);assert.equal(disabledCalls,0);report.passed=true
}catch(e){report.errorName=e.name;fs.writeFileSync('/app/backend/acceptance/bgp216-private.log',String(e.stack??e),{mode:0o600})}finally{if(app)await app.close();for(const n of natives){n.closeAllConnections();await new Promise(r=>n.close(r))}await closePool();report.nativeWrites=nativeWrites;report.disabledHomeRouterCalls=disabledCalls;fs.writeFileSync('/app/backend/acceptance/bgp216-'+(baseline?'baseline':'fixed')+'.json',JSON.stringify(report,null,2));console.log(JSON.stringify(report))}if(!report.passed)process.exitCode=1
@@ -0,0 +1,22 @@
/** Separate output: never replace an in-flight or accepted poll-memory tree. */
import assert from 'node:assert/strict'
import { mkdir, lstat } from 'node:fs/promises'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import ts from 'typescript'
assert(process.argv.length===2||process.argv.length===3,'Supply at most one new output name')
const outputName=process.argv[2]??'dist-installation-memory'
assert(/^dist-installation-memory(?:-[a-z0-9-]{3,64})?$/.test(outputName),'Invalid compiled output name')
const backend=resolve(dirname(fileURLToPath(import.meta.url)),'../..'), output=resolve(backend,outputName)
const existing=await lstat(output).catch(error=>{if(error.code!=='ENOENT')throw error})
assert(!existing,'Use a new output directory; no implicit cleanup or concurrent rebuild')
await mkdir(output)
const configPath=resolve(backend,'tsconfig.json'),config=ts.readConfigFile(configPath,ts.sys.readFile)
assert(!config.error)
const parsed=ts.parseJsonConfigFileContent(config.config,ts.sys,backend,undefined,configPath)
const options={...parsed.options,rootDir:resolve(backend,'src'),outDir:output,noEmit:false,noEmitOnError:true,declaration:false,declarationMap:false,sourceMap:false,incremental:false,composite:false,tsBuildInfoFile:undefined}
const roots=['src/services/installation-co-load.owned.test.ts','src/services/traffic-flow-collector-worker.ts'].map(file=>resolve(backend,file))
const program=ts.createProgram({rootNames:roots,options}),diagnostics=ts.getPreEmitDiagnostics(program)
if(diagnostics.length)throw Error(ts.formatDiagnosticsWithColorAndContext(diagnostics,{getCurrentDirectory:()=>backend,getCanonicalFileName:file=>file,getNewLine:()=> '\n'}))
assert(!program.emit().emitSkipped)
console.log(JSON.stringify({output,rootEntries:roots.length,actualWorkerIncluded:true,productionModulesSubstituted:false}))
@@ -0,0 +1,36 @@
/** Emit one owned memory entry plus its actual production import tree. The
* measured child runs plain node, not the TypeScript compiler or tsx. */
import assert from 'node:assert/strict'
import { lstat, rm } from 'node:fs/promises'
import { dirname, basename, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import ts from 'typescript'
const backend = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
const output = resolve(backend, 'dist-mcp-memory')
assert.equal(dirname(output), backend)
assert.equal(basename(output), 'dist-mcp-memory')
const existing = await lstat(output).catch(error => { if (error.code !== 'ENOENT') throw error })
assert(!existing?.isSymbolicLink(), 'Refuse to clean a redirected output directory')
if (existing) await rm(output, { recursive: true })
const configPath = resolve(backend, 'tsconfig.json')
const config = ts.readConfigFile(configPath, ts.sys.readFile)
const format = diagnostics => ts.formatDiagnosticsWithColorAndContext(diagnostics, {
getCurrentDirectory: () => backend, getCanonicalFileName: name => name, getNewLine: () => '\n',
})
if (config.error) throw Error(format([config.error]))
const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, backend, undefined, configPath)
if (parsed.errors.length) throw Error(format(parsed.errors))
const entry = resolve(backend, 'src/services/server-poll-memory.owned.test.ts')
const options = { ...parsed.options, rootDir: resolve(backend, 'src'), outDir: output,
noEmit: false, noEmitOnError: true, declaration: false, declarationMap: false, sourceMap: false,
incremental: false, composite: false, tsBuildInfoFile: undefined }
// Deliberately override tsconfig include/exclude with this single test root.
// TypeScript still follows all normal imports; no module/path/IO substitutes.
const program = ts.createProgram({ rootNames: [entry], options })
const diagnostics = ts.getPreEmitDiagnostics(program)
if (diagnostics.length) throw Error(format(diagnostics))
const emitted = program.emit()
if (emitted.emitSkipped || emitted.diagnostics.length) throw Error(format(emitted.diagnostics) || 'Memory entry emit skipped')
console.log(JSON.stringify({ compiledPollMemoryEntry: resolve(output, 'services/server-poll-memory.owned.test.js'),
outputDirectory: output, rootEntries: 1, noEmitOnError: true, measuredChildUsesTsx: false }))
@@ -0,0 +1,169 @@
import { request as httpRequest } from 'node:http';
import { request as httpsRequest } from 'node:https';
import { createHash } from 'node:crypto';
import { mkdir, realpath, lstat, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..');
export const SOURCES = Object.freeze({
'194.226.121.150': Object.freeze({ serverId: 1, port: 19081 }),
'104.165.244.7': Object.freeze({ serverId: 2, port: 19082 }),
});
export const PATHS = Object.freeze([
'/system/resource', '/routing/route', '/routing/nexthop', '/interface',
'/routing/table', '/ip/vrf', '/routing/nexthop', '/routing/route', '/system/resource',
]);
const LIMITS = Object.freeze({ response: 1048576, bytes: 8388608, requests: 256, rows: 1000, totalRows: 10000, duration: 120000 });
const sha = value => createHash('sha256').update(value).digest('hex');
const deny = code => { throw new Error(code); };
export function configuration(args) {
const options = {};
for (let i = 0; i < args.length; i += 2) {
if (!['--source', '--transport', '--output', '--owned-tunnel-attestation'].includes(args[i]) || !args[i + 1] || options[args[i]] !== undefined) deny('invalid-options');
options[args[i]] = args[i + 1];
}
const source = options['--source'], transport = options['--transport'];
if (!Object.hasOwn(SOURCES, source) || !['https', 'ssh-loopback'].includes(transport) || !options['--output']) deny('invalid-source-or-transport');
const attestation = options['--owned-tunnel-attestation'];
if (transport === 'ssh-loopback' && attestation !== 'verified-known-host-161.104.46.222-owned-forward') deny('owned-tunnel-attestation-required');
if (transport === 'https' && attestation !== undefined) deny('irrelevant-tunnel-attestation');
return { source, serverId: SOURCES[source].serverId, transport, output: options['--output'],
endpoint: transport === 'https' ? `https://${source}:443` : `http://127.0.0.1:${SOURCES[source].port}`,
tunnelAttestation: attestation ?? null };
}
export async function privateDirectory(output, root = ROOT) {
// Reject symlinks/junctions at every existing component, including tmp itself.
const base = path.resolve(root, 'tmp'), target = path.resolve(root, output);
const relative = path.relative(base, target);
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) deny('output-must-be-inside-tmp');
await mkdir(base, { recursive: true, mode: 0o700 });
if ((await lstat(base)).isSymbolicLink() || await realpath(base) !== base) deny('output-symlink-refused');
let current = base;
for (const part of relative.split(path.sep)) {
current = path.join(current, part);
try { await mkdir(current, { mode: 0o700 }); } catch (error) { if (error.code !== 'EEXIST') throw error; }
const stat = await lstat(current);
if (!stat.isDirectory() || stat.isSymbolicLink() || await realpath(current) !== current) deny('output-symlink-refused');
}
// Exclusive claim prevents mixing runs and refuses existing captures.
await writeFile(path.join(target, 'capture.claim'), 'chr-recursive-observation-v1\n', { flag: 'wx', mode: 0o600 });
return target;
}
export function decodeBody(body) {
let text, payload;
try { text = new TextDecoder('utf-8', { fatal: true }).decode(body); payload = JSON.parse(text); } catch { deny('invalid-utf8-or-json'); }
const rows = Array.isArray(payload) ? payload : payload && typeof payload === 'object' ? [payload] : deny('invalid-row-shape');
if (rows.length > LIMITS.rows || rows.some(row => !row || typeof row !== 'object' || Array.isArray(row))) deny('row-limit-or-shape');
return { rows };
}
export function readResponse(endpoint, resource, authorization, budget) {
const allowed = Object.entries(SOURCES).flatMap(([host, item]) => [`https://${host}:443`, `http://127.0.0.1:${item.port}`]);
if (!allowed.includes(endpoint)) deny('non-whitelisted-endpoint');
if (!PATHS.includes(resource) || budget.bytes >= LIMITS.bytes || budget.rows >= LIMITS.totalRows || ++budget.requests > LIMITS.requests || Date.now() >= budget.deadline) deny('request-budget');
const url = new URL('/rest' + resource, endpoint);
return new Promise((resolve, reject) => {
const fail = (code, status) => reject(Object.assign(new Error(code), Number.isInteger(status) ? { httpStatus: status } : {}));
const req = (url.protocol === 'https:' ? httpsRequest : httpRequest)(url, {
method: 'GET', agent: false, rejectUnauthorized: true, maxHeaderSize: 16384,
headers: { Authorization: authorization, Accept: 'application/json', 'Accept-Encoding': 'identity' },
}, res => {
const chunks = []; let size = 0;
res.on('data', chunk => {
size += chunk.length; budget.bytes += chunk.length;
if (size > LIMITS.response || budget.bytes > LIMITS.bytes) { req.destroy(); fail('byte-limit'); return; }
chunks.push(chunk);
});
res.on('aborted', () => fail('response-aborted'));
res.on('error', () => fail('response-failed'));
res.on('end', () => {
const body = Buffer.concat(chunks);
// Raw failed HTTP responses are excluded: error bodies can reflect secrets.
if (res.statusCode !== 200) return fail('http-status-refused', res.statusCode);
const type = res.headers['content-type'];
if (typeof type !== 'string' || !/^application\/json(?:\s*;\s*charset=utf-8)?$/i.test(type) || res.headers['content-encoding'] && res.headers['content-encoding'] !== 'identity') return fail('response-encoding-refused');
try {
const { rows } = decodeBody(body);
budget.rows += rows.length;
if (budget.rows > LIMITS.totalRows || Date.now() >= budget.deadline) return fail('response-budget');
resolve({ body, rows, status: res.statusCode, contentType: type });
} catch { fail('invalid-response'); }
});
});
const timer = setTimeout(() => { req.destroy(); fail('absolute-deadline'); }, Math.max(1, budget.deadline - Date.now()));
req.on('error', () => fail('transport-failed'));
req.on('close', () => clearTimeout(timer));
req.end();
});
}
export async function acquire(config, credentials) {
if (typeof credentials?.username !== 'string' || !credentials.username || credentials.username.length > 256 || /[:\r\n]/.test(credentials.username) || typeof credentials.password !== 'string' || !credentials.password || credentials.password.length > 4096 || /[\r\n]/.test(credentials.password)) deny('invalid-private-credentials');
// Validate caller configuration again; exported API cannot supply a custom host.
const fixed = configuration(['--source', config.source, '--transport', config.transport, '--output', config.output,
...(config.tunnelAttestation ? ['--owned-tunnel-attestation', config.tunnelAttestation] : [])]);
const directory = await privateDirectory(fixed.output);
const budget = { requests: 0, bytes: 0, rows: 0, deadline: Date.now() + LIMITS.duration };
const authorization = 'Basic ' + Buffer.from(credentials.username + ':' + credentials.password).toString('base64');
const report = { format: 'chr-recursive-raw-acquisition-v1', source: fixed.source, serverId: fixed.serverId,
transport: fixed.transport, tunnelAttestation: fixed.tunnelAttestation, startedAt: new Date().toISOString(),
semanticProof: false, complete: false, records: [], limits: LIMITS };
let version, attemptedResource;
try {
for (const [index, resource] of PATHS.entries()) {
attemptedResource = resource;
const startedAt = new Date().toISOString();
const response = await readResponse(fixed.endpoint, resource, authorization, budget);
// Never persist a body reflecting the private credential or Basic value.
if ([credentials.password, authorization].some(secret => response.body.includes(Buffer.from(secret)))) deny('credential-reflection-refused');
if (resource === '/system/resource') {
const current = response.rows.length === 1 && response.rows[0].version;
if (typeof current !== 'string' || !/^7\.2[0-6](?:\.\d{1,3})?(?: \((?:stable|long-term)\))?$/.test(current) || version && version !== current) deny('unsupported-or-drifting-version');
version = current;
}
const file = `${String(index + 1).padStart(2, '0')}-${resource.slice(1).replaceAll('/', '-')}.json`;
await writeFile(path.join(directory, file), response.body, { flag: 'wx', mode: 0o600 });
report.records.push({ file, resource, startedAt, finishedAt: new Date().toISOString(), version,
status: response.status, contentType: response.contentType, bytes: response.body.length, rows: response.rows.length, rawSha256: sha(response.body) });
}
report.complete = true;
} catch (error) {
report.failure = 'acquisition-incomplete';
report.failedResource = attemptedResource;
if (Number.isInteger(error.httpStatus) && error.httpStatus >= 100 && error.httpStatus <= 599) report.failedHttpStatus = error.httpStatus;
}
report.finishedAt = new Date().toISOString();
report.usage = { requests: budget.requests, bytes: budget.bytes, rows: budget.rows };
await writeFile(path.join(directory, 'report.json'), JSON.stringify(report, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
return report;
}
async function privateStdin() {
// Piped stdin is private (no terminal echo). TTY raw mode hides all input.
const input = process.stdin, chunks = []; let size = 0;
if (input.isTTY) { process.stderr.write('Private credentials JSON (hidden), then Enter: '); input.setRawMode(true); }
try {
for await (const chunk of input) {
size += chunk.length;
if (size > 8192) deny('private-input-limit');
if (input.isTTY && chunk.includes(3)) deny('private-input-cancelled');
chunks.push(chunk);
if (input.isTTY && (chunk.includes(13) || chunk.includes(10))) break;
}
return JSON.parse(Buffer.concat(chunks).toString('utf8').trim());
} finally { if (input.isTTY) { input.setRawMode(false); process.stderr.write('\n'); } }
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
try {
const config = configuration(process.argv.slice(2));
const report = await acquire(config, await privateStdin());
// Stdout contains metadata only; no native bodies, credentials or errors.
process.stdout.write(JSON.stringify({ complete: report.complete, semanticProof: false, source: report.source, records: report.records.length }) + '\n');
if (!report.complete) process.exitCode = 1;
} catch { process.stderr.write('Read-only acquisition refused or incomplete.\n'); process.exitCode = 1; }
}
@@ -0,0 +1,81 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { createServer } from 'node:http';
import { randomUUID, createHash } from 'node:crypto';
import { readFile, readdir } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { configuration, decodeBody, readResponse, acquire, privateDirectory, PATHS } from './chr-recursive-observation.mjs';
test('source/transport allowlist refuses arbitrary hosts, plaintext external, missing tunnel attestation and option injection', () => {
const args = ['--source', '194.226.121.150', '--transport', 'https', '--output', 'tmp/acquisition-test'];
assert.equal(configuration(args).endpoint, 'https://194.226.121.150:443');
for (const changes of [args.map(x => x === '194.226.121.150' ? 'example.org' : x), args.map(x => x === 'https' ? 'http' : x),
args.map(x => x === 'https' ? 'ssh-loopback' : x), [...args, '--password', 'forbidden'], [...args, '--source', '104.165.244.7']]) assert.throws(() => configuration(changes));
const tunnel = configuration([...args.map(x => x === 'https' ? 'ssh-loopback' : x), '--owned-tunnel-attestation', 'verified-known-host-161.104.46.222-owned-forward']);
assert.equal(tunnel.endpoint, 'http://127.0.0.1:19081');
});
test('full own fixture saves nine exact immutable captures and metadata; reflected errors never enter reports', async () => {
const output = fileURLToPath(new URL('../../../tmp/chr-recursive-own-fixture-' + randomUUID(), import.meta.url)), seen = [];
let reflect = false;
const server = createServer((req, res) => {
seen.push(req.url); res.setHeader('Content-Type', 'application/json');
if (reflect) { res.statusCode = 401; res.end('{"message":"own-fixture-secret-only"}'); return; }
res.end(req.url === '/rest/system/resource' ? '[{"version":"7.26"}]\n' : '[{".id":"*1"}]\n');
});
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(19081, '127.0.0.1', resolve); });
const makeConfig = destination => configuration(['--source', '194.226.121.150', '--transport', 'ssh-loopback', '--output', destination,
'--owned-tunnel-attestation', 'verified-known-host-161.104.46.222-owned-forward']);
try {
const report = await acquire(makeConfig(output), { username: 'own-fixture', password: 'own-fixture-secret-only' });
assert.equal(report.complete, true); assert.equal(report.semanticProof, false);
assert.deepEqual(seen, PATHS.map(resource => '/rest' + resource));
for (const record of report.records) {
const body = await readFile(output + '/' + record.file);
assert.equal(createHash('sha256').update(body).digest('hex'), record.rawSha256);
assert.equal(body.length, record.bytes); assert.equal(record.version, '7.26');
}
await assert.rejects(acquire(makeConfig(output), { username: 'own-fixture', password: 'own-fixture-secret-only' }));
assert.equal(seen.length, 9);
await assert.rejects(privateDirectory('../outside-tmp'));
reflect = true;
const failedOutput = output + '-failed';
const failed = await acquire(makeConfig(failedOutput), { username: 'own-fixture', password: 'own-fixture-secret-only' });
assert.equal(failed.complete, false); assert.equal(failed.records.length, 0);
assert.equal(failed.failedResource, '/system/resource'); assert.equal(failed.failedHttpStatus, 401);
assert.deepEqual((await readdir(failedOutput)).sort(), ['capture.claim', 'report.json']);
assert(!String(await readFile(failedOutput + '/report.json')).includes('own-fixture-secret-only'));
} finally { await new Promise(resolve => server.close(resolve)); }
});
test('exact UTF8 and row bounds fail closed', () => {
assert.equal(decodeBody(Buffer.from('[{"version":"7.26"}]')).rows.length, 1);
for (const body of [Buffer.from([0xff]), Buffer.from('null'), Buffer.from('[[]]'), Buffer.from(JSON.stringify(Array.from({ length: 1001 }, () => ({}))))]) assert.throws(() => decodeBody(body));
});
test('own loopback fixture observes exact GET paths, refuses redirect/error/encoding/bytes, no retry or follow', async () => {
let mode = 'normal', requests = 0;
const server = createServer((req, res) => {
requests++;
assert.equal(req.method, 'GET'); assert.equal(req.url, '/rest/system/resource');
res.setHeader('Content-Type', 'application/json');
if (mode === 'redirect') { res.statusCode = 302; res.setHeader('Location', 'http://example.invalid'); res.end('[]'); }
else if (mode === 'error') { res.statusCode = 401; res.end('{"error":"reflected-private-input"}'); }
else if (mode === 'encoding') { res.setHeader('Content-Encoding', 'gzip'); res.end('[]'); }
else if (mode === 'oversized') res.end(' '.repeat(1048577));
else res.end('[{"version":"7.26"}]\n');
});
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(19081, '127.0.0.1', resolve); });
const budget = () => ({ requests: 0, bytes: 0, rows: 0, deadline: Date.now() + 5000 });
try {
const first = await readResponse('http://127.0.0.1:19081', PATHS[0], 'Basic own-fixture', budget());
assert.equal(first.body.toString(), '[{"version":"7.26"}]\n');
for (mode of ['redirect', 'error', 'encoding', 'oversized']) await assert.rejects(readResponse('http://127.0.0.1:19081', PATHS[0], 'Basic own-fixture', budget()));
assert.equal(requests, 5);
assert.throws(() => readResponse('http://194.226.121.150', PATHS[0], '', budget()));
assert.throws(() => readResponse('http://127.0.0.1:19081', '/system/reboot', '', budget()));
assert.throws(() => readResponse('http://127.0.0.1:19081', PATHS[0], '', { ...budget(), requests: 256 }));
assert.throws(() => readResponse('http://127.0.0.1:19081', PATHS[0], '', { ...budget(), deadline: 0 }));
assert.equal(requests, 5);
} finally { await new Promise(resolve => server.close(resolve)); }
});
@@ -0,0 +1,9 @@
import assert from 'node:assert/strict';import fs from 'node:fs';import path from 'node:path';import{pathToFileURL}from'node:url';import{createHash}from'node:crypto';
const vantage=process.env.MMAPP_TEST_VANTAGE;assert(['workstation','debian'].includes(vantage));const output=process.env.MMAPP_TEST_REPORT;assert(output);const hosts=['194.226.121.150','104.165.244.7'],root=path.resolve('backend/dist'),{MikrotikClient}=await import(pathToFileURL(root+'/services/mikrotik.js'));let rows;
if(vantage==='debian'){const{pool}=await import(pathToFileURL(root+'/db/index.js'));try{rows=(await pool.query("SELECT host,port,use_ssl,enabled,username,password FROM servers WHERE host IN ('194.226.121.150','104.165.244.7') ORDER BY host")).rows}finally{await pool.end()}}else{assert(process.env.MMAPP_TEST_CHR_USER&&process.env.MMAPP_TEST_CHR_PASSWORD);rows=hosts.map(host=>({host,port:80,use_ssl:false,enabled:true,username:process.env.MMAPP_TEST_CHR_USER,password:process.env.MMAPP_TEST_CHR_PASSWORD}))}assert.equal(rows.length,2);assert.deepEqual(rows.map(r=>r.host).sort(),[...hosts].sort());assert(rows.every(r=>r.enabled&&r.port===80&&!r.use_ssl));
const cycles=Number(process.env.MMAPP_REST_CYCLES??8),periodMs=Number(process.env.MMAPP_REST_PERIOD_MS??12000),maxMs=Number(process.env.MMAPP_REST_MAX_MS??960000);assert(Number.isInteger(cycles)&&cycles>=1&&cycles<=32);assert(Number.isInteger(periodMs)&&periodMs>=1000&&periodMs<=60000);assert(Number.isInteger(maxMs)&&maxMs>=10000&&maxMs<=960000);const deadline=Date.now()+maxMs;const report={date:'2026-10-14',block:process.env.MMAPP_REST_BLOCK??'12.5.236',vantage,cycles,periodMs,maxMs,credentialSource:vantage==='debian'?'stored-enabled-server-rows':'explicit-owned-test-env',startedAt:new Date().toISOString(),requests:[],diagnostics:[],configurationWrites:0,passwordChanges:0,rootCauseKnown:false};const sleep=ms=>new Promise(r=>setTimeout(r,ms));const clients=new Map(rows.map(r=>[r.host,new MikrotikClient({host:r.host,port:80,useSsl:false,verifySsl:false,username:r.username,password:r.password})]));
function errorMetadata(e,body){let obj;try{obj=JSON.parse(body)}catch{}const detail=String(obj?.detail??'');return{errorName:e?.name??null,errorBodySha256:body?createHash('sha256').update(body).digest('hex'):null,errorKeys:obj&&typeof obj==='object'?Object.keys(obj).sort():[],detailCategory:/not enough permissions/i.test(detail)?'insufficient-policy':/not allowed/i.test(detail)?'not-allowed':/session closed/i.test(detail)?'session-closed':detail?'unclassified':'absent'}}
async function read(row,endpoint,mode,cycle,kind='sample'){const begin=Date.now(),record={host:row.host,path:endpoint,mode,cycle,kind,startedAt:new Date().toISOString()};let data;try{if(mode==='compiled-client'){data=await clients.get(row.host).get(endpoint,6000);record.status=200}else{const authorization='Basic '+Buffer.from(row.username+':'+row.password).toString('base64');const r=await fetch('http://'+row.host+'/rest'+endpoint,{headers:{authorization,Connection:'close'},signal:AbortSignal.timeout(6000)}),body=await r.text();record.status=r.status;record.basicChallenge=/Basic/i.test(r.headers.get('www-authenticate')??'');if(r.ok)data=JSON.parse(body);else Object.assign(record,errorMetadata(null,body))}}catch(e){record.status=e.statusCode??null;Object.assign(record,errorMetadata(e,e.body??''))}record.durationMs=Date.now()-begin;report.requests.push(record);await sleep(125);return data}
async function diagnostic(row,phase){const userRows=await read(row,'/user','compiled-client',0,'diagnostic'),users=Array.isArray(userRows)?userRows.filter(u=>u.name===row.username):[];const groups=await read(row,'/user/group','compiled-client',0,'diagnostic'),group=Array.isArray(groups)?groups.find(g=>g.name===users[0]?.group):null;const active=await read(row,'/user/active','compiled-client',0,'diagnostic');const apiSessions=Array.isArray(active)?active.filter(s=>s.name===row.username&&s.via==='api').map(s=>createHash('sha256').update(String(s['.id'])+'|'+String(s.when)).digest('hex')).sort():null;const services=await read(row,'/ip/service','compiled-client',0,'diagnostic'),resource=await read(row,'/system/resource','compiled-client',0,'diagnostic'),logs=await read(row,'/log','compiled-client',0,'diagnostic');const auth=Array.isArray(logs)?logs.filter(l=>/login failure/i.test(String(l.message))):[];const events=auth.map(l=>{const message=String(l.message);return{time:l.time??null,via:message.match(/via (\S+)/)?.[1]??null,fromTestDebian:/from 161\.104\.46\.222\b/.test(message),forConfiguredUser:message.includes('for user '+row.username+' ')}});const www=Array.isArray(services)?services.filter(s=>s.name==='www'):[];report.diagnostics.push({host:row.host,phase,apiSessionFingerprints:apiSessions,retainedRestSessions:Array.isArray(active)?active.filter(s=>s.via==='rest-api').length:null,userPresent:users.length===1,userDisabled:users[0]?.disabled??null,userAddressRestriction:users[0]?.address??null,hasApiPolicy:group?String(group.policy).split(',').includes('api'):null,hasRestApiPolicy:group?String(group.policy).split(',').includes('rest-api'):null,wwwEnabled:www.some(s=>s.disabled==='false'),wwwMaxSessions:www.find(s=>s['max-sessions'])?.['max-sessions']??null,wwwDynamicRows:www.filter(s=>s.dynamic==='true').length,resource:resource?{version:resource.version,uptime:resource.uptime,cpuLoad:resource['cpu-load'],freeMemory:resource['free-memory']}:null,retainedAuthFailureCount:auth.length,recentFailures:events.slice(-15),retainedLogIsNotCurrentRate:true})}
function save(){const samples=report.requests.filter(r=>r.kind==='sample');report.completedAt=new Date().toISOString();report.summary={sampleRequests:samples.length,failedSamples:samples.filter(r=>r.status!==200).length,allRequests:report.requests.length,failures:report.requests.filter(r=>r.status!==200).map(r=>({host:r.host,path:r.path,mode:r.mode,status:r.status,kind:r.kind,startedAt:r.startedAt}))};fs.writeFileSync(output,JSON.stringify(report,null,2)+'\n')}
try{for(const row of rows)await diagnostic(row,'before');const started=Date.now(),paths=['/system/resource','/interface/gre','/routing/bgp/session','/routing/ospf/neighbor','/interface/wireguard/peers','/interface/vxlan'];for(let cycle=1;cycle<=cycles&&Date.now()<deadline;cycle++){for(const row of rows)for(const endpoint of paths){const modes=cycle%2?['compiled-client','fetch-close']:['fetch-close','compiled-client'];for(const mode of modes){const previous=report.requests.length;await read(row,endpoint,mode,cycle);if(report.requests[previous].status!==200){await read(row,'/system/resource','fetch-close',cycle,'failure-control');await diagnostic(row,'failure-'+cycle)}}}save();console.log(JSON.stringify({vantage,cycle,samples:report.summary.sampleRequests,failed:report.summary.failedSamples}));if(cycle<cycles)await sleep(Math.max(0,Math.min(deadline,started+cycle*periodMs)-Date.now()))}for(const row of rows)await diagnostic(row,'after')}finally{save();console.log(JSON.stringify({vantage,summary:report.summary,rootCauseKnown:false}))}
@@ -0,0 +1,51 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
const hosts = (process.env.MMAPP_TEST_CHR_HOSTS ?? '').split(',')
assert.deepEqual(hosts, ['194.226.121.150', '104.165.244.7'], 'Explicit owned lab hosts required')
const { MMAPP_TEST_CHR_USER: username, MMAPP_TEST_CHR_PASSWORD: password } = process.env
assert(username && password, 'Explicit test credentials required')
const vantage = process.env.MMAPP_TEST_VANTAGE
assert(['workstation', 'debian'].includes(vantage), 'Explicit vantage required')
const output = process.env.MMAPP_TEST_REPORT
assert(output, 'Explicit report path required')
const authorization = 'Basic ' + Buffer.from(username + ':' + password).toString('base64')
const report = { block: '12.5.225', date: '2026-10-14', vantage, startedAt: new Date().toISOString(), requests: [], diagnostics: [], configurationWrites: 0, passwordChanges: 0, rootCauseKnown: false }
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
async function read(host, path) {
const startedAt = new Date().toISOString(), start = Date.now()
try {
const response = await fetch(`http://${host}/rest${path}`, { headers: { authorization, Connection: 'close' }, signal: AbortSignal.timeout(8000) })
const body = await response.text()
report.requests.push({ host, path, startedAt, durationMs: Date.now() - start, status: response.status, headers: Object.fromEntries(['date', 'server', 'www-authenticate', 'connection'].map(key => [key, response.headers.get(key)])) })
if (!response.ok) return null
try { return JSON.parse(body) } catch { return null }
} catch (error) {
report.requests.push({ host, path, startedAt, durationMs: Date.now() - start, status: null, errorName: error.name })
return null
} finally { await sleep(300) }
}
for (let cycle = 1; cycle <= 3; cycle++) {
for (const host of hosts) for (const path of ['/system/resource', '/interface/gre']) await read(host, path)
if (cycle < 3) await sleep(5000)
}
for (const host of hosts) {
const clock = await read(host, '/system/clock')
const services = await read(host, '/ip/service')
const logs = await read(host, '/log')
const failures = Array.isArray(logs) ? logs.filter(row => /login failure/i.test(String(row.message))) : null
const sources = {}
const events = failures?.map(row => {
const message = String(row.message), source = message.match(/from ([\d.]+)/)?.[1] ?? 'unparsed'
const origin = source === '161.104.46.222' ? 'test-debian' : 'other-or-unidentified'
sources[origin] = (sources[origin] ?? 0) + 1
return { time: row.time ?? null, origin, via: message.match(/via (\w+)/)?.[1] ?? null }
})
report.diagnostics.push({ host, clock: clock ? { date: clock.date, time: clock.time, gmtOffset: clock['gmt-offset'] } : null, wwwServices: Array.isArray(services) ? services.filter(row => row.name === 'www').map(row => ({ disabled: row.disabled, maxSessions: row['max-sessions'] ?? null, dynamic: row.dynamic ?? null })) : null, retainedAuthFailures: failures?.length ?? null, sources, recentFailures: events?.slice(-20) ?? null, retainedLogIsNotCurrentRate: true })
}
report.completedAt = new Date().toISOString()
report.summary = hosts.map(host => { const rows = report.requests.filter(row => row.host === host); return { host, requests: rows.length, ok: rows.filter(row => row.status === 200).length, unauthorized: rows.filter(row => row.status === 401).length, serverErrors: rows.filter(row => row.status >= 500).length, transportErrors: rows.filter(row => row.status === null).length } })
fs.writeFileSync(output, JSON.stringify(report, null, 2) + '\n')
console.log(JSON.stringify({ vantage, summary: report.summary, configurationWrites: 0 }))
@@ -0,0 +1,12 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
const hosts=(process.env.MMAPP_TEST_CHR_HOSTS??'').split(',');assert.deepEqual(hosts,['194.226.121.150','104.165.244.7'],'Explicit owned lab hosts required')
const username=process.env.MMAPP_TEST_CHR_USER,password=process.env.MMAPP_TEST_CHR_PASSWORD;assert(username&&password,'Explicit test credentials required')
const authorization='Basic '+Buffer.from(username+':'+password).toString('base64'),paths=['/system/resource','/interface/gre','/routing/bgp/session','/routing/ospf/neighbor'],cases=[],start=Date.now(),deadline=start+360000,samples=20,periodMs=10000
const sleep=ms=>new Promise(r=>setTimeout(r,ms)),output='docs/ux-ui/acceptance/chr224-rest-observation-2026-10-14.json'
function save(completed){const failed=cases.filter(c=>c.status!==200);fs.writeFileSync(output,JSON.stringify({date:'2026-10-14',block:'12.5.224',startedAt:new Date(start).toISOString(),completedAt:new Date().toISOString(),plannedSamples:samples,completedSamples:completed,periodMs,requests:cases.length,failedRequests:failed.length,cases,configurationWrites:0,passwordChanges:0,scope:'Low-rate direct native REST with Connection close; short window, no burst/load/physical WAN/SLA/root-cause proof'},null,2)+'\n')}
for(let sample=1;sample<=samples;sample++){
if(Date.now()>deadline)break
for(const host of hosts)for(const path of paths){const began=Date.now();try{const r=await fetch('http://'+host+'/rest'+path,{headers:{authorization,Connection:'close'},signal:AbortSignal.timeout(8000)});await r.arrayBuffer();cases.push({sample,host,path,status:r.status,durationMs:Date.now()-began})}catch(e){cases.push({sample,host,path,status:null,errorName:e.name,durationMs:Date.now()-began})}await sleep(150)}
save(sample);console.log(JSON.stringify({completedSamples:sample,requests:cases.length,failedRequests:cases.filter(c=>c.status!==200).length}));if(sample<samples)await sleep(Math.max(0,start+sample*periodMs-Date.now()))
}
@@ -0,0 +1,71 @@
// Real Portal login; credentials stay in the caller's ignored private fixture.
import fs from 'node:fs';
import assert from 'node:assert/strict';
import { chromium, expect } from '@playwright/test';
(async () => {
const phase = process.argv[2];
assert(['guard', 'remove', 'repeat'].includes(phase));
const login = JSON.parse(fs.readFileSync(process.env.MMAPP_PORTAL_FIXTURE || 'tmp/portal195-private.json'));
const own = JSON.parse(fs.readFileSync(process.env.MMAPP_CONTAINER_FIXTURE || 'tmp/state248-public.json'));
const reportPath = 'docs/ux-ui/acceptance/containers248-live-ui.json';
const report = fs.existsSync(reportPath) ? JSON.parse(fs.readFileSync(reportPath)) : { date: '2026-10-14', block: '12.5.248', realPortalLogin: true, noMocks: true, steps: [], pageErrors: [] };
const browser = await chromium.launch({ channel: 'msedge', headless: true });
try {
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
page.on('pageerror', error => report.pageErrors.push(error.message));
await page.goto((process.env.MMAPP_TEST_URL || 'http://localhost:3000') + '/containers');
await page.getByLabel('Email', { exact: true }).fill(login.email);
await page.getByLabel('Пароль', { exact: true }).fill(login.password);
await page.getByRole('button', { name: 'Войти', exact: true }).click();
await expect(page.getByRole('navigation', { name: 'Путь страницы' })).toContainText('Контейнеры', { timeout: 30000 });
const endpoint = `/api/servers/${own.serverId}/containers/remove`;
const direct = () => page.evaluate(async ({ endpoint, rosId }) => {
const r = await fetch(endpoint, { method: 'POST', headers: { Authorization: 'Bearer ' + localStorage.getItem('mmapp_token'), 'Content-Type': 'application/json' }, body: JSON.stringify({ rosId }) });
const body = await r.json();
return { status: r.status, ok: body.ok === true, error: body.error };
}, { endpoint, rosId: own.rosId });
if (phase === 'repeat') {
const result = await direct(); assert.equal(result.status, 409); assert.equal(result.ok, false); assert.match(result.error, /уже отсутствует/);
report.steps.push({ phase, status: result.status, absentConflict: true });
} else {
await page.getByRole('button', { name: 'Действия контейнера ' + own.name, exact: true }).click();
await page.getByRole('menuitem', { name: 'Удалить', exact: true }).click();
const dialog = page.getByRole('alertdialog');
await expect(dialog).toContainText(own.name);
if (phase === 'guard') {
report.layout = [];
for (const width of [1440, 390]) {
await page.setViewportSize({ width, height: 1000 });
await expect(dialog).toContainText('после подтверждённой остановки');
await expect(dialog.getByRole('button', { name: 'Удалить', exact: true })).toBeDisabled();
await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth > innerWidth), { timeout: 5000 }).toBe(false);
const layout = await page.evaluate(() => ({ viewport: innerWidth, scrollWidth: document.documentElement.scrollWidth }));
(report.layout ??= []).push(layout);
if (layout.scrollWidth > layout.viewport) {
report.overflow = await page.evaluate(() => Array.from(document.querySelectorAll('*')).map(el => ({tag:el.tagName,slot:el.getAttribute('data-slot'),left:el.getBoundingClientRect().left,right:el.getBoundingClientRect().right,width:el.getBoundingClientRect().width,position:getComputedStyle(el).position})).filter(el=>el.right>innerWidth+1).slice(0,20));
await page.screenshot({ path: `tmp/containers248-guard-${width}.png`, fullPage: true });
}
assert.equal(layout.scrollWidth > layout.viewport, false);
}
const result = await direct(); assert.equal(result.status, 409); assert.equal(result.ok, false);
report.steps.push({ phase, viewports: [1440, 390], uiDeleteDisabled: true, backendConflict: result.status });
} else {
await expect(dialog).toContainText('остановленный контейнер');
const response = page.waitForResponse(r => new URL(r.url()).pathname === endpoint && r.request().method() === 'POST');
await dialog.getByRole('button', { name: 'Удалить', exact: true }).click();
const r = await response; assert.equal(r.request().postDataJSON().rosId, own.rosId); assert.equal(r.status(), 200); assert.equal((await r.json()).ok, true);
await expect(dialog).toBeHidden();
await expect(page.getByRole('button', { name: 'Действия контейнера ' + own.name, exact: true })).toHaveCount(0);
report.steps.push({ phase, status: 200, receiptOk: true, targetMatched: true, rowAbsent: true });
}
}
assert.deepEqual(report.pageErrors, []);
delete report.overflow;
if (report.failure?.phase === phase) { (report.harnessFailures ??= []).push(report.failure); delete report.failure; }
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + '\n');
console.log('Live container removal: ' + phase + ' passed');
} catch (error) {
report.failure = { phase, name: error.name, ...(error.name === 'AssertionError' ? { actual: error.actual, expected: error.expected } : {}) }; fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + '\n'); throw error;
} finally { await browser.close(); }
})().catch(error => { console.error('Live container removal failed: ' + error.name); process.exitCode = 1; });
@@ -0,0 +1,97 @@
import fs from 'node:fs';
import assert from 'node:assert/strict';
import { randomUUID, createHash } from 'node:crypto';
const { pool } = await import('/app/backend/dist/db/index.js');
const { MikrotikClient } = await import('/app/backend/dist/services/mikrotik.js');
const rows = (await pool.query("SELECT * FROM servers WHERE host='104.165.244.7' AND enabled=true")).rows; assert.equal(rows.length, 1);
const server = rows[0], client = new MikrotikClient({ host: server.host, port: 80, useSsl: false, verifySsl: false, username: server.username, password: server.password });
const controlRows = (await pool.query("SELECT * FROM servers WHERE host='194.226.121.150' AND enabled=true")).rows; assert.equal(controlRows.length, 1);
const control = new MikrotikClient({ host: controlRows[0].host, port: 80, useSsl: false, verifySsl: false, username: controlRows[0].username, password: controlRows[0].password });
const statePath = '/checks/state247-private.json', phase = process.argv[2];
const save = state => fs.writeFileSync(statePath, JSON.stringify(state), { mode: 0o600 });
const sha = value => createHash('sha256').update(value).digest('hex');
const normalizedExport = value => value.split(/\r?\n/).filter(line => !line.startsWith('#') && line.trim()).join('\n');
// RouterOS may list available, uninstalled extra packages after reboot; they have no installed version.
const packages = rows => rows.filter(p => typeof p.version === 'string' && p.version.trim()).map(p => ({ name: p.name, version: p.version, disabled: p.disabled ?? 'false' })).sort((a,b) => a.name.localeCompare(b.name));
const ownContainers = async state => (await client.get('/container', 5000)).filter(c => c.interface === state.veth && c.comment === state.comment);
const nativeStatus = row => row.status ?? (row.running === 'true' && row.stopped !== 'true' ? 'running' : row.stopped === 'true' && row.running !== 'true' ? 'stopped' : 'unknown');
async function poll(read, accept, maxMs = 150000) {
const until = Date.now() + maxMs; let last;
while (Date.now() < until) { try { last = await read(); if (accept(last)) return last; } catch {} await new Promise(r => setTimeout(r, 1500)); }
throw Error('Native state not confirmed within polling budget');
}
async function reboot(state, key) {
assert(!state[key], 'Never repeat a possibly applied reboot');
state[key] = { requested: true, acknowledgement: 'unknown' }; save(state);
try { await client.post('/system/reboot', {}, 5000); state[key].acknowledgement = 'confirmed'; }
catch (e) { state[key].errorName = e.name; state[key].status = e.statusCode ?? null; }
save(state); console.log(JSON.stringify({ phase, rebootRequestedOnce: true, acknowledgement: state[key].acknowledgement }));
}
try {
if (phase === 'init') {
assert(!fs.existsSync(statePath));
const uuid = randomUUID().slice(0,8), state = { serverId: Number(server.id), targetHost: server.host, veth: 'mmc247-' + uuid, root: 'mmc247-' + uuid + '-root', comment: 'mmc247-' + uuid, imageFile: 'mmc247-busybox.tar', packageFile: 'container-7.24.5.npk', packageSize: 1163409, imageSize: 2226176, beforePackages: packages(await client.get('/system/package')), beforeFiles: await client.get('/file'), beforeExport: normalizedExport(await client.exportConfigScript()), stages: [] };
assert(!state.beforePackages.some(p => p.name === 'container')); assert(state.beforePackages.every(p => p.version === '7.24.5'));
assert(!state.beforeFiles.some(f => f.name === state.imageFile || f.name === state.packageFile || f.name.startsWith(state.root)));
assert(!(await client.get('/interface')).some(i => i.name === state.veth)); await control.get('/system/resource', 5000);
state.beforeExportSha256 = sha(state.beforeExport); save(state); console.log('Private native baseline captured; control CHR reachable');
} else {
const state = JSON.parse(fs.readFileSync(statePath)); assert(/^mmc247-[a-f0-9]{8}$/.test(state.veth)); assert.equal(state.root, state.veth + '-root');
if (phase === 'reboot-install') {
const files = await client.get('/file');
assert(files.some(f => f.name === state.packageFile && Number(f.size) === state.packageSize));
assert(files.some(f => f.name === state.imageFile && Number(f.size) === state.imageSize));
await control.get('/system/resource', 5000); await reboot(state, 'installReboot');
} else if (phase === 'installed') {
const p = await poll(() => client.get('/system/package', 4000), p => p.some(p => p.name === 'container' && p.version === '7.24.5' && p.disabled === 'false'));
assert.equal((await client.get('/container')).length, 0); await control.get('/system/resource', 5000);
state.afterInstallPackages = packages(p); state.stages.push({ phase, packageInstalled: true, version: '7.24.5', controlReachable: true }); save(state); console.log('Container package installed on7.24.5; native endpoint200/control reachable');
} else if (phase === 'create') {
assert(state.afterInstallPackages); assert.equal((await ownContainers(state)).length, 0);
await client.put('/interface/veth', { name: state.veth, address: '198.18.247.2/30', gateway: '198.18.247.1', comment: state.comment });
await client.put('/container', { file: state.imageFile, interface: state.veth, 'root-dir': state.root, cmd: '/bin/sleep 600', 'start-on-boot': 'false', comment: state.comment });
const containers = await poll(() => ownContainers(state), rows => rows.length === 1 && nativeStatus(rows[0]) === 'stopped'); assert.equal(containers.length, 1);
state.containerId = containers[0]['.id']; state.stages.push({ phase, initialStatus: 'stopped' }); save(state);
fs.writeFileSync('/checks/state247-public.json', JSON.stringify({ serverId: state.serverId, rosId: state.containerId, interface: state.veth, comment: state.comment, root: state.root })); console.log('Own container extracted/stopped; public identity saved');
} else if (phase === 'adopt-created') {
const containers = await ownContainers(state); assert.equal(containers.length, 1); assert.equal(nativeStatus(containers[0]), 'stopped');
state.containerId = containers[0]['.id']; state.stages.push({ phase: 'create', initialStatus: 'stopped', observedNativeStoppedFlag: containers[0].stopped }); save(state);
fs.writeFileSync('/checks/state247-public.json', JSON.stringify({ serverId: state.serverId, rosId: state.containerId, name: containers[0].name, interface: state.veth, comment: state.comment, root: state.root })); console.log('Existing own extracted container adopted read-only');
} else if (phase === 'running' || phase === 'stopped') {
const containers = await poll(() => ownContainers(state), rows => rows.length === 1 && nativeStatus(rows[0]) === phase, 30000);
assert.equal(containers[0]['.id'], state.containerId); state.stages.push({ phase, nativeConfirmed: true }); save(state); console.log('Native status confirmed: ' + phase);
} else if (phase === 'cleanup-network') {
const containers = await ownContainers(state); assert(containers.length <= 1);
if (containers.length) { if (nativeStatus(containers[0]) !== 'stopped') { await client.post('/container/stop', { '.id': containers[0]['.id'] }); await poll(() => ownContainers(state), rows => rows.length === 1 && nativeStatus(rows[0]) === 'stopped', 30000); } await client.delete('/container/' + encodeURIComponent(containers[0]['.id'])); }
const veth = (await client.get('/interface/veth')).filter(i => i.name === state.veth); assert(veth.length <= 1);
for (const i of veth) { assert.equal(i.comment, state.comment); await client.delete('/interface/veth/' + encodeURIComponent(i['.id'])); }
const files = await client.get('/file');
const ownFile = f => { const name = String(f.name).replace(/^\/+/, ''); return name === state.imageFile || name === state.packageFile || name === state.root || name.startsWith(state.root + '/'); };
assert(!state.beforeFiles.some(ownFile), 'Never remove an original file');
const own = files.filter(ownFile).sort((a,b) => b.name.length - a.name.length);
assert(own.every(f => !f.name.split('/').includes('..')));
for (const f of own) { const current = (await client.get('/file')).find(x => x.name === f.name); if (current) await client.removeFile(current['.id']); }
assert.equal((await ownContainers(state)).length, 0); assert(!(await client.get('/interface/veth')).some(i => i.name === state.veth));
assert(!(await client.get('/file')).some(ownFile));
state.stages.push({ phase, ownObjectsAbsent: true }); save(state); console.log('Own container/veth/image/root removed');
} else if (phase === 'uninstall') {
assert(state.stages.some(s => s.phase === 'cleanup-network'));
const p = (await client.get('/system/package')).filter(p => p.name === 'container'); assert.equal(p.length, 1); assert.equal(p[0].version, '7.24.5');
await client.post('/system/package/uninstall', { '.id': p[0]['.id'] });
const scheduled = (await client.get('/system/package')).find(p => p.name === 'container'); assert(['uninstall','scheduled for uninstall'].includes(scheduled.scheduled));
await reboot(state, 'uninstallReboot');
} else if (phase === 'reboot-uninstall') {
const scheduled = (await client.get('/system/package')).find(p => p.name === 'container'); assert(scheduled && ['uninstall','scheduled for uninstall'].includes(scheduled.scheduled));
await reboot(state, 'uninstallReboot');
} else if (phase === 'final') {
const p = await poll(() => client.get('/system/package', 4000), p => !packages(p).some(p => p.name === 'container'));
assert.deepEqual(packages(p), state.beforePackages);
// Automatic timezone discovery can settle after package removal/reboot.
// Wait for equality without excluding settings or logging the private export.
await poll(async () => normalizedExport(await client.exportConfigScript()), value => value === state.beforeExport);
await control.get('/system/resource', 5000);
const report = { date: '2026-10-14', block: '12.5.247', targetHost: server.host, controlHost: '194.226.121.150', stages: state.stages, installReboot: state.installReboot, uninstallReboot: state.uninstallReboot, originalPackagesMatched: true, originalNormalizedExportSha256: state.beforeExportSha256, originalNormalizedExportMatched: true, controlReachable: true, passwordChanges: 0, deviceModeChanges: 0, firmwareVersionChanged: false, passed: true };
fs.writeFileSync('/checks/containers247-native.json', JSON.stringify(report, null, 2) + '\n'); fs.unlinkSync(statePath); console.log(JSON.stringify(report));
} else throw Error('Unsupported phase');
}
} finally { await pool.end(); }
@@ -0,0 +1,34 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { createHash } from 'node:crypto';
const { pool } = await import('/app/backend/dist/db/index.js');
const { MikrotikClient } = await import('/app/backend/dist/services/mikrotik.js');
const { getAcmeSettingsPublic } = await import('/app/backend/dist/services/certificates-service.js');
const rows = (await pool.query("SELECT * FROM servers WHERE host IN ('194.226.121.150','104.165.244.7') ORDER BY host")).rows;
assert.equal(rows.length, 2); assert(rows.every(s => s.enabled && s.port === 80 && !s.use_ssl));
const report = { date: '2026-10-14', block: '12.5.243', scope: 'Read-only availability, not container lifecycle or ACME issuance', hosts: [], requests: [], routerosWrites: 0, databaseWrites: 0, dnsWrites: 0, passwordChanges: 0 };
async function read(client, host, endpoint) {
try { const result = await client.get(endpoint, 8000); report.requests.push({ host, path: endpoint, status: 200 }); return result; }
catch (e) { report.requests.push({ host, path: endpoint, status: e.statusCode ?? null, errorName: e.name, errorBodySha256: e.body ? createHash('sha256').update(e.body).digest('hex') : null }); return undefined; }
}
try {
for (const server of rows) {
const client = new MikrotikClient({ host: server.host, port: 80, useSsl: false, verifySsl: false, username: server.username, password: server.password });
const packages = await read(client, server.host, '/system/package');
const resource = await read(client, server.host, '/system/resource');
const mode = await read(client, server.host, '/system/device-mode');
const disks = await read(client, server.host, '/disk');
const containers = await read(client, server.host, '/container');
const m = Array.isArray(mode) ? mode[0] : mode;
report.hosts.push({ host: server.host, packages: Array.isArray(packages) ? packages.map(p => ({ name: p.name, version: p.version, disabled: p.disabled })) : null, resource: resource ? { version: resource.version, architecture: resource['architecture-name'], boardName: resource['board-name'], freeHddSpace: resource['free-hdd-space'], totalHddSpace: resource['total-hdd-space'], freeMemory: resource['free-memory'] } : null, deviceMode: m ? { mode: m.mode ?? null, container: m.container ?? null, flagged: m.flagged ?? null } : null, disks: Array.isArray(disks) ? disks.length : null, containers: Array.isArray(containers) ? { count: containers.length, statuses: containers.map(c => c.status ?? null) } : null });
}
const acme = await getAcmeSettingsPublic();
report.acme = { tokenConfigured: acme.tokenConfigured, defaultZoneConfigured: Boolean(acme.defaultZoneId), directoryHost: null, directoryStatus: null };
const directory = new URL(acme.directoryUrl);
assert(!directory.username && !directory.password);
report.acme.directoryHost = directory.hostname;
if (directory.protocol === 'https:' && ['acme-v02.api.letsencrypt.org','acme-staging-v02.api.letsencrypt.org'].includes(directory.hostname) && directory.pathname === '/directory') {
const r = await fetch(directory, { signal: AbortSignal.timeout(8000) }); report.acme.directoryStatus = r.status;
if (r.ok) { const data = await r.json(); report.acme.directoryKeys = Object.keys(data).sort(); }
}
} finally { await pool.end(); fs.writeFileSync('/checks/external-jobs243-availability.json', JSON.stringify(report, null, 2) + '\n'); console.log(JSON.stringify(report)); }
@@ -0,0 +1,25 @@
import fs from 'node:fs'
import assert from 'node:assert/strict'
import {execFileSync} from 'node:child_process'
import pg from 'pg'
const database=process.env.MMAPP_OWN_SQL_DB,root='/app/backend/acceptance'
assert(/^mm_accept_166_\d+$/.test(database))
const url=new URL(process.env.DATABASE_URL);assert.equal(url.pathname,'/'+database);assert.equal(url.username,database)
const client=new pg.Client({connectionString:url.toString()});await client.connect()
assert.equal((await client.query('SELECT current_database() AS name')).rows[0].name,database)
const role=(await client.query('SELECT rolsuper,rolcreatedb,rolcreaterole,rolbypassrls FROM pg_roles WHERE rolname=current_user')).rows[0]
assert(Object.values(role).every(value=>value===false));await client.end()
const pkg=JSON.parse(fs.readFileSync('/app/backend/package.json'))
const profiles=pkg.scripts['test:all'].split(/\s*&&\s*/).map(c=>{const m=/^npm run (test:[\w-]+)$/.exec(c);assert(m);return m[1]})
const report={date:'2026-10-14',block:'12.5.222',scope:'full backend profile list on owned isolated PostgreSQL; no production DSN',database,profiles:[],passed:false}
for(const profile of profiles){
const started=Date.now();let output='',passed=true,errorName
try{output=execFileSync('npm',['--prefix','backend','run',profile],{cwd:'/app',timeout:240_000,maxBuffer:16*1024**2,encoding:'utf8',env:{...process.env,NODE_ENV:'test',GEOIP_DIR:'/tmp/'+database+'/geoip',DATABASE_PATH:'/tmp/'+database+'/legacy.db'},stdio:['ignore','pipe','pipe']})}
catch(e){passed=false;errorName=e.name;output=String(e.stdout??'')+'\n'+String(e.stderr??'')}
fs.writeFileSync(root+'/'+profile.replace(':','-')+'-private.log',output,{mode:0o600})
const skippedLines=output.split('\n').filter(s=>/\bskip(?:ped)?\b/i.test(s));const unavailableSql=skippedLines.filter(s=>/PostgreSQL|owned|own SQL|explicit owned|database|MMAPP_OWN/i.test(s)&&!s.includes('skip/release/recovery passed'));
report.profiles.push({profile,passed,durationMs:Date.now()-started,errorName,skippedLineCount:skippedLines.length,unavailableSqlBranches:unavailableSql.map(line=>line.trim())})
fs.writeFileSync(root+'/owned222-result.json',JSON.stringify(report,null,2));console.log(JSON.stringify(report.profiles.at(-1)))
}
report.passed=report.profiles.every(p=>p.passed);fs.writeFileSync(root+'/owned222-result.json',JSON.stringify(report,null,2))
if(!report.passed)process.exitCode=1
@@ -0,0 +1,66 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { createHash } from 'node:crypto';
const database = process.env.MMAPP_OWN_SQL_DB;
assert(/^mm_accept_166_\d+$/.test(database));
const url = new URL(process.env.DATABASE_URL); assert.equal(url.pathname, '/' + database); assert.equal(url.username, database);
assert.equal(process.env.GEOIP_DIR, '/tmp/' + database + '/geoip');
fs.mkdirSync('/tmp/' + database, { recursive: true }); process.chdir('/tmp/' + database);
const { pool, closePool } = await import('/app/backend/dist/db/index.js');
const { initDatabase } = await import('/app/backend/dist/db/bootstrap.js');
const role = (await pool.query('SELECT rolsuper,rolcreatedb,rolcreaterole,rolbypassrls FROM pg_roles WHERE rolname=current_user')).rows[0];
assert(Object.values(role).every(v => v === false)); await initDatabase();
const { collectGeoipUpdateOnce, setGeoipFetchForTests, resetGeoipUpdateForTests } = await import('/app/backend/dist/services/geoip-update-collector.js');
const { getGeoipSettings, updateGeoipSettings } = await import('/app/backend/dist/services/geoip-settings.js');
const { geoipReadersStatus, lookupGeoip, GEOIP_COUNTRY_FILE, GEOIP_ASN_FILE } = await import('/app/backend/dist/services/traffic-flow-geoip.js');
const { databaseBackgroundWork } = await import('/app/backend/dist/services/database-background-work.js');
const report = { date: '2026-10-14', block: '12.5.242', passed: false, scope: 'Compiled GeoIP collector + owned PostgreSQL/files + actual public mirror; controlled negative bodies separately marked', mirrorRequests: [], checks: [], chrContacts: 0, mainDatabaseWrites: 0 };
const file = name => path.join(process.env.GEOIP_DIR, name);
const sha = name => createHash('sha256').update(fs.readFileSync(file(name))).digest('hex');
const save = () => fs.writeFileSync('/checks/geoip-mirror242.json', JSON.stringify(report, null, 2) + '\n');
const check = (name, detail = {}) => { report.checks.push({ name, ...detail }); save(); };
const realFetch = globalThis.fetch.bind(globalThis);
async function observedFetch(url, options) {
const started = Date.now(), response = await realFetch(url, options);
report.mirrorRequests.push({ file: new URL(String(url)).pathname.split('/').at(-1), conditional: Boolean(options?.headers?.['If-None-Match']), status: response.status, headersMs: Date.now() - started }); save(); return response;
}
try {
await updateGeoipSettings({ enabled: false }); setGeoipFetchForTests(observedFetch);
let snapshot = await collectGeoipUpdateOnce({ force: true });
assert(!snapshot.fatalError); assert.deepEqual(snapshot.errors, []); assert.equal(snapshot.downloaded, 2);
assert(geoipReadersStatus().countryLoaded && geoipReadersStatus().asnLoaded);
const initial = { country: sha(GEOIP_COUNTRY_FILE), asn: sha(GEOIP_ASN_FILE) };
const lookup = lookupGeoip('8.8.8.8'); assert.equal(lookup?.country, 'US'); assert.equal(lookup?.asn, 15169);
check('actual-two-downloads-valid-mmdb-and-lookup', { bytes: snapshot.bytes, hashes: initial, settings: await getGeoipSettings(), lookup: { country: lookup.country, asn: lookup.asn } });
const first = collectGeoipUpdateOnce({ force: true });
await assert.rejects(collectGeoipUpdateOnce({ force: true }), e => e.statusCode === 409);
assert.equal((await collectGeoipUpdateOnce()).skipped, true);
snapshot = await first; assert.deepEqual(snapshot.errors, []); assert.equal(snapshot.skippedUnchanged, 2); assert.equal(snapshot.downloaded, 0);
check('actual-conditional304-and-concurrent409', { skippedUnchanged: 2, hashesUnchanged: sha(GEOIP_COUNTRY_FILE) === initial.country && sha(GEOIP_ASN_FILE) === initial.asn });
fs.unlinkSync(file(GEOIP_COUNTRY_FILE));
const beforeMissing = report.mirrorRequests.length; snapshot = await collectGeoipUpdateOnce({ force: true });
assert.deepEqual(snapshot.errors, []); assert.equal(snapshot.downloaded, 1); assert.equal(snapshot.skippedUnchanged, 1);
assert(report.mirrorRequests.slice(beforeMissing).some(r => r.file === GEOIP_COUNTRY_FILE && !r.conditional && r.status === 200));
assert.equal(sha(GEOIP_COUNTRY_FILE), initial.country);
check('missing-country-refetched-without-conditional-header');
fs.writeFileSync(file(GEOIP_COUNTRY_FILE), 'Own corrupt GeoIP fixture');
snapshot = await collectGeoipUpdateOnce({ force: true }); assert.deepEqual(snapshot.errors, []); assert.equal(snapshot.downloaded, 1); assert.equal(sha(GEOIP_COUNTRY_FILE), initial.country);
check('corrupt-local-country-refetched-and-validated');
const lastSuccess = (await getGeoipSettings()).lastSuccessAt;
setGeoipFetchForTests(async (url, options) => String(url).endsWith(GEOIP_COUNTRY_FILE) ? new Response('Own invalid MMDB body', { status: 200 }) : observedFetch(url, options));
snapshot = await collectGeoipUpdateOnce({ force: true }); assert.equal(snapshot.errors.length, 1); assert.equal(snapshot.downloaded, 0); assert.equal(snapshot.skippedUnchanged, 1);
assert.equal(sha(GEOIP_COUNTRY_FILE), initial.country); assert.equal((await getGeoipSettings()).lastSuccessAt, lastSuccess);
assert((await getGeoipSettings()).lastError); assert(!fs.existsSync(file(GEOIP_COUNTRY_FILE) + '.tmp'));
check('controlled-invalid-body-preserves-file-and-last-success', { negativeCountryResponse: 'controlled-200-invalid-MMDB', realAsnStatus: 304 });
fs.unlinkSync(file(GEOIP_COUNTRY_FILE));
setGeoipFetchForTests(async (url, options) => String(url).endsWith(GEOIP_COUNTRY_FILE) ? new Response(null, { status: 304 }) : observedFetch(url, options));
snapshot = await collectGeoipUpdateOnce({ force: true }); assert.equal(snapshot.errors.length, 1); assert.equal(snapshot.skippedUnchanged, 1); assert(snapshot.errors[0].includes('304 без проверенной локальной базы'));
assert.equal((await getGeoipSettings()).lastSuccessAt, lastSuccess);
check('controlled-uncached304-is-error-with-last-success-retained');
setGeoipFetchForTests(observedFetch); snapshot = await collectGeoipUpdateOnce({ force: true }); assert.deepEqual(snapshot.errors, []); assert.equal(snapshot.downloaded, 1);
assert.equal(sha(GEOIP_COUNTRY_FILE), initial.country); assert.equal(sha(GEOIP_ASN_FILE), initial.asn);
const beforeSkip = report.mirrorRequests.length; assert.equal((await collectGeoipUpdateOnce()).skipped, true); assert.equal(report.mirrorRequests.length, beforeSkip);
check('actual-recovery-and-disabled-background-no-fetch'); report.passed = true;
} catch (e) { report.failure = { name: e.name }; process.exitCode = 1; }
finally { resetGeoipUpdateForTests(); await databaseBackgroundWork.drain(); await closePool(); save(); console.log(JSON.stringify(report)); }
@@ -0,0 +1,18 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import {createServer} from 'node:http'
import {randomBytes} from 'node:crypto'
const database=process.env.MMAPP_OWN_SQL_DB,url=new URL(process.env.DATABASE_URL)
assert(/^mm_accept_166_\d+$/.test(database??''));assert.equal(url.pathname,'/'+database);assert.equal(url.username,database)
process.env.AUTH_REQUIRED='true';process.env.AUTH_JWT_SECRET=randomBytes(32).toString('hex');process.env.AUTH_ISSUER='https://own212.invalid';process.env.AUTH_PORTAL_URL='https://own212.invalid'
const {pool,closePool}=await import('/app/backend/dist/db/index.js'),{initDatabase}=await import('/app/backend/dist/db/bootstrap.js'),{buildApp}=await import('/app/backend/dist/index.js')
assert.equal((await pool.query('SELECT current_database() AS name')).rows[0].name,database);assert(Object.values((await pool.query('SELECT rolsuper,rolcreatedb,rolcreaterole,rolbypassrls FROM pg_roles WHERE rolname=current_user')).rows[0]).every(v=>v===false))
const rows=[],writes=[],cases=[];let app,serverId,nativeCalls=0;
const native=createServer(async(req,res)=>{nativeCalls++;let text='';for await(const chunk of req)text+=chunk;const body=text?JSON.parse(text):{};res.setHeader('content-type','application/json');if(req.method==='GET'){res.end(JSON.stringify(req.url.endsWith('/interface/gre')?rows:[]));return}writes.push({method:req.method,path:req.url,body});if(req.method==='PUT'){const row={...body,'.id':'*'+(rows.length+1),running:'false'};rows.push(row);res.end(JSON.stringify(row));return}if(req.method==='PATCH'){const id=decodeURIComponent(req.url.split('/').pop()),row=rows.find(r=>r['.id']===id);assert(row);Object.assign(row,body);res.end(JSON.stringify(row));return}res.statusCode=500;res.end('{}')});await new Promise(r=>native.listen(0,'127.0.0.1',r));
const report={date:'2026-10-13',block:'12.5.212',database,cases,scope:'Production compiled GRE create/patch using isolated PostgreSQL and native HTTP fixture',passed:false};
try{await initDatabase();serverId=Number((await pool.query("INSERT INTO servers(name,host,port,use_ssl,enabled,username,password) VALUES('own212-native','127.0.0.1',$1,false,true,'fixture','fixture') RETURNING id",[native.address().port])).rows[0].id);app=await buildApp({logger:false,startScheduler:false});await app.ready();const token=app.jwt.sign({sub:'own212-writer',apps:['mm'],permissions:['mm:network:write'],iss:process.env.AUTH_ISSUER},{expiresIn:'5m'}),headers={authorization:'Bearer '+token};
for(const field of ['keepaliveInterval','keepaliveRetries'])for(const value of [0,-1,1.5,4294967296]){const before=nativeCalls;const r=await app.inject({method:'POST',url:'/api/gre/tunnels',headers,payload:{serverId:String(serverId),name:'invalid',remoteAddress:'192.0.2.2',[field]:value}});assert.equal(r.statusCode,400);assert.equal(nativeCalls,before);cases.push({field,value,status:400,nativeCalls:0})}
for(const params of [{},{keepaliveInterval:60},{keepaliveRetries:3},{keepaliveInterval:90,keepaliveRetries:7}]){const name='own212-'+rows.length;const r=await app.inject({method:'POST',url:'/api/gre/tunnels',headers,payload:{serverId:String(serverId),name,remoteAddress:'192.0.2.2',enabled:false,...params}});assert.equal(r.statusCode,201);const expected=(params.keepaliveInterval??10)+'s,'+(params.keepaliveRetries??10);assert.equal(rows.at(-1).keepalive,expected);cases.push({operation:'create',params,keepalive:expected,status:201})}
rows[0].keepalive='1m,3';const patch=await app.inject({method:'PATCH',url:'/api/gre/tunnels',headers,payload:{serverId:String(serverId),rosId:rows[0]['.id'],comment:'own212 updated'}});assert.equal(patch.statusCode,200);assert.equal(rows[0].keepalive,'1m,3');assert.equal(rows[0].comment,'own212 updated');cases.push({operation:'patch-other-field',nativeKeepalivePreserved:'1m,3',status:200});const read=await app.inject({method:'GET',url:'/api/gre/tunnels?serverId='+serverId,headers});assert.equal(read.statusCode,200);assert.equal(read.json().tunnels.find(t=>t.name===rows[0].name).keepaliveInterval,60);cases.push({operation:'read-minutes',seconds:60,status:200});const encrypted=await app.inject({method:'POST',url:'/api/gre/tunnels',headers,payload:{serverId:String(serverId),name:'own212-psk',remoteAddress:'192.0.2.2',enabled:false,ipsecSecret:'own212-synthetic-psk'}});assert.equal(encrypted.statusCode,201);const secured=rows.find(r=>r.name==='own212-psk');assert.equal(secured['allow-fast-path'],'no');cases.push({operation:'psk-create-default-fastpath',status:201,fastPath:'no'});let before=nativeCalls;const conflict=await app.inject({method:'POST',url:'/api/gre/tunnels',headers,payload:{serverId:String(serverId),name:'conflict',remoteAddress:'192.0.2.2',ipsecSecret:'own212-synthetic-psk',allowFastPath:true}});assert.equal(conflict.statusCode,400);assert.equal(nativeCalls,before);cases.push({operation:'explicit-psk-fastpath-conflict',status:400,nativeCalls:0});before=writes.length;const invalidPatch=await app.inject({method:'PATCH',url:'/api/gre/tunnels',headers,payload:{serverId:String(serverId),rosId:secured['.id'],allowFastPath:true}});assert.equal(invalidPatch.statusCode,400);assert.equal(writes.length,before);assert.equal(secured['allow-fast-path'],'no');cases.push({operation:'existing-psk-fastpath-conflict',status:400,nativeWrites:0});const upgrade=await app.inject({method:'PATCH',url:'/api/gre/tunnels',headers,payload:{serverId:String(serverId),rosId:rows[1]['.id'],ipsecSecret:'own212-synthetic-psk'}});assert.equal(upgrade.statusCode,200);assert.equal(rows[1]['allow-fast-path'],'no');cases.push({operation:'enable-psk-with-omitted-fastpath',status:200,fastPath:'no'});report.passed=true;report.nativeWrites=writes.length;
}catch(e){report.errorName=e.name;fs.writeFileSync('/app/backend/acceptance/gre212-private.log',String(e.stack??e),{mode:0o600})}finally{if(app)await app.close();native.closeAllConnections();await new Promise(r=>native.close(r));await closePool();fs.writeFileSync('/app/backend/acceptance/gre212-keepalive-result.json',JSON.stringify(report,null,2));console.log(JSON.stringify(report))}if(!report.passed)process.exitCode=1;
@@ -0,0 +1,24 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import {createServer} from 'node:http'
const database=process.env.MMAPP_OWN_SQL_DB,url=new URL(process.env.DATABASE_URL)
assert(/^mm_accept_166_\d+$/.test(database??''));assert.equal(url.pathname,'/'+database);assert.equal(url.username,database)
const {pool,closePool}=await import('/app/backend/dist/db/index.js'),{initDatabase}=await import('/app/backend/dist/db/bootstrap.js'),{collectGreBgpSnapshotOnce}=await import('/app/backend/dist/services/gre-bgp-snapshot-collector.js')
assert.equal((await pool.query('SELECT current_database() AS name')).rows[0].name,database);assert(Object.values((await pool.query('SELECT rolsuper,rolcreatedb,rolcreaterole,rolbypassrls FROM pg_roles WHERE rolname=current_user')).rows[0]).every(v=>v===false))
const baseline=process.env.MMAPP_BASELINE==='true',cases=[];let phase='populated',nativeCalls=0,nativeWrites=0,disabledCalls=0
const fixture=label=>createServer((req,res)=>{nativeCalls++;if(label==='disabled')disabledCalls++;if(req.method!=='GET')nativeWrites++;res.setHeader('content-type','application/json');const gre=req.url==='/rest/interface/gre',failed=(gre&&phase==='gre-failed')||(label==='second'&&(gre&&['gre-partial','both-partial'].includes(phase)||!gre&&['bgp-partial','both-partial'].includes(phase)));if(failed){res.statusCode=401;res.end(JSON.stringify({error:401,message:'own219 denied'}));return}const row=gre?{'.id':'*1',name:'own219-gre',running:'true',disabled:'false'}:{'.id':'*1',name:'own219-peer',established:'true','remote.as':'65219','local.as':'65220'};const rows=phase==='empty'?[]:gre&&label==='second'&&phase==='malformed'?[{unknown:'fixture'}]:gre&&label==='second'&&phase==='duplicate'?[row,row]:[row];res.end(JSON.stringify(rows))})
const natives=['first','second','disabled'].map(fixture);for(const n of natives)await new Promise(r=>n.listen(0,'127.0.0.1',r))
const report={date:'2026-10-14',block:'12.5.219',baseline,database,cases,scope:'Actual compiled GRE/BGP sources and snapshot collector, owned SQL/native HTTP fixture; no API auth claim',passed:false}
try{
await initDatabase();for(let i=0;i<natives.length;i++)await pool.query("INSERT INTO servers(name,host,port,use_ssl,enabled,username,password,type) VALUES($1,'127.0.0.1',$2,false,$3,'fixture','fixture',$4)",['own219-'+i,natives[i].address().port,i<2,i===2?'home-router':'exit-node'])
for(phase of ['populated','empty','gre-partial','gre-failed','bgp-partial','both-partial','malformed','duplicate','disabled','recovery']){
if(phase==='disabled')await pool.query('UPDATE servers SET enabled=false');if(phase==='recovery')await pool.query("UPDATE servers SET enabled=true WHERE name IN ('own219-0','own219-1')")
const before=nativeCalls,snapshot=await collectGreBgpSnapshotOnce(),greFail=['gre-partial','gre-failed','both-partial','malformed','duplicate'].includes(phase),bgpFail=['bgp-partial','both-partial'].includes(phase),empty=['empty','disabled'].includes(phase),greCount=empty?0:greFail?(baseline?phase==='gre-failed'?0:phase==='malformed'?2:phase==='duplicate'?3:1:0):2,bgpCount=empty||bgpFail?0:2,errorCount=Number(bgpFail)+Number(greFail&&!baseline)
assert.equal(snapshot.greWritten,greCount);assert.equal(snapshot.bgpWritten,bgpCount);assert.equal(snapshot.errors.length,errorCount);assert.equal(snapshot.fatalError,undefined)
if(greFail&&!baseline)assert(snapshot.errors.some(e=>e.startsWith('GRE: Не опрошены устройства GRE:')))
if(bgpFail)assert(snapshot.errors.some(e=>e.startsWith('BGP: Не опрошены устройства BGP:')))
if(phase==='disabled')assert.equal(nativeCalls,before)
cases.push({phase,greWritten:snapshot.greWritten,bgpWritten:snapshot.bgpWritten,sourceErrors:snapshot.errors.length,nativeCalls:nativeCalls-before})
}
assert.equal(nativeWrites,0);assert.equal(disabledCalls,0);report.passed=true
}catch(e){report.errorName=e.name;fs.writeFileSync('/app/backend/acceptance/gre219-private.log',String(e.stack??e),{mode:0o600})}finally{for(const n of natives){n.closeAllConnections();await new Promise(r=>n.close(r))}await closePool();report.nativeWrites=nativeWrites;report.disabledHomeRouterCalls=disabledCalls;fs.writeFileSync('/app/backend/acceptance/gre219-'+(baseline?'baseline':'fixed')+'.json',JSON.stringify(report,null,2));console.log(JSON.stringify(report))}if(!report.passed)process.exitCode=1
@@ -0,0 +1,18 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import {createServer} from 'node:http'
import {randomBytes} from 'node:crypto'
const database=process.env.MMAPP_OWN_SQL_DB,url=new URL(process.env.DATABASE_URL)
assert(/^mm_accept_166_\d+$/.test(database??''));assert.equal(url.pathname,'/'+database);assert.equal(url.username,database)
process.env.AUTH_REQUIRED='true';process.env.AUTH_JWT_SECRET=randomBytes(32).toString('hex');process.env.AUTH_ISSUER='https://own210.invalid';process.env.AUTH_PORTAL_URL='https://own210.invalid'
const {pool,closePool}=await import('/app/backend/dist/db/index.js'),{initDatabase}=await import('/app/backend/dist/db/bootstrap.js'),{buildApp}=await import('/app/backend/dist/index.js')
assert.equal((await pool.query('SELECT current_database() AS name')).rows[0].name,database);assert(Object.values((await pool.query('SELECT rolsuper,rolcreatedb,rolcreaterole,rolbypassrls FROM pg_roles WHERE rolname=current_user')).rows[0]).every(v=>v===false))
const rows=[],writes=[],cases=[];let app,serverId,nativeCalls=0;
const native=createServer(async(req,res)=>{nativeCalls++;let text='';for await(const chunk of req)text+=chunk;const body=text?JSON.parse(text):{};res.setHeader('content-type','application/json');if(req.method==='GET'){res.end(JSON.stringify(req.url.endsWith('/interface/gre')?rows:[]));return}writes.push({method:req.method,path:req.url,body});if(req.method==='PUT'){const row={...body,'.id':'*'+(rows.length+1),running:'false'};rows.push(row);res.end(JSON.stringify(row));return}if(req.method==='PATCH'){const id=decodeURIComponent(req.url.split('/').pop()),row=rows.find(r=>r['.id']===id);assert(row);Object.assign(row,body);res.end(JSON.stringify(row));return}res.statusCode=500;res.end('{}')});await new Promise(r=>native.listen(0,'127.0.0.1',r));
const report={date:'2026-10-13',block:'12.5.210',database,cases,scope:'Production compiled GRE create/patch using isolated PostgreSQL and native HTTP fixture',passed:false};
try{await initDatabase();serverId=Number((await pool.query("INSERT INTO servers(name,host,port,use_ssl,enabled,username,password) VALUES('own210-native','127.0.0.1',$1,false,true,'fixture','fixture') RETURNING id",[native.address().port])).rows[0].id);app=await buildApp({logger:false,startScheduler:false});await app.ready();const token=app.jwt.sign({sub:'own210-writer',apps:['mm'],permissions:['mm:network:write'],iss:process.env.AUTH_ISSUER},{expiresIn:'5m'}),headers={authorization:'Bearer '+token};
for(const field of ['keepaliveInterval','keepaliveRetries'])for(const value of [0,-1,1.5,4294967296]){const before=nativeCalls;const r=await app.inject({method:'POST',url:'/api/gre/tunnels',headers,payload:{serverId:String(serverId),name:'invalid',remoteAddress:'192.0.2.2',[field]:value}});assert.equal(r.statusCode,400);assert.equal(nativeCalls,before);cases.push({field,value,status:400,nativeCalls:0})}
for(const params of [{},{keepaliveInterval:60},{keepaliveRetries:3},{keepaliveInterval:90,keepaliveRetries:7}]){const name='own210-'+rows.length;const r=await app.inject({method:'POST',url:'/api/gre/tunnels',headers,payload:{serverId:String(serverId),name,remoteAddress:'192.0.2.2',enabled:false,...params}});assert.equal(r.statusCode,201);const expected=(params.keepaliveInterval??10)+'s,'+(params.keepaliveRetries??10);assert.equal(rows.at(-1).keepalive,expected);cases.push({operation:'create',params,keepalive:expected,status:201})}
rows[0].keepalive='1m,3';const patch=await app.inject({method:'PATCH',url:'/api/gre/tunnels',headers,payload:{serverId:String(serverId),rosId:rows[0]['.id'],comment:'own210 updated'}});assert.equal(patch.statusCode,200);assert.equal(rows[0].keepalive,'1m,3');assert.equal(rows[0].comment,'own210 updated');cases.push({operation:'patch-other-field',nativeKeepalivePreserved:'1m,3',status:200});const read=await app.inject({method:'GET',url:'/api/gre/tunnels?serverId='+serverId,headers});assert.equal(read.statusCode,200);assert.equal(read.json().tunnels.find(t=>t.name===rows[0].name).keepaliveInterval,60);cases.push({operation:'read-minutes',seconds:60,status:200});report.passed=true;report.nativeWrites=writes.length;
}catch(e){report.errorName=e.name;fs.writeFileSync('/app/backend/acceptance/gre210-private.log',String(e.stack??e),{mode:0o600})}finally{if(app)await app.close();native.closeAllConnections();await new Promise(r=>native.close(r));await closePool();fs.writeFileSync('/app/backend/acceptance/gre210-keepalive-result.json',JSON.stringify(report,null,2));console.log(JSON.stringify(report))}if(!report.passed)process.exitCode=1;
@@ -0,0 +1,26 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import {createServer} from 'node:http'
import {randomBytes} from 'node:crypto'
const database=process.env.MMAPP_OWN_SQL_DB,url=new URL(process.env.DATABASE_URL)
assert(/^mm_accept_166_\d+$/.test(database??''));assert.equal(url.pathname,'/'+database);assert.equal(url.username,database)
process.env.AUTH_REQUIRED='true';process.env.AUTH_JWT_SECRET=randomBytes(32).toString('hex');process.env.AUTH_ISSUER='https://own221.invalid';process.env.AUTH_PORTAL_URL='https://own221.invalid'
const {pool,closePool}=await import('/app/backend/dist/db/index.js'),{initDatabase}=await import('/app/backend/dist/db/bootstrap.js'),{collectGreBgpSnapshotOnce}=await import('/app/backend/dist/services/gre-bgp-snapshot-collector.js'),{buildApp}=await import('/app/backend/dist/index.js')
assert.equal((await pool.query('SELECT current_database() AS name')).rows[0].name,database);assert(Object.values((await pool.query('SELECT rolsuper,rolcreatedb,rolcreaterole,rolbypassrls FROM pg_roles WHERE rolname=current_user')).rows[0]).every(v=>v===false))
const baseline=process.env.MMAPP_BASELINE==='true',cases=[];let app,nativeCalls=0,nativeWrites=0,disabledCalls=0,variant='valid'
const fixture=label=>createServer((req,res)=>{nativeCalls++;if(label==='disabled')disabledCalls++;if(req.method!=='GET')nativeWrites++;res.setHeader('content-type','application/json');const gre=req.url==='/rest/interface/gre',bgp=req.url==='/rest/routing/bgp/session',normal={'.id':'*A',name:'own221-GRE «тест» / peer',disabled:'false',running:'true'},kinds={valid:[normal],empty:[],'missing-id':[{name:normal.name,disabled:'false',running:'true'}],'missing-name':[{'.id':'*A',disabled:'false',running:'true'}],'blank-id':[{...normal,'.id':''}],'blank-name':[{...normal,name:' '}],'whitespace-id':[{...normal,'.id':' *A'}],'number-field':[{...normal,mtu:1400}],'duplicate-id':[normal,{...normal,name:'other-GRE'}],'duplicate-name':[normal,{...normal,'.id':'*B'}],'trimmed-name-collision':[normal,{...normal,'.id':'*B',name:' '+normal.name+' '}]},rows=gre?(label==='second'?kinds[variant]:[normal]):bgp?[{'.id':'*1',name:'own221-peer',established:'true','remote.as':'65221','local.as':'65222'}]:[];res.end(JSON.stringify(rows))})
const natives=['first','second','disabled'].map(fixture);for(const n of natives)await new Promise(r=>n.listen(0,'127.0.0.1',r))
const report={date:'2026-10-14',block:'12.5.221',baseline,database,cases,scope:'Actual compiled Native GRE identity and actual API/config/snapshot paths, owned SQL/native HTTP fixture; signed reader/authenticated GRE API',passed:false}
try{
await initDatabase();const ids=[];for(let i=0;i<natives.length;i++)ids.push(Number((await pool.query("INSERT INTO servers(name,host,port,use_ssl,enabled,username,password,type) VALUES($1,'127.0.0.1',$2,false,$3,'fixture','fixture',$4) RETURNING id",['own221-'+i,natives[i].address().port,i<2,i===2?'home-router':'exit-node'])).rows[0].id))
app=await buildApp({logger:false,startScheduler:false});await app.ready();const token=app.jwt.sign({sub:'own221-reader',apps:['mm'],permissions:['mm:network:read'],iss:process.env.AUTH_ISSUER},{expiresIn:'5m'}),headers={authorization:'Bearer '+token};
for(variant of ['valid','empty','missing-id','missing-name','blank-id','blank-name','whitespace-id','number-field','duplicate-id','duplicate-name','trimmed-name-collision']){
const bad=!['valid','empty'].includes(variant),oldSourceAccepts=['valid','empty','whitespace-id','duplicate-name','trimmed-name-collision'].includes(variant),sourceUnknown=baseline?!oldSourceAccepts:bad,oldCount=['duplicate-id','duplicate-name','trimmed-name-collision'].includes(variant)?3:variant==='empty'?1:2,snapshot=await collectGreBgpSnapshotOnce()
assert.equal(snapshot.greWritten,sourceUnknown?0:oldCount);assert.equal(snapshot.bgpWritten,2);assert.equal(snapshot.errors.length,sourceUnknown?1:0);assert.equal(snapshot.fatalError,undefined)
cases.push({phase:variant,endpoint:'compiled collector',greWritten:snapshot.greWritten,bgpWritten:2,sourceErrors:snapshot.errors.length})
const before=Number((await pool.query("SELECT count(*) AS count FROM config_revisions WHERE server_id=$1 AND section='gre'",[ids[1]])).rows[0].count),api=await app.inject({method:'GET',url:'/api/gre/tunnels',headers}),body=api.json();assert.equal(api.statusCode,200);assert.equal(body.failures.length,bad&&!baseline?1:0);assert.equal(body.tunnels.length,bad&&!baseline?1:oldCount);const after=Number((await pool.query("SELECT count(*) AS count FROM config_revisions WHERE server_id=$1 AND section='gre'",[ids[1]])).rows[0].count);if(bad&&!baseline)assert.equal(after,before)
cases.push({phase:variant,endpoint:'signed GRE catalog',status:200,tunnels:body.tunnels.length,failures:body.failures.length,observedRevisionsAdded:after-before})
}
assert.equal(nativeWrites,0);assert.equal(disabledCalls,0);report.passed=true
}catch(e){report.errorName=e.name;fs.writeFileSync('/app/backend/acceptance/gre221-private.log',String(e.stack??e),{mode:0o600})}finally{if(app)await app.close();for(const n of natives){n.closeAllConnections();await new Promise(r=>n.close(r))}await closePool();report.nativeWrites=nativeWrites;report.disabledHomeRouterCalls=disabledCalls;fs.writeFileSync('/app/backend/acceptance/gre221-'+(baseline?'baseline':'fixed')+'.json',JSON.stringify(report,null,2));console.log(JSON.stringify(report))}if(!report.passed)process.exitCode=1
@@ -0,0 +1,27 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import {createServer} from 'node:http'
import {randomBytes} from 'node:crypto'
const database=process.env.MMAPP_OWN_SQL_DB,url=new URL(process.env.DATABASE_URL)
assert(/^mm_accept_166_\d+$/.test(database??''));assert.equal(url.pathname,'/'+database);assert.equal(url.username,database)
process.env.AUTH_REQUIRED='true';process.env.AUTH_JWT_SECRET=randomBytes(32).toString('hex');process.env.AUTH_ISSUER='https://own220.invalid';process.env.AUTH_PORTAL_URL='https://own220.invalid'
const {pool,closePool}=await import('/app/backend/dist/db/index.js'),{initDatabase}=await import('/app/backend/dist/db/bootstrap.js'),{collectGreBgpSnapshotOnce}=await import('/app/backend/dist/services/gre-bgp-snapshot-collector.js'),{buildApp}=await import('/app/backend/dist/index.js')
assert.equal((await pool.query('SELECT current_database() AS name')).rows[0].name,database);assert(Object.values((await pool.query('SELECT rolsuper,rolcreatedb,rolcreaterole,rolbypassrls FROM pg_roles WHERE rolname=current_user')).rows[0]).every(v=>v===false))
const baseline=process.env.MMAPP_BASELINE==='true',cases=[];let app,nativeCalls=0,nativeWrites=0,disabledCalls=0,flags={running:'true',disabled:'false'}
const fixture=label=>createServer((req,res)=>{nativeCalls++;if(label==='disabled')disabledCalls++;if(req.method!=='GET')nativeWrites++;res.setHeader('content-type','application/json');const gre=req.url==='/rest/interface/gre',bgp=req.url==='/rest/routing/bgp/session',row=gre?{'.id':'*1',name:'own220-gre',...(label==='second'?flags:{running:'true',disabled:'false'})}:{'.id':'*1',name:'own220-peer',established:'true','remote.as':'65220','local.as':'65221'};res.end(JSON.stringify(gre||bgp?[row]:[]))})
const natives=['first','second','disabled'].map(fixture);for(const n of natives)await new Promise(r=>n.listen(0,'127.0.0.1',r))
const report={date:'2026-10-14',block:'12.5.220',baseline,database,cases,scope:'Actual compiled GRE operational flags and actual snapshot collector, owned SQL/native HTTP fixture; signed reader/authenticated GRE API',passed:false}
try{
await initDatabase();for(let i=0;i<natives.length;i++)await pool.query("INSERT INTO servers(name,host,port,use_ssl,enabled,username,password,type) VALUES($1,'127.0.0.1',$2,false,$3,'fixture','fixture',$4)",['own220-'+i,natives[i].address().port,i<2,i===2?'home-router':'exit-node'])
app=await buildApp({logger:false,startScheduler:false});await app.ready();const token=app.jwt.sign({sub:'own220-reader',apps:['mm'],permissions:['mm:network:read'],iss:process.env.AUTH_ISSUER},{expiresIn:'5m'}),headers={authorization:'Bearer '+token};
const flagCases=[{name:'up',flags:{running:'true',disabled:'false'}},{name:'degraded',flags:{running:'false',disabled:'false'}},{name:'down',flags:{running:'false',disabled:'true'}},{name:'yes-up',flags:{running:'yes',disabled:'no'}},{name:'yes-degraded',flags:{running:'no',disabled:'no'}},{name:'yes-down',flags:{running:'no',disabled:'yes'}},{name:'missing-disabled',flags:{running:'true'}},{name:'missing-running',flags:{disabled:'false'}},{name:'blank-disabled',flags:{running:'true',disabled:''}},{name:'invalid-running',flags:{running:'junk',disabled:'false'}},{name:'invalid-disabled',flags:{running:'true',disabled:'junk'}},{name:'both-missing',flags:{}},{name:'disabled-missing-running',flags:{disabled:'true'}},{name:'disabled-invalid-running',flags:{disabled:'true',running:'junk'}},{name:'recovery',flags:{running:'true',disabled:'false'}}]
for(const item of flagCases){
flags=item.flags;const off=['true','yes'].includes(flags.disabled??''),on=['false','no'].includes(flags.disabled??''),runningKnown=['true','yes','false','no'].includes(flags.running??''),unknown=!off&&!(on&&runningKnown),before=nativeCalls,snapshot=await collectGreBgpSnapshotOnce()
assert.equal(snapshot.greWritten,unknown&&!baseline?0:2);assert.equal(snapshot.bgpWritten,2);assert.equal(snapshot.errors.length,unknown&&!baseline?1:0);assert.equal(snapshot.fatalError,undefined)
if(unknown&&!baseline){assert.match(snapshot.errors[0],/Не подтверждено состояние GRE/)}else{const row=snapshot.greTunnels.find(row=>row.targetLabel.endsWith('own220-1'));assert(row);const expected=baseline?flags.disabled==='true'?'down':flags.running==='true'?'up':'degraded':off?'down':['true','yes'].includes(flags.running??'')?'up':'degraded';assert.equal(row.status,expected)}
const api=await app.inject({method:'GET',url:'/api/gre/tunnels',headers}),body=api.json();assert.equal(api.statusCode,200);assert.equal(body.failures.length,unknown&&!baseline?1:0);assert.equal(body.tunnels.length,unknown&&!baseline?1:2);cases.push({phase:item.name,endpoint:'GRE authenticated catalog',status:200,tunnels:body.tunnels.length,failures:body.failures.length});
cases.push({phase:item.name,flags,greWritten:snapshot.greWritten,bgpWritten:2,sourceErrors:snapshot.errors.length,secondStatus:snapshot.greTunnels.find(row=>row.targetLabel.endsWith('own220-1'))?.status??null,nativeCalls:nativeCalls-before})
}
assert.equal(nativeWrites,0);assert.equal(disabledCalls,0);report.passed=true
}catch(e){report.errorName=e.name;fs.writeFileSync('/app/backend/acceptance/gre220-private.log',String(e.stack??e),{mode:0o600})}finally{if(app)await app.close();for(const n of natives){n.closeAllConnections();await new Promise(r=>n.close(r))}await closePool();report.nativeWrites=nativeWrites;report.disabledHomeRouterCalls=disabledCalls;fs.writeFileSync('/app/backend/acceptance/gre220-'+(baseline?'baseline':'fixed')+'.json',JSON.stringify(report,null,2));console.log(JSON.stringify(report))}if(!report.passed)process.exitCode=1
@@ -0,0 +1,18 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import {createServer} from 'node:http'
import {randomBytes} from 'node:crypto'
const database=process.env.MMAPP_OWN_SQL_DB,url=new URL(process.env.DATABASE_URL)
assert(/^mm_accept_166_\d+$/.test(database??''));assert.equal(url.pathname,'/'+database);assert.equal(url.username,database)
process.env.AUTH_REQUIRED='true';process.env.AUTH_JWT_SECRET=randomBytes(32).toString('hex');process.env.AUTH_ISSUER='https://own209.invalid';process.env.AUTH_PORTAL_URL='https://own209.invalid'
const {pool,closePool}=await import('/app/backend/dist/db/index.js'),{initDatabase}=await import('/app/backend/dist/db/bootstrap.js'),{buildApp}=await import('/app/backend/dist/index.js')
assert.equal((await pool.query('SELECT current_database() AS name')).rows[0].name,database);assert(Object.values((await pool.query('SELECT rolsuper,rolcreatedb,rolcreaterole,rolbypassrls FROM pg_roles WHERE rolname=current_user')).rows[0]).every(v=>v===false))
const psk='own209-'+randomBytes(24).toString('base64url'),baseline=process.env.MMAPP_GRE_SECRET_EXPECT==='leak',cases=[];let app,serverId,nativeWrites=0
const native=createServer((req,res)=>{if(req.method!=='GET')nativeWrites++;res.setHeader('content-type','application/json');res.end(JSON.stringify(req.url.endsWith('/interface/gre')?[{'.id':'*1',name:'own209-encrypted','local-address':'192.0.2.1','remote-address':'192.0.2.2','ipsec-secret':psk,mtu:'1476',keepalive:'10s,3',disabled:'false',running:'true','allow-fast-path':'false'},{'.id':'*2',name:'own209-plain','remote-address':'192.0.2.3','ipsec-secret':'',disabled:'false',running:'true'}]:[]))});await new Promise(r=>native.listen(0,'127.0.0.1',r))
const report={date:'2026-10-13',block:'12.5.209',database,baseline,cases,scope:'Production compiled GRE/Filters GET and auth with own restricted PostgreSQL, local native HTTP fixture, synthetic nonempty PSK and isolated signed JWT',passed:false}
try{await initDatabase();serverId=Number((await pool.query("INSERT INTO servers(name,host,port,use_ssl,enabled,username,password) VALUES('own209-native','127.0.0.1',$1,false,true,'fixture','fixture') RETURNING id",[native.address().port])).rows[0].id);app=await buildApp({logger:false,startScheduler:false});await app.ready()
for(const role of ['reader','filter-writer','network-writer','network-admin','is-admin-only']){const permissions=['mm:network:read','mm:filters:read',...(role==='filter-writer'?['mm:filters:write']:role==='network-writer'?['mm:network:write']:role==='network-admin'?['mm:network:admin']:[])],token=app.jwt.sign({sub:'own209-'+role,apps:['mm'],permissions,is_admin:role==='is-admin-only',iss:process.env.AUTH_ISSUER},{expiresIn:'5m'});for(const path of ['/api/gre/tunnels','/api/filters/gre-tunnels']){const r=await app.inject({method:'GET',url:path+'?serverId='+serverId,headers:{authorization:'Bearer '+token}});assert.equal(r.statusCode,200);const rows=r.json().tunnels;assert.equal(rows.length,2);const encrypted=rows.find(t=>t.name==='own209-encrypted'),plain=rows.find(t=>t.name==='own209-plain');assert.equal(plain.ipsec,null);const direct=path==='/api/gre/tunnels',privileged=role==='network-writer'||role==='network-admin';if(direct){assert(encrypted.ipsec);assert.equal(encrypted.ipsec.secret,baseline||privileged?psk:'')}else if(baseline)assert.equal(encrypted.ipsec,null);else{assert(encrypted.ipsec);assert.equal(encrypted.ipsec.secret,'')}const exposed=r.body.includes(psk);assert.equal(exposed,direct&&(baseline||privileged));cases.push({role,path,status:200,pskExposed:exposed,configuredStatePresent:!!encrypted.ipsec,plainStateNull:true})}}
for(const path of ['/api/gre/tunnels','/api/filters/gre-tunnels']){assert.equal((await app.inject({method:'GET',url:path})).statusCode,401);cases.push({role:'unauthenticated',path,status:401})}
const revisions=(await pool.query("SELECT payload FROM config_revisions WHERE server_id=$1 AND section='gre'",[serverId])).rows;assert(revisions.length>0);assert(revisions.some(r=>JSON.stringify(r.payload).includes(psk)));assert.equal(nativeWrites,0);report.internalSnapshotRetainsPsk=true;report.nativeWrites=0;report.passed=true
}catch(e){report.errorName=e.name;fs.writeFileSync('/app/backend/acceptance/gre209-private.log',String(e.stack??e),{mode:0o600})}finally{if(app)await app.close();native.closeAllConnections();await new Promise(r=>native.close(r));await closePool();fs.writeFileSync('/app/backend/acceptance/gre209-'+(baseline?'baseline':'fixed')+'-result.json',JSON.stringify(report,null,2));console.log(JSON.stringify(report))}
if(!report.passed)process.exitCode=1
@@ -0,0 +1,155 @@
/** Arithmetic preflight only. No Docker/host access, changes or workload admission.
* Roles are operator-supplied labels, not authenticated service identities.
* Even a fitting budget cannot prove runtime <=2GB or permission to change limits. */
const MiB = 1024 * 1024
const roles = new Set(['backend', 'postgres', 'frontend', 'portal', 'proxy', 'agent', 'other'])
const singleton = new Set(['backend', 'postgres', 'frontend', 'portal'])
const integer = value => Number.isSafeInteger(value) && value >= 0
const exact = (value, fields) => value !== null && typeof value === 'object' && !Array.isArray(value)
&& Object.keys(value).sort().join('|') === [...fields].sort().join('|')
const id = value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value)
export function evaluateInstallationMemoryBudget(input) {
if (!exact(input, ['hostMemTotalBytes', 'osReserveBytes', 'observedContainerIds', 'containers'])
|| !integer(input.hostMemTotalBytes) || !integer(input.osReserveBytes)
|| !Array.isArray(input.observedContainerIds) || !Array.isArray(input.containers)
|| input.observedContainerIds.length > 128 || input.containers.length > 128) throw Error('Invalid bounded installation inventory')
if (input.observedContainerIds.some(value => !id(value)) || new Set(input.observedContainerIds).size !== input.observedContainerIds.length) throw Error('Invalid observed container set')
const seen = new Set(), counts = new Map(), reasons = new Set()
let configuredBytes = 0
for (const row of input.containers) {
if (!exact(row, ['id', 'role', 'configuredLimitBytes']) || !id(row.id) || !roles.has(row.role)
|| !integer(row.configuredLimitBytes) || seen.has(row.id)) throw Error('Invalid container budget')
seen.add(row.id); counts.set(row.role, (counts.get(row.role) ?? 0) + 1)
if (row.configuredLimitBytes === 0) reasons.add('UNBOUNDED_CONTAINER')
if (row.configuredLimitBytes > input.hostMemTotalBytes) reasons.add('CONTAINER_LIMIT_EXCEEDS_HOST')
if (!Number.isSafeInteger(configuredBytes + row.configuredLimitBytes)) throw Error('Container budget overflow')
configuredBytes += row.configuredLimitBytes
}
if (seen.size !== input.observedContainerIds.length || input.observedContainerIds.some(value => !seen.has(value))) reasons.add('INCOMPLETE_HOST_CONTAINER_INVENTORY')
for (const role of ['backend', 'postgres', 'frontend']) if (!counts.has(role)) reasons.add('REQUIRED_SERVICE_MISSING')
for (const role of singleton) if ((counts.get(role) ?? 0) > 1) reasons.add('DUPLICATE_APPLICATION_STACK')
if (input.hostMemTotalBytes === 0 || input.hostMemTotalBytes > 2 * 1024 * MiB) reasons.add('HOST_OUTSIDE_2GIB_TARGET')
if (input.osReserveBytes < 512 * MiB) reasons.add('OS_RESERVE_BELOW_CANDIDATE_FLOOR')
if (!Number.isSafeInteger(configuredBytes + input.osReserveBytes)) throw Error('Installation budget overflow')
const plannedBytes = configuredBytes + input.osReserveBytes
if (plannedBytes > input.hostMemTotalBytes) reasons.add('INSTALLATION_BUDGET_EXCEEDS_HOST')
return Object.freeze({ scope: 'arithmetic-preflight-only', budgetFits: reasons.size === 0,
wholeInstallation2GbAccepted: false, runtimeAcceptanceClaimed: false, changesAuthorized: false,
configuredContainerBytes: configuredBytes, osReserveBytes: input.osReserveBytes,
plannedBytes, remainingBytes: input.hostMemTotalBytes - plannedBytes,
observedContainers: input.observedContainerIds.length, reasons: Object.freeze([...reasons].sort()) })
}
/** Project the whitelist-only read-only inventory. Never substitute idle RSS or
* Docker's reported host ceiling for HostConfig.Memory. Labels/freshness are
* not authenticated here; this result must not be used as mutation authority. */
export function evaluateObservedInstallationBudget(inventory, roleById, osReserveBytes = 512 * MiB) {
if (!inventory || inventory.readOnly !== true || inventory.containerSetStable !== true
|| inventory.environmentRead !== false || inventory.serviceChanges !== false || inventory.newFixtureStarted !== false
|| !Array.isArray(inventory.containers) || inventory.containers.length > 128
|| !roleById || typeof roleById !== 'object' || Array.isArray(roleById)) throw Error('Invalid read-only installation inventory')
const containers = inventory.containers.map(row => {
if (!row || !id(row.id) || row.running !== true || !integer(row.memoryLimitBytes)
|| !/^sha256:[a-f0-9]{64}$/.test(row.imageId) || !Number.isFinite(Date.parse(row.startedAt))) throw Error('Incomplete configured-limit observation')
const label = Object.getOwnPropertyDescriptor(roleById, row.id)
if (!label || typeof label.value !== 'string' || !roles.has(label.value)) throw Error('Explicit container role missing')
return { id: row.id, role: label.value, configuredLimitBytes: row.memoryLimitBytes }
})
const observedContainerIds = containers.map(row => row.id)
if (Object.keys(roleById).length !== observedContainerIds.length || Object.keys(roleById).some(key => !observedContainerIds.includes(key))) throw Error('Foreign or incomplete role inventory')
const budget = evaluateInstallationMemoryBudget({ hostMemTotalBytes: inventory.hostMemTotalBytes, osReserveBytes, observedContainerIds, containers })
return Object.freeze({ ...budget, configuredLimitsSource: 'observed-HostConfig.Memory', roleLabelsAuthenticated: false, inventoryFreshnessProven: false })
}
/** NEW pure raw-text bridge for measure-memory schema2/3. This is NOT the
* separate GET-only inventory contract consumed by evaluateObserved above.
* No file/container/socket access or mutation authority. Parse raw text only:
* precision lost by a caller's previous JSON.parse cannot be recovered. */
export function evaluateMeasuredInstallationBudget(rawText, roleById, osReserveBytes = 512 * MiB) {
if (typeof rawText !== 'string') throw Error('Raw measurement JSON text required; preparsed precision cannot be recovered')
if (Buffer.byteLength(rawText, 'utf8') > 32 * 1024 * 1024) throw Error('Measurement text bound exceeded')
// A narrow numeric lexical gate BEFORE JSON.parse, not a JSON parser. The
// Python producer emits only integer JSON numbers (PSI averages are strings).
// Reject decimal/exponent/unsafe integers even where JSON.parse would round
// a fractional lexeme into a seemingly safe integer. JSON.parse checks syntax.
const number = /-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/y
let quoted = false, escaped = false
for (let index = 0; index < rawText.length; index++) {
const char = rawText[index]
if (quoted) { if (escaped) escaped = false; else if (char === '\\') escaped = true; else if (char === '"') quoted = false; continue }
if (char === '"') { quoted = true; continue }
if (char === '-' || char >= '0' && char <= '9') {
number.lastIndex = index; const token = number.exec(rawText)?.[0]
if (!token || token === '-0' || !/^-?(?:0|[1-9][0-9]*)$/.test(token) || token.replace('-', '').length > 16) throw Error('Unsafe or noninteger measurement JSON number')
const value = BigInt(token)
if (value < -9007199254740991n || value > 9007199254740991n) throw Error('Unsafe measurement JSON integer')
index = number.lastIndex - 1
}
}
const data = JSON.parse(rawText)
if (!data || ![2, 3].includes(data.schemaVersion) || data.readOnlyDockerAndProc !== true || data.serviceChanges !== 0
|| data.summary?.acceptancePassed !== false || !Array.isArray(data.observations) || !data.observations.length || data.observations.length > 300
|| !roleById || typeof roleById !== 'object' || Array.isArray(roleById)) throw Error('Unsupported or incomplete memory measurement')
const nonnegativeSafe = value => Number.isSafeInteger(value) && value >= 0
const limit = value => value === 'max' || nonnegativeSafe(value) ? value : null
const swap = row => row.configuredMemorySwapStatus === 'available' && Number.isSafeInteger(row.configuredMemorySwapBytes) && row.configuredMemorySwapBytes >= -1 ? row.configuredMemorySwapBytes : null
const cpu = value => value && ['nr_periods', 'nr_throttled', 'throttled_usec'].every(key => nonnegativeSafe(value[key]))
? Object.fromEntries(['nr_periods', 'nr_throttled', 'throttled_usec'].map(key => [key, value[key]])) : null
const pressure = host => {
if (host.memoryPressure?.status !== 'available') return null
const result = {}
for (const key of ['some', 'full']) {
const value = host.memoryPressure.metrics?.[key]
if (!value || !nonnegativeSafe(value.totalMicros)) return null
for (const avg of ['avg10Percent', 'avg60Percent', 'avg300Percent']) {
if (typeof value[avg] !== 'string' || !/^[0-9]{1,3}\.[0-9]{2}$/.test(value[avg]) || Number(value[avg]) > 100) return null
}
result[key] = Object.fromEntries(['avg10Percent', 'avg60Percent', 'avg300Percent', 'totalMicros'].map(name => [name, value[name]]))
}
return result
}
let baseline, hostMemory, priorElapsed = -1, complete = data.schemaVersion === 3
const extendedObservations = []
for (const observation of data.observations) {
if (!observation?.host || !nonnegativeSafe(observation.elapsedMs) || observation.elapsedMs < priorElapsed || observation.elapsedMs > 360000
|| !nonnegativeSafe(observation.host.MemTotalBytes) || !observation.host.MemTotalBytes
|| !Array.isArray(observation.containers) || !observation.containers.length || observation.containers.length > 16) throw Error('Incomplete bounded measurement sample')
priorElapsed = observation.elapsedMs
hostMemory ??= observation.host.MemTotalBytes
if (hostMemory !== observation.host.MemTotalBytes) throw Error('Measurement host memory changed')
const ids = new Set(), identities = [], extended = []
for (const row of observation.containers) {
if (!row || !id(row.id) || ids.has(row.id) || !/^sha256:[a-f0-9]{64}$/.test(row.imageId) || (typeof row.startedAt !== 'string' || !Number.isFinite(Date.parse(row.startedAt)))
|| row.running !== true || row.runningAfter !== true || row.identityStableDuringSample !== true || row.oomKilled !== false || row.oomKilledAfter !== false
|| !nonnegativeSafe(row.restartCount) || !Number.isSafeInteger(row.pid) || row.pid < 1 || row.pid > 2147483647 || !nonnegativeSafe(row.configuredLimitBytes)) throw Error('Unavailable measurement container identity or configured cap')
ids.add(row.id); identities.push([row.id, row.imageId, row.startedAt, row.pid, row.restartCount, row.configuredLimitBytes, data.schemaVersion === 3 ? swap(row) : null])
if (data.schemaVersion === 3) {
const metrics = row.cgroupV2?.metrics, memoryMax = limit(metrics?.['memory.max']), swapMax = limit(metrics?.['memory.swap.max'])
const cpuThrottling = cpu(metrics?.['cpu.stat']), configuredMemorySwapBytes = swap(row)
const available = row.cgroupV2?.status === 'available' && row.resourceLimitsStableDuringSample === true
&& memoryMax !== null && swapMax !== null && cpuThrottling !== null && configuredMemorySwapBytes !== null
complete &&= available
extended.push({id: row.id, memoryMax, swapMax, configuredMemorySwapBytes, cpuThrottling, available})
} else extended.push({id: row.id, memoryMax: null, swapMax: null, configuredMemorySwapBytes: null, cpuThrottling: null, available: false})
}
identities.sort((a, b) => a[0].localeCompare(b[0]))
const encoded = JSON.stringify(identities)
baseline ??= encoded
if (encoded !== baseline) throw Error('Measurement container set, identity or memory cap changed')
const memoryPressure = data.schemaVersion === 3 ? pressure(observation.host) : null
complete &&= memoryPressure !== null
extendedObservations.push({elapsedMs: observation.elapsedMs, memoryPressure, containers: extended})
}
const captured = JSON.parse(baseline)
if (Object.keys(roleById).length !== captured.length || Object.keys(roleById).some(key => !captured.some(row => row[0] === key))) throw Error('Incomplete or foreign measured role inventory')
const containers = captured.map(([identifier, _image, _start, _pid, _restart, configuredLimitBytes]) => {
const descriptor = Object.getOwnPropertyDescriptor(roleById, identifier)
if (!descriptor || typeof descriptor.value !== 'string' || !roles.has(descriptor.value)) throw Error('Explicit measured container role missing')
return {id: identifier, role: descriptor.value, configuredLimitBytes}
})
const budget = evaluateInstallationMemoryBudget({hostMemTotalBytes: hostMemory, osReserveBytes, observedContainerIds: containers.map(row => row.id), containers})
return Object.freeze({...budget, measurementSchemaVersion: data.schemaVersion, measurementSamples: data.observations.length,
extendedMetricsComplete: complete, extendedObservations: Object.freeze(extendedObservations),
roleLabelsAuthenticated: false, inventoryFreshnessProven: false, hostContainerInventoryComplete: false, measurementSelectedContainersOnly: true, noSwapAccepted: false, sourceGenerationAuthenticated: false})
}
@@ -0,0 +1,46 @@
import assert from 'node:assert/strict'
import { test } from 'node:test'
import { evaluateInstallationMemoryBudget as evaluate } from './installation-memory-budget.mjs'
const MiB = 1024 * 1024
function fixture() {
const containers = [['backend',512],['postgres',384],['frontend',160],['portal',192]].map(([role, limit], i) => ({ id: String(i+1).repeat(64), role, configuredLimitBytes: limit * MiB }))
return { hostMemTotalBytes: 2021212 * 1024, osReserveBytes: 512 * MiB, observedContainerIds: containers.map(row => row.id), containers }
}
test('complete four-service candidate fits arithmetic, never runtime acceptance or change permission', () => {
const input = fixture(), before = structuredClone(input), result = evaluate(input)
assert.equal(result.budgetFits,true); assert.equal(result.plannedBytes,1760 * MiB)
assert.equal(result.wholeInstallation2GbAccepted,false); assert.equal(result.runtimeAcceptanceClaimed,false); assert.equal(result.changesAuthorized,false)
assert(Object.isFrozen(result)); assert(Object.isFrozen(result.reasons)); assert.deepEqual(input,before)
})
test('coexisting baseline and owned app/PG cannot represent one installation', () => {
const input = fixture()
for (const [i,role,limit] of [[5,'backend',320],[6,'postgres',192]]) { input.containers.push({id:String(i).repeat(64),role,configuredLimitBytes:limit*MiB}); input.observedContainerIds.push(String(i).repeat(64)) }
const result = evaluate(input)
assert.equal(result.budgetFits,false); assert(result.reasons.includes('DUPLICATE_APPLICATION_STACK')); assert(result.reasons.includes('INSTALLATION_BUDGET_EXCEEDS_HOST'))
})
test('unknown host containers, missing and foreign inventory entries refuse', () => {
for (const mode of ['omitted','foreign','missing-service']) {
const input = fixture()
if (mode === 'omitted') input.observedContainerIds.push('a'.repeat(64))
if (mode === 'foreign') input.containers[0].id='b'.repeat(64)
if (mode === 'missing-service') { input.containers.splice(0,1); input.observedContainerIds.splice(0,1) }
const result = evaluate(input); assert.equal(result.budgetFits,false)
assert(result.reasons.includes(mode === 'missing-service'?'REQUIRED_SERVICE_MISSING':'INCOMPLETE_HOST_CONTAINER_INVENTORY'))
}
})
test('unlimited cap is distinct from observed idle RSS or Docker host limit display', () => {
const input = fixture(); input.containers[0].configuredLimitBytes=0
assert(evaluate(input).reasons.includes('UNBOUNDED_CONTAINER'))
input.containers[0].configuredLimitBytes=3*1024*MiB
assert(evaluate(input).reasons.includes('CONTAINER_LIMIT_EXCEEDS_HOST'))
})
test('extra service, reserve and physical host all count toward the same budget', () => {
const input = fixture(); input.containers.push({id:'a'.repeat(64),role:'other',configuredLimitBytes:256*MiB}); input.observedContainerIds.push('a'.repeat(64))
assert(evaluate(input).reasons.includes('INSTALLATION_BUDGET_EXCEEDS_HOST'))
const low = fixture(); low.osReserveBytes=128*MiB; assert(evaluate(low).reasons.includes('OS_RESERVE_BELOW_CANDIDATE_FLOOR'))
const large = fixture(); large.hostMemTotalBytes=4*1024*MiB; assert(evaluate(large).reasons.includes('HOST_OUTSIDE_2GIB_TARGET'))
})
test('malformed, duplicate, fractional, oversized or unsafe inventory rejects without I/O', () => {
const mutations = [input=>{input.inject='unknown'},input=>{input.containers[0].role='invented'},input=>{input.containers[0].configuredLimitBytes=-1},input=>{input.containers[0].configuredLimitBytes=0.5},input=>{input.containers[0].id='../foreign'},input=>{input.containers[1].id=input.containers[0].id},input=>{input.observedContainerIds.push(input.observedContainerIds[0])},input=>{input.containers[0].configuredLimitBytes=Number.MAX_SAFE_INTEGER},input=>{input.observedContainerIds=Array(129).fill('a'.repeat(64))}]
for (const mutate of mutations) { const input=fixture();mutate(input);assert.throws(()=>evaluate(input)) }
})
@@ -0,0 +1,25 @@
import assert from 'node:assert/strict'
import {test} from 'node:test'
import {evaluateMeasuredInstallationBudget as evaluate,evaluateInstallationMemoryBudget as legacy} from './installation-memory-budget.mjs'
const MiB=1024*1024
function fixture(version=3){
const roles=['backend','postgres','frontend','portal'],caps=[512,384,160,192]
const containers=roles.map((role,i)=>({id:String(i+1).repeat(64),imageId:'sha256:'+'a'.repeat(64),startedAt:'2026-11-02T00:00:00Z',pid:i+1,restartCount:0,running:true,runningAfter:true,identityStableDuringSample:true,oomKilled:false,oomKilledAfter:false,configuredLimitBytes:caps[i]*MiB,reportedLimitBytes:2*1024*MiB,
...(version===3?{configuredMemorySwapBytes:caps[i]*MiB,configuredMemorySwapStatus:'available',resourceLimitsStableDuringSample:true,cgroupV2:{status:'available',metrics:{'memory.max':caps[i]*MiB,'memory.swap.max':0,'cpu.stat':{nr_periods:10,nr_throttled:2,throttled_usec:1234,notWhitelisted:999}}}}:{cgroupV2:{status:'available',metrics:{'memory.current':1,'memory.peak':2}}})}))
const p={avg10Percent:'0.00',avg60Percent:'0.01',avg300Percent:'1.25',totalMicros:123456}
const host={MemTotalBytes:2069721088,MemAvailableBytes:1000000000,pswpin:0,pswpout:0,oom_kill:0,...(version===3?{memoryPressure:{status:'available',metrics:{some:p,full:{...p,totalMicros:0}}}}:{})}
const data={schemaVersion:version,readOnlyDockerAndProc:true,serviceChanges:0,summary:{acceptancePassed:false},observations:[{elapsedMs:0,host,containers},{elapsedMs:1000,host:structuredClone(host),containers:structuredClone(containers)}]}
return {data,roleById:Object.fromEntries(containers.map((c,i)=>[c.id,roles[i]])),raw:()=>JSON.stringify(data)}
}
test('real schema2 producer contract remains arithmetic-only; missing extended measurements stay unknown',()=>{const f=fixture(2),r=evaluate(f.raw(),f.roleById);assert.equal(r.budgetFits,true);assert.equal(r.plannedBytes,1760*MiB);assert.equal(r.measurementSchemaVersion,2);assert.equal(r.extendedMetricsComplete,false);assert.equal(r.extendedObservations[0].memoryPressure,null);assert.equal(r.extendedObservations[0].containers[0].memoryMax,null);assert.equal(r.wholeInstallation2GbAccepted,false)})
test('schema3 projects only fixed new fields and never converts data to runtime/no-swap authority',()=>{const f=fixture(),r=evaluate(f.raw(),f.roleById);assert.equal(r.extendedMetricsComplete,true);assert.equal(r.measurementSamples,2);assert.equal(r.extendedObservations[0].containers[0].cpuThrottling.throttled_usec,1234);assert.equal(r.extendedObservations[0].memoryPressure.some.avg300Percent,'1.25');assert(!JSON.stringify(r).includes('notWhitelisted'));assert.equal(r.runtimeAcceptanceClaimed,false);assert.equal(r.changesAuthorized,false);assert.equal(r.noSwapAccepted,false);assert.equal(r.inventoryFreshnessProven,false);assert.equal(r.hostContainerInventoryComplete,false);assert.equal(r.measurementSelectedContainersOnly,true)})
test('numeric unlimited max/zero/-1/daemon-default preserve their different observed meanings',()=>{const f=fixture();for(const o of f.data.observations){const c=o.containers[0];c.cgroupV2.metrics['memory.max']='max';c.cgroupV2.metrics['memory.swap.max']='max';c.configuredMemorySwapBytes=-1}let r=evaluate(f.raw(),f.roleById);assert.equal(r.extendedObservations[0].containers[0].memoryMax,'max');assert.equal(r.extendedObservations[0].containers[0].configuredMemorySwapBytes,-1);assert.equal(r.noSwapAccepted,false);for(const o of f.data.observations)o.containers[0].configuredMemorySwapBytes=0;r=evaluate(f.raw(),f.roleById);assert.equal(r.extendedObservations[0].containers[0].configuredMemorySwapBytes,0)})
test('missing schema3 fields, partial cgroup and invalid PSI are unknown rather than zero',()=>{for(const mutate of [o=>{delete o.containers[0].configuredMemorySwapBytes},o=>{o.containers[0].cgroupV2.status='partial'},o=>{o.containers[0].cgroupV2.metrics['memory.max']=null},o=>{delete o.containers[0].cgroupV2.metrics['cpu.stat'].nr_periods},o=>{o.host.memoryPressure.metrics.some.avg10Percent='100.01'},o=>{o.host.memoryPressure.status='unavailable'}]){const f=fixture();for(const o of f.data.observations)mutate(o);const r=evaluate(f.raw(),f.roleById);assert.equal(r.extendedMetricsComplete,false);assert.equal(r.wholeInstallation2GbAccepted,false)}})
test('unsafe uint64 raw lexemes fail before JSON.parse; no rounded Number is recovered',()=>{for(const token of ['9007199254740992','9007199254740993','18446744073709551615','1.0000000000000001','9007199254740991.1','1e0','-0']){const f=fixture(),raw=f.raw().replace('123456',token);assert.throws(()=>evaluate(raw,f.roleById),/Unsafe|noninteger/);assert.throws(()=>evaluate(JSON.parse(raw),f.roleById),/Raw measurement JSON text required/)} })
test('safe integer boundary is exact and large number-like text inside strings is not parsed as a counter',()=>{const f=fixture();for(const o of f.data.observations)o.host.memoryPressure.metrics.some.totalMicros=Number.MAX_SAFE_INTEGER;f.data.ignored='9007199254740993 1.0000000000000001 \\" escaped';const r=evaluate(f.raw(),f.roleById);assert.equal(r.extendedObservations[0].memoryPressure.some.totalMicros,Number.MAX_SAFE_INTEGER);assert(!JSON.stringify(r).includes('ignored'))})
test('malformed JSON, unsupported schemas or claimed producer acceptance refuse',()=>{const f=fixture();assert.throws(()=>evaluate('{bad',f.roleById));for(const mutate of [d=>{d.schemaVersion=1},d=>{d.schemaVersion=4},d=>{d.serviceChanges=1},d=>{d.readOnlyDockerAndProc=false},d=>{d.summary.acceptancePassed=true},d=>{d.observations=[]}]){const x=fixture();mutate(x.data);assert.throws(()=>evaluate(x.raw(),x.roleById))}})
test('full IDs, complete stable container set, OOM/running/process/image/caps stay mandatory',()=>{for(const mutate of [d=>{d.observations[1].containers.pop()},d=>{d.observations[1].containers.push(d.observations[1].containers[0])},d=>{d.observations[1].containers[0].pid++},d=>{d.observations[1].containers[0].runningAfter=false},d=>{d.observations[1].containers[0].oomKilledAfter=true},d=>{d.observations[1].containers[0].configuredLimitBytes++},d=>{d.observations[1].containers[0].configuredMemorySwapBytes++},d=>{d.observations[1].host.MemTotalBytes++},d=>{d.observations[1].containers[0].startedAt=123}]){const f=fixture();mutate(f.data);assert.throws(()=>evaluate(f.raw(),f.roleById))}})
test('role labels remain exact own DATA descriptors, never an inherited/accessor authority',()=>{for(const kind of ['getter','inherited','missing','foreign']){const f=fixture(),key=f.data.observations[0].containers[0].id;if(kind==='getter')Object.defineProperty(f.roleById,key,{enumerable:true,get(){assert.fail('Getter executed')}});if(kind==='inherited'){const x=Object.create({[key]:'backend'});for(const [id,role]of Object.entries(f.roleById))if(id!==key)x[id]=role;f.roleById=x}if(kind==='missing')delete f.roleById[key];if(kind==='foreign')f.roleById['f'.repeat(64)]='other';assert.throws(()=>evaluate(f.raw(),f.roleById))}})
test('legacy arithmetic export keeps soft metadata and no-runtime semantics',()=>{const f=fixture(),input={hostMemTotalBytes:2069721088,osReserveBytes:512*MiB,observedContainerIds:Object.keys(f.roleById),containers:f.data.observations[0].containers.map(c=>({id:c.id,role:f.roleById[c.id],configuredLimitBytes:c.configuredLimitBytes}))};assert.equal(legacy(input).budgetFits,true);assert.equal(legacy(input).changesAuthorized,false)})
import {readFileSync} from 'node:fs'
test('actual base/candidate Python producers interoperate through synthetic raw wire files',()=>{for(const version of [2,3]){const raw=readFileSync(new URL('./schema'+version+'.main.synthetic.json',import.meta.url),'utf8');const r=evaluate(raw,{['a'.repeat(64)]:'backend'});assert.equal(r.measurementSchemaVersion,version);assert.equal(r.extendedMetricsComplete,version===3);assert.equal(r.budgetFits,false);assert.equal(r.wholeInstallation2GbAccepted,false);if(version===3){assert.equal(r.extendedObservations[0].containers[0].configuredMemorySwapBytes,1024);assert.equal(r.extendedObservations[0].memoryPressure.some.totalMicros,123456)}}})
@@ -0,0 +1,31 @@
import assert from 'node:assert/strict'
import { test } from 'node:test'
import { evaluateObservedInstallationBudget as evaluate } from './installation-memory-budget.mjs'
const MiB=1024*1024
function fixture() {
const containers=['backend','postgres','frontend','portal'].map((role,index)=>({id:String(index+1).repeat(64),role,running:true,imageId:'sha256:'+'a'.repeat(64),startedAt:'2026-10-28T12:00:00Z',memoryLimitBytes:0,reportedLimitBytes:2*1024*MiB}))
const roleById=Object.fromEntries(containers.map(row=>[row.id,row.role]))
return {inventory:{readOnly:true,containerSetStable:true,environmentRead:false,serviceChanges:false,newFixtureStarted:false,hostMemTotalBytes:2021212*1024,containers},roleById}
}
test('observed zero configured caps stay unbounded despite positive Docker reported host limits',()=>{
const f=fixture(), result=evaluate(f.inventory,f.roleById)
assert.equal(result.budgetFits,false);assert(result.reasons.includes('UNBOUNDED_CONTAINER'));assert.equal(result.configuredContainerBytes,0)
assert.equal(result.wholeInstallation2GbAccepted,false);assert.equal(result.roleLabelsAuthenticated,false);assert.equal(result.inventoryFreshnessProven,false)
})
test('complete explicit configured caps can fit arithmetic without becoming runtime acceptance',()=>{
const f=fixture();[512,384,160,192].forEach((limit,index)=>{f.inventory.containers[index].memoryLimitBytes=limit*MiB})
const result=evaluate(f.inventory,f.roleById);assert(result.budgetFits);assert.equal(result.plannedBytes,1760*MiB);assert.equal(result.changesAuthorized,false)
})
test('uncertain snapshots, started fixtures and missing limit/identity fields refuse',()=>{
for(const mutate of [f=>{f.inventory.containerSetStable=false},f=>{f.inventory.serviceChanges=true},f=>{f.inventory.newFixtureStarted=true},f=>{delete f.inventory.containers[0].memoryLimitBytes},f=>{f.inventory.containers[0].running=false},f=>{f.inventory.containers[0].imageId='unknown'},f=>{f.inventory.containers[0].startedAt='invalid'}]){const f=fixture();mutate(f);assert.throws(()=>evaluate(f.inventory,f.roleById))}
})
test('missing, foreign, inherited and accessor role labels cannot silently classify a service',()=>{
for(const mode of ['missing','foreign','inherited','accessor']){
const f=fixture(),key=f.inventory.containers[0].id
if(mode==='missing')delete f.roleById[key]
if(mode==='foreign')f.roleById['f'.repeat(64)]='other'
if(mode==='inherited'){const labels=Object.create({[key]:'backend'});for(const [id,role]of Object.entries(f.roleById))if(id!==key)labels[id]=role;f.roleById=labels}
if(mode==='accessor')Object.defineProperty(f.roleById,key,{enumerable:true,get(){assert.fail('Role getter must not execute')}})
assert.throws(()=>evaluate(f.inventory,f.roleById))
}
})
@@ -0,0 +1,22 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import {randomBytes} from 'node:crypto'
const database=process.env.MMAPP_OWN_SQL_DB,url=new URL(process.env.DATABASE_URL)
assert(/^mm_accept_166_\d+$/.test(database??''));assert.equal(url.pathname,'/'+database);assert.equal(url.username,database)
process.env.AUTH_REQUIRED='true';process.env.AUTH_JWT_SECRET=randomBytes(32).toString('hex');process.env.AUTH_ISSUER='https://own203.invalid';process.env.AUTH_PORTAL_URL='https://own203.invalid'
const {pool,closePool}=await import('/app/backend/dist/db/index.js'),{initDatabase}=await import('/app/backend/dist/db/bootstrap.js'),{buildApp}=await import('/app/backend/dist/index.js')
assert.equal((await pool.query('SELECT current_database() AS name')).rows[0].name,database);assert(Object.values((await pool.query('SELECT rolsuper,rolcreatedb,rolcreaterole,rolbypassrls FROM pg_roles WHERE rolname=current_user')).rows[0]).every(v=>v===false))
const secrets={s3:'own203-'+randomBytes(16).toString('hex'),evo:'own203-'+randomBytes(16).toString('hex'),telegram:'12345:'+randomBytes(24).toString('base64url')},cases=[];let app,requests=0;const originalFetch=globalThis.fetch;globalThis.fetch=async()=>{requests++;throw Error('External fetch forbidden in owned secret DTO test')}
const report={date:'2026-10-13',block:'12.5.203',database,scope:'Actual compiled buildApp/auth/GET handlers and own restricted PostgreSQL with nonempty synthetic credentials; isolated signed test JWT, not real Portal login',cases,passed:false}
try{
await initDatabase()
await pool.query("INSERT INTO backup_storage_settings(id,provider,s3_endpoint,s3_access_key_id,s3_secret_access_key) VALUES(1,'local','https://s3.own.invalid','OWN203_PUBLIC_ACCESS_ID',$1) ON CONFLICT(id) DO UPDATE SET s3_secret_access_key=$1",[secrets.s3])
await pool.query("INSERT INTO evobgp_settings(id,base_url,api_key,enabled) VALUES(1,'https://evo.own.invalid',$1,false) ON CONFLICT(id) DO UPDATE SET api_key=$1",[secrets.evo])
await pool.query("INSERT INTO alert_telegram_settings(id,bot_token,chat_id) VALUES(1,$1,'0') ON CONFLICT(id) DO UPDATE SET bot_token=$1",[secrets.telegram])
app=await buildApp({logger:false,startScheduler:false});await app.ready()
for(const role of ['reader','admin']){const permissions=['mm:backups:read','mm:alerts:read',...(role==='admin'?['mm:settings:admin']:[])];const jwt=app.jwt.sign({sub:'own203-'+role,apps:['mm'],permissions,iss:process.env.AUTH_ISSUER},{expiresIn:'5m'});for(const path of ['/api/backups/storage','/api/evobgp/settings','/api/alerts']){const r=await app.inject({method:'GET',url:path,headers:{authorization:'Bearer '+jwt}});assert.equal(r.statusCode,role==='reader'&&path.includes('evobgp')?403:200);for(const secret of Object.values(secrets))assert(!r.body.includes(secret));if(r.statusCode===200){const dto=r.json();if(path.includes('storage')||path.includes('evobgp'))assert.equal(dto.secretConfigured,true);else assert.equal(dto.telegram.tokenConfigured,true)}cases.push({role,path,status:r.statusCode,syntheticSecretExposed:false,configuredFlag:r.statusCode===200});}}
for(const path of ['/api/backups/storage','/api/evobgp/settings','/api/alerts']){assert.equal((await app.inject({method:'GET',url:path})).statusCode,401);cases.push({role:'unauthenticated',path,status:401})}
assert.equal((await pool.query('SELECT s3_secret_access_key FROM backup_storage_settings WHERE id=1')).rows[0].s3_secret_access_key,secrets.s3);assert.equal((await pool.query('SELECT api_key FROM evobgp_settings WHERE id=1')).rows[0].api_key,secrets.evo);assert.equal((await pool.query('SELECT bot_token FROM alert_telegram_settings WHERE id=1')).rows[0].bot_token,secrets.telegram);assert.equal(requests,0);report.externalRequests=0;report.storedSyntheticSecretsUnchanged=true;report.passed=true
}catch(e){report.errorName=e.name;fs.writeFileSync('/app/backend/acceptance/secret203-private.log',String(e.stack??e),{mode:0o600})}
finally{globalThis.fetch=originalFetch;if(app)await app.close();await closePool();fs.writeFileSync('/app/backend/acceptance/secret203-result.json',JSON.stringify(report,null,2));console.log(JSON.stringify(report))}
if(!report.passed)process.exitCode=1
@@ -0,0 +1,193 @@
"""Synthetic-only: no Docker/socket, real /proc/cgroup reads, process enumeration or main()."""
import copy
import importlib.util
from pathlib import Path
from types import SimpleNamespace
import unittest
from unittest.mock import patch
AREA = Path(__file__).parent
spec = importlib.util.spec_from_file_location('memory_measurement_extended', AREA / 'measure-memory.py')
m = importlib.util.module_from_spec(spec)
spec.loader.exec_module(m)
MEMINFO = 'MemTotal: 2048 kB\nMemAvailable: 1024 kB\nSwapTotal: 4096 kB\nSwapFree: 3072 kB\n'
VMSTAT = 'pswpin 20\npswpout 30\noom_kill 0\n'
PSI = 'some avg10=0.00 avg60=1.25 avg300=100.00 total=123456\nfull avg10=0.00 avg60=0.01 avg300=1.00 total=0\n'
ID = 'a' * 64
CG = '/sys/fs/cgroup/system.slice/docker-' + ID + '.scope'
class Synthetic(unittest.TestCase):
def setUp(self):
self.reads = []
self.gets = []
self.before = {'Id': ID, 'Image': 'sha256:' + 'b' * 64, 'RestartCount': 0,
'State': {'StartedAt': '2026-11-02T00:00:00Z', 'Pid': 123, 'Running': True, 'OOMKilled': False},
'HostConfig': {'Memory': 512, 'MemorySwap': 1024}, 'Config': {'Env': ['SECRET=never-retain']}}
self.stats = {'id': ID, 'memory_stats': {'usage': 100, 'limit': 2048, 'stats': {'inactive_file': 30}}}
self.files = {'/proc/meminfo': MEMINFO, '/proc/vmstat': VMSTAT, '/proc/pressure/memory': PSI,
'/proc/self/mountinfo': '1 0 0:1 / /sys/fs/cgroup rw - cgroup2 cgroup rw\n',
'/proc/123/cgroup': '0::/system.slice/docker-' + ID + '.scope\n',
'/proc/123/stat': '123 (owned fixture) S ' + ' '.join(['0'] * 18 + ['987'] + ['0'] * 8),
CG + '/memory.current': '100\n', CG + '/memory.peak': '200\n',
CG + '/memory.events': 'low 0\nhigh 0\nmax 0\noom 0\noom_kill 0\n',
CG + '/memory.swap.current': '0\n', CG + '/memory.swap.peak': '0\n',
CG + '/memory.swap.events': 'high 0\nmax 0\nfail 0\n',
CG + '/memory.max': '512\n', CG + '/memory.swap.max': '512\n',
CG + '/cpu.stat': 'usage_usec 50\nuser_usec 30\nsystem_usec 20\nnr_periods 10\nnr_throttled 2\nthrottled_usec 1234\nnr_bursts 0\n'}
def deny(*_args, **_kwargs):
raise AssertionError('Real external/OS I/O is forbidden by synthetic test')
# Module-local OS proxy avoids changing unittest/Python's filesystem operations.
fake_os = SimpleNamespace(**vars(m.os))
fake_os.path = SimpleNamespace(**vars(m.os.path))
fake_os.sep = '/' # synthetic Linux path contract, not this Windows host
fake_os.open = deny
fake_os.stat = deny
fake_os.path.realpath = deny
self.enterContext(patch.object(m, 'os', fake_os))
self.enterContext(patch.object(m, 'read_bounded', side_effect=deny))
self.enterContext(patch.object(m, 'docker_get', side_effect=deny))
self.enterContext(patch.object(m.socket, 'socket', side_effect=deny))
def read(self, name, expected_device=None, deadline=None):
self.reads.append(name)
if name not in self.files:
raise FileNotFoundError('synthetic missing')
value = self.files[name]
if isinstance(value, BaseException):
raise value
return value
def observe(self, after=None, realpath=None, statter=None):
after = copy.deepcopy(self.before) if after is None else after
def get(name, deadline=None):
self.gets.append(name)
if self.gets == ['/containers/' + ID + '/json']:
return copy.deepcopy(self.before)
if self.gets == ['/containers/' + ID + '/json', '/containers/' + ID + '/stats?stream=false&one-shot=true']:
return copy.deepcopy(self.stats)
if self.gets == ['/containers/' + ID + '/json', '/containers/' + ID + '/stats?stream=false&one-shot=true', '/containers/' + ID + '/json']:
return copy.deepcopy(after)
raise AssertionError('Unexpected Docker path or extra request')
def statter_default(name):
allowed = {'/sys/fs/cgroup': (8, 1), CG: (8, 2), '/proc/123/root': (8, 3)}
if name not in allowed:
raise AssertionError('Unexpected stat path')
dev, ino = allowed[name]
return SimpleNamespace(st_dev=dev, st_ino=ino)
with patch.object(m, 'read_bounded', side_effect=self.read), patch.object(m, 'docker_get', side_effect=get), \
patch.object(m.os.path, 'realpath', side_effect=realpath or (lambda p: p)), \
patch.object(m.os, 'stat', side_effect=statter or statter_default):
return m.collect_container(ID)
def test_limits_max_zero_finite_are_not_missing_or_host_ram(self):
for value, expected in [('max\n', 'max'), ('max', 'max'), ('0\n', 0), ('512\n', 512), (str(2**64-1), 2**64-1)]:
self.assertEqual(m.parse_limit(value), expected)
for bad in ('MAX', '-1', ' 0', '00', '1.0', str(2**64), 'max\n\n'):
with self.assertRaises(ValueError): m.parse_limit(bad)
def test_cpu_throttling_units_required_fields_and_output_whitelist(self):
parsed = m.parse_cpu_throttling(self.files[CG + '/cpu.stat'])
self.assertEqual(parsed, {'nr_periods':10,'nr_throttled':2,'throttled_usec':1234})
for bad in ('nr_periods 1\nnr_throttled 0\n', 'nr_periods 1\nnr_periods 2\nnr_throttled 0\nthrottled_usec 0\n', 'nr_periods -1\nnr_throttled 0\nthrottled_usec 0\n'):
with self.assertRaises(ValueError): m.parse_cpu_throttling(bad)
def test_pressure_fixed_groups_percent_and_microseconds(self):
parsed = m.parse_memory_pressure(PSI)
self.assertEqual(parsed['some'], {'avg10Percent':'0.00','avg60Percent':'1.25','avg300Percent':'100.00','totalMicros':123456})
self.assertEqual(parsed['full']['totalMicros'], 0)
for bad in (PSI.replace('100.00','100.01'), PSI.replace('1.25','nan'), PSI.replace('total=0','total=-1'), PSI.splitlines()[0], PSI.replace('full','some'), PSI+'extra\n', PSI.replace('total=0','total='+str(2**64))):
with self.assertRaises(ValueError): m.parse_memory_pressure(bad)
def test_docker_memoryswap_raw_minus1_zero_finite_and_missing_differ(self):
for raw in (-1,0,512,2**63-1):
item = copy.deepcopy(self.before); item['HostConfig']['MemorySwap'] = raw
self.assertEqual(m.configured_memory_swap(item), raw)
for bad in (None,False,True,'0',-2,2**63):
item = copy.deepcopy(self.before); item['HostConfig']['MemorySwap'] = bad
self.assertIsNone(m.configured_memory_swap(item))
for host in ({},None,[]):
self.assertIsNone(m.configured_memory_swap({'HostConfig':host}))
def test_real_collection_paths_are_only_fixed_host_and_retained_cgroup(self):
row = self.observe()
self.assertTrue(row['identityStableDuringSample'])
self.assertTrue(row['resourceLimitsStableDuringSample'])
self.assertEqual(row['configuredMemorySwapBytes'], 1024)
self.assertEqual(row['cgroupV2']['status'], 'available')
self.assertEqual(row['cgroupV2']['metrics']['memory.max'], 512)
self.assertEqual(row['cgroupV2']['metrics']['cpu.stat']['throttled_usec'], 1234)
self.assertTrue(set(self.reads).issubset(set(self.files)))
self.assertNotIn('SECRET', str(row)); self.assertNotIn('Env', str(row))
self.assertEqual(len(self.gets), 3)
def test_unlimited_limits_do_not_become_numeric_zero(self):
self.files[CG+'/memory.max']='max\n'; self.files[CG+'/memory.swap.max']='max\n'
row=self.observe();self.assertEqual(row['cgroupV2']['metrics']['memory.max'],'max');self.assertEqual(row['cgroupV2']['metrics']['memory.swap.max'],'max')
def test_each_new_cgroup_source_missing_or_malformed_is_unknown(self):
for name in ('memory.max','memory.swap.max','cpu.stat'):
for value in (None,'not-a-counter\n'):
with self.subTest(name=name,value=value):
self.setUp_fixture_only()
if value is None: self.files.pop(CG+'/'+name)
else: self.files[CG+'/'+name]=value
row=self.observe();self.assertEqual(row['cgroupV2']['status'],'partial');self.assertIsNone(row['cgroupV2']['metrics'][name]);self.assertIn(name,row['cgroupV2']['unavailable'])
def setUp_fixture_only(self):
# Recreate dictionary, not guards/patches; no real I/O.
self.files.update({CG+'/memory.max':'512\n',CG+'/memory.swap.max':'512\n',CG+'/cpu.stat':'nr_periods 10\nnr_throttled 2\nthrottled_usec 1234\n'});self.gets=[]
def test_missing_or_changed_swap_configuration_cannot_authenticate_limits(self):
after=copy.deepcopy(self.before);after['HostConfig']['MemorySwap']=2048
row=self.observe(after);self.assertFalse(row['resourceLimitsStableDuringSample']);self.assertEqual(row['cgroupV2']['status'],'partial');self.assertIsNone(row['cgroupV2']['metrics']['memory.swap.max'])
self.gets=[];self.before['HostConfig'].pop('MemorySwap');after=copy.deepcopy(self.before)
row=self.observe(after);self.assertIsNone(row['configuredMemorySwapBytes']);self.assertEqual(row['configuredMemorySwapStatus'],'unavailable');self.assertIsNone(row['resourceLimitsStableDuringSample']);self.assertEqual(row['cgroupV2']['status'],'partial')
def test_memory_limit_change_cannot_reuse_stable_container_identity_as_limit_proof(self):
after=copy.deepcopy(self.before);after['HostConfig']['Memory']=1024
row=self.observe(after)
self.assertTrue(row['identityStableDuringSample'])
self.assertFalse(row['resourceLimitsStableDuringSample'])
for key in ('memory.max','memory.swap.max'):
self.assertIsNone(row['cgroupV2']['metrics'][key])
self.assertEqual(row['cgroupV2']['unavailable'][key],'container-resource-limits-changed-or-unavailable')
def test_legacy_projection_fields_keep_cache_limit_and_rss_distinctions(self):
item=copy.deepcopy(self.before);item['HostConfig'].pop('MemorySwap');item['HostConfig']['Memory']=0
row=m.project_container(item,self.stats,ID)
self.assertEqual(row['usageIncludingCacheBytes'],100)
self.assertEqual(row['usageMinusInactiveFileBytes'],70)
self.assertEqual(row['configuredLimitBytes'],0)
self.assertEqual(row['reportedLimitBytes'],2048)
self.assertEqual(row['configuredMemorySwapStatus'],'unavailable')
self.assertIsNone(row['configuredMemorySwapBytes'])
def test_identity_drift_symlink_and_pid_generation_still_refuse(self):
after=copy.deepcopy(self.before);after['State']['Pid']=456
row=self.observe(after);self.assertFalse(row['identityStableDuringSample']);self.assertIsNone(row['resourceLimitsStableDuringSample']);self.assertEqual(row['cgroupV2']['status'],'unavailable')
self.gets=[];row=self.observe(realpath=lambda p:'/other' if p.endswith('memory.swap.max') else p);self.assertEqual(row['cgroupV2']['status'],'partial');self.assertIsNone(row['cgroupV2']['metrics']['memory.swap.max'])
self.gets=[]
with patch.object(m,'process_generation',side_effect=[(1,8,3),(2,8,3)]):
self.assertEqual(self.observe()['cgroupV2']['status'],'unavailable')
def test_host_collection_whitelists_one_psi_file_and_missing_is_explicit(self):
with patch.object(m,'read_bounded',side_effect=self.read):
host=m.collect_host();self.assertEqual(host['memoryPressure']['status'],'available');self.assertEqual(host['MemTotalBytes'],2048*1024)
self.assertEqual(self.reads,['/proc/meminfo','/proc/vmstat','/proc/pressure/memory'])
self.files.pop('/proc/pressure/memory');host=m.collect_host();self.assertEqual(host['memoryPressure']['status'],'unavailable');self.assertIsNone(host['memoryPressure']['metrics'])
def test_deadline_never_becomes_partial_or_success(self):
self.files[CG+'/cpu.stat']=m.ObservationDeadlineExceeded('fixture-deadline')
with self.assertRaises(m.ObservationDeadlineExceeded): self.observe()
with patch.object(m,'read_bounded',side_effect=[MEMINFO,VMSTAT,m.ObservationDeadlineExceeded('fixture-deadline')]):
with self.assertRaises(m.ObservationDeadlineExceeded): m.collect_host()
def test_summary_exposes_unknown_and_never_authorizes_runtime(self):
row=self.observe()
with patch.object(m,'read_bounded',side_effect=self.read):host=m.collect_host()
sample={'elapsedMs':0,'host':host,'containers':[row]}
out=m.summarize([sample]);self.assertTrue(out['allResourceLimitsStableDuringSample']);self.assertTrue(out['allHostMemoryPressureAvailable']);self.assertFalse(out['acceptancePassed'])
row['resourceLimitsStableDuringSample']=None;host['memoryPressure']['status']='unavailable'
out=m.summarize([sample]);self.assertFalse(out['allResourceLimitsStableDuringSample']);self.assertFalse(out['allHostMemoryPressureAvailable']);self.assertFalse(out['acceptancePassed'])
if __name__=='__main__': unittest.main()
@@ -0,0 +1,459 @@
"""Bounded Linux/Docker memory observation; GET only, no service changes.
Run alongside the named acceptance workload. Output is evidence, never an
automatic declaration that a 2 GB deployment has passed its load acceptance.
Requires Python 3 and read access to /proc and the local Docker socket.
"""
import argparse
import http.client
import json
import os
import re
import socket
import stat
import threading
import time
MAX_RESPONSE = 4 * 1024 * 1024
MAX_FILE_BYTES = 65536
MAX_FILE_LINES = 256
CGROUP_ROOT = '/sys/fs/cgroup'
CGROUP_FILES = ('memory.current', 'memory.peak', 'memory.events',
'memory.swap.current', 'memory.swap.peak', 'memory.swap.events',
'memory.max', 'memory.swap.max', 'cpu.stat')
class CgroupUnavailable(ValueError):
"""Fixed reason codes only; never include private paths or raw content."""
class ObservationDeadlineExceeded(RuntimeError):
pass
def check_deadline(deadline):
if deadline is not None and time.monotonic() >= deadline:
raise ObservationDeadlineExceeded('Observation time bound exceeded')
def read_bounded(path, expected_device=None, deadline=None):
# Fixed production paths only; no CLI path or executable is accepted.
check_deadline(deadline)
descriptor = os.open(path, os.O_RDONLY | getattr(os, 'O_NOFOLLOW', 0) | getattr(os, 'O_NONBLOCK', 0))
with os.fdopen(descriptor, 'rb') as handle:
identity = os.fstat(handle.fileno())
if not stat.S_ISREG(identity.st_mode) or (expected_device is not None and identity.st_dev != expected_device):
raise ValueError('Non-regular observation')
raw = handle.read(MAX_FILE_BYTES + 1)
check_deadline(deadline)
if len(raw) > MAX_FILE_BYTES:
raise ValueError('Observation file too large')
text = raw.decode('ascii')
if len(text.splitlines()) > MAX_FILE_LINES:
raise ValueError('Too many observation lines')
return text
def parse_counter(text):
if not re.fullmatch(r'(?:0|[1-9][0-9]{0,19})\n?', text):
raise ValueError('Invalid counter')
value = int(text)
if value > 2 ** 64 - 1:
raise ValueError('Counter overflow')
return value
def parse_events(text, required):
lines = text.splitlines()
if not 1 <= len(lines) <= 32:
raise ValueError('Invalid event count')
result = {}
for line in lines:
fields = line.split(' ')
if len(fields) != 2 or not re.fullmatch(r'[a-z_]{1,64}', fields[0]) or fields[0] in result:
raise ValueError('Invalid event')
result[fields[0]] = parse_counter(fields[1])
if not set(required).issubset(result):
raise ValueError('Incomplete events')
return result
def parse_limit(text):
# Preserve unlimited vs zero vs unavailable; never substitute host RAM.
return 'max' if text in ('max', 'max\n') else parse_counter(text)
def parse_cpu_throttling(text):
required = ('nr_periods', 'nr_throttled', 'throttled_usec')
parsed = parse_events(text, required)
# Kernel extensions are validated but do not expand the output whitelist.
return {key: parsed[key] for key in required}
def parse_memory_pressure(text):
rows = text.splitlines()
if len(rows) != 2:
raise ValueError('Incomplete memory pressure')
result = {}
for line in rows:
match = re.fullmatch(r'(some|full) avg10=([0-9]{1,3}\.[0-9]{2}) avg60=([0-9]{1,3}\.[0-9]{2}) avg300=([0-9]{1,3}\.[0-9]{2}) total=((?:0|[1-9][0-9]{0,19}))', line)
if not match or match[1] in result:
raise ValueError('Invalid memory pressure')
for value in match.group(2, 3, 4):
whole, fraction = value.split('.')
if int(whole) * 100 + int(fraction) > 10000:
raise ValueError('Invalid memory pressure percentage')
result[match[1]] = {'avg10Percent': match[2], 'avg60Percent': match[3],
'avg300Percent': match[4], 'totalMicros': parse_counter(match[5])}
if set(result) != {'some', 'full'}:
raise ValueError('Incomplete memory pressure')
return result
def configured_memory_swap(inspect):
# Docker MemorySwap is TOTAL memory+swap; 0 means daemon default, -1 unlimited.
host = inspect.get('HostConfig')
value = host.get('MemorySwap') if isinstance(host, dict) else None
if type(value) is not int or not -1 <= value <= 2 ** 63 - 1:
return None
return value
def resource_limits_stable(before, after):
values = []
for item in (before, after):
host = item.get('HostConfig')
memory = host.get('Memory') if isinstance(host, dict) else None
swap = configured_memory_swap(item)
if type(memory) is not int or not 0 <= memory <= 2 ** 63 - 1 or swap is None:
return None
values.append((memory, swap))
return values[0] == values[1]
def collect_host(deadline=None):
memory = parse_host(read_bounded('/proc/meminfo', deadline=deadline),
read_bounded('/proc/vmstat', deadline=deadline))
try:
pressure = parse_memory_pressure(read_bounded('/proc/pressure/memory', deadline=deadline))
memory['memoryPressure'] = {'status': 'available', 'metrics': pressure, 'reason': None}
except (OSError, ValueError, UnicodeError):
memory['memoryPressure'] = {'status': 'unavailable', 'metrics': None,
'reason': 'missing-unreadable-or-malformed'}
return memory
def container_identity(inspect, identifier):
state = inspect['State']
if (inspect.get('Id') != identifier or not re.fullmatch(r'[a-f0-9]{64}', identifier)
or type(state.get('Pid')) is not int or not 0 < state['Pid'] <= 2 ** 31 - 1
or state.get('Running') is not True or type(state.get('OOMKilled')) is not bool
or not isinstance(state.get('StartedAt'), str) or not state['StartedAt']
or type(inspect.get('RestartCount')) is not int or inspect['RestartCount'] < 0
or not isinstance(inspect.get('Image'), str) or not inspect['Image']):
raise ValueError('Unavailable container identity')
return (identifier, inspect['Image'], state['StartedAt'], state['Pid'], inspect['RestartCount'], state['OOMKilled'])
def cgroup_location(pid, identifier, deadline=None):
check_deadline(deadline)
mounts = read_bounded('/proc/self/mountinfo', deadline=deadline).splitlines()
# Namespace-relative/subtree mounts are deliberately unsupported: their
# root cannot authenticate a host container's complete cgroup membership.
matches = [line.split(' - ', 1) for line in mounts if ' - ' in line]
if not any(left.split()[3:5] == ['/', CGROUP_ROOT] and right.split()[0] == 'cgroup2'
for left, right in matches):
raise CgroupUnavailable('unsupported-or-unavailable-host-cgroup2-mount')
membership = read_bounded('/proc/' + str(pid) + '/cgroup', deadline=deadline)
lines = membership.splitlines()
if len(lines) != 1 or not lines[0].startswith('0::/'):
raise CgroupUnavailable('unsupported-v1-hybrid-or-malformed-membership')
relative = lines[0][3:]
parts = relative.split('/')[1:]
if (not parts or any(not p or p in ('.', '..') or not re.fullmatch(r'[A-Za-z0-9_.:-]{1,128}', p) for p in parts)
or parts[-1] not in (identifier, 'docker-' + identifier + '.scope')):
raise ValueError('Unbound cgroup membership')
root = os.path.realpath(CGROUP_ROOT)
path = os.path.realpath(CGROUP_ROOT + relative)
if root != CGROUP_ROOT or path != root + relative or not path.startswith(root + os.sep):
raise ValueError('Cgroup path escaped')
return path, membership
def process_generation(pid, deadline=None):
check_deadline(deadline)
raw = read_bounded('/proc/' + str(pid) + '/stat', deadline=deadline)
prefix, suffix = raw.rsplit(')', 1)
if prefix.split(' ', 1)[0] != str(pid) or ' (' not in prefix:
raise ValueError('Invalid process identity')
fields = suffix.split()
if len(fields) < 20:
raise ValueError('Incomplete process identity')
start = parse_counter(fields[19])
root = os.stat('/proc/' + str(pid) + '/root')
return start, root.st_dev, root.st_ino
def unavailable_cgroup(reason):
return {'version': 2, 'status': 'unavailable', 'reason': reason,
'metrics': {name: None for name in CGROUP_FILES}, 'unavailable': {name: reason for name in CGROUP_FILES}}
def collect_container(identifier, deadline=None):
check_deadline(deadline)
before = docker_get('/containers/' + identifier + '/json', deadline)
stats = docker_get('/containers/' + identifier + '/stats?stream=false&one-shot=true', deadline)
check_deadline(deadline)
result = project_container(before, stats, identifier)
cgroup = unavailable_cgroup('identity-unavailable')
try:
identity = container_identity(before, identifier)
pid = identity[3]
generation = process_generation(pid, deadline)
path, membership = cgroup_location(pid, identifier, deadline)
directory = os.stat(path)
if directory.st_dev != os.stat(CGROUP_ROOT).st_dev:
raise ValueError('Cgroup filesystem changed')
metrics, missing = {}, {}
for name in CGROUP_FILES:
check_deadline(deadline)
try:
candidate = path + '/' + name
if os.path.realpath(candidate) != candidate:
raise ValueError('Symlinked metric')
raw = read_bounded(candidate, directory.st_dev, deadline=deadline)
check_deadline(deadline)
if name == 'memory.events':
metrics[name] = parse_events(raw, ('low', 'high', 'max', 'oom', 'oom_kill'))
elif name == 'memory.swap.events':
metrics[name] = parse_events(raw, ('high', 'max', 'fail'))
elif name in ('memory.max', 'memory.swap.max'):
metrics[name] = parse_limit(raw)
elif name == 'cpu.stat':
metrics[name] = parse_cpu_throttling(raw)
else:
metrics[name] = parse_counter(raw)
except (OSError, ValueError, UnicodeError):
metrics[name] = None
missing[name] = 'missing-unreadable-or-malformed'
check_deadline(deadline)
after_path, after_membership = cgroup_location(pid, identifier, deadline)
after_directory = os.stat(path)
if (process_generation(pid, deadline) != generation or after_path != path or after_membership != membership
or (directory.st_dev, directory.st_ino) != (after_directory.st_dev, after_directory.st_ino)):
cgroup = unavailable_cgroup('process-or-cgroup-changed')
else:
cgroup = {'version': 2, 'status': 'partial' if missing else 'available',
'reason': 'some-metrics-unavailable' if missing else None,
'metrics': metrics, 'unavailable': missing,
'peakScope': 'cgroup lifetime or since external reset; not an isolated workload peak',
'provenance': 'fixed host cgroup2 mount + exact Docker ID membership + process/root generation'}
except CgroupUnavailable as error:
cgroup = unavailable_cgroup(str(error))
except (OSError, ValueError, KeyError, IndexError, UnicodeError):
cgroup = unavailable_cgroup('identity-process-or-cgroup-unavailable-or-unsupported')
# Even failed cgroup observations are bracketed by a fresh Docker inspect.
check_deadline(deadline)
after = docker_get('/containers/' + identifier + '/json', deadline)
check_deadline(deadline)
try:
stable = container_identity(before, identifier) == container_identity(after, identifier)
except (ValueError, KeyError, TypeError):
stable = False
if not stable:
cgroup = unavailable_cgroup('container-identity-changed-or-unavailable')
limits_stable = resource_limits_stable(before, after) if stable else None
if stable and limits_stable is not True and cgroup['status'] != 'unavailable':
for key in ('memory.max', 'memory.swap.max'):
cgroup['metrics'][key] = None
cgroup['unavailable'][key] = 'container-resource-limits-changed-or-unavailable'
cgroup['status'] = 'partial'
cgroup['reason'] = 'some-metrics-unavailable'
result.update({'resourceLimitsStableDuringSample': limits_stable,
'pid': before['State'].get('Pid'), 'identityStableDuringSample': stable,
'runningAfter': after.get('State', {}).get('Running'),
'oomKilledAfter': after.get('State', {}).get('OOMKilled'), 'cgroupV2': cgroup})
return result
def docker_get(path, deadline=None):
deadline = time.monotonic() + 5 if deadline is None else deadline
check_deadline(deadline)
connection = http.client.HTTPConnection('localhost', timeout=min(5, deadline - time.monotonic()))
owned_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
connection.sock = owned_socket
# A socket timeout alone permits an endless body that drips bytes. The
# watchdog owns precisely this request's socket, including after HTTP's
# Connection:close detaches it from HTTPConnection into HTTPResponse.
def expire():
try:
owned_socket.shutdown(socket.SHUT_RDWR)
except OSError:
pass
finally:
owned_socket.close()
watchdog = threading.Timer(max(0, deadline - time.monotonic()), expire)
watchdog.daemon = True
watchdog_started = False
response = None
try:
watchdog.start()
watchdog_started = True
owned_socket.settimeout(min(5, max(0.001, deadline - time.monotonic())))
check_deadline(deadline)
owned_socket.connect('/var/run/docker.sock')
check_deadline(deadline)
connection.request('GET', '/v1.45' + path, headers={'Connection': 'close'})
response = connection.getresponse()
raw = response.read(MAX_RESPONSE + 1)
check_deadline(deadline)
if response.status != 200 or len(raw) > MAX_RESPONSE:
raise ValueError('Docker observation unavailable')
result = json.loads(raw)
check_deadline(deadline)
return result
except Exception:
check_deadline(deadline)
raise
finally:
watchdog.cancel()
if watchdog_started:
watchdog.join()
try:
if response is not None:
response.close()
finally:
try:
connection.close()
finally:
owned_socket.close()
def nonnegative(value):
if type(value) is not int or value < 0:
raise ValueError('Invalid numeric observation')
return value
def parse_host(meminfo, vmstat):
memory = {}
for line in meminfo.splitlines():
fields = line.split()
if fields and fields[0] in ('MemTotal:', 'MemAvailable:', 'SwapTotal:', 'SwapFree:'):
if len(fields) != 3 or fields[2] != 'kB':
raise ValueError('Invalid meminfo units')
memory[fields[0][:-1] + 'Bytes'] = nonnegative(int(fields[1])) * 1024
if len(memory) != 4 or memory['MemAvailableBytes'] > memory['MemTotalBytes'] or memory['SwapFreeBytes'] > memory['SwapTotalBytes']:
raise ValueError('Incomplete host memory observation')
for line in vmstat.splitlines():
fields = line.split()
if fields and fields[0] in ('pswpin', 'pswpout', 'oom_kill'):
memory[fields[0]] = nonnegative(int(fields[1]))
return memory
def project_container(inspect, stats, identifier):
if inspect.get('Id') != identifier or stats.get('id') != identifier:
raise ValueError('Pinned container changed')
state = inspect['State']
memory = stats['memory_stats']
usage = nonnegative(memory['usage'])
inactive = memory.get('stats', {}).get('inactive_file')
if inactive is not None:
inactive = nonnegative(inactive)
if inactive > usage:
raise ValueError('Invalid cache observation')
return {
'id': identifier, 'imageId': inspect['Image'],
'startedAt': state['StartedAt'], 'running': state['Running'] is True,
'oomKilled': state.get('OOMKilled'), 'restartCount': inspect.get('RestartCount'),
'usageIncludingCacheBytes': usage, 'inactiveFileBytes': inactive,
'usageMinusInactiveFileBytes': usage - inactive if inactive is not None else None,
'configuredLimitBytes': nonnegative(inspect['HostConfig']['Memory']),
'configuredMemorySwapBytes': configured_memory_swap(inspect),
'configuredMemorySwapStatus': 'available' if configured_memory_swap(inspect) is not None else 'unavailable',
'reportedLimitBytes': nonnegative(memory['limit']),
'swapUsageBytes': memory.get('swap_usage'),
}
def summarize(samples):
if not samples:
raise ValueError('No observations')
baseline = {c['id']: (c['imageId'], c['startedAt'], c['restartCount'], c.get('pid'), c.get('oomKilled')) for c in samples[0]['containers']}
stable = all({c['id']: (c['imageId'], c['startedAt'], c['restartCount'], c.get('pid'), c.get('oomKilled')) for c in sample['containers']} == baseline
and all(c.get('identityStableDuringSample', True) for c in sample['containers']) for sample in samples)
counters = {}
for field in ('pswpin', 'pswpout', 'oom_kill'):
first = samples[0]['host'].get(field)
last = samples[-1]['host'].get(field)
counters[field + 'Delta'] = last - first if first is not None and last is not None and last >= first else None
return {
'samples': len(samples), 'elapsedMs': samples[-1]['elapsedMs'],
'hostMemTotalBytes': samples[0]['host']['MemTotalBytes'],
'minHostAvailableBytes': min(s['host']['MemAvailableBytes'] for s in samples),
'maxObservedContainerUsageIncludingCacheBytes': max(sum(c['usageIncludingCacheBytes'] for c in s['containers']) for s in samples),
'containerIdentityAndStartStable': stable,
'allObservedRunning': all(c['running'] is True and c.get('runningAfter') is True
and c.get('identityStableDuringSample') is True for s in samples for c in s['containers']),
'anyObservedOomKilled': any(c['oomKilled'] is True or c.get('oomKilledAfter') is True for s in samples for c in s['containers']),
'allCgroupV2MetricsAvailable': all(s['containers'] and all(c.get('cgroupV2', {}).get('status') == 'available' for c in s['containers']) for s in samples),
**counters,
'allResourceLimitsStableDuringSample': all(c.get('resourceLimitsStableDuringSample') is True for s in samples for c in s['containers']) and all(s['containers'] for s in samples),
'allHostMemoryPressureAvailable': all(s['host'].get('memoryPressure', {}).get('status') == 'available' for s in samples),
'acceptancePassed': False,
'scope': 'sampled evidence only; correlate with workload, limits, errors and latency; unsampled peaks are unknown',
}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--daemon-id', required=True)
parser.add_argument('--container', action='append', required=True)
parser.add_argument('--samples', type=int, default=30)
parser.add_argument('--interval', type=float, default=1)
parser.add_argument('--output', required=True)
args = parser.parse_args()
if os.name != 'posix' or not hasattr(socket, 'AF_UNIX'):
parser.error('Run on the Linux Docker host')
if not re.fullmatch(r'[A-Za-z0-9-]{8,128}', args.daemon_id) or not 1 <= len(args.container) <= 16 or len(set(args.container)) != len(args.container) or any(not re.fullmatch(r'[a-f0-9]{64}', c) for c in args.container):
parser.error('Pin exact daemon ID and 1..16 distinct full container IDs')
if not 1 <= args.samples <= 300 or not 0.2 <= args.interval <= 10 or args.samples * args.interval > 300:
parser.error('Observation bound: 300 samples / 300 seconds')
started = time.monotonic()
deadline = started + 360
if docker_get('/info', deadline).get('ID') != args.daemon_id:
raise ValueError('Pinned Docker daemon changed')
samples = []
for index in range(args.samples):
check_deadline(deadline)
host = collect_host(deadline)
containers = []
for identifier in args.container:
check_deadline(deadline)
containers.append(collect_container(identifier, deadline))
check_deadline(deadline)
samples.append({'elapsedMs': round((time.monotonic() - started) * 1000), 'host': host, 'containers': containers})
if index + 1 < args.samples:
time.sleep(min(args.interval, max(0, deadline - time.monotonic())))
check_deadline(deadline)
if docker_get('/info', deadline).get('ID') != args.daemon_id:
raise ValueError('Pinned Docker daemon changed')
result = {'schemaVersion': 3, 'readOnlyDockerAndProc': True, 'serviceChanges': 0, 'summary': summarize(samples), 'observations': samples}
encoded = json.dumps(result, indent=2) + '\n'
check_deadline(deadline)
# Refuse to replace an existing evidence file. Never dump Docker env/logs.
with open(args.output, 'x', encoding='utf-8') as handle:
check_deadline(deadline)
handle.write(encoded)
check_deadline(deadline)
print(json.dumps(result['summary']))
if __name__ == '__main__':
try:
main()
except Exception as error:
print(json.dumps({'status': 'observation-refused', 'errorClass': type(error).__name__, 'serviceChanges': 0}))
raise SystemExit(1)
@@ -0,0 +1,384 @@
import copy
import importlib.util
from pathlib import Path
import os
import tempfile
import io
import sys
import threading
import time
import unittest
from unittest.mock import patch
spec = importlib.util.spec_from_file_location('memory_measure', Path(__file__).with_name('measure-memory.py'))
measure = importlib.util.module_from_spec(spec)
spec.loader.exec_module(measure)
class MemoryEvidenceTests(unittest.TestCase):
def test_host_units_and_missing_fields(self):
raw = 'MemTotal: 2048 kB\nMemAvailable: 1024 kB\nSwapTotal: 4096 kB\nSwapFree: 3072 kB\n'
parsed = measure.parse_host(raw, 'pswpin 20\npswpout 30\noom_kill 1\n')
self.assertEqual(parsed['MemTotalBytes'], 2048 * 1024)
self.assertEqual(parsed['pswpin'], 20)
for invalid in (raw.replace('kB', 'MB'), raw.replace('MemAvailable: 1024 kB\n', ''), raw.replace('MemAvailable: 1024', 'MemAvailable: 4096')):
with self.assertRaises(ValueError):
measure.parse_host(invalid, '')
def test_cache_and_no_configured_limit_are_distinct(self):
item = {'Id': 'a' * 64, 'Image': 'sha256:fixed', 'State': {'StartedAt': 't0', 'Running': True}, 'HostConfig': {'Memory': 0}}
stats = {'id': 'a' * 64, 'memory_stats': {'usage': 100, 'limit': 2048, 'stats': {'inactive_file': 30}}}
result = measure.project_container(item, stats, 'a' * 64)
self.assertEqual(result['usageIncludingCacheBytes'], 100)
self.assertEqual(result['usageMinusInactiveFileBytes'], 70)
self.assertEqual(result['configuredLimitBytes'], 0)
with self.assertRaises(ValueError):
measure.project_container(item, stats, 'b' * 64)
def test_simultaneous_peak_is_not_sum_of_separate_peaks(self):
host = {'MemTotalBytes': 2048, 'MemAvailableBytes': 1024, 'pswpin': 3, 'pswpout': 5, 'oom_kill': 1}
def container(identifier, usage):
return {'id': identifier, 'imageId': 'i', 'startedAt': 't0', 'restartCount': 0, 'running': True, 'oomKilled': False, 'usageIncludingCacheBytes': usage}
samples = [{'elapsedMs': 0, 'host': host, 'containers': [container('a', 100), container('b', 20)]},
{'elapsedMs': 1000, 'host': {**host, 'pswpin': 4}, 'containers': [container('a', 20), container('b', 100)]}]
result = measure.summarize(samples)
self.assertEqual(result['maxObservedContainerUsageIncludingCacheBytes'], 120)
self.assertEqual(result['pswpinDelta'], 1)
self.assertTrue(result['containerIdentityAndStartStable'])
self.assertFalse(result['acceptancePassed'])
changed = copy.deepcopy(samples)
changed[-1]['containers'][0]['startedAt'] = 't1'
changed[-1]['containers'][1]['oomKilled'] = True
self.assertFalse(measure.summarize(changed)['containerIdentityAndStartStable'])
self.assertTrue(measure.summarize(changed)['anyObservedOomKilled'])
def test_counter_rollback_is_unknown(self):
samples = [{'elapsedMs': i, 'host': {'MemTotalBytes': 2, 'MemAvailableBytes': 1, 'pswpin': value}, 'containers': []} for i, value in enumerate([100, 2])]
self.assertIsNone(measure.summarize(samples)['pswpinDelta'])
class CgroupEvidenceTests(unittest.TestCase):
identifier = 'a' * 64
path = '/sys/fs/cgroup/system.slice/docker-' + identifier + '.scope'
def setUp(self):
self.before = {'Id': self.identifier, 'Image': 'sha256:fixed', 'RestartCount': 0,
'State': {'StartedAt': 'original-start', 'Pid': 123, 'Running': True, 'OOMKilled': False},
'HostConfig': {'Memory': 512, 'MemorySwap': 1024}}
self.stats = {'id': self.identifier, 'memory_stats': {'usage': 100, 'limit': 512, 'stats': {'inactive_file': 30}}}
self.files = {
'/proc/self/mountinfo': '1 0 0:1 / /sys/fs/cgroup rw - cgroup2 cgroup rw\n',
'/proc/123/cgroup': '0::/system.slice/docker-' + self.identifier + '.scope\n',
'/proc/123/stat': '123 (a name with ) parentheses) S ' + ' '.join(['0'] * 18 + ['987'] + ['0'] * 8),
self.path + '/memory.max': '512\n', self.path + '/memory.swap.max': '512\n',
self.path + '/cpu.stat': 'nr_periods 10\nnr_throttled 2\nthrottled_usec 1234\n',
self.path + '/memory.current': '100\n', self.path + '/memory.peak': '200\n',
self.path + '/memory.events': 'low 0\nhigh 1\nmax 2\noom 3\noom_kill 4\n',
self.path + '/memory.swap.current': '5\n', self.path + '/memory.swap.peak': '6\n',
self.path + '/memory.swap.events': 'high 0\nmax 2\nfail 1\n',
}
def observe(self, after=None, reader=None, resolver=None, statter=None):
def read(path, _expected_device=None, deadline=None):
if path not in self.files:
raise FileNotFoundError()
return self.files[path]
queries = []
def get(path, deadline=None):
queries.append(path)
if '/stats?' in path:
return self.stats
return self.before if len(queries) == 1 else (after or self.before)
with patch.object(measure, 'docker_get', side_effect=get), \
patch.object(measure, 'read_bounded', side_effect=reader or read), \
patch.object(measure.os.path, 'realpath', side_effect=resolver or (lambda path: path)), \
patch.object(measure.os, 'stat', side_effect=statter or (lambda path: os.stat_result((0, 7, 8, 0, 0, 0, 0, 0, 0, 0)))), \
patch.object(measure.os, 'sep', '/'):
result = measure.collect_container(self.identifier)
self.assertEqual(queries, ['/containers/' + self.identifier + '/json',
'/containers/' + self.identifier + '/stats?stream=false&one-shot=true',
'/containers/' + self.identifier + '/json'])
return result
def test_exact_identity_metrics_and_docker_cache_are_distinct(self):
result = self.observe()
self.assertTrue(result['identityStableDuringSample'])
self.assertEqual(result['usageMinusInactiveFileBytes'], 70)
self.assertEqual(result['cgroupV2']['status'], 'available')
self.assertEqual(result['cgroupV2']['metrics']['memory.current'], 100)
self.assertEqual(result['cgroupV2']['metrics']['memory.peak'], 200)
self.assertEqual(result['cgroupV2']['metrics']['memory.events']['oom_kill'], 4)
self.assertEqual(result['cgroupV2']['metrics']['memory.swap.current'], 5)
self.assertEqual(result['cgroupV2']['metrics']['memory.swap.peak'], 6)
self.assertEqual(result['cgroupV2']['metrics']['memory.swap.events']['fail'], 1)
def test_optional_swap_peak_missing_is_unknown_not_zero(self):
del self.files[self.path + '/memory.swap.peak']
result = self.observe()['cgroupV2']
self.assertEqual(result['status'], 'partial')
self.assertIsNone(result['metrics']['memory.swap.peak'])
self.assertEqual(result['metrics']['memory.peak'], 200)
self.assertIn('memory.swap.peak', result['unavailable'])
def test_each_missing_metric_is_explicit_unknown(self):
for name in measure.CGROUP_FILES:
with self.subTest(name=name):
value = self.files.pop(self.path + '/' + name)
result = self.observe()['cgroupV2']
self.assertIsNone(result['metrics'][name])
self.assertEqual(result['status'], 'partial')
self.files[self.path + '/' + name] = value
def test_malformed_metrics_never_coerce_to_zero(self):
for value in ('-1\n', '+1\n', ' 1\n', '01\n', '1\n2\n', '18446744073709551616\n', 'max\n'):
with self.subTest(value=value):
self.files[self.path + '/memory.current'] = value
self.assertIsNone(self.observe()['cgroupV2']['metrics']['memory.current'])
for value in ('oom 1\noom 2\n', 'low 0\nhigh -1\nmax 0\noom 0\noom_kill 0\n', 'low 0\n', 'low 0\n'):
with self.subTest(value=value):
self.files[self.path + '/memory.events'] = value
self.assertIsNone(self.observe()['cgroupV2']['metrics']['memory.events'])
def test_docker_identity_drift_invalidates_entire_cgroup_sample(self):
for field, value in [('Pid', 124), ('StartedAt', 'new'), ('OOMKilled', True), ('Running', False)]:
after = copy.deepcopy(self.before); after['State'][field] = value
result = self.observe(after)
self.assertFalse(result['identityStableDuringSample'])
self.assertTrue(all(value is None for value in result['cgroupV2']['metrics'].values()))
for field, value in [('Id', 'b' * 64), ('RestartCount', 1), ('Image', 'sha256:new')]:
after = copy.deepcopy(self.before); after[field] = value
self.assertEqual(self.observe(after)['cgroupV2']['status'], 'unavailable')
def test_process_reuse_rootfs_swap_and_membership_move_are_unknown(self):
for changed in ('/proc/123/stat', '/proc/123/cgroup'):
count = 0
def read(path, _expected_device=None, deadline=None):
nonlocal count
if path == changed:
count += 1
if count == 2:
return self.files[path].replace('987', '988') if path.endswith('/stat') else '0::/docker/' + 'b' * 64 + '\n'
return self.files[path]
self.assertEqual(self.observe(reader=read)['cgroupV2']['status'], 'unavailable')
count = 0
def statter(path):
nonlocal count
if path == '/proc/123/root':
count += 1
return os.stat_result((0, count, 8, 0, 0, 0, 0, 0, 0, 0))
self.assertEqual(self.observe(statter=statter)['cgroupV2']['status'], 'unavailable')
def test_unsupported_v1_namespace_subtree_and_unbound_membership(self):
original = self.files['/proc/123/cgroup']
for value in ('2:memory:/docker/' + self.identifier + '\n', '0::/\n', '0::/docker/' + 'b' * 64 + '\n',
'0::/docker/' + self.identifier + '/../other\n', original.rstrip() + '/child\n', original + original):
self.files['/proc/123/cgroup'] = value
self.assertEqual(self.observe()['cgroupV2']['status'], 'unavailable')
self.files['/proc/123/cgroup'] = original
self.files['/proc/self/mountinfo'] = '1 0 0:1 /subtree /sys/fs/cgroup rw - cgroup2 cgroup rw\n'
self.assertEqual(self.observe()['cgroupV2']['status'], 'unavailable')
def test_symlink_outside_or_to_other_container_is_refused(self):
for destination in ('/etc', '/sys/fs/cgroup/docker/' + 'b' * 64):
self.assertEqual(self.observe(resolver=lambda path: destination if path == self.path else path)['cgroupV2']['status'], 'unavailable')
result = self.observe(resolver=lambda path: '/etc/passwd' if path.endswith('/memory.current') else path)
self.assertIsNone(result['cgroupV2']['metrics']['memory.current'])
def test_cgroup_directory_replacement_or_nested_filesystem_is_unknown(self):
for nested in (False, True):
calls = 0
def statter(path):
nonlocal calls
if path == self.path:
calls += 1
inode = 8 if path == self.path and calls > 1 else 7
device = 9 if nested and path == self.path else 8
return os.stat_result((0, inode, device, 0, 0, 0, 0, 0, 0, 0))
result = self.observe(statter=statter)['cgroupV2']
self.assertEqual(result['status'], 'unavailable')
self.assertTrue(all(value is None for value in result['metrics'].values()))
def test_pid_reuse_unreadable_process_and_after_oom_stay_unknown(self):
after = copy.deepcopy(self.before); after['State']['OOMKilled'] = True
result = self.observe(after)
host = {'MemTotalBytes': 2048, 'MemAvailableBytes': 1024}
summary = measure.summarize([{'elapsedMs': 0, 'host': host, 'containers': [result]}])
self.assertTrue(summary['anyObservedOomKilled'])
self.assertFalse(summary['containerIdentityAndStartStable'])
self.assertFalse(summary['allCgroupV2MetricsAvailable'])
self.assertFalse(summary['acceptancePassed'])
def read(path, _expected_device=None, deadline=None):
if path == '/proc/123/stat':
raise PermissionError()
return self.files[path]
self.assertEqual(self.observe(reader=read)['cgroupV2']['status'], 'unavailable')
def test_real_temp_files_are_bounded_and_strictly_ascii(self):
with tempfile.TemporaryDirectory(dir=Path(__file__).resolve().parent) as directory:
path = Path(directory) / 'memory.current'
path.write_bytes(b'123\n')
self.assertEqual(measure.parse_counter(measure.read_bounded(path)), 123)
with self.assertRaises(ValueError):
measure.read_bounded(path, path.stat().st_dev + 1)
for raw in (b'0' * (measure.MAX_FILE_BYTES + 1), b'0\n' * (measure.MAX_FILE_LINES + 1), b'\xff'):
path.write_bytes(raw)
with self.assertRaises((ValueError, UnicodeError)):
measure.read_bounded(path)
def test_after_stopped_or_unknown_identity_cannot_summarize_running(self):
for value in (False, None):
after = copy.deepcopy(self.before); after['State']['Running'] = value
result = self.observe(after)
self.assertIs(result['runningAfter'], value)
summary = measure.summarize([{'elapsedMs': 0, 'host': {'MemTotalBytes': 2048, 'MemAvailableBytes': 1024}, 'containers': [result]}])
self.assertFalse(summary['allObservedRunning'])
result = self.observe()
summary = measure.summarize([{'elapsedMs': 0, 'host': {'MemTotalBytes': 2048, 'MemAvailableBytes': 1024}, 'containers': [result]}])
self.assertTrue(summary['allObservedRunning'])
def test_event_future_counters_retained_but_duplicates_and_overflow_refuse(self):
self.assertEqual(measure.parse_events('high 0\nmax 1\nfail 2\nfuture 3\n', ('high', 'max', 'fail'))['future'], 3)
for value in ('x 1\nx 2\n', 'x 18446744073709551616\n', ''.join('x_' + str(i) + ' 0\n' for i in range(33))):
with self.assertRaises(ValueError):
measure.parse_events(value, ())
class DeadlineEvidenceTests(unittest.TestCase):
def test_expired_http_never_opens_socket_or_schedules_watchdog(self):
with patch.object(measure.time, 'monotonic', return_value=10), \
patch.object(measure.socket, 'socket') as own_socket, patch.object(measure.threading, 'Timer') as timer:
with self.assertRaises(measure.ObservationDeadlineExceeded):
measure.docker_get('/info', 10)
own_socket.assert_not_called()
timer.assert_not_called()
def test_expired_container_read_never_starts_io(self):
with patch.object(measure.time, 'monotonic', return_value=10), patch.object(measure, 'docker_get') as get:
with self.assertRaises(measure.ObservationDeadlineExceeded):
measure.collect_container('a' * 64, 10)
get.assert_not_called()
def test_near_cutoff_after_stats_stops_before_proc_or_more_gets(self):
clock, queries = [9.9], []
def get(path, deadline):
self.assertEqual(deadline, 10)
queries.append(path)
if len(queries) == 2:
clock[0] = 10
return {}
with patch.object(measure.time, 'monotonic', side_effect=lambda: clock[0]), \
patch.object(measure, 'docker_get', side_effect=get), patch.object(measure, 'read_bounded') as read:
with self.assertRaises(measure.ObservationDeadlineExceeded):
measure.collect_container('a' * 64, 10)
self.assertEqual(len(queries), 2)
read.assert_not_called()
def test_metric_deadline_is_not_downgraded_to_partial_or_followed_by_io(self):
fixture = CgroupEvidenceTests(); fixture.setUp()
clock, reads, queries = [9.9], [], []
def read(path, _device=None, deadline=None):
self.assertEqual(deadline, 10)
reads.append(path)
if path.endswith('/memory.current'):
clock[0] = 10
return fixture.files[path]
def get(path, deadline):
queries.append(path)
return fixture.stats if '/stats?' in path else fixture.before
with patch.object(measure.time, 'monotonic', side_effect=lambda: clock[0]), \
patch.object(measure, 'docker_get', side_effect=get), patch.object(measure, 'read_bounded', side_effect=read), \
patch.object(measure.os.path, 'realpath', side_effect=lambda path: path), \
patch.object(measure.os, 'stat', return_value=os.stat_result((0, 7, 8, 0, 0, 0, 0, 0, 0, 0))), \
patch.object(measure.os, 'sep', '/'):
with self.assertRaises(measure.ObservationDeadlineExceeded):
measure.collect_container(fixture.identifier, 10)
self.assertEqual(reads[-1], fixture.path + '/memory.current')
self.assertEqual(len(queries), 2)
def test_final_daemon_check_crosses_deadline_without_writing_report(self):
clock, calls = [0], []
def get(path, deadline):
self.assertEqual(deadline, 360)
calls.append(path)
if len(calls) == 2:
clock[0] = 361
return {'ID': 'owned-daemon'}
host = 'MemTotal: 2048 kB\nMemAvailable: 1024 kB\nSwapTotal: 0 kB\nSwapFree: 0 kB\n'
args = ['measure-memory.py', '--daemon-id', 'owned-daemon', '--container', 'a' * 64, '--samples', '1', '--output', 'NEVER_CREATED']
with patch.object(sys, 'argv', args), patch.object(measure.os, 'name', 'posix'), \
patch.object(measure.socket, 'AF_UNIX', 1, create=True), \
patch.object(measure.time, 'monotonic', side_effect=lambda: clock[0]), patch.object(measure, 'docker_get', side_effect=get), \
patch.object(measure, 'read_bounded', side_effect=[host, '', 'some avg10=0.00 avg60=0.00 avg300=0.00 total=0\nfull avg10=0.00 avg60=0.00 avg300=0.00 total=0\n']), patch.object(measure, 'collect_container', return_value={}), \
patch.object(measure, 'summarize', return_value={}), patch('builtins.open') as output:
with self.assertRaises(measure.ObservationDeadlineExceeded):
measure.main()
output.assert_not_called()
def test_slow_body_absolute_deadline_closes_only_own_socket_and_joins(self):
# A real HTTPResponse/BufferedReader and real watchdog timer, with an
# in-memory socket transport. No AF_UNIX availability or local network
# permissions are needed on Windows. Inactivity timeouts never fire:
# the body keeps producing bytes until shutdown interrupts the read.
class DripBody(io.RawIOBase):
def __init__(self, stopped):
self.header = b'HTTP/1.1 200 OK\r\nContent-Length: 1000\r\nConnection: close\r\n\r\n'
self.stopped = stopped
self.body_reads = 0
def readable(self):
return True
def readinto(self, buffer):
if self.header:
value, self.header = self.header[:len(buffer)], self.header[len(buffer):]
buffer[:len(value)] = value
return len(value)
if self.stopped.wait(0.01):
return 0
self.body_reads += 1
buffer[0] = 32
return 1
class SocketTransport:
def __init__(self):
self.stopped = threading.Event()
self.body = DripBody(self.stopped)
self.closed = False
self.request = b''
def connect(self, path):
self.path = path
def settimeout(self, value):
self.timeout = value
def sendall(self, value):
self.request += value
def makefile(self, _mode):
return io.BufferedReader(self.body)
def shutdown(self, _how):
self.stopped.set()
def close(self):
self.closed = True
client, unrelated = SocketTransport(), SocketTransport()
started = time.monotonic()
original_timer = measure.threading.Timer
timers = []
def timer(*args, **kwargs):
value = original_timer(*args, **kwargs)
timers.append(value)
return value
with patch.object(measure.socket, 'AF_UNIX', 1, create=True), \
patch.object(measure.socket, 'socket', return_value=client), \
patch.object(measure.threading, 'Timer', side_effect=timer):
with self.assertRaises(measure.ObservationDeadlineExceeded):
measure.docker_get('/info', started + 0.12)
self.assertLess(time.monotonic() - started, 1.5)
self.assertTrue(client.closed)
self.assertTrue(client.stopped.is_set())
self.assertGreater(client.body.body_reads, 0)
self.assertFalse(unrelated.closed)
self.assertFalse(unrelated.stopped.is_set())
self.assertEqual(client.path, '/var/run/docker.sock')
self.assertIn(b'GET /v1.45/info ', client.request)
self.assertEqual(len(timers), 1)
self.assertFalse(timers[0].is_alive(), 'No deadline callback can perform late I/O after return')
if __name__ == '__main__':
unittest.main()
@@ -0,0 +1,30 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import {createServer} from 'node:http'
import {randomBytes} from 'node:crypto'
const database=process.env.MMAPP_OWN_SQL_DB,url=new URL(process.env.DATABASE_URL)
assert(/^mm_accept_166_\d+$/.test(database??''));assert.equal(url.pathname,'/'+database);assert.equal(url.username,database)
process.env.AUTH_REQUIRED='true';process.env.AUTH_JWT_SECRET=randomBytes(32).toString('hex');process.env.AUTH_ISSUER='https://own215.invalid';process.env.AUTH_PORTAL_URL='https://own215.invalid'
const {pool,closePool}=await import('/app/backend/dist/db/index.js'),{initDatabase}=await import('/app/backend/dist/db/bootstrap.js'),{buildApp}=await import('/app/backend/dist/index.js')
assert.equal((await pool.query('SELECT current_database() AS name')).rows[0].name,database);assert(Object.values((await pool.query('SELECT rolsuper,rolcreatedb,rolcreaterole,rolbypassrls FROM pg_roles WHERE rolname=current_user')).rows[0]).every(v=>v===false))
let phase='populated',app,nativeCalls=0,nativeWrites=0,disabledCalls=0;const cases=[]
const fixture=label=>createServer((req,res)=>{nativeCalls++;if(label==='disabled')disabledCalls++;if(req.method!=='GET')nativeWrites++;res.setHeader('content-type','application/json');if(phase==='failed'||(phase==='partial'&&label==='second')){res.statusCode=401;res.end(JSON.stringify({error:401,message:'own215 fixture denied'}));return}const rows=phase==='populated'&&req.url==='/rest/routing/ospf/instance'?[{'.id':'*1',name:'own215-ospf',disabled:'false'}]:phase==='populated'&&req.url==='/rest/routing/bfd/session'?[{'.id':'*1','local-address':'192.0.2.1','remote-address':'192.0.2.2',state:'up'}]:[];res.end(JSON.stringify(rows))})
const natives=['first','second','disabled'].map(fixture);for(const n of natives)await new Promise(r=>n.listen(0,'127.0.0.1',r))
const baseline=process.env.MMAPP_BASELINE==='true';const report={date:'2026-10-14',block:'12.5.215',baseline,database,cases,scope:'Compiled production routes/auth, isolated PostgreSQL and RouterOS HTTP fixture; real CHR unchanged',passed:false}
try{
await initDatabase();const ids=[];for(let i=0;i<natives.length;i++)ids.push(Number((await pool.query("INSERT INTO servers(name,host,port,use_ssl,enabled,username,password,type) VALUES($1,'127.0.0.1',$2,false,$3,'fixture','fixture',$4) RETURNING id",['own215-'+i,natives[i].address().port,i<2,i===2?'home-router':'exit-node'])).rows[0].id))
app=await buildApp({logger:false,startScheduler:false});await app.ready();const token=app.jwt.sign({sub:'own215-reader',apps:['mm'],permissions:['mm:network:read','mm:dashboard:read'],iss:process.env.AUTH_ISSUER},{expiresIn:'5m'}),headers={authorization:'Bearer '+token}
for(phase of ['populated','empty','partial','failed','disabled']){
if(phase==='disabled')await pool.query('UPDATE servers SET enabled=false')
for(const endpoint of ['ospf/neighbors','ospf/interfaces','ospf/instances','ospf/all','bfd/sessions']){
const before=nativeCalls,r=await app.inject({method:'GET',url:'/api/'+endpoint,headers}),body=r.json(),hasFailure=['partial','failed'].includes(phase)
assert.equal(r.statusCode,!baseline&&hasFailure?503:200)
if(!baseline&&hasFailure){const expected=phase==='partial'?[ids[1]]:ids.slice(0,2);assert.deepEqual(body.failures.map(f=>f.serverId),expected);assert(body.failures.every(f=>f.error.includes('401')));assert.match(body.error,/OSPF\/BFD/)}
else if(endpoint==='ospf/all'){assert.deepEqual(Object.keys(body).sort(),['bfdSessions','instances','interfaces','neighbors','routes']);assert.equal(body.instances.length,phase==='populated'?2:0);assert.equal(body.bfdSessions.length,phase==='populated'?2:0)}
else{assert(Array.isArray(body));assert.equal(body.length,phase==='populated'&&['ospf/instances','bfd/sessions'].includes(endpoint)?2:0)}
if(phase==='disabled')assert.equal(nativeCalls,before)
cases.push({phase,endpoint,status:r.statusCode,failures:body.failures?.length??0,nativeCalls:nativeCalls-before})
}
}
assert.equal(nativeWrites,0);assert.equal(disabledCalls,0);report.passed=true
}catch(e){report.errorName=e.name;fs.writeFileSync('/app/backend/acceptance/ospf215-private.log',String(e.stack??e),{mode:0o600})}finally{if(app)await app.close();for(const n of natives){n.closeAllConnections();await new Promise(r=>n.close(r))}await closePool();report.nativeWrites=nativeWrites;report.disabledHomeRouterCalls=disabledCalls;fs.writeFileSync('/app/backend/acceptance/ospf215-'+(baseline?'baseline':'fixed')+'.json',JSON.stringify(report,null,2));console.log(JSON.stringify(report))}if(!report.passed)process.exitCode=1
@@ -0,0 +1,69 @@
import fs from 'node:fs';
import assert from 'node:assert/strict';
import { chromium, expect } from '@playwright/test';
const output = 'tmp/rest262-soak.json';
assert(!fs.existsSync(output), 'Never overwrite acceptance evidence');
const credentials = JSON.parse(fs.readFileSync('tmp/portal195-private.json'));
const browser = await chromium.launch({ channel: 'msedge', headless: true });
let token;
try {
const page = await browser.newPage();
await page.goto('http://localhost:3000/certificates');
await page.getByLabel('Email', { exact: true }).fill(credentials.email);
await page.getByLabel('Пароль', { exact: true }).fill(credentials.password);
await page.getByRole('button', { name: 'Войти', exact: true }).click();
await expect(page.getByRole('navigation', { name: 'Путь страницы' })).toContainText('Сертификаты', { timeout: 30000 });
token = await page.evaluate(() => localStorage.getItem('mmapp_token'));
assert(token);
} finally { await browser.close(); }
const report = {
date: '2026-10-15', block: '12.5.262', realPortalLogin: true, mocks: false,
backend: 'ux-certificates258', endpoint: '/api/certificates/state', directBackendTunnel: true,
concurrentRequestsPerWave: 12, waveIntervalMs: 30000, plannedWindowMs: 600000,
nonAuthApiWrites: 0, nativeConfigurationWrites: 0, waves: [],
bounds: ['One backend, two CHR over HTTP REST; not a global cap for external clients or multiple backend instances', 'Concurrent normal collectors and UI acceptance may run; not maximum throughput', 'A finite successful window does not prove all historical 401/500 causes or indefinite stability'],
};
const start = Date.now();
const lastReceipts = new Map();
try {
for (let wave = 0; wave <= 20; wave++) {
const wait = start + wave * report.waveIntervalMs - Date.now();
if (wait > 0) await new Promise(resolve => setTimeout(resolve, wait));
const began = Date.now();
const samples = await Promise.all(Array.from({ length: 12 }, async () => {
const requested = Date.now();
try {
const response = await fetch('http://localhost:8000/api/certificates/state', {
headers: { Authorization: 'Bearer ' + token }, signal: AbortSignal.timeout(20000),
});
const body = await response.json();
return { status: response.status, durationMs: Date.now() - requested, servers: body.servers?.map(s => ({ serverId: s.serverId, collectedAt: s.collectedAt, error: s.error !== null })) ?? [] };
} catch (error) { return { status: null, durationMs: Date.now() - requested, failure: error.name, servers: [] }; }
}));
const item = { wave, startedAt: new Date(began).toISOString(), elapsedMs: Date.now() - start, durationMs: Date.now() - began, samples };
report.waves.push(item);
fs.writeFileSync(output, JSON.stringify(report, null, 2) + '\n');
assert(samples.every(s => s.status === 200 && s.servers.length === 2 && s.servers.every(row => !row.error) && s.servers.map(row => row.serverId).sort().join(',') === '1,2'), 'Request or device read failed; stopping without automatic replay');
for (const id of ['1', '2']) {
const receipts = samples.flatMap(s => s.servers.filter(row => row.serverId === id).map(row => Date.parse(row.collectedAt)));
assert(receipts.every(Number.isFinite));
const earliest = Math.min(...receipts);
if (lastReceipts.has(id)) assert(earliest > lastReceipts.get(id), 'A later wave returned an old receipt');
lastReceipts.set(id, Math.max(...receipts));
}
console.log(JSON.stringify({ wave, durationMs: item.durationMs, requests: samples.length, errors: 0 }));
}
report.completed = true;
report.durationMs = Date.now() - start;
assert(report.durationMs >= report.plannedWindowMs);
report.passed = true;
} catch (error) {
report.passed = false;
report.failure = { name: error.name, message: error.message.slice(0, 200) };
process.exitCode = 1;
} finally {
token = undefined;
fs.writeFileSync(output, JSON.stringify(report, null, 2) + '\n');
}
@@ -0,0 +1,32 @@
#!/bin/bash
set -euo pipefail
umask 077
cd /opt/mmapp-test
mm233_db="mm_accept_166_$(date +%s%3N)"
mm233_pass="$(openssl rand -hex 24)"
mm233_role_created=false
mm233_db_created=false
cleanup233() {
docker rm mmapp-s3app233-owned >/dev/null 2>&1 || true
if $mm233_db_created; then docker exec mmapp-test-postgres psql -U mmapp -d mmapp -v ON_ERROR_STOP=1 -c "DROP DATABASE \"$mm233_db\" WITH (FORCE)"; fi
if $mm233_role_created; then docker exec mmapp-test-postgres psql -U mmapp -d mmapp -v ON_ERROR_STOP=1 -c "DROP ROLE \"$mm233_db\""; fi
remaining=$(docker exec mmapp-test-postgres psql -U mmapp -d mmapp -Atc "SELECT (SELECT count(*) FROM pg_database WHERE datname='$mm233_db')+(SELECT count(*) FROM pg_roles WHERE rolname='$mm233_db')")
[ "$remaining" = 0 ]
python3 - <<'PY'
import json,pathlib
p=pathlib.Path('/opt/mmapp-test/artifacts/backup233-checks/storage-origin233.json')
if p.exists():
r=json.loads(p.read_text());r['ownedDatabaseAndRoleRemoved']=True;p.write_text(json.dumps(r,indent=2)+'\n')
PY
printf 'OWNED_DATABASE_AND_ROLE_REMOVED\n'
unset mm233_pass
}
trap cleanup233 EXIT
[[ "$mm233_db" =~ ^mm_accept_166_[0-9]+$ ]]
existing=$(docker exec mmapp-test-postgres psql -U mmapp -d mmapp -Atc "SELECT (SELECT count(*) FROM pg_database WHERE datname='$mm233_db')+(SELECT count(*) FROM pg_roles WHERE rolname='$mm233_db')")
[ "$existing" = 0 ]
printf "CREATE ROLE \"%s\" LOGIN PASSWORD '%s' NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS;\n" "$mm233_db" "$mm233_pass" | docker exec -i mmapp-test-postgres psql -U mmapp -d mmapp -v ON_ERROR_STOP=1
mm233_role_created=true
docker exec mmapp-test-postgres psql -U mmapp -d mmapp -v ON_ERROR_STOP=1 -c "CREATE DATABASE \"$mm233_db\" OWNER \"$mm233_db\""
mm233_db_created=true
docker run --name mmapp-s3app233-owned --network mmapp-test_default --cap-drop ALL --security-opt no-new-privileges -e "MMAPP_ORIGIN_BASELINE=${2:-false}" -e NODE_ENV=test -e "DATABASE_URL=postgres://$mm233_db:$mm233_pass@postgres:5432/$mm233_db" -e "MMAPP_OWN_SQL_DB=$mm233_db" -e "DATABASE_PATH=/tmp/$mm233_db/legacy.db" -e "GEOIP_DIR=/tmp/$mm233_db/geoip" -v /opt/mmapp-test/artifacts/backup233-checks:/checks --entrypoint node "${1:-mmapp-test/backend:ux-backup233}" /checks/backup-storage-origin.mjs
@@ -0,0 +1,32 @@
#!/bin/bash
set -euo pipefail
umask 077
cd /opt/mmapp-test
mm242_db="mm_accept_166_$(date +%s%3N)"
mm242_pass="$(openssl rand -hex 24)"
mm242_role_created=false
mm242_db_created=false
cleanup242() {
docker rm mmapp-s3app242-owned >/dev/null 2>&1 || true
if $mm242_db_created; then docker exec mmapp-test-postgres psql -U mmapp -d mmapp -v ON_ERROR_STOP=1 -c "DROP DATABASE \"$mm242_db\" WITH (FORCE)"; fi
if $mm242_role_created; then docker exec mmapp-test-postgres psql -U mmapp -d mmapp -v ON_ERROR_STOP=1 -c "DROP ROLE \"$mm242_db\""; fi
remaining=$(docker exec mmapp-test-postgres psql -U mmapp -d mmapp -Atc "SELECT (SELECT count(*) FROM pg_database WHERE datname='$mm242_db')+(SELECT count(*) FROM pg_roles WHERE rolname='$mm242_db')")
[ "$remaining" = 0 ]
python3 - <<'PY'
import json,pathlib
p=pathlib.Path('/opt/mmapp-test/artifacts/backup242-checks/geoip-mirror242.json')
if p.exists():
r=json.loads(p.read_text());r['ownedDatabaseAndRoleRemoved']=True;p.write_text(json.dumps(r,indent=2)+'\n')
PY
printf 'OWNED_DATABASE_AND_ROLE_REMOVED\n'
unset mm242_pass
}
trap cleanup242 EXIT
[[ "$mm242_db" =~ ^mm_accept_166_[0-9]+$ ]]
existing=$(docker exec mmapp-test-postgres psql -U mmapp -d mmapp -Atc "SELECT (SELECT count(*) FROM pg_database WHERE datname='$mm242_db')+(SELECT count(*) FROM pg_roles WHERE rolname='$mm242_db')")
[ "$existing" = 0 ]
printf "CREATE ROLE \"%s\" LOGIN PASSWORD '%s' NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS;\n" "$mm242_db" "$mm242_pass" | docker exec -i mmapp-test-postgres psql -U mmapp -d mmapp -v ON_ERROR_STOP=1
mm242_role_created=true
docker exec mmapp-test-postgres psql -U mmapp -d mmapp -v ON_ERROR_STOP=1 -c "CREATE DATABASE \"$mm242_db\" OWNER \"$mm242_db\""
mm242_db_created=true
docker run --name mmapp-s3app242-owned --network mmapp-test_default --cap-drop ALL --security-opt no-new-privileges -e "MMAPP_ORIGIN_BASELINE=${2:-false}" -e NODE_ENV=test -e "DATABASE_URL=postgres://$mm242_db:$mm242_pass@postgres:5432/$mm242_db" -e "MMAPP_OWN_SQL_DB=$mm242_db" -e "DATABASE_PATH=/tmp/$mm242_db/legacy.db" -e "GEOIP_DIR=/tmp/$mm242_db/geoip" -v /opt/mmapp-test/artifacts/backup242-checks:/checks --entrypoint node "${1:-mmapp-test/backend:ux-geoip242}" /checks/geoip-mirror.mjs
@@ -0,0 +1,63 @@
/** Local smoke preparation only: owns a new embedded PG and one compiled child.
* Does not represent frontend/portal/Docker/whole-installation acceptance. */
import assert from 'node:assert/strict'
import fs from 'node:fs/promises'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { join,resolve,dirname,basename } from 'node:path'
import { fileURLToPath } from 'node:url'
import { createServer } from 'node:net'
import { randomBytes,createHash } from 'node:crypto'
import { spawn } from 'node:child_process'
const root=resolve(dirname(fileURLToPath(import.meta.url)),'../../..')
assert(process.argv.length>=3&&process.argv.length<=5,'Supply a unique evidence name, optional compiled output and delayed-approval scenario')
const delayedApproval=process.argv[4]==='delayed-approval'
assert(process.argv[4]===undefined||delayedApproval,'Unknown isolated scenario')
const name=process.argv[2];assert(/^[a-z0-9][a-z0-9-]{3,99}$/.test(name),'Invalid evidence name')
const outputName=process.argv[3]??'dist-installation-memory'
assert(/^dist-installation-memory(?:-[a-z0-9-]{3,64})?$/.test(outputName),'Invalid compiled output name')
const prefix=resolve(root,'tmp',name), statusPath=prefix+'-status.json'
const exclusive=await fs.open(statusPath,'wx');await exclusive.close()
const state={scope:'local-backend-coload-only',wholeInstallation2GbAccepted:false,compiledOutputName:outputName,delayedHumanApprovalScenario:delayedApproval,status:'starting',nodeExitCode:null,sourcePinsUnchanged:false,cleanupConfirmed:false,startedAt:new Date().toISOString(),wrapperPid:process.pid}
const save=()=>fs.writeFile(statusPath,JSON.stringify(state,null,2)+'\n');await save()
const require=createRequire(new URL('../../package.json',import.meta.url)),EmbeddedPostgres=require('embedded-postgres').default
const directory=await fs.mkdtemp(join(tmpdir(),'mm-mcp-test-')),server=createServer()
await new Promise(done=>server.listen(0,'127.0.0.1',done));const port=server.address().port;await new Promise(done=>server.close(done))
const password=randomBytes(24).toString('hex')
const postgres=new EmbeddedPostgres({databaseDir:join(directory,'pg'),port,user:'mcp_test',password,persistent:true,createPostgresUser:false,
initdbFlags:['--encoding=UTF8'],postgresFlags:['-h','127.0.0.1'],onLog:()=>{},onError:()=>{}})
const output=resolve(root,'backend',outputName),entry=resolve(output,'services/installation-co-load.owned.test.js')
async function pins() {
const rows=[]
async function walk(dir){for(const item of await fs.readdir(dir,{withFileTypes:true})){const file=join(dir,item.name);if(item.isDirectory())await walk(file);else rows.push({path:file.slice(root.length+1).replaceAll('\\','/'),sha256:createHash('sha256').update(await fs.readFile(file)).digest('hex')})}}
await walk(output);return rows.sort((a,b)=>a.path.localeCompare(b.path))
}
const before=await pins();await fs.writeFile(prefix+'-pins.json',JSON.stringify({before},null,2)+'\n')
let log,timer
try {
await postgres.initialise();await postgres.start()
log=await fs.open(prefix+'.log','wx')
const retained=Object.fromEntries(['SystemRoot','WINDIR','TEMP','TMP','LANG','LC_ALL'].filter(key=>process.env[key]).map(key=>[key,process.env[key]]))
const child=spawn(process.execPath,['--max-old-space-size=192','--max-semi-space-size=16',entry],{cwd:directory,windowsHide:true,stdio:['ignore',log.fd,log.fd],env:{...retained,
NODE_OPTIONS:'',NODE_ENV:'test',PORT:'8000',AUTH_ISSUER:'https://auth.shnt.top',AUTH_PORTAL_URL:'http://localhost:5175',SQLITE_IMPORT_FULL_HISTORY:'false',
MM_FLOW_READ_RESOURCE_PROFILE:'constrained',DATABASE_URL:`postgresql://mcp_test:${password}@127.0.0.1:${port}/postgres`,DATABASE_PATH:join(directory,'no-legacy.sqlite'),
MCP_STATE_DIR:join(directory,'state'),AUTH_REQUIRED:'true',AUTH_JWT_SECRET:randomBytes(32).toString('hex'),MCP_PUBLIC_URL:'http://localhost:8000/mcp',MMAPP_OWN_INSTALLATION_COLOAD:'true',MMAPP_OWN_COLOAD_DELAYED_APPROVAL:String(delayedApproval)}})
state.status='running';state.childPid=child.pid;await save()
timer=setTimeout(()=>{state.outerTimedOut=true;child.kill()},300000)
state.nodeExitCode=await new Promise((done,reject)=>{child.once('error',reject);child.once('exit',code=>done(code??1))})
clearTimeout(timer);await log.close();log=undefined
const after=await pins();state.sourcePinsUnchanged=JSON.stringify(before)===JSON.stringify(after)
await fs.writeFile(prefix+'-pins.json',JSON.stringify({before,after,unchanged:state.sourcePinsUnchanged},null,2)+'\n')
process.exitCode=state.nodeExitCode===0&&state.sourcePinsUnchanged?0:1
}catch(error){state.failure=error instanceof Error?error.name:'UnknownError';process.exitCode=1}
finally {
if(timer)clearTimeout(timer);if(log)await log.close()
try{
if(postgres.process?.exitCode===null)await postgres.stop()
const actual=await fs.realpath(directory),parent=await fs.realpath(tmpdir())
assert.equal(dirname(actual),parent);assert.match(basename(actual),/^mm-mcp-test-[A-Za-z0-9-]+$/);assert.equal((await fs.lstat(directory)).isSymbolicLink(),false)
await fs.rm(actual,{recursive:true,force:true,maxRetries:20,retryDelay:100});state.cleanupConfirmed=true
}catch{state.cleanupConfirmed=false;process.exitCode=1}
state.status='terminal';state.finishedAt=new Date().toISOString();await save();console.log(JSON.stringify(state))
}
if(state.cleanupConfirmed)process.exit(process.exitCode??1)
@@ -0,0 +1,32 @@
#!/bin/bash
set -euo pipefail
umask 077
cd /opt/mmapp-test
mm240_db="mm_accept_166_$(date +%s%3N)"
mm240_pass="$(openssl rand -hex 24)"
mm240_role_created=false
mm240_db_created=false
cleanup240() {
docker rm mmapp-s3app240-owned >/dev/null 2>&1 || true
if $mm240_db_created; then docker exec mmapp-test-postgres psql -U mmapp -d mmapp -v ON_ERROR_STOP=1 -c "DROP DATABASE \"$mm240_db\" WITH (FORCE)"; fi
if $mm240_role_created; then docker exec mmapp-test-postgres psql -U mmapp -d mmapp -v ON_ERROR_STOP=1 -c "DROP ROLE \"$mm240_db\""; fi
remaining=$(docker exec mmapp-test-postgres psql -U mmapp -d mmapp -Atc "SELECT (SELECT count(*) FROM pg_database WHERE datname='$mm240_db')+(SELECT count(*) FROM pg_roles WHERE rolname='$mm240_db')")
[ "$remaining" = 0 ]
python3 - <<'PY'
import json,pathlib
p=pathlib.Path('/opt/mmapp-test/artifacts/s3-retention240/s3-retention240.json')
if p.exists():
r=json.loads(p.read_text());r['ownedDatabaseAndRoleRemoved']=True;p.write_text(json.dumps(r,indent=2)+'\n')
PY
printf 'OWNED_DATABASE_AND_ROLE_REMOVED\n'
unset mm240_pass
}
trap cleanup240 EXIT
[[ "$mm240_db" =~ ^mm_accept_166_[0-9]+$ ]]
existing=$(docker exec mmapp-test-postgres psql -U mmapp -d mmapp -Atc "SELECT (SELECT count(*) FROM pg_database WHERE datname='$mm240_db')+(SELECT count(*) FROM pg_roles WHERE rolname='$mm240_db')")
[ "$existing" = 0 ]
printf "CREATE ROLE \"%s\" LOGIN PASSWORD '%s' NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS;\n" "$mm240_db" "$mm240_pass" | docker exec -i mmapp-test-postgres psql -U mmapp -d mmapp -v ON_ERROR_STOP=1
mm240_role_created=true
docker exec mmapp-test-postgres psql -U mmapp -d mmapp -v ON_ERROR_STOP=1 -c "CREATE DATABASE \"$mm240_db\" OWNER \"$mm240_db\""
mm240_db_created=true
docker run --name mmapp-s3app240-owned --network mmapp-test_default --cap-drop ALL --security-opt no-new-privileges -e "MMAPP_ORIGIN_BASELINE=${2:-false}" --env-file /opt/mmapp-test/artifacts/s3-retention240/private.env -e NODE_ENV=test -e "DATABASE_URL=postgres://$mm240_db:$mm240_pass@postgres:5432/$mm240_db" -e "MMAPP_OWN_SQL_DB=$mm240_db" -e "DATABASE_PATH=/tmp/$mm240_db/legacy.db" -e "GEOIP_DIR=/tmp/$mm240_db/geoip" -v /opt/mmapp-test/artifacts/s3-retention240:/checks --entrypoint node "${1:-mmapp-test/backend:ux-backups237}" /checks/s3-backup-retention.mjs
@@ -0,0 +1,32 @@
#!/bin/bash
set -euo pipefail
umask 077
cd /opt/mmapp-test
mm229_db="mm_accept_166_$(date +%s%3N)"
mm229_pass="$(openssl rand -hex 24)"
mm229_role_created=false
mm229_db_created=false
cleanup229() {
docker rm mmapp-s3app229-owned >/dev/null 2>&1 || true
if $mm229_db_created; then docker exec mmapp-test-postgres psql -U mmapp -d mmapp -v ON_ERROR_STOP=1 -c "DROP DATABASE \"$mm229_db\" WITH (FORCE)"; fi
if $mm229_role_created; then docker exec mmapp-test-postgres psql -U mmapp -d mmapp -v ON_ERROR_STOP=1 -c "DROP ROLE \"$mm229_db\""; fi
remaining=$(docker exec mmapp-test-postgres psql -U mmapp -d mmapp -Atc "SELECT (SELECT count(*) FROM pg_database WHERE datname='$mm229_db')+(SELECT count(*) FROM pg_roles WHERE rolname='$mm229_db')")
[ "$remaining" = 0 ]
python3 - <<'PY'
import json,pathlib
p=pathlib.Path('/opt/mmapp-test/artifacts/s3-229/s3-application229.json')
if p.exists():
r=json.loads(p.read_text());r['ownedDatabaseAndRoleRemoved']=True;p.write_text(json.dumps(r,indent=2)+'\n')
PY
printf 'OWNED_DATABASE_AND_ROLE_REMOVED\n'
unset mm229_pass
}
trap cleanup229 EXIT
[[ "$mm229_db" =~ ^mm_accept_166_[0-9]+$ ]]
existing=$(docker exec mmapp-test-postgres psql -U mmapp -d mmapp -Atc "SELECT (SELECT count(*) FROM pg_database WHERE datname='$mm229_db')+(SELECT count(*) FROM pg_roles WHERE rolname='$mm229_db')")
[ "$existing" = 0 ]
printf "CREATE ROLE \"%s\" LOGIN PASSWORD '%s' NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS;\n" "$mm229_db" "$mm229_pass" | docker exec -i mmapp-test-postgres psql -U mmapp -d mmapp -v ON_ERROR_STOP=1
mm229_role_created=true
docker exec mmapp-test-postgres psql -U mmapp -d mmapp -v ON_ERROR_STOP=1 -c "CREATE DATABASE \"$mm229_db\" OWNER \"$mm229_db\""
mm229_db_created=true
docker run --name mmapp-s3app229-owned --network mmapp-test_default --cap-drop ALL --security-opt no-new-privileges --env-file /opt/mmapp-test/artifacts/s3-228-private.env -e NODE_ENV=test -e "DATABASE_URL=postgres://$mm229_db:$mm229_pass@postgres:5432/$mm229_db" -e "MMAPP_OWN_SQL_DB=$mm229_db" -e "DATABASE_PATH=/tmp/$mm229_db/legacy.db" -e "GEOIP_DIR=/tmp/$mm229_db/geoip" -v /opt/mmapp-test/artifacts/s3-229:/checks --entrypoint node mmapp-test/backend:ux-regression222 /checks/s3-backups-api.mjs
@@ -0,0 +1,32 @@
#!/bin/bash
set -euo pipefail
umask 077
cd /opt/mmapp-test
mm245_db="mm_accept_166_$(date +%s%3N)"
mm245_pass="$(openssl rand -hex 24)"
mm245_role_created=false
mm245_db_created=false
cleanup245() {
docker rm mmapp-s3app245-owned >/dev/null 2>&1 || true
if $mm245_db_created; then docker exec mmapp-test-postgres psql -U mmapp -d mmapp -v ON_ERROR_STOP=1 -c "DROP DATABASE \"$mm245_db\" WITH (FORCE)"; fi
if $mm245_role_created; then docker exec mmapp-test-postgres psql -U mmapp -d mmapp -v ON_ERROR_STOP=1 -c "DROP ROLE \"$mm245_db\""; fi
remaining=$(docker exec mmapp-test-postgres psql -U mmapp -d mmapp -Atc "SELECT (SELECT count(*) FROM pg_database WHERE datname='$mm245_db')+(SELECT count(*) FROM pg_roles WHERE rolname='$mm245_db')")
[ "$remaining" = 0 ]
python3 - <<'PY'
import json,pathlib
p=pathlib.Path('/opt/mmapp-test/artifacts/traffic-flow245/traffic-flow245.json')
if p.exists():
r=json.loads(p.read_text());r['ownedDatabaseAndRoleRemoved']=True;p.write_text(json.dumps(r,indent=2)+'\n')
PY
printf 'OWNED_DATABASE_AND_ROLE_REMOVED\n'
unset mm245_pass
}
trap cleanup245 EXIT
[[ "$mm245_db" =~ ^mm_accept_166_[0-9]+$ ]]
existing=$(docker exec mmapp-test-postgres psql -U mmapp -d mmapp -Atc "SELECT (SELECT count(*) FROM pg_database WHERE datname='$mm245_db')+(SELECT count(*) FROM pg_roles WHERE rolname='$mm245_db')")
[ "$existing" = 0 ]
printf "CREATE ROLE \"%s\" LOGIN PASSWORD '%s' NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS;\n" "$mm245_db" "$mm245_pass" | docker exec -i mmapp-test-postgres psql -U mmapp -d mmapp -v ON_ERROR_STOP=1
mm245_role_created=true
docker exec mmapp-test-postgres psql -U mmapp -d mmapp -v ON_ERROR_STOP=1 -c "CREATE DATABASE \"$mm245_db\" OWNER \"$mm245_db\""
mm245_db_created=true
docker run --name mmapp-s3app245-owned --network mmapp-test_default --cap-drop ALL --security-opt no-new-privileges -e "MMAPP_ORIGIN_BASELINE=${2:-false}" -e NODE_ENV=test -e "DATABASE_URL=postgres://$mm245_db:$mm245_pass@postgres:5432/$mm245_db" -e "MMAPP_OWN_SQL_DB=$mm245_db" -e "DATABASE_PATH=/tmp/$mm245_db/legacy.db" -e "GEOIP_DIR=/tmp/$mm245_db/geoip" -v /opt/mmapp-test/artifacts/traffic-flow245:/checks --entrypoint node "${1:-mmapp-test/backend:ux-geoip242-acceptance}" /checks/traffic-flow-regression.mjs
@@ -0,0 +1,84 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { randomUUID, createHash } from 'node:crypto';
import { createRequire } from 'node:module';
const database = process.env.MMAPP_OWN_SQL_DB;
assert(/^mm_accept_166_\d+$/.test(database));
const url = new URL(process.env.DATABASE_URL); assert.equal(url.pathname, '/' + database); assert.equal(url.username, database);
fs.mkdirSync('/tmp/' + database, { recursive: true }); process.chdir('/tmp/' + database);
const require = createRequire('/app/backend/package.json'), { ListObjectVersionsCommand, DeleteObjectCommand } = require('@aws-sdk/client-s3');
const { pool, db, closePool } = await import('/app/backend/dist/db/index.js');
const { initDatabase } = await import('/app/backend/dist/db/bootstrap.js');
const { servers } = await import('/app/backend/dist/db/schema.js');
const { databaseBackgroundWork } = await import('/app/backend/dist/services/database-background-work.js');
const { createS3ClientFromConfig, s3PutObject, s3GetObject } = await import('/app/backend/dist/services/s3-backup-client.js');
const { updateBackupStorageSettings, pruneBackupsForServer, readBackupContent, listBackups } = await import('/app/backend/dist/services/backup-service.js');
const role = (await pool.query('SELECT rolsuper,rolcreatedb,rolcreaterole,rolbypassrls FROM pg_roles WHERE rolname=current_user')).rows[0];
assert(Object.values(role).every(v => v === false)); await initDatabase();
const prefix = 'mmapp-acceptance/' + randomUUID(), secret = process.env.MMAPP_TEST_S3_SECRET_KEY;
const cfg = { endpoint: process.env.MMAPP_TEST_S3_ENDPOINT, region: process.env.MMAPP_TEST_S3_REGION, bucket: process.env.MMAPP_TEST_S3_BUCKET, accessKeyId: process.env.MMAPP_TEST_S3_ACCESS_KEY, secretAccessKey: secret, forcePathStyle: false };
assert.equal(cfg.endpoint, 'https://s3.ru-1.storage.selcloud.ru'); assert.equal(cfg.bucket, 'mikrotik-test-s3'); assert(secret);
const s3 = createS3ClientFromConfig(cfg), plannedKeys = [], seededIds = [], payloads = new Map();
const report = { date: '2026-10-14', block: '12.5.240', scope: 'Owned PostgreSQL + compiled retention service + real Selectel S3; no CHR contacts', prefix, passed: false, checks: [], cleanup: {}, chrContacts: 0, bucketSettingsWrites: 0 };
const save = () => fs.writeFileSync('/checks/s3-retention240.json', JSON.stringify(report, null, 2) + '\n');
const check = (name, details = {}) => { report.checks.push({ name, ...details }); save(); };
const catalog = async () => (await pool.query('SELECT id,storage,s3_key FROM backup_entries ORDER BY id')).rows;
async function versions() {
let keyMarker, versionMarker; const all = [];
for (let n = 0; n < 10; n++) {
const r = await s3.send(new ListObjectVersionsCommand({ Bucket: cfg.bucket, Prefix: prefix + '/', KeyMarker: keyMarker, VersionIdMarker: versionMarker }));
all.push(...(r.Versions ?? []).map(v => ({ ...v, kind: 'version' })), ...(r.DeleteMarkers ?? []).map(v => ({ ...v, kind: 'delete-marker' })));
if (!r.IsTruncated) return all;
assert(r.NextKeyMarker || r.NextVersionIdMarker); keyMarker = r.NextKeyMarker; versionMarker = r.NextVersionIdMarker;
} throw Error('Own pagination limit');
}
try {
assert.equal((await versions()).length, 0);
const hosts = await db.insert(servers).values(['a', 'b', 'c'].map(s => ({ name: 'owned-retention240-' + s, host: '127.0.0.1', port: 9, useSsl: false, username: 'fixture', password: 'fixture', enabled: true, type: 'exit-node' }))).returning();
await updateBackupStorageSettings({ provider: 's3', s3Endpoint: cfg.endpoint, s3Region: cfg.region, s3Bucket: cfg.bucket, s3Prefix: prefix, s3AccessKeyId: cfg.accessKeyId, s3SecretAccessKey: secret, s3ForcePathStyle: false, keepLocalCopy: true });
fs.mkdirSync('storage/backups', { recursive: true });
const fixtures = [['a4',0,4,'both'],['a3',0,3,'s3'],['a2',0,2,'local'],['a1',0,1,'s3'],['b2',1,2,'both'],['b1',1,1,'local'],['b0',1,0,'s3'],['c0',2,0,'local']];
for (const [id, hostIndex, seconds, storage] of fixtures) {
const filename = id + '-retention240.rsc', bytes = Buffer.from('# Own retention240 ' + id + '\n'), key = storage === 'local' ? null : prefix + '/' + filename;
seededIds.push(id); payloads.set(id, bytes);
if (key) { plannedKeys.push(key); await s3PutObject(s3, cfg.bucket, key, bytes); assert.deepEqual(await s3GetObject(s3, cfg.bucket, key), bytes); }
if (storage !== 's3') fs.writeFileSync('storage/backups/' + filename, bytes);
await pool.query('INSERT INTO backup_entries(id,server_id,server_name,filename,size_bytes,kind,storage,s3_key,created_at) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9)', [id, hosts[hostIndex].id, hosts[hostIndex].name, filename, bytes.length, id.endsWith('0') ? 'manual' : 'auto', storage, key, new Date(Date.UTC(2026,9,14,0,0,seconds)).toISOString()]);
}
check('eight-fixtures-three-servers', { remoteObjects: plannedKeys.length, localFiles: fixtures.filter(f => f[3] !== 's3').length });
const untouched = (await catalog()).filter(r => !r.id.startsWith('a'));
await updateBackupStorageSettings({ s3SecretAccessKey: secret + '-invalid' });
let failure;
try { await pruneBackupsForServer(String(hosts[0].id), 2); } catch (e) { failure = e; }
assert.equal(failure?.name, 'BackupPruneError'); assert.equal(failure.pruned, 1);
let remaining = await catalog(); assert.deepEqual(remaining.filter(r => !r.id.startsWith('a')), untouched);
assert.deepEqual(remaining.filter(r => r.id.startsWith('a')).map(r => r.id), ['a1','a3','a4']);
assert(!fs.existsSync('storage/backups/a2-retention240.rsc'));
assert.deepEqual(await s3GetObject(s3, cfg.bucket, prefix + '/a1-retention240.rsc'), payloads.get('a1'));
check('partial-failure-acknowledges-one-local-deletion', { pruned: failure.pruned, failedRowRetained: true, remoteObjectRetained: true, otherServersUnchanged: true });
await updateBackupStorageSettings({ s3SecretAccessKey: secret });
assert.equal(await pruneBackupsForServer(String(hosts[0].id), 2), 1);
assert.equal(await pruneBackupsForServer(String(hosts[0].id), 2), 0);
assert.deepEqual((await catalog()).filter(r => r.id.startsWith('a')).map(r => r.id), ['a3','a4']);
check('retry-retains-latest-two-and-repeat-noop', { retryPruned: 1, repeatPruned: 0 });
assert.equal(await pruneBackupsForServer(String(hosts[1].id), 1), 2);
assert.deepEqual((await catalog()).map(r => r.id), ['a3','a4','b2','c0']);
check('second-server-retains-latest-and-unselected-manual-copy', { pruned: 2, retained: ['a3','a4','b2','c0'] });
const copies = await listBackups();
for (const copy of copies) assert.deepEqual(await readBackupContent(copy), payloads.get(copy.id));
assert.equal(copies.find(r => r.id === 'a3').localFileStatus, 'not-stored');
assert(copies.filter(r => r.id !== 'a3').every(r => r.localFileStatus === 'available'));
check('retained-content-hashes-and-local-states', { copies: copies.map(r => ({ id: r.id, localFileStatus: r.localFileStatus, sha256: createHash('sha256').update(payloads.get(r.id)).digest('hex') })) });
report.passed = true;
} catch (e) { report.failure = { name: e.name }; process.exitCode = 1; }
finally {
try {
const existing = await versions(); assert(existing.every(v => plannedKeys.includes(v.Key) && v.VersionId));
report.cleanup = { plannedKeys, versionsBefore: existing.filter(v => v.kind === 'version').length, deleteMarkersBefore: existing.filter(v => v.kind === 'delete-marker').length, removed: 0 }; save();
for (const v of existing) { await s3.send(new DeleteObjectCommand({ Bucket: cfg.bucket, Key: v.Key, VersionId: v.VersionId })); report.cleanup.removed++; save(); }
assert.equal((await versions()).length, 0); report.cleanup.versionsAndMarkersRemaining = 0;
if (seededIds.length) await pool.query('DELETE FROM backup_entries WHERE id=ANY($1::text[])', [seededIds]);
report.cleanup.ownCatalogRemaining = Number((await pool.query('SELECT count(*) AS n FROM backup_entries')).rows[0].n); assert.equal(report.cleanup.ownCatalogRemaining, 0);
} catch (e) { report.cleanup.failure = { name: e.name }; report.passed = false; process.exitCode = 1; }
await databaseBackgroundWork.drain(); await closePool(); s3.destroy(); save(); console.log(JSON.stringify(report));
}
@@ -0,0 +1,28 @@
import assert from 'node:assert/strict';import fs from 'node:fs';import{randomUUID,createHash}from'node:crypto';import{createServer}from'node:http';import{createRequire}from'node:module';
assert.equal(process.env.MMAPP_TEST_S3_OWNED_SCOPE,'yes');assert.equal(process.env.MMAPP_TEST_S3_RUN,'confirmed');assert.equal(process.env.MMAPP_TEST_S3_PATH_STYLE,'false');const database=process.env.MMAPP_OWN_SQL_DB;assert(/^mm_accept_166_\d+$/.test(database));const url=new URL(process.env.DATABASE_URL);assert.equal(url.pathname,'/'+database);assert.equal(url.username,database);fs.mkdirSync('/tmp/'+database,{recursive:true});process.chdir('/tmp/'+database);
const require=createRequire('/app/backend/package.json'),Fastify=require('fastify'),{validatorCompiler,serializerCompiler}=require('@fastify/type-provider-zod');const{db,pool,closePool}=await import('/app/backend/dist/db/index.js');const{initDatabase}=await import('/app/backend/dist/db/bootstrap.js');const{servers}=await import('/app/backend/dist/db/schema.js');const{eq}=await import('/app/node_modules/drizzle-orm/index.js');
assert.equal((await pool.query('SELECT current_database() AS name')).rows[0].name,database);const role=(await pool.query('SELECT rolsuper,rolcreatedb,rolcreaterole,rolbypassrls FROM pg_roles WHERE rolname=current_user')).rows[0];assert(Object.values(role).every(v=>v===false));await initDatabase();
const{createBackupsRoutes}=await import('/app/backend/dist/routes/backups.js');const{databaseBackgroundWork}=await import('/app/backend/dist/services/database-background-work.js');const{s3ListObjects,s3PutObject,s3DeleteObject,createS3ClientFromConfig}=await import('/app/backend/dist/services/s3-backup-client.js');
const prefix='mmapp-acceptance/'+randomUUID(),secret=process.env.MMAPP_TEST_S3_SECRET_KEY,config={provider:'s3',s3Endpoint:process.env.MMAPP_TEST_S3_ENDPOINT,s3Region:process.env.MMAPP_TEST_S3_REGION,s3Bucket:process.env.MMAPP_TEST_S3_BUCKET,s3Prefix:prefix,s3AccessKeyId:process.env.MMAPP_TEST_S3_ACCESS_KEY,s3SecretAccessKey:secret,s3ForcePathStyle:false,keepLocalCopy:true};assert(secret&&config.s3Endpoint==='https://s3.ru-1.storage.selcloud.ru'&&config.s3Bucket==='mikrotik-test-s3');const client=createS3ClientFromConfig({endpoint:config.s3Endpoint,region:config.s3Region,bucket:config.s3Bucket,prefix,accessKeyId:config.s3AccessKeyId,secretAccessKey:secret,forcePathStyle:false});
const report={date:'2026-10-14',block:'12.5.229',scope:'Compiled backup API + owned PostgreSQL + real Selectel S3; synthetic RouterOS HTTP export fixture, no CHR contacts',prefix,checks:[],passed:false,cleanup:{},nativeChrContacts:0,nativeRestoreCalls:0};const file='/checks/s3-application229.json';const save=()=>fs.writeFileSync(file,JSON.stringify(report,null,2)+'\n');const check=(name,extra={})=>{report.checks.push({name,passed:true,...extra});save()};const sha=b=>createHash('sha256').update(b).digest('hex');let exportCalls=0;const body='# Owned MikroTik Manager S3 application acceptance\n';
const fixture=createServer((req,res)=>{if(req.method==='POST'&&req.url==='/rest/console/export'){exportCalls++;res.setHeader('content-type','application/json');res.end(JSON.stringify({ret:body}));}else{res.statusCode=404;res.end('{}')}});await new Promise(r=>fixture.listen(0,'127.0.0.1',r));const names=['s3-positive-'+randomUUID(),'s3-negative-'+randomUUID(),'s3-only-'+randomUUID()];const [server]=await db.insert(servers).values({name:names[0],host:'127.0.0.1',port:fixture.address().port,useSsl:false,username:'fixture',password:'fixture',enabled:true,type:'exit-node'}).returning();const app=Fastify({logger:false});app.setValidatorCompiler(validatorCompiler);app.setSerializerCompiler(serializerCompiler);await app.register(createBackupsRoutes(),{prefix:'/api'});await app.ready();
async function api(method,path,payload,status=200){const r=await app.inject({method,url:'/api'+path,...(payload?{payload}:{})});assert.equal(r.statusCode,status,method+' '+path+' HTTP '+r.statusCode);return r}
async function job(){const requestId=randomUUID(),before=exportCalls;await api('POST','/backups/create',{serverIds:[server.id],requestId},202);for(let n=0;n<100;n++){const r=(await api('GET','/backups/jobs/'+requestId)).json();if(r.status==='done'){assert.equal(r.completed,1);assert.equal(r.failures.length,0);assert.equal(r.created.length,1);assert.equal(exportCalls,before+1);await api('POST','/backups/create',{serverIds:[server.id],requestId},202);assert.equal(exportCalls,before+1);return r.created[0]}await new Promise(r=>setTimeout(r,100))}throw Error('Job did not complete')}
try{
assert.equal((await s3ListObjects(client,config.s3Bucket,prefix)).length,0);check('empty-prefix-preflight');
let settings=(await api('PUT','/backups/storage',config)).json();assert(settings.secretConfigured&&settings.lastTestAt===null);assert(!JSON.stringify(settings).includes(secret));check('settings-save-without-secret-disclosure');
settings=(await api('POST','/backups/storage/test')).json();assert(settings.lastTestAt&&!settings.lastTestError);check('real-headbucket-test-receipt');
const both=await job();assert.equal(both.storage,'both');assert(!both.uploadError&&both.s3Key.startsWith(prefix+'/'));check('async-create-both-and-request-id-no-repeat');
const downloaded=await api('GET','/backups/'+both.id+'/download');assert.equal(sha(downloaded.rawPayload),sha(Buffer.from(body)));check('download-exact-bytes');
await api('PUT','/backups/storage',{s3SecretAccessKey:secret+'-invalid'});const denied=(await api('POST','/backups/storage/test',undefined,400)).json();assert(denied.settings.lastTestError&&denied.settings.secretConfigured);check('denied-credentials-test-preserves-settings');
const fallback=await api('GET','/backups/'+both.id+'/download');assert.equal(sha(fallback.rawPayload),sha(Buffer.from(body)));check('download-local-fallback-during-s3-denial');
await db.update(servers).set({name:names[1]}).where(eq(servers.id,server.id));const partial=await job();assert.equal(partial.storage,'local');assert(partial.uploadError&&!partial.s3Key);check('upload-denial-keeps-local-backup-and-error');
await api('PUT','/backups/storage',{s3SecretAccessKey:secret,keepLocalCopy:false});await db.update(servers).set({name:names[2]}).where(eq(servers.id,server.id));const remote=await job();assert.equal(remote.storage,'s3');assert(!remote.uploadError);assert(!fs.existsSync('storage/backups/'+remote.filename));const remoteBytes=await api('GET','/backups/'+remote.id+'/download');assert.equal(sha(remoteBytes.rawPayload),sha(Buffer.from(body)));check('s3-only-removes-local-copy-and-downloads-remote');
const key=prefix+'/seed/probe-sync.rsc';await s3PutObject(client,config.s3Bucket,key,body);const sync=(await api('POST','/backups/storage/sync')).json();assert.equal(sync.imported,1);const again=(await api('POST','/backups/storage/sync')).json();assert.equal(again.imported,0);check('sync-import-and-idempotent-repeat');
const catalog=(await api('GET','/backups')).json();assert.equal(catalog.length,4);const imported=catalog.find(r=>r.s3Key===key);assert(imported&&imported.serverId===null);assert.equal((await api('GET','/backups/'+imported.id+'/download')).payload,body);check('catalog-and-imported-object-download');
for(const row of catalog)await api('DELETE','/backups/'+row.id,undefined,204);assert.equal((await api('GET','/backups')).json().length,0);assert.equal((await s3ListObjects(client,config.s3Bucket,prefix)).length,0);await api('DELETE','/backups/'+both.id,undefined,404);check('delete-catalog-and-current-remote-objects');report.passed=true;
}catch(e){report.failure={name:e.name};}
finally{
try{await api('PUT','/backups/storage',{s3SecretAccessKey:secret});const rows=(await api('GET','/backups')).json();for(const row of rows){assert(!row.s3Key||row.s3Key.startsWith(prefix+'/'));await api('DELETE','/backups/'+row.id,undefined,204)}const leftovers=await s3ListObjects(client,config.s3Bucket,prefix);for(const row of leftovers){assert(row.key===prefix+'/seed/probe-sync.rsc'||names.some(n=>row.key.startsWith(prefix+'/'+n+'/'+n+'_')),'Unexpected key; do not delete');await s3DeleteObject(client,config.s3Bucket,row.key)}report.cleanup.currentObjectsRemaining=(await s3ListObjects(client,config.s3Bucket,prefix)).length;report.cleanup.catalogRowsRemaining=(await api('GET','/backups')).json().length;report.cleanup.localFilesRemaining=fs.existsSync('storage/backups')?fs.readdirSync('storage/backups').length:0;assert.equal(report.cleanup.currentObjectsRemaining,0);assert.equal(report.cleanup.localFilesRemaining,0)}catch(e){report.cleanup.failure={name:e.name};report.passed=false}
await databaseBackgroundWork.drain();await app.close();await new Promise(r=>fixture.close(r));await closePool();client.destroy();report.fixtureExportCalls=exportCalls;report.completedAt=new Date().toISOString();report.bounds=['Native export is HTTP fixture; no real device backup/restore or UI repeated','RBAC not repeated in this standalone injected API','S3 current-object deletion only; retained versions/retention not inspected'];save();console.log(JSON.stringify({passed:report.passed,checks:report.checks.length,cleanup:report.cleanup,fixtureExportCalls:exportCalls,nativeChrContacts:0}));if(!report.passed)process.exitCode=1;
}
@@ -0,0 +1,4 @@
import assert from 'node:assert/strict';import fs from 'node:fs';import{createRequire}from'node:module';import{createS3ClientFromConfig}from'/app/backend/dist/services/s3-backup-client.js';const require=createRequire('/app/backend/package.json'),{ListObjectVersionsCommand,DeleteObjectCommand}=require('@aws-sdk/client-s3');const bucket=process.env.MMAPP_TEST_S3_BUCKET;assert.equal(bucket,'mikrotik-test-s3');const client=createS3ClientFromConfig({endpoint:process.env.MMAPP_TEST_S3_ENDPOINT,region:process.env.MMAPP_TEST_S3_REGION,bucket,prefix:'mmapp-acceptance',accessKeyId:process.env.MMAPP_TEST_S3_ACCESS_KEY,secretAccessKey:process.env.MMAPP_TEST_S3_SECRET_KEY,forcePathStyle:false});const reports=JSON.parse(fs.readFileSync('/checks/owned-prefixes-public.json'));const plans=[],out={at:new Date().toISOString(),bucketConfigurationWrites:0,prefixes:[],passed:false};
async function list(prefix){let keyMarker,versionMarker,rows=[];for(let page=0;page<50;page++){const r=await client.send(new ListObjectVersionsCommand({Bucket:bucket,Prefix:prefix+'/',KeyMarker:keyMarker,VersionIdMarker:versionMarker}));rows.push(...(r.Versions??[]).map(v=>({...v,kind:'version'})),...(r.DeleteMarkers??[]).map(v=>({...v,kind:'delete-marker'})));if(!r.IsTruncated)return rows;assert(r.NextKeyMarker||r.NextVersionIdMarker);keyMarker=r.NextKeyMarker;versionMarker=r.NextVersionIdMarker;}throw Error('Version pagination limit')}
function isOwn(r,key){if(r.plannedKeys)return r.plannedKeys.includes(key);const relative=key.slice(r.prefix.length+1);return relative==='seed/probe-sync.rsc'||/^(s3-(?:positive|only)-[a-f0-9-]{36})\/\1_\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}\.rsc$/.test(relative)}
try{for(const r of reports){assert(/^mmapp-acceptance\/[a-f0-9-]{36}$/.test(r.prefix));const rows=await list(r.prefix);for(const row of rows)assert(row.Key?.startsWith(r.prefix+'/')&&isOwn(r,row.Key)&&typeof row.VersionId==='string'&&row.VersionId.length>0,'Unexpected version: do not delete');plans.push({r,rows});}for(const{r,rows}of plans){let removed=0;for(const row of rows){await client.send(new DeleteObjectCommand({Bucket:bucket,Key:row.Key,VersionId:row.VersionId}));removed++;}const after=await list(r.prefix);assert.equal(after.length,0);out.prefixes.push({prefix:r.prefix,versionsBefore:rows.filter(r=>r.kind==='version').length,deleteMarkersBefore:rows.filter(r=>r.kind==='delete-marker').length,removed,versionsAndMarkersRemaining:0});}out.passed=true}catch(e){out.failure={name:e.name,httpStatus:e.$metadata?.httpStatusCode??null}}finally{client.destroy();fs.writeFileSync('/checks/owned-versions-cleanup-public.json',JSON.stringify(out,null,2)+'\n');console.log(JSON.stringify(out));if(!out.passed)process.exitCode=1}
@@ -0,0 +1,76 @@
import assert from 'node:assert/strict'
import { createHash, randomUUID } from 'node:crypto'
import fs from 'node:fs'
// Run in the compiled test backend image. Never upload a device configuration here.
async function main() {
const endpoint = new URL(process.env.MMAPP_TEST_S3_ENDPOINT ?? '')
assert.equal(endpoint.protocol, 'https:', 'Verified HTTPS endpoint required')
assert(!endpoint.username && !endpoint.password && !endpoint.search && !endpoint.hash, 'Endpoint must not contain credentials or query parameters')
assert.equal(process.env.MMAPP_TEST_S3_OWNED_SCOPE, 'yes', 'Explicit bucket/prefix authorization required')
const bucket = process.env.MMAPP_TEST_S3_BUCKET
assert(bucket && bucket.trim() === bucket, 'Explicit bucket required')
const accessKeyId = process.env.MMAPP_TEST_S3_ACCESS_KEY, secretAccessKey = process.env.MMAPP_TEST_S3_SECRET_KEY
assert(accessKeyId && secretAccessKey, 'Explicit test credentials required')
const region = process.env.MMAPP_TEST_S3_REGION
assert(region, 'Explicit region required')
const pathStyle = process.env.MMAPP_TEST_S3_PATH_STYLE
assert(['true', 'false'].includes(pathStyle), 'Explicit path-style choice required')
const prefix = 'mmapp-acceptance/' + randomUUID()
const keys = [prefix + '/probe-1.rsc', prefix + '/probe-2.rsc']
if (process.env.MMAPP_TEST_S3_DRY_RUN === 'true') {
console.log(JSON.stringify({ dryRun: true, networkRequests: 0, plannedKeys: keys, deviceConfigurationUploads: 0 }))
} else {
assert.equal(process.env.MMAPP_TEST_S3_RUN, 'confirmed', 'Explicit live execution required')
const output = process.env.MMAPP_TEST_REPORT
assert(output, 'Explicit report path required')
const { createS3ClientFromConfig, s3PutObject, s3GetObject, s3ListObjects, s3DeleteObject } = await import('/app/backend/dist/services/s3-backup-client.js')
const client = createS3ClientFromConfig({ endpoint: endpoint.toString(), region, bucket, prefix, accessKeyId, secretAccessKey, forcePathStyle: pathStyle === 'true' })
const report = { startedAt: new Date().toISOString(), scope: 'Compiled application S3 client; harmless synthetic bodies; dedicated UUID prefix', prefix, passed: false, checks: [], cleanup: [], deviceConfigurationUploads: 0, bounds: ['No application backup jobs, SQL catalog, native restore or UI acceptance', 'Cleanup covers current objects; retained versions/delete markers/retention policies are not inspected'] }
const attempted = []
report.plannedKeys = keys
report.attemptedKeys = attempted
const save = () => fs.writeFileSync(output, JSON.stringify(report, null, 2) + '\n')
const failure = error => ({ name: /^[\w:.-]+$/.test(error.name) ? error.name : 'Error', httpStatus: error.$metadata?.httpStatusCode ?? null })
const sha = bytes => createHash('sha256').update(bytes).digest('hex')
save()
try {
// An empty namespace is checked before recording ownership or attempting a write.
assert.equal((await s3ListObjects(client, bucket, prefix)).length, 0, 'Namespace already contains objects; no writes allowed')
report.checks.push({ operation: 'empty-prefix-preflight', passed: true }); save()
for (const key of keys) {
const body = Buffer.from('# MikroTik Manager owned S3 roundtrip probe\n# ' + key + '\n')
attempted.push(key); save()
await s3PutObject(client, bucket, key, body)
const received = await s3GetObject(client, bucket, key)
assert.equal(sha(received), sha(body), 'Object content mismatch')
report.checks.push({ operation: 'put-get-content', key, bytes: received.length, passed: true }); save()
}
const listed = (await s3ListObjects(client, bucket, prefix)).map(row => row.key).sort()
assert.deepEqual(listed, [...keys].sort(), 'Unexpected listing in owned namespace')
report.checks.push({ operation: 'list-exact-owned-keys', passed: true })
report.passed = true
} catch (error) { report.failure = failure(error) }
finally {
for (const key of attempted) {
try { await s3DeleteObject(client, bucket, key); report.cleanup.push({ key, acknowledged: true }) }
catch (error) { report.cleanup.push({ key, acknowledged: false, failure: failure(error) }); report.passed = false }
}
if (attempted.length) {
try {
const remaining = (await s3ListObjects(client, bucket, prefix)).filter(row => attempted.includes(row.key)).length
report.currentOwnedObjectsRemaining = remaining
if (remaining) report.passed = false
} catch (error) { report.cleanupReadbackFailure = failure(error); report.passed = false }
}
report.completedAt = new Date().toISOString(); save(); client.destroy()
}
console.log(JSON.stringify({ passed: report.passed, checks: report.checks.length, attemptedObjects: attempted.length, currentOwnedObjectsRemaining: report.currentOwnedObjectsRemaining ?? null }))
if (!report.passed) process.exitCode = 1
}
}
await main().catch(error => {
const name = /^[\w:.-]+$/.test(error.name) ? error.name : 'Error'
console.error('S3_ACCEPTANCE_FAILED: ' + name)
process.exitCode = 1
})
@@ -0,0 +1,66 @@
const fs = require('fs'), assert = require('node:assert/strict'), { createHash } = require('crypto');
const { chromium, expect } = require('@playwright/test');
(async () => {
const phase = process.argv[2], portal = JSON.parse(fs.readFileSync('tmp/portal195-private.json'));
const state = JSON.parse(fs.readFileSync('tmp/state239-public.json'));
const browser = await chromium.launch({ channel: 'msedge', headless: true });
try {
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } }), errors = [], receipts = [];
page.on('pageerror', e => errors.push(e.message));
page.on('response', r => { if (new URL(r.url()).pathname.startsWith('/api/backups') && r.request().method() !== 'GET') receipts.push({ method: r.request().method(), path: new URL(r.url()).pathname, status: r.status() }); });
await page.goto('http://localhost:3000/backups');
await page.getByLabel('Email', { exact: true }).fill(portal.email);
await page.getByLabel('Пароль', { exact: true }).fill(portal.password);
await page.getByRole('button', { name: 'Войти', exact: true }).click();
await expect(page.getByRole('heading', { name: 'Резервные копии', exact: true })).toBeVisible({ timeout: 30000 });
if (phase === 'configure') {
const s = JSON.parse(fs.readFileSync('tmp/s3-ui239-private.json'));
const paused = await page.evaluate(async () => {
const r = await fetch('/api/backups/schedule', { method: 'PUT', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + localStorage.getItem('mmapp_token') }, body: JSON.stringify({ enabled: false }) });
return { status: r.status, body: await r.json() };
});
assert.equal(paused.status, 200); assert.equal(paused.body.enabled, false);
await page.getByRole('tab', { name: 'Настройки', exact: true }).click();
await page.getByRole('button', { name: 'S3', exact: true }).click();
for (const [label, value] of Object.entries({ Endpoint: s.endpoint, Region: s.region, Bucket: s.bucket, Prefix: state.prefix, 'Access key': s.accessKey, 'Secret key': s.secretKey })) await page.getByLabel(label, { exact: true }).fill(value);
const path = page.getByRole('switch', { name: 'Path-style', exact: true });
if (await path.getAttribute('aria-checked') === 'true') await path.click();
const saved = page.waitForResponse(r => new URL(r.url()).pathname === '/api/backups/storage' && r.request().method() === 'PUT');
await page.getByRole('button', { name: 'Сохранить хранилище', exact: true }).click();
assert.equal((await saved).status(), 200);
await expect(page.getByLabel('Secret key', { exact: true })).toHaveValue('');
const tested = page.waitForResponse(r => new URL(r.url()).pathname === '/api/backups/storage/test' && r.request().method() === 'POST');
await page.getByRole('button', { name: 'Проверить', exact: true }).click();
assert.equal((await tested).status(), 200);
} else {
await page.getByRole('button', { name: 'Обновить', exact: true }).click();
await expect(page.getByText(state.filename, { exact: true })).toBeVisible();
if (phase === 'restore') {
const download = page.waitForEvent('download');
await page.getByRole('button', { name: 'Скачать ' + state.filename, exact: true }).click();
const d = await download; assert.equal(d.suggestedFilename(), state.filename);
const bytes = fs.readFileSync(await d.path());
const expected = Buffer.from(`# Own limited UI restore 239\n/interface bridge add name=${state.bridge} comment=mm-ui239\n`);
assert.deepEqual(bytes, expected);
await page.getByRole('button', { name: 'Восстановить ' + state.filename, exact: true }).click();
const dialog = page.getByRole('alertdialog');
await expect(dialog).toContainText(state.filename);
const restored = page.waitForResponse(r => new URL(r.url()).pathname === '/api/backups/' + state.id + '/restore' && r.request().method() === 'POST');
await dialog.getByRole('button', { name: 'Восстановить', exact: true }).click();
const r = await restored; assert.equal(r.status(), 200);
const result = await r.json(); assert.equal(result.backupId, state.id); assert.equal(result.serverId, state.serverId);
await expect(dialog).toBeHidden();
fs.writeFileSync('docs/ux-ui/acceptance/s3-ui239-browser.json', JSON.stringify({ date: '2026-10-14', block: '12.5.239', realPortalLogin: true, noMocks: true, passed: true, downloadBytes: bytes.length, downloadSha256: createHash('sha256').update(bytes).digest('hex'), restoreStatus: 200, restoreReceiptMatched: true, receipts, pageErrors: errors, fullConfigurationApplied: false }, null, 2) + '\n');
} else if (phase === 'delete') {
await page.getByRole('button', { name: 'Удалить ' + state.filename, exact: true }).click();
const deleted = page.waitForResponse(r => new URL(r.url()).pathname === '/api/backups/' + state.id && r.request().method() === 'DELETE');
await page.getByRole('alertdialog').getByRole('button', { name: 'Удалить', exact: true }).click();
assert.equal((await deleted).status(), 204); await expect(page.getByRole('alertdialog')).toBeHidden();
await expect(page.getByText(state.filename, { exact: true })).toHaveCount(0);
const p = 'docs/ux-ui/acceptance/s3-ui239-browser.json', report = JSON.parse(fs.readFileSync(p));
Object.assign(report, { deleteStatus: 204, ownRowRemovedThroughUi: true, deleteReceipts: receipts }); fs.writeFileSync(p, JSON.stringify(report, null, 2) + '\n');
} else throw Error('Unsupported phase');
}
assert.deepEqual(errors, []); console.log('S3 UI239 ' + phase + ' passed');
} finally { await browser.close(); }
})().catch(e => { console.error(e.message); process.exitCode = 1; });
@@ -0,0 +1,89 @@
import fs from 'node:fs';
import assert from 'node:assert/strict';
import { randomUUID, createHash } from 'node:crypto';
import { createRequire } from 'node:module';
const require = createRequire('/app/backend/package.json');
const { ListObjectVersionsCommand, DeleteObjectCommand } = require('@aws-sdk/client-s3');
const { pool } = await import('/app/backend/dist/db/index.js');
const { MikrotikClient } = await import('/app/backend/dist/services/mikrotik.js');
const { createS3ClientFromConfig, s3PutObject, s3GetObject } = await import('/app/backend/dist/services/s3-backup-client.js');
const phase = process.argv[2], statePath = '/checks/state239-private.json', publicPath = '/checks/s3-ui239-native.json';
const config = { endpoint: process.env.MMAPP_TEST_S3_ENDPOINT, region: process.env.MMAPP_TEST_S3_REGION, bucket: process.env.MMAPP_TEST_S3_BUCKET, accessKeyId: process.env.MMAPP_TEST_S3_ACCESS_KEY, secretAccessKey: process.env.MMAPP_TEST_S3_SECRET_KEY, forcePathStyle: false };
assert.equal(config.endpoint, 'https://s3.ru-1.storage.selcloud.ru');
assert.equal(config.bucket, 'mikrotik-test-s3'); assert(config.secretAccessKey);
const s3 = createS3ClientFromConfig(config);
const server = (await pool.query("SELECT * FROM servers WHERE host='194.226.121.150' AND enabled=true")).rows;
assert.equal(server.length, 1);
const client = new MikrotikClient({ host: server[0].host, port: server[0].port, useSsl: server[0].use_ssl, verifySsl: false, username: server[0].username, password: server[0].password });
const rows = async table => (await pool.query(`SELECT to_jsonb(${table}) AS row FROM ${table} ORDER BY id`)).rows.map(r => r.row);
const read = endpoint => client.get(endpoint, 10000);
const save = state => fs.writeFileSync(statePath, JSON.stringify(state), { mode: 0o600 });
async function versions(state) {
const result = []; let keyMarker, versionMarker;
for (let n = 0; n < 10; n++) {
const r = await s3.send(new ListObjectVersionsCommand({ Bucket: config.bucket, Prefix: state.prefix + '/', KeyMarker: keyMarker, VersionIdMarker: versionMarker }));
result.push(...(r.Versions ?? []).map(v => ({ ...v, kind: 'version' })), ...(r.DeleteMarkers ?? []).map(v => ({ ...v, kind: 'delete-marker' })));
if (!r.IsTruncated) return result;
assert(r.NextKeyMarker || r.NextVersionIdMarker); keyMarker = r.NextKeyMarker; versionMarker = r.NextVersionIdMarker;
} throw Error('Own version pagination limit');
}
async function restoreTable(table, original) {
assert.equal(original.length, 1); assert.equal(original[0].id, 1);
const columns = Object.keys(original[0]).filter(k => k !== 'id');
assert(columns.every(k => /^[a-z][a-z0-9_]*$/.test(k)));
await pool.query(`UPDATE ${table} SET ${columns.map((k, n) => `"${k}"=$${n + 1}`).join(',')} WHERE id=1`, columns.map(k => {
const value = original[0][k]; return value && typeof value === 'object' ? JSON.stringify(value) : value;
}));
assert.deepEqual(await rows(table), original);
}
try {
if (phase === 'init') {
assert(!fs.existsSync(statePath));
const uuid = randomUUID(), bridge = 'mm-ui239-' + uuid.slice(0, 12), filename = 'mm-ui239-' + uuid + '.rsc';
const state = { id: 'mm-ui239-' + uuid, prefix: 'mmapp-acceptance/' + uuid, bridge, filename, key: 'mmapp-acceptance/' + uuid + '/' + filename, serverId: Number(server[0].id), serverName: server[0].name, storage: await rows('backup_storage_settings'), schedule: await rows('backup_schedule_settings'), catalog: await rows('backup_entries'), bridges: await read('/interface/bridge'), files: await read('/file') };
assert.equal(state.storage[0].provider, 'local'); assert(state.catalog.every(r => r.storage === 'local' && !r.s3_key));
assert(!state.bridges.some(b => b.name === bridge)); assert(!state.files.some(f => f.name === filename));
assert.equal((await versions(state)).length, 0); save(state);
fs.writeFileSync('/checks/state239-public.json', JSON.stringify({ id: state.id, prefix: state.prefix, filename, bridge, serverId: state.serverId }));
console.log('Own scope initialized; original settings/catalog saved privately');
} else {
const state = JSON.parse(fs.readFileSync(statePath));
assert(/^mm-ui239-[a-f0-9-]+\.rsc$/.test(state.filename)); assert(state.key === state.prefix + '/' + state.filename);
if (phase === 'upload') {
const storage = (await rows('backup_storage_settings'))[0];
assert.equal(storage.provider, 's3'); assert.equal(storage.s3_prefix, state.prefix); assert.equal(storage.s3_bucket, config.bucket);
assert.equal((await rows('backup_schedule_settings'))[0].enabled, false);
const script = Buffer.from(`# Own limited UI restore 239\n/interface bridge add name=${state.bridge} comment=mm-ui239\n`);
assert.equal((await versions(state)).length, 0);
await s3PutObject(s3, config.bucket, state.key, script);
assert.deepEqual(await s3GetObject(s3, config.bucket, state.key), script);
await pool.query('INSERT INTO backup_entries(id,server_id,server_name,filename,size_bytes,kind,notes,storage,s3_key,created_at) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,now())', [state.id, state.serverId, state.serverName, state.filename, script.length, 'manual', 'Own limited UI restore 239: bridge only', 's3', state.key]);
state.bytes = script.length; state.sha256 = createHash('sha256').update(script).digest('hex'); save(state);
console.log(JSON.stringify({ ownS3ScriptUploaded: true, bytes: state.bytes, catalogRowCreated: true }));
} else if (phase === 'verify') {
const bridges = await read('/interface/bridge'), own = bridges.filter(b => b.name === state.bridge);
assert.equal(own.length, 1); assert.equal(own[0].comment, 'mm-ui239');
assert.equal((await read('/interface/bridge/port')).filter(p => p.bridge === state.bridge).length, 0);
assert.equal((await read('/ip/address')).filter(p => p.interface === state.bridge).length, 0);
assert.deepEqual(bridges.filter(b => b.name !== state.bridge), state.bridges);
state.nativeVerified = true; save(state);
console.log('Native bridge confirmed; no ports/addresses, original bridges unchanged');
} else if (phase === 'cleanup') {
const own = (await read('/interface/bridge')).filter(b => b.name === state.bridge);
assert(own.length <= 1);
if (own.length) { assert.equal(own[0].comment, 'mm-ui239'); await client.delete('/interface/bridge/' + encodeURIComponent(own[0]['.id'])); }
const ownFiles = (await read('/file')).filter(f => f.name === state.filename); assert(ownFiles.length <= 1);
for (const file of ownFiles) await client.removeFile(file['.id']);
await pool.query('DELETE FROM backup_entries WHERE id=$1 AND filename=$2 AND s3_key=$3', [state.id, state.filename, state.key]);
const existing = await versions(state); assert(existing.every(v => v.Key === state.key && v.VersionId));
for (const v of existing) await s3.send(new DeleteObjectCommand({ Bucket: config.bucket, Key: v.Key, VersionId: v.VersionId }));
assert.equal((await versions(state)).length, 0);
assert.deepEqual(await rows('backup_entries'), state.catalog);
assert.deepEqual(await read('/interface/bridge'), state.bridges);
assert(!(await read('/file')).some(f => f.name === state.filename));
await restoreTable('backup_storage_settings', state.storage); await restoreTable('backup_schedule_settings', state.schedule);
const report = { date: '2026-10-14', block: '12.5.239', host: server[0].host, prefix: state.prefix, limitedScriptBytes: state.bytes ?? null, sha256: state.sha256 ?? null, nativeBridgeVerified: Boolean(state.nativeVerified), bridgeAndUploadedFileRemoved: true, originalBridgesUnchanged: true, originalCatalogAllFieldsMatched: true, originalCatalogRows: state.catalog.length, originalSettingsAllFieldsMatched: true, s3VersionsAndMarkersRemoved: existing.length, s3VersionsAndMarkersRemaining: 0, passwordChanges: 0, fullConfigurationApplied: false };
fs.writeFileSync(publicPath, JSON.stringify(report, null, 2) + '\n'); fs.unlinkSync(statePath); console.log(JSON.stringify(report));
} else throw Error('Unsupported phase');
}
} finally { s3.destroy(); await pool.end(); }
@@ -0,0 +1,81 @@
{
"schemaVersion": 2,
"readOnlyDockerAndProc": true,
"serviceChanges": 0,
"summary": {
"samples": 1,
"elapsedMs": 0,
"hostMemTotalBytes": 2097152,
"minHostAvailableBytes": 1048576,
"maxObservedContainerUsageIncludingCacheBytes": 100,
"containerIdentityAndStartStable": true,
"allObservedRunning": true,
"anyObservedOomKilled": false,
"allCgroupV2MetricsAvailable": true,
"pswpinDelta": 0,
"pswpoutDelta": 0,
"oom_killDelta": 0,
"acceptancePassed": false,
"scope": "sampled evidence only; correlate with workload, limits, errors and latency; unsampled peaks are unknown"
},
"observations": [
{
"elapsedMs": 0,
"host": {
"MemTotalBytes": 2097152,
"MemAvailableBytes": 1048576,
"SwapTotalBytes": 4194304,
"SwapFreeBytes": 3145728,
"pswpin": 20,
"pswpout": 30,
"oom_kill": 0
},
"containers": [
{
"id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"imageId": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"startedAt": "2026-11-02T00:00:00Z",
"running": true,
"oomKilled": false,
"restartCount": 0,
"usageIncludingCacheBytes": 100,
"inactiveFileBytes": 30,
"usageMinusInactiveFileBytes": 70,
"configuredLimitBytes": 512,
"reportedLimitBytes": 2048,
"swapUsageBytes": null,
"pid": 123,
"identityStableDuringSample": true,
"runningAfter": true,
"oomKilledAfter": false,
"cgroupV2": {
"version": 2,
"status": "available",
"reason": null,
"metrics": {
"memory.current": 100,
"memory.peak": 200,
"memory.events": {
"low": 0,
"high": 0,
"max": 0,
"oom": 0,
"oom_kill": 0
},
"memory.swap.current": 0,
"memory.swap.peak": 0,
"memory.swap.events": {
"high": 0,
"max": 0,
"fail": 0
}
},
"unavailable": {},
"peakScope": "cgroup lifetime or since external reset; not an isolated workload peak",
"provenance": "fixed host cgroup2 mount + exact Docker ID membership + process/root generation"
}
}
]
}
]
}
Loaded 100 of 3835 files, more files were not shown because too many files have changed in this diff. Show more