direxio-deployer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +92 -0
- package/LICENSE +21 -0
- package/README.md +221 -0
- package/README_zh.md +218 -0
- package/SKILL.md +722 -0
- package/agents/README.md +25 -0
- package/agents/openai.yaml +12 -0
- package/bin/direxio-deployer.mjs +375 -0
- package/package.json +28 -0
- package/references/agent-targets.md +128 -0
- package/references/architecture.md +44 -0
- package/references/bug-history.md +78 -0
- package/references/deployment-lessons.md +218 -0
- package/references/deployment-optimization-audit.md +317 -0
- package/references/deployment-workflow.md +341 -0
- package/references/iam-policy.json +52 -0
- package/references/runtime-wiring.md +209 -0
- package/references/state-machine.md +46 -0
- package/references/token-refresh.md +81 -0
- package/references/tooling.md +106 -0
- package/references/troubleshooting.md +26 -0
- package/references/user-journey.md +75 -0
- package/references/verification-recovery.md +84 -0
- package/references/voip-turn-runbook.md +154 -0
- package/references/windows-deployment-notes.md +119 -0
- package/scripts/aws-credentials.sh +195 -0
- package/scripts/cloud-init/Caddyfile +48 -0
- package/scripts/cloud-init/docker-compose.yml +125 -0
- package/scripts/cloud-init/init-tokens.sh +238 -0
- package/scripts/cloud-init/user-data.yaml +40 -0
- package/scripts/destroy.ps1 +77 -0
- package/scripts/destroy.sh +589 -0
- package/scripts/lib/aws.sh +73 -0
- package/scripts/lib/domain.sh +175 -0
- package/scripts/lib/operation_report.sh +240 -0
- package/scripts/lib/ops.sh +230 -0
- package/scripts/lib/paths.sh +35 -0
- package/scripts/lib/state.sh +137 -0
- package/scripts/mcp-tools-list.mjs +95 -0
- package/scripts/orchestrate.ps1 +112 -0
- package/scripts/orchestrate.sh +1126 -0
- package/scripts/phases/s0_prereq_aws.sh +39 -0
- package/scripts/phases/s1_preflight.sh +72 -0
- package/scripts/phases/s2_domain.sh +103 -0
- package/scripts/phases/s3_provision.sh +421 -0
- package/scripts/phases/s4_bootstrap_stack.sh +38 -0
- package/scripts/phases/s5_init_tokens.sh +118 -0
- package/scripts/phases/s6_wire_local.sh +1435 -0
- package/scripts/phases/s7_verify_e2e.sh +136 -0
- package/scripts/pricing-estimate.sh +256 -0
- package/scripts/render/render-userdata.sh +86 -0
- package/scripts/reset-app-data.sh +40 -0
- package/scripts/update.sh +30 -0
- package/tests/aws_credentials_test.sh +139 -0
- package/tests/connect_daemon_runtime_check_test.sh +120 -0
- package/tests/default_paths_test.sh +58 -0
- package/tests/destroy_local_bridge_test.sh +154 -0
- package/tests/destroy_root_identity_test.sh +91 -0
- package/tests/destroy_route53_zone_test.sh +80 -0
- package/tests/domain_authoritative_dns_test.sh +49 -0
- package/tests/mcp_doctor_runtime_check_test.sh +86 -0
- package/tests/mcp_smoke_runtime_check_test.sh +121 -0
- package/tests/mcp_tools_runtime_check_test.sh +123 -0
- package/tests/npm_skill_distribution_test.sh +95 -0
- package/tests/operation_report_test.sh +258 -0
- package/tests/orchestrate_status_recovery_test.sh +91 -0
- package/tests/phase_timeout_test.sh +88 -0
- package/tests/pricing_estimate_test.sh +159 -0
- package/tests/render_userdata_remote_nodes_test.sh +40 -0
- package/tests/root_volume_tracking_test.sh +41 -0
- package/tests/route53_overwrite_guard_test.sh +86 -0
- package/tests/route53_zone_auto_create_test.sh +66 -0
- package/tests/runtime_summary_check_test.sh +203 -0
- package/tests/s6_wire_local_test.sh +405 -0
- package/tests/skill_structure_test.sh +298 -0
- package/tests/update_reset_ops_test.sh +230 -0
- package/tests/user_confirmation_gates_test.sh +152 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# aws-credentials.sh - import/verify AWS deployment credentials.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
HERE=$(cd "$(dirname "$0")" && pwd)
|
|
6
|
+
# shellcheck disable=SC1090
|
|
7
|
+
source "$HERE/lib/aws.sh"
|
|
8
|
+
|
|
9
|
+
usage() {
|
|
10
|
+
cat >&2 <<'EOF'
|
|
11
|
+
Usage:
|
|
12
|
+
scripts/aws-credentials.sh import-csv <aws-access-key.csv> [profile] [region]
|
|
13
|
+
scripts/aws-credentials.sh verify [profile]
|
|
14
|
+
|
|
15
|
+
Default profile: direxio-deployer
|
|
16
|
+
Root identities are allowed when the operator explicitly chooses them.
|
|
17
|
+
EOF
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
aws_credentials_file() {
|
|
21
|
+
printf '%s\n' "${AWS_SHARED_CREDENTIALS_FILE:-$HOME/.aws/credentials}"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
aws_config_file() {
|
|
25
|
+
printf '%s\n' "${AWS_CONFIG_FILE:-$HOME/.aws/config}"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
ensure_aws_file() {
|
|
29
|
+
local file=$1
|
|
30
|
+
mkdir -p "$(dirname "$file")"
|
|
31
|
+
chmod 700 "$(dirname "$file")" 2>/dev/null || true
|
|
32
|
+
[ -f "$file" ] || : > "$file"
|
|
33
|
+
chmod 600 "$file" 2>/dev/null || true
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
csv_column_index() {
|
|
37
|
+
local header=$1 wanted=$2
|
|
38
|
+
awk -v header="$header" -v wanted="$wanted" '
|
|
39
|
+
BEGIN {
|
|
40
|
+
n = split(header, cols, ",")
|
|
41
|
+
for (i = 1; i <= n; i++) {
|
|
42
|
+
gsub(/^"|"$/, "", cols[i])
|
|
43
|
+
gsub(/^[[:space:]]+|[[:space:]]+$/, "", cols[i])
|
|
44
|
+
if (tolower(cols[i]) == tolower(wanted)) {
|
|
45
|
+
print i
|
|
46
|
+
exit
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
csv_field() {
|
|
54
|
+
local row=$1 index=$2
|
|
55
|
+
awk -v row="$row" -v idx="$index" '
|
|
56
|
+
BEGIN {
|
|
57
|
+
n = split(row, cols, ",")
|
|
58
|
+
value = cols[idx]
|
|
59
|
+
gsub(/^"|"$/, "", value)
|
|
60
|
+
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
|
|
61
|
+
print value
|
|
62
|
+
}
|
|
63
|
+
'
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
read_csv_credentials() {
|
|
67
|
+
local csv=$1 header row ak_i sk_i token_i access_key secret_key session_token
|
|
68
|
+
[ -f "$csv" ] || {
|
|
69
|
+
echo "CSV file not found: $csv" >&2
|
|
70
|
+
return 1
|
|
71
|
+
}
|
|
72
|
+
IFS= read -r header < "$csv" || return 1
|
|
73
|
+
row=$(tail -n +2 "$csv" | sed '/^[[:space:]]*$/d' | head -n 1)
|
|
74
|
+
[ -n "$row" ] || {
|
|
75
|
+
echo "CSV has no credential row: $csv" >&2
|
|
76
|
+
return 1
|
|
77
|
+
}
|
|
78
|
+
ak_i=$(csv_column_index "$header" "Access key ID")
|
|
79
|
+
[ -n "$ak_i" ] || ak_i=$(csv_column_index "$header" "Access key id")
|
|
80
|
+
sk_i=$(csv_column_index "$header" "Secret access key")
|
|
81
|
+
token_i=$(csv_column_index "$header" "Session token")
|
|
82
|
+
[ -n "$ak_i" ] && [ -n "$sk_i" ] || {
|
|
83
|
+
echo "CSV must contain Access key ID and Secret access key columns" >&2
|
|
84
|
+
return 1
|
|
85
|
+
}
|
|
86
|
+
access_key=$(csv_field "$row" "$ak_i")
|
|
87
|
+
secret_key=$(csv_field "$row" "$sk_i")
|
|
88
|
+
session_token=""
|
|
89
|
+
[ -n "$token_i" ] && session_token=$(csv_field "$row" "$token_i")
|
|
90
|
+
[ -n "$access_key" ] && [ -n "$secret_key" ] || {
|
|
91
|
+
echo "CSV credential values are incomplete" >&2
|
|
92
|
+
return 1
|
|
93
|
+
}
|
|
94
|
+
printf '%s\t%s\t%s\n' "$access_key" "$secret_key" "$session_token"
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
profile_header() {
|
|
98
|
+
local profile=$1 config=${2:-0}
|
|
99
|
+
if [ "$config" = "1" ] && [ "$profile" != "default" ]; then
|
|
100
|
+
printf 'profile %s\n' "$profile"
|
|
101
|
+
else
|
|
102
|
+
printf '%s\n' "$profile"
|
|
103
|
+
fi
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
remove_profile_section() {
|
|
107
|
+
local file=$1 header=$2 tmp
|
|
108
|
+
tmp="$file.tmp.$$"
|
|
109
|
+
awk -v target="[$header]" '
|
|
110
|
+
/^\[/ {
|
|
111
|
+
skip = ($0 == target)
|
|
112
|
+
}
|
|
113
|
+
!skip { print }
|
|
114
|
+
' "$file" > "$tmp"
|
|
115
|
+
mv "$tmp" "$file"
|
|
116
|
+
chmod 600 "$file" 2>/dev/null || true
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
write_profile() {
|
|
120
|
+
local profile=$1 region=$2 access_key=$3 secret_key=$4 session_token=${5:-}
|
|
121
|
+
local credentials config cred_header config_header
|
|
122
|
+
credentials=$(aws_credentials_file)
|
|
123
|
+
config=$(aws_config_file)
|
|
124
|
+
ensure_aws_file "$credentials"
|
|
125
|
+
ensure_aws_file "$config"
|
|
126
|
+
cred_header=$(profile_header "$profile" 0)
|
|
127
|
+
config_header=$(profile_header "$profile" 1)
|
|
128
|
+
remove_profile_section "$credentials" "$cred_header"
|
|
129
|
+
remove_profile_section "$config" "$config_header"
|
|
130
|
+
{
|
|
131
|
+
printf '[%s]\n' "$cred_header"
|
|
132
|
+
printf 'aws_access_key_id = %s\n' "$access_key"
|
|
133
|
+
printf 'aws_secret_access_key = %s\n' "$secret_key"
|
|
134
|
+
[ -n "$session_token" ] && printf 'aws_session_token = %s\n' "$session_token"
|
|
135
|
+
printf '\n'
|
|
136
|
+
} >> "$credentials"
|
|
137
|
+
{
|
|
138
|
+
printf '[%s]\n' "$config_header"
|
|
139
|
+
printf 'region = %s\n' "$region"
|
|
140
|
+
printf 'output = json\n\n'
|
|
141
|
+
} >> "$config"
|
|
142
|
+
chmod 600 "$credentials" "$config" 2>/dev/null || true
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
verify_env_identity() {
|
|
146
|
+
local access_key=$1 secret_key=$2 session_token=${3:-} arn
|
|
147
|
+
arn=$(AWS_ACCESS_KEY_ID="$access_key" AWS_SECRET_ACCESS_KEY="$secret_key" AWS_SESSION_TOKEN="$session_token" aws_identity_arn)
|
|
148
|
+
[ -n "$arn" ] && [ "$arn" != "None" ] || {
|
|
149
|
+
echo "AWS credentials could not be verified with sts get-caller-identity" >&2
|
|
150
|
+
return 1
|
|
151
|
+
}
|
|
152
|
+
printf '%s\n' "$arn"
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
verify_profile() {
|
|
156
|
+
local profile=$1 arn root_identity=false
|
|
157
|
+
arn=$(AWS_PROFILE="$profile" aws_identity_arn)
|
|
158
|
+
[ -n "$arn" ] && [ "$arn" != "None" ] || {
|
|
159
|
+
echo "AWS profile could not be verified with sts get-caller-identity: $profile" >&2
|
|
160
|
+
return 1
|
|
161
|
+
}
|
|
162
|
+
aws_arn_is_root "$arn" && root_identity=true
|
|
163
|
+
printf 'AWS identity verified: profile=%s root=%s arn=%s\n' "$profile" "$root_identity" "$(aws_redact_arn "$arn")"
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
cmd_import_csv() {
|
|
167
|
+
local csv=${1:-} profile=${2:-direxio-deployer} region=${3:-${AWS_DEFAULT_REGION:-${AWS_REGION:-us-east-1}}}
|
|
168
|
+
local access_key secret_key session_token arn root_identity=false
|
|
169
|
+
[ -n "$csv" ] || {
|
|
170
|
+
usage
|
|
171
|
+
return 1
|
|
172
|
+
}
|
|
173
|
+
IFS=$'\t' read -r access_key secret_key session_token < <(read_csv_credentials "$csv")
|
|
174
|
+
arn=$(verify_env_identity "$access_key" "$secret_key" "$session_token")
|
|
175
|
+
aws_arn_is_root "$arn" && root_identity=true
|
|
176
|
+
write_profile "$profile" "$region" "$access_key" "$secret_key" "$session_token"
|
|
177
|
+
printf 'AWS credentials imported: profile=%s region=%s root=%s arn=%s\n' "$profile" "$region" "$root_identity" "$(aws_redact_arn "$arn")"
|
|
178
|
+
printf 'Credentials file: %s (0600)\n' "$(aws_credentials_file)"
|
|
179
|
+
printf 'Config file: %s (0600)\n' "$(aws_config_file)"
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
case "${1:-}" in
|
|
183
|
+
import-csv)
|
|
184
|
+
shift
|
|
185
|
+
cmd_import_csv "$@"
|
|
186
|
+
;;
|
|
187
|
+
verify)
|
|
188
|
+
shift
|
|
189
|
+
verify_profile "${1:-${AWS_PROFILE:-direxio-deployer}}"
|
|
190
|
+
;;
|
|
191
|
+
*)
|
|
192
|
+
usage
|
|
193
|
+
exit 1
|
|
194
|
+
;;
|
|
195
|
+
esac
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Caddyfile - automatic TLS reverse proxy for the Direxio message server.
|
|
2
|
+
#
|
|
3
|
+
# Public exposure is limited to 443 and 80 for ACME redirects. The message
|
|
4
|
+
# server listens only inside the compose network on port 8008.
|
|
5
|
+
|
|
6
|
+
{$DOMAIN} {
|
|
7
|
+
handle /.well-known/matrix/server {
|
|
8
|
+
header Content-Type application/json
|
|
9
|
+
respond `{"m.server":"{$DOMAIN}:443"}` 200
|
|
10
|
+
}
|
|
11
|
+
handle /.well-known/matrix/client {
|
|
12
|
+
header Content-Type application/json
|
|
13
|
+
header Access-Control-Allow-Origin *
|
|
14
|
+
respond `{"m.homeserver":{"base_url":"https://{$DOMAIN}"}}` 200
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
handle_path /.well-known/portal/* {
|
|
18
|
+
header Access-Control-Allow-Origin *
|
|
19
|
+
header Access-Control-Allow-Methods "GET, OPTIONS"
|
|
20
|
+
header Access-Control-Allow-Headers "Content-Type"
|
|
21
|
+
@portal_options method OPTIONS
|
|
22
|
+
respond @portal_options 204
|
|
23
|
+
root * /srv/p2p/wellknown
|
|
24
|
+
file_server
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
handle /healthz {
|
|
28
|
+
rewrite * /_p2p/health
|
|
29
|
+
reverse_proxy message-server:8008
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
handle /_matrix/* {
|
|
33
|
+
reverse_proxy message-server:8008
|
|
34
|
+
}
|
|
35
|
+
handle /_dendrite/* {
|
|
36
|
+
reverse_proxy message-server:8008
|
|
37
|
+
}
|
|
38
|
+
handle /_synapse/* {
|
|
39
|
+
reverse_proxy message-server:8008
|
|
40
|
+
}
|
|
41
|
+
handle /_p2p/* {
|
|
42
|
+
reverse_proxy message-server:8008
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
handle {
|
|
46
|
+
reverse_proxy message-server:8008
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# docker-compose.yml - cloud-side Direxio message-server stack.
|
|
2
|
+
#
|
|
3
|
+
# Layers: Caddy (public 443/TLS) -> Direxio message-server (Matrix + P2P API).
|
|
4
|
+
# PostgreSQL 18 persists Matrix and P2P business tables. The local agent bridge
|
|
5
|
+
# process is not started in the cloud by default.
|
|
6
|
+
|
|
7
|
+
networks:
|
|
8
|
+
p2p-net:
|
|
9
|
+
|
|
10
|
+
volumes:
|
|
11
|
+
postgres-data:
|
|
12
|
+
message-config:
|
|
13
|
+
message-data:
|
|
14
|
+
caddy-data:
|
|
15
|
+
caddy-config:
|
|
16
|
+
|
|
17
|
+
services:
|
|
18
|
+
postgres:
|
|
19
|
+
image: postgres:18-alpine
|
|
20
|
+
networks: [p2p-net]
|
|
21
|
+
environment:
|
|
22
|
+
POSTGRES_USER: direxio_message_server
|
|
23
|
+
POSTGRES_PASSWORD: direxio_message_server
|
|
24
|
+
POSTGRES_DB: direxio_message_server
|
|
25
|
+
volumes:
|
|
26
|
+
- postgres-data:/var/lib/postgresql
|
|
27
|
+
healthcheck:
|
|
28
|
+
test: ["CMD-SHELL", "pg_isready -U direxio_message_server -d direxio_message_server"]
|
|
29
|
+
interval: 5s
|
|
30
|
+
timeout: 3s
|
|
31
|
+
retries: 30
|
|
32
|
+
restart: unless-stopped
|
|
33
|
+
|
|
34
|
+
message-init:
|
|
35
|
+
image: ${MESSAGE_SERVER_IMAGE}
|
|
36
|
+
networks: [p2p-net]
|
|
37
|
+
depends_on:
|
|
38
|
+
postgres:
|
|
39
|
+
condition: service_healthy
|
|
40
|
+
environment:
|
|
41
|
+
TURN_SECRET: ${TURN_SECRET}
|
|
42
|
+
entrypoint: ["/bin/sh", "-c"]
|
|
43
|
+
command:
|
|
44
|
+
- |
|
|
45
|
+
set -eu
|
|
46
|
+
mkdir -p /etc/direxio-message-server /var/direxio-message-server /var/direxio-message-server/p2p
|
|
47
|
+
if [ ! -f /etc/direxio-message-server/matrix_key.pem ]; then
|
|
48
|
+
/usr/bin/generate-keys -private-key /etc/direxio-message-server/matrix_key.pem
|
|
49
|
+
fi
|
|
50
|
+
CFG=/etc/direxio-message-server/message-server.yaml
|
|
51
|
+
DB="postgres://direxio_message_server:direxio_message_server@postgres/direxio_message_server?sslmode=disable"
|
|
52
|
+
/usr/bin/generate-config -dir /var/direxio-message-server -db "$$DB" -server "${DOMAIN}" > "$$CFG"
|
|
53
|
+
printf '\nclient_api:\n turn:\n turn_shared_secret: "%s"\n turn_user_lifetime: "24h"\n turn_uris:\n - "turn:%s:3478?transport=udp"\n - "turn:%s:3478?transport=tcp"\n' "$$TURN_SECRET" "${DOMAIN}" "${DOMAIN}" >> "$$CFG"
|
|
54
|
+
volumes:
|
|
55
|
+
- message-config:/etc/direxio-message-server
|
|
56
|
+
- message-data:/var/direxio-message-server
|
|
57
|
+
restart: "no"
|
|
58
|
+
|
|
59
|
+
message-server:
|
|
60
|
+
image: ${MESSAGE_SERVER_IMAGE}
|
|
61
|
+
networks: [p2p-net]
|
|
62
|
+
depends_on:
|
|
63
|
+
postgres:
|
|
64
|
+
condition: service_healthy
|
|
65
|
+
message-init:
|
|
66
|
+
condition: service_completed_successfully
|
|
67
|
+
environment:
|
|
68
|
+
P2P_PORTAL_CREDENTIALS_FILE: /var/direxio-message-server/p2p/bootstrap.json
|
|
69
|
+
P2P_PORTAL_PASSWORD: ${P2P_PORTAL_PASSWORD}
|
|
70
|
+
command:
|
|
71
|
+
- --config
|
|
72
|
+
- /etc/direxio-message-server/message-server.yaml
|
|
73
|
+
- --http-bind-address
|
|
74
|
+
- :8008
|
|
75
|
+
volumes:
|
|
76
|
+
- message-config:/etc/direxio-message-server
|
|
77
|
+
- message-data:/var/direxio-message-server
|
|
78
|
+
- /opt/p2p:/opt/p2p
|
|
79
|
+
healthcheck:
|
|
80
|
+
test: ["CMD-SHELL", "wget -q -O- http://127.0.0.1:8008/_p2p/health >/dev/null"]
|
|
81
|
+
interval: 10s
|
|
82
|
+
timeout: 5s
|
|
83
|
+
retries: 30
|
|
84
|
+
start_period: 15s
|
|
85
|
+
restart: unless-stopped
|
|
86
|
+
|
|
87
|
+
caddy:
|
|
88
|
+
image: caddy:2
|
|
89
|
+
networks: [p2p-net]
|
|
90
|
+
depends_on:
|
|
91
|
+
message-server:
|
|
92
|
+
condition: service_healthy
|
|
93
|
+
ports:
|
|
94
|
+
- "80:80"
|
|
95
|
+
- "443:443"
|
|
96
|
+
environment:
|
|
97
|
+
- DOMAIN=${DOMAIN}
|
|
98
|
+
- ACME_EMAIL=${ACME_EMAIL}
|
|
99
|
+
volumes:
|
|
100
|
+
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
|
101
|
+
- caddy-data:/data
|
|
102
|
+
- caddy-config:/config
|
|
103
|
+
- /opt/p2p:/srv/p2p:ro
|
|
104
|
+
restart: unless-stopped
|
|
105
|
+
|
|
106
|
+
coturn:
|
|
107
|
+
image: coturn/coturn:latest
|
|
108
|
+
network_mode: host
|
|
109
|
+
restart: unless-stopped
|
|
110
|
+
command:
|
|
111
|
+
- -n
|
|
112
|
+
- --realm=${DOMAIN}
|
|
113
|
+
- --listening-port=3478
|
|
114
|
+
- --min-port=49160
|
|
115
|
+
- --max-port=49200
|
|
116
|
+
- --external-ip=${PUBLIC_IP}
|
|
117
|
+
- --use-auth-secret
|
|
118
|
+
- --static-auth-secret=${TURN_SECRET}
|
|
119
|
+
- --no-cli
|
|
120
|
+
- --no-tls
|
|
121
|
+
- --no-dtls
|
|
122
|
+
- --no-multicast-peers
|
|
123
|
+
- --denied-peer-ip=10.0.0.0-10.255.255.255
|
|
124
|
+
- --denied-peer-ip=172.16.0.0-172.31.255.255
|
|
125
|
+
- --denied-peer-ip=192.168.0.0-192.168.255.255
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# init-tokens.sh - wait for message-server bootstrap credentials after compose is up.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
P2P_DIR=${P2P_DIR:-/opt/p2p}
|
|
6
|
+
COMPOSE="docker compose -f ${P2P_DIR}/docker-compose.yml --env-file ${P2P_DIR}/.env"
|
|
7
|
+
DOMAIN=${DOMAIN:?DOMAIN is required (e.g. __DOMAIN__)}
|
|
8
|
+
CONTAINER_BOOTSTRAP_FILE=${CONTAINER_BOOTSTRAP_FILE:-/var/direxio-message-server/p2p/bootstrap.json}
|
|
9
|
+
BOOTSTRAP_FILE=${BOOTSTRAP_FILE:-/opt/p2p/bootstrap.json}
|
|
10
|
+
WELLKNOWN_DIR=${WELLKNOWN_DIR:-/opt/p2p/wellknown}
|
|
11
|
+
|
|
12
|
+
log() { echo "[init-tokens] $*" >&2; }
|
|
13
|
+
|
|
14
|
+
env_string() {
|
|
15
|
+
local key=$1
|
|
16
|
+
grep -E "^${key}=" "${P2P_DIR}/.env" 2>/dev/null \
|
|
17
|
+
| tail -1 \
|
|
18
|
+
| cut -d= -f2- \
|
|
19
|
+
|| true
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
json_string() {
|
|
23
|
+
local key=$1 file=$2
|
|
24
|
+
grep -oE '"'"$key"'"[[:space:]]*:[[:space:]]*"[^"]+"' "$file" 2>/dev/null \
|
|
25
|
+
| head -1 \
|
|
26
|
+
| sed -E 's/.*:[[:space:]]*"([^"]+)".*/\1/' \
|
|
27
|
+
|| true
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
matrix_room_path() {
|
|
31
|
+
printf '%s' "$1" | sed 's/%/%25/g; s/!/%21/g; s/:/%3A/g'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
container_post_json() {
|
|
35
|
+
local path=$1 json=$2 token=${3:-}
|
|
36
|
+
if [ -n "$token" ]; then
|
|
37
|
+
$COMPOSE exec -T message-server wget -q -O - \
|
|
38
|
+
--header='Content-Type: application/json' \
|
|
39
|
+
--header="Authorization: Bearer ${token}" \
|
|
40
|
+
--post-data="$json" \
|
|
41
|
+
"http://127.0.0.1:8008${path}"
|
|
42
|
+
else
|
|
43
|
+
$COMPOSE exec -T message-server wget -q -O - \
|
|
44
|
+
--header='Content-Type: application/json' \
|
|
45
|
+
--post-data="$json" \
|
|
46
|
+
"http://127.0.0.1:8008${path}"
|
|
47
|
+
fi
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
wait_for_message_server() {
|
|
51
|
+
log "waiting for message-server /_p2p/health ..."
|
|
52
|
+
for i in $(seq 1 90); do
|
|
53
|
+
if $COMPOSE exec -T message-server wget -q -O - http://127.0.0.1:8008/_p2p/health >/dev/null 2>&1; then
|
|
54
|
+
log "message-server is healthy."
|
|
55
|
+
return 0
|
|
56
|
+
fi
|
|
57
|
+
sleep 5
|
|
58
|
+
done
|
|
59
|
+
log "message-server did not become healthy in time"
|
|
60
|
+
return 1
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
copy_bootstrap_file() {
|
|
64
|
+
local tmp
|
|
65
|
+
tmp=$(mktemp)
|
|
66
|
+
if ! $COMPOSE exec -T message-server sh -c "test -s '$CONTAINER_BOOTSTRAP_FILE' && cat '$CONTAINER_BOOTSTRAP_FILE'" > "$tmp" 2>/dev/null; then
|
|
67
|
+
rm -f "$tmp"
|
|
68
|
+
return 1
|
|
69
|
+
fi
|
|
70
|
+
install -m 0600 "$tmp" "$BOOTSTRAP_FILE"
|
|
71
|
+
rm -f "$tmp"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
bootstrap_has_core_credentials() {
|
|
75
|
+
local password agent_token access_token file=$1
|
|
76
|
+
password=$(json_string password "$file")
|
|
77
|
+
agent_token=$(json_string agent_token "$file")
|
|
78
|
+
access_token=$(json_string access_token "$file")
|
|
79
|
+
[ -n "$password" ] && [ -n "$agent_token" ] && [ -n "$access_token" ]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
bootstrap_has_real_agent_room() {
|
|
83
|
+
local room file=$1
|
|
84
|
+
room=$(json_string agent_room_id "$file")
|
|
85
|
+
case "$room" in
|
|
86
|
+
!agent:*|"") return 1 ;;
|
|
87
|
+
!*) return 0 ;;
|
|
88
|
+
*) return 1 ;;
|
|
89
|
+
esac
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
bootstrap_portal() {
|
|
93
|
+
local password tmp
|
|
94
|
+
password=${P2P_PORTAL_PASSWORD:-}
|
|
95
|
+
[ -n "$password" ] || password=$(env_string P2P_PORTAL_PASSWORD)
|
|
96
|
+
if [ -z "$password" ]; then
|
|
97
|
+
log "FATAL: P2P_PORTAL_PASSWORD is missing from environment and ${P2P_DIR}/.env"
|
|
98
|
+
return 1
|
|
99
|
+
fi
|
|
100
|
+
tmp=$(mktemp)
|
|
101
|
+
if container_post_json "/_p2p/command" "{\"action\":\"portal.bootstrap\",\"params\":{\"password\":\"${password}\"}}" > "$tmp" 2>/dev/null; then
|
|
102
|
+
log "portal.bootstrap accepted."
|
|
103
|
+
rm -f "$tmp"
|
|
104
|
+
return 0
|
|
105
|
+
fi
|
|
106
|
+
log "portal.bootstrap failed: $(head -c 160 "$tmp" 2>/dev/null)"
|
|
107
|
+
rm -f "$tmp"
|
|
108
|
+
return 1
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
wait_for_core_bootstrap_file() {
|
|
112
|
+
local password agent_token access_token
|
|
113
|
+
log "waiting for ${CONTAINER_BOOTSTRAP_FILE} ..."
|
|
114
|
+
for i in $(seq 1 90); do
|
|
115
|
+
if copy_bootstrap_file; then
|
|
116
|
+
if bootstrap_has_core_credentials "$BOOTSTRAP_FILE"; then
|
|
117
|
+
log "credentials file is ready."
|
|
118
|
+
return 0
|
|
119
|
+
fi
|
|
120
|
+
log "credentials file exists but is missing password/access/agent token"
|
|
121
|
+
fi
|
|
122
|
+
sleep 5
|
|
123
|
+
done
|
|
124
|
+
log "FATAL: ${CONTAINER_BOOTSTRAP_FILE} was not written with complete credentials in time."
|
|
125
|
+
return 1
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
copy_host_bootstrap_to_container() {
|
|
129
|
+
$COMPOSE exec -T message-server sh -c "mkdir -p '$(dirname "$CONTAINER_BOOTSTRAP_FILE")' && cat > '$CONTAINER_BOOTSTRAP_FILE' && chmod 600 '$CONTAINER_BOOTSTRAP_FILE'" < "$BOOTSTRAP_FILE"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
write_agent_room_to_bootstrap() {
|
|
133
|
+
local room_id=$1
|
|
134
|
+
python3 - "$BOOTSTRAP_FILE" "$room_id" "$DOMAIN" <<'PY'
|
|
135
|
+
import json
|
|
136
|
+
import sys
|
|
137
|
+
|
|
138
|
+
path, room_id, domain = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
139
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
140
|
+
data = json.load(fh)
|
|
141
|
+
data["agent_room_id"] = room_id
|
|
142
|
+
data.setdefault("domain", domain)
|
|
143
|
+
with open(path, "w", encoding="utf-8") as fh:
|
|
144
|
+
json.dump(data, fh, separators=(",", ":"))
|
|
145
|
+
fh.write("\n")
|
|
146
|
+
PY
|
|
147
|
+
chmod 0600 "$BOOTSTRAP_FILE"
|
|
148
|
+
copy_host_bootstrap_to_container
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
ensure_agent_room() {
|
|
152
|
+
local owner_token agent_user session room_resp join_resp agent_token room_id room_path
|
|
153
|
+
if copy_bootstrap_file && bootstrap_has_real_agent_room "$BOOTSTRAP_FILE"; then
|
|
154
|
+
log "agent_room_id is already present."
|
|
155
|
+
return 0
|
|
156
|
+
fi
|
|
157
|
+
|
|
158
|
+
owner_token=$(json_string access_token "$BOOTSTRAP_FILE")
|
|
159
|
+
if [ -z "$owner_token" ]; then
|
|
160
|
+
log "FATAL: access_token is missing; cannot create agent room"
|
|
161
|
+
return 1
|
|
162
|
+
fi
|
|
163
|
+
agent_user="@agent:${DOMAIN}"
|
|
164
|
+
session=$(mktemp)
|
|
165
|
+
if ! container_post_json "/_p2p/command" '{"action":"agent.matrix_session.create","params":{"device_id":"DIREXIO_DEPLOY_BOOTSTRAP"}}' "$owner_token" > "$session" 2>/dev/null; then
|
|
166
|
+
log "FATAL: agent.matrix_session.create failed: $(head -c 160 "$session" 2>/dev/null)"
|
|
167
|
+
rm -f "$session"
|
|
168
|
+
return 1
|
|
169
|
+
fi
|
|
170
|
+
agent_token=$(json_string access_token "$session")
|
|
171
|
+
if [ -z "$agent_token" ]; then
|
|
172
|
+
log "FATAL: agent.matrix_session.create did not return access_token: $(head -c 160 "$session" 2>/dev/null)"
|
|
173
|
+
rm -f "$session"
|
|
174
|
+
return 1
|
|
175
|
+
fi
|
|
176
|
+
rm -f "$session"
|
|
177
|
+
|
|
178
|
+
room_resp=$(mktemp)
|
|
179
|
+
if ! container_post_json "/_matrix/client/v3/createRoom" "{\"preset\":\"private_chat\",\"visibility\":\"private\",\"name\":\"Direxio Agent\",\"invite\":[\"${agent_user}\"],\"is_direct\":false}" "$owner_token" > "$room_resp" 2>/dev/null; then
|
|
180
|
+
log "FATAL: Matrix createRoom failed: $(head -c 160 "$room_resp" 2>/dev/null)"
|
|
181
|
+
rm -f "$room_resp"
|
|
182
|
+
return 1
|
|
183
|
+
fi
|
|
184
|
+
room_id=$(json_string room_id "$room_resp")
|
|
185
|
+
rm -f "$room_resp"
|
|
186
|
+
if [ -z "$room_id" ]; then
|
|
187
|
+
log "FATAL: Matrix createRoom did not return room_id"
|
|
188
|
+
return 1
|
|
189
|
+
fi
|
|
190
|
+
|
|
191
|
+
room_path=$(matrix_room_path "$room_id")
|
|
192
|
+
join_resp=$(mktemp)
|
|
193
|
+
if ! container_post_json "/_matrix/client/v3/rooms/${room_path}/join" '{}' "$agent_token" > "$join_resp" 2>/dev/null; then
|
|
194
|
+
log "FATAL: agent join failed for ${room_id}: $(head -c 160 "$join_resp" 2>/dev/null)"
|
|
195
|
+
rm -f "$join_resp"
|
|
196
|
+
return 1
|
|
197
|
+
fi
|
|
198
|
+
rm -f "$join_resp"
|
|
199
|
+
|
|
200
|
+
write_agent_room_to_bootstrap "$room_id"
|
|
201
|
+
log "created and persisted agent_room_id=${room_id}"
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
wait_for_complete_bootstrap_file() {
|
|
205
|
+
log "waiting for complete bootstrap credentials with agent_room_id ..."
|
|
206
|
+
for i in $(seq 1 30); do
|
|
207
|
+
if copy_bootstrap_file && bootstrap_has_core_credentials "$BOOTSTRAP_FILE" && bootstrap_has_real_agent_room "$BOOTSTRAP_FILE"; then
|
|
208
|
+
log "complete credentials file is ready."
|
|
209
|
+
return 0
|
|
210
|
+
fi
|
|
211
|
+
sleep 2
|
|
212
|
+
done
|
|
213
|
+
log "FATAL: bootstrap credentials never contained a real agent_room_id."
|
|
214
|
+
return 1
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
write_owner_json() {
|
|
218
|
+
local user_id homeserver
|
|
219
|
+
mkdir -p "$WELLKNOWN_DIR"
|
|
220
|
+
user_id=$(json_string user_id "$BOOTSTRAP_FILE")
|
|
221
|
+
[ -n "$user_id" ] || user_id=$(json_string owner_user_id "$BOOTSTRAP_FILE")
|
|
222
|
+
[ -n "$user_id" ] || user_id="@owner:${DOMAIN}"
|
|
223
|
+
homeserver=$(json_string homeserver "$BOOTSTRAP_FILE")
|
|
224
|
+
[ -n "$homeserver" ] || homeserver="https://${DOMAIN}"
|
|
225
|
+
cat > "${WELLKNOWN_DIR}/owner.json" <<EOF
|
|
226
|
+
{"user_id":"${user_id}","owner_user_id":"${user_id}","display_name":"owner","domain":"${DOMAIN}","homeserver":"${homeserver}"}
|
|
227
|
+
EOF
|
|
228
|
+
chmod 0644 "${WELLKNOWN_DIR}/owner.json"
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
mkdir -p "$(dirname "$BOOTSTRAP_FILE")" "$WELLKNOWN_DIR"
|
|
232
|
+
wait_for_message_server
|
|
233
|
+
bootstrap_portal
|
|
234
|
+
wait_for_core_bootstrap_file
|
|
235
|
+
ensure_agent_room
|
|
236
|
+
wait_for_complete_bootstrap_file
|
|
237
|
+
write_owner_json
|
|
238
|
+
echo "$BOOTSTRAP_FILE"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#cloud-config
|
|
2
|
+
# user-data.yaml - runs once on first EC2 boot as cloud-init user-data.
|
|
3
|
+
#
|
|
4
|
+
# Flow: install Docker -> write deployment files -> generate local secrets ->
|
|
5
|
+
# docker compose up -> wait for message-server bootstrap credentials.
|
|
6
|
+
|
|
7
|
+
package_update: true
|
|
8
|
+
package_upgrade: false
|
|
9
|
+
|
|
10
|
+
write_files:
|
|
11
|
+
- path: /opt/p2p/.env
|
|
12
|
+
permissions: '0600'
|
|
13
|
+
content: |
|
|
14
|
+
DOMAIN=__DOMAIN__
|
|
15
|
+
ACME_EMAIL=__ACME_EMAIL__
|
|
16
|
+
MESSAGE_SERVER_IMAGE=__MESSAGE_SERVER_IMAGE__
|
|
17
|
+
|
|
18
|
+
runcmd:
|
|
19
|
+
- |
|
|
20
|
+
TOK=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
|
|
21
|
+
-H "X-aws-ec2-metadata-token-ttl-seconds: 300")
|
|
22
|
+
IP=$(curl -s -H "X-aws-ec2-metadata-token: $TOK" \
|
|
23
|
+
http://169.254.169.254/latest/meta-data/public-ipv4)
|
|
24
|
+
[ -n "$IP" ] || IP=$(curl -s https://api.ipify.org || curl -s https://ifconfig.me)
|
|
25
|
+
grep -q '^PUBLIC_IP=' /opt/p2p/.env || echo "PUBLIC_IP=$IP" >> /opt/p2p/.env
|
|
26
|
+
grep -q '^TURN_SECRET=' /opt/p2p/.env || \
|
|
27
|
+
echo "TURN_SECRET=$(head -c 32 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' | head -c 40)" >> /opt/p2p/.env
|
|
28
|
+
grep -q '^P2P_PORTAL_PASSWORD=' /opt/p2p/.env || \
|
|
29
|
+
echo "P2P_PORTAL_PASSWORD=$(od -An -N4 -tu4 /dev/urandom | awk '{printf "%08d", $1 % 100000000}')" >> /opt/p2p/.env
|
|
30
|
+
- export DOMAIN=$(grep '^DOMAIN=' /opt/p2p/.env | cut -d= -f2)
|
|
31
|
+
- curl -fsSL https://get.docker.com | sh
|
|
32
|
+
- systemctl enable --now docker
|
|
33
|
+
- mkdir -p /opt/p2p/wellknown
|
|
34
|
+
- chmod 700 /opt/p2p
|
|
35
|
+
- |
|
|
36
|
+
set -e
|
|
37
|
+
cd /opt/p2p
|
|
38
|
+
docker compose --env-file .env up -d
|
|
39
|
+
DOMAIN=$(grep '^DOMAIN=' .env | cut -d= -f2) bash init-tokens.sh
|
|
40
|
+
touch /opt/p2p/.deploy-done
|