vibed-infra 0.6.0 → 0.7.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/README.md CHANGED
@@ -8,30 +8,33 @@ Published on npm as **`vibed-infra`**.
8
8
 
9
9
  | File | Purpose |
10
10
  |------|---------|
11
- | `vibed-infra-config.yml` | Name, templates, `network.edge` (default `vps-edge`), `gateway.sites[]`, auto-update |
11
+ | `vibed-infra-config.yml` | Name, templates, `network.edge`, `gateway.publicIp` / `tlsEmail` / `sites[]`, auto-update |
12
12
  | `api-config.yaml` / `ui-config.yaml` / `nodes-config.yaml` | Images + opaque config |
13
13
 
14
14
  ```bash
15
15
  ./package.sh
16
16
  git add dist && git commit && git push
17
- # Copy dist/DNS-SKILL.md into AU agent browser extension; give it the VPS IP
17
+ # Copy dist/DNS-SKILL.md into AU agent (domains + publicIp already filled when configured)
18
18
  ```
19
19
 
20
20
  ## Operator flow (VPS)
21
21
 
22
22
  ```bash
23
+ # 1) DNS — paste dist/DNS-SKILL.md into AU browser agent
24
+ # 2) App roles
23
25
  wget -qO- .../dist/install-api.sh | bash
24
26
  wget -qO- .../dist/install-ui.sh | bash
25
27
  wget -qO- .../dist/install-nodes.sh | bash
26
- wget -qO- .../dist/install-gateway.sh | bash # bootstraps ~/services/gateway once + apps/{name}/
28
+ # 3) Host gateway + TLS (setup-tls.sh; re-run with --force if host/IP changes)
29
+ wget -qO- .../dist/install-gateway.sh | bash
27
30
  ```
28
31
 
29
32
  | Profile | Role |
30
33
  |---------|------|
31
34
  | `api` / `ui` / `nodes` | Join shared `vps-edge` |
32
- | `gateway` | Host nginx + this app’s `sites.conf` under `apps/` |
35
+ | `gateway` | Host nginx + `apps/{name}/sites.conf` + HTTPS via `~/services/gateway/setup-tls.sh` |
33
36
 
34
- Multi-app: further products’ `install-gateway.sh` only add `apps/{other}/sites.conf` and reload — no second 80/443 bind.
37
+ Multi-app: further products’ `install-gateway.sh` only add `apps/{other}/sites.conf`, refresh TLS SANs, and reload — no second 80/443 bind.
35
38
 
36
39
  ## Machine services (installed once)
37
40
 
@@ -41,7 +44,7 @@ Multi-app: further products’ `install-gateway.sh` only add `apps/{other}/sites
41
44
  | `~/services/vibed-infra/update-agent` | Serial pull queue + optional GHCR webhook |
42
45
  | `~/services/vibed-infra/persist-logs` | Per-app WALs + optional R2/S3 ship |
43
46
 
44
- Auto-update cron **enqueues** work; the agent processes one job at a time. Updates prune dangling images (`DOCKER_AUTO_PRUNE=1`).
47
+ Auto-update cron **enqueues** work; the agent processes one job at a time. After a GHCR push, the reusable image workflow mints a GitHub Actions OIDC JWT and notifies `https://{domain}/_vibed/hooks/ghcr` (fallback `http://{publicIp}/…`) so the pull does not wait for cron. Updates prune dangling images (`DOCKER_AUTO_PRUNE=1`).
45
48
 
46
49
  ## Layout
47
50
 
@@ -60,6 +63,8 @@ Auto-update cron **enqueues** work; the agent processes one job at a time. Updat
60
63
  |----------|---------|
61
64
  | `GATEWAY_HOME` | Host gateway dir (default `~/services/gateway`) |
62
65
  | `VIBED_HOME` | Machine vibed root (default `~/services/vibed-infra`) |
66
+ | `GATEWAY_PUBLIC_IP` | VPS IPv4 (from `gateway.publicIp`) |
67
+ | `TLS_EMAIL` / `TLS_MODE` | Let’s Encrypt email; `lab` or `letsencrypt` |
63
68
  | `PERSIST_LOG_DIR` | Per-service event log dir |
64
69
  | `DOCKER_AUTO_PRUNE` | Prune dangling images after update (default on) |
65
70
 
@@ -68,6 +73,7 @@ Auto-update cron **enqueues** work; the agent processes one job at a time. Updat
68
73
  ```bash
69
74
  npm test
70
75
  npm run test:dist
76
+ npm run test:e2e-multi # two apps, localhost wget|bash, shared host gateway
71
77
  ```
72
78
 
73
79
  ## Publishing
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env python3
2
+ """Notify a vibed-infra VPS to pull a GHCR image.
3
+
4
+ Prefers GitHub Actions OIDC (no shared secret). Falls back to compiled
5
+ config token / VIBED_WEBHOOK_SECRET for local curl.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import json
11
+ import os
12
+ import sys
13
+ import urllib.error
14
+ import urllib.parse
15
+ import urllib.request
16
+ from pathlib import Path
17
+
18
+ # Keep in sync with lib/webhook.py
19
+ HOOK_PATH = "/_vibed/hooks/ghcr"
20
+ TOKEN_VERSION = "vibed-webhook|v1|"
21
+
22
+
23
+ def _derive_token(name: str, public_ip: str, host: str) -> str:
24
+ import hashlib
25
+
26
+ raw = f"{TOKEN_VERSION}{name}|{public_ip}|{host}"
27
+ return hashlib.sha256(raw.encode()).hexdigest()[:32]
28
+
29
+
30
+ def _strip(s: str) -> str:
31
+ s = s.strip()
32
+ if (s.startswith('"') and s.endswith('"')) or (s.startswith("'") and s.endswith("'")):
33
+ return s[1:-1]
34
+ return s
35
+
36
+
37
+ def _load_webhook(path: Path) -> dict[str, str]:
38
+ text = path.read_text(encoding="utf-8")
39
+ url = fallback = token = name = public_ip = host = ""
40
+ in_webhook = False
41
+ in_sites = False
42
+ for raw in text.splitlines():
43
+ line = raw.rstrip()
44
+ stripped = line.lstrip()
45
+ if stripped.startswith("webhook:"):
46
+ in_webhook = True
47
+ in_sites = False
48
+ continue
49
+ if in_webhook:
50
+ if stripped and not line.startswith(" ") and not line.startswith("\t"):
51
+ in_webhook = False
52
+ elif stripped.startswith("url:"):
53
+ url = _strip(stripped.split(":", 1)[1])
54
+ elif stripped.startswith("fallbackUrl:"):
55
+ fallback = _strip(stripped.split(":", 1)[1])
56
+ elif stripped.startswith("token:"):
57
+ token = _strip(stripped.split(":", 1)[1])
58
+ if stripped.startswith("name:") and not name:
59
+ name = _strip(stripped.split(":", 1)[1])
60
+ if stripped.startswith("publicIp:"):
61
+ public_ip = _strip(stripped.split(":", 1)[1])
62
+ if stripped.startswith("sites:"):
63
+ in_sites = True
64
+ continue
65
+ if in_sites and stripped.startswith("- host:"):
66
+ host = _strip(stripped.split(":", 1)[1])
67
+ in_sites = False
68
+ elif in_sites and stripped.startswith("host:"):
69
+ host = _strip(stripped.split(":", 1)[1])
70
+ in_sites = False
71
+ if not token and name and (public_ip or host):
72
+ token = _derive_token(name, public_ip, host)
73
+ if not url and host:
74
+ url = f"https://{host}{HOOK_PATH}"
75
+ if not fallback and public_ip:
76
+ fallback = f"http://{public_ip}{HOOK_PATH}"
77
+ if not url:
78
+ url = fallback
79
+ return {"url": url, "fallbackUrl": fallback, "token": token}
80
+
81
+
82
+ def _package_from_image(image: str) -> str:
83
+ img = image.split("@", 1)[0]
84
+ if "/" in img:
85
+ return img.rsplit("/", 1)[-1].split(":")[0]
86
+ return img.split(":")[0]
87
+
88
+
89
+ def fetch_oidc_token(audience: str) -> str:
90
+ url = os.environ.get("ACTIONS_ID_TOKEN_REQUEST_URL") or ""
91
+ tok = os.environ.get("ACTIONS_ID_TOKEN_REQUEST_TOKEN") or ""
92
+ if not url or not tok:
93
+ return ""
94
+ sep = "&" if "?" in url else "?"
95
+ req = urllib.request.Request(
96
+ f"{url}{sep}audience={urllib.parse.quote(audience, safe='')}",
97
+ headers={"Authorization": f"Bearer {tok}", "Accept": "application/json"},
98
+ )
99
+ try:
100
+ with urllib.request.urlopen(req, timeout=20) as resp:
101
+ data = json.loads(resp.read().decode())
102
+ return str(data.get("value") or "")
103
+ except Exception as e:
104
+ print(f"notify-vps: OIDC mint failed: {e}", file=sys.stderr)
105
+ return ""
106
+
107
+
108
+ def _post(url: str, body: dict, *, bearer: str = "", secret: str = "") -> tuple[int, str]:
109
+ headers = {"Content-Type": "application/json"}
110
+ if bearer:
111
+ headers["Authorization"] = f"Bearer {bearer}"
112
+ if secret:
113
+ headers["X-Vibed-Secret"] = secret
114
+ req = urllib.request.Request(
115
+ url,
116
+ data=json.dumps(body).encode(),
117
+ method="POST",
118
+ headers=headers,
119
+ )
120
+ try:
121
+ with urllib.request.urlopen(req, timeout=15) as resp:
122
+ return resp.status, resp.read().decode()
123
+ except urllib.error.HTTPError as e:
124
+ return e.code, e.read().decode(errors="replace")
125
+ except Exception as e:
126
+ return 0, str(e)
127
+
128
+
129
+ def main() -> int:
130
+ ap = argparse.ArgumentParser()
131
+ ap.add_argument("--image", required=True)
132
+ ap.add_argument("--tag", default="main")
133
+ ap.add_argument("--config", default="")
134
+ ap.add_argument("--url", action="append", default=[], help="Override webhook URL (repeatable)")
135
+ ap.add_argument("--strict", action="store_true", help="Exit 1 if notify fails")
136
+ args = ap.parse_args()
137
+
138
+ spec = {"url": "", "fallbackUrl": "", "token": ""}
139
+ if not args.url:
140
+ roots = []
141
+ if args.config:
142
+ roots.append(Path(args.config))
143
+ cwd = Path.cwd()
144
+ roots.extend(
145
+ [
146
+ cwd / "dist" / "packageconfig.yaml",
147
+ cwd / "packageconfig.yaml",
148
+ cwd / "templates" / "vibed-infra-config.yml",
149
+ ]
150
+ )
151
+ conf_path = next((p for p in roots if p.is_file()), None)
152
+ if conf_path:
153
+ spec = _load_webhook(conf_path)
154
+ elif not args.url:
155
+ print("notify-vps: no packageconfig (set gateway.publicIp + sites[].host)", file=sys.stderr)
156
+ return 1 if args.strict else 0
157
+
158
+ token = os.environ.get("VIBED_WEBHOOK_SECRET") or spec.get("token") or ""
159
+ urls = list(args.url) if args.url else [u for u in (spec.get("url"), spec.get("fallbackUrl")) if u]
160
+ seen: set[str] = set()
161
+ urls = [u for u in urls if not (u in seen or seen.add(u))]
162
+ has_oidc = bool(os.environ.get("ACTIONS_ID_TOKEN_REQUEST_URL"))
163
+ if not urls:
164
+ print("notify-vps: skipped (no webhook url)", file=sys.stderr)
165
+ return 1 if args.strict else 0
166
+ if not has_oidc and not token:
167
+ print("notify-vps: skipped (no OIDC env and no token)", file=sys.stderr)
168
+ return 1 if args.strict else 0
169
+
170
+ pkg = _package_from_image(args.image)
171
+ payload = {"package": pkg, "tag": args.tag, "image": args.image}
172
+ last_err = ""
173
+ for url in urls:
174
+ bearer = fetch_oidc_token(url) if has_oidc else ""
175
+ secret = "" if bearer else token
176
+ if not bearer and not secret:
177
+ last_err = f"{url}: no OIDC token"
178
+ print(f"notify-vps: {last_err}", file=sys.stderr)
179
+ continue
180
+ try:
181
+ code, text = _post(url, payload, bearer=bearer, secret=secret)
182
+ except Exception as e:
183
+ code, text = 0, str(e)
184
+ if code == 200:
185
+ print(f"notify-vps: {url} → {text.strip()}")
186
+ return 0
187
+ last_err = f"{url} HTTP {code}: {text[:200]}"
188
+ print(f"notify-vps: {last_err}", file=sys.stderr)
189
+ print(f"notify-vps: failed ({last_err})", file=sys.stderr)
190
+ return 1 if args.strict else 0
191
+
192
+
193
+ if __name__ == "__main__":
194
+ raise SystemExit(main())
@@ -4,12 +4,19 @@
4
4
  #
5
5
  # jobs:
6
6
  # build-api:
7
- # uses: ./infra/github/workflows/docker-build-reusable.yml
7
+ # uses: naiemk/vibed-infra/.github/workflows/docker-build-reusable.yml@main
8
8
  # with:
9
9
  # dockerfile: deploy/Dockerfile.api
10
10
  # image: ghcr.io/${{ github.repository_owner }}/my-api
11
11
  # context: .
12
+ # permissions:
13
+ # contents: read
14
+ # packages: write
15
+ # id-token: write
12
16
  # secrets: inherit
17
+ #
18
+ # After push to the default branch, notifies the VPS using GitHub Actions OIDC
19
+ # (audience = webhook URL). No GitHub Packages webhook and no extra secrets.
13
20
 
14
21
  name: Reusable docker build (single image)
15
22
 
@@ -26,14 +33,26 @@ on:
26
33
  required: false
27
34
  type: string
28
35
  default: .
36
+ notify:
37
+ required: false
38
+ type: boolean
39
+ default: true
40
+ secrets:
41
+ VIBED_WEBHOOK_SECRET:
42
+ required: false
29
43
 
30
44
  permissions:
31
45
  contents: read
32
46
  packages: write
47
+ id-token: write
33
48
 
34
49
  jobs:
35
50
  build-push:
36
51
  runs-on: ubuntu-latest
52
+ permissions:
53
+ contents: read
54
+ packages: write
55
+ id-token: write
37
56
  steps:
38
57
  - uses: actions/checkout@v4
39
58
  - uses: docker/setup-buildx-action@v3
@@ -59,3 +78,15 @@ jobs:
59
78
  labels: |
60
79
  org.opencontainers.image.revision=${{ github.sha }}
61
80
  org.opencontainers.image.source=https://github.com/${{ github.repository }}
81
+ - name: Notify VPS to pull
82
+ if: inputs.notify && github.event.repository.default_branch == github.ref_name
83
+ continue-on-error: true
84
+ env:
85
+ VIBED_WEBHOOK_SECRET: ${{ secrets.VIBED_WEBHOOK_SECRET }}
86
+ run: |
87
+ script=dist/notify-vps-pull.py
88
+ if [[ ! -f "$script" ]]; then
89
+ echo "notify-vps: dist/notify-vps-pull.py missing — re-package and commit dist/"
90
+ exit 0
91
+ fi
92
+ python3 "$script" --image "${{ inputs.image }}" --tag main
package/install.sh CHANGED
@@ -57,7 +57,7 @@ LOCAL_PACKAGER=0
57
57
  if [[ "$PACKAGER_RAW" =~ ^/ ]] && [[ -d "$PACKAGER_RAW/lib" ]]; then
58
58
  LOCAL_PACKAGER=1
59
59
  fi
60
- for lib in fetch.sh env.sh prompt.sh tls.sh load_config.py generate.py host_gateway.sh; do
60
+ for lib in fetch.sh env.sh prompt.sh tls.sh load_config.py generate.py host_gateway.sh update_queue.sh webhook.py github_oidc.py; do
61
61
  if [[ "$LOCAL_PACKAGER" == "1" ]]; then
62
62
  cp -f "${PACKAGER_RAW}/lib/${lib}" "${INFRA_LIB}/${lib}"
63
63
  else
@@ -319,8 +319,83 @@ EOF
319
319
  chmod +x "$DEST/${START_SCRIPT}" 2>/dev/null || true
320
320
 
321
321
  # Ensure update-agent + persist-logs on first install (idempotent)
322
- if [[ -f "${PACKAGER_RAW}/templates/update-agent/install-agent.sh" ]]; then
323
- PACKAGER_RAW="$PACKAGER_RAW" VIBED_HOME="${VIBED_HOME:-}" bash "${PACKAGER_RAW}/templates/update-agent/install-agent.sh" || true
322
+ _install_update_agent() {
323
+ local agent_packager="$PACKAGER_RAW"
324
+ if [[ "$LOCAL_PACKAGER" != "1" ]]; then
325
+ agent_packager="${DEST}/.infra-packager"
326
+ mkdir -p "$agent_packager/lib" "$agent_packager/templates/update-agent"
327
+ cp -f "${INFRA_LIB}/update_queue.sh" "$agent_packager/lib/update_queue.sh" 2>/dev/null || true
328
+ cp -f "${INFRA_LIB}/webhook.py" "$agent_packager/lib/webhook.py" 2>/dev/null || true
329
+ cp -f "${INFRA_LIB}/github_oidc.py" "$agent_packager/lib/github_oidc.py" 2>/dev/null || true
330
+ for f in install-agent.sh agent.sh enqueue.sh webhook_server.py; do
331
+ _fetch "${PACKAGER_RAW}/templates/update-agent/${f}" "$agent_packager/templates/update-agent/${f}" || true
332
+ done
333
+ fi
334
+ if [[ -f "${agent_packager}/templates/update-agent/install-agent.sh" ]]; then
335
+ chmod +x "$agent_packager/templates/update-agent/"*.sh 2>/dev/null || true
336
+ PACKAGER_RAW="$agent_packager" VIBED_HOME="${VIBED_HOME:-}" bash "${agent_packager}/templates/update-agent/install-agent.sh" || true
337
+ fi
338
+ }
339
+ _install_update_agent
340
+
341
+ # Register this install so GHCR notify can enqueue without waiting for cron
342
+ PRODUCT_NAME="${PRODUCT_NAME:-$(
343
+ python3 -c "
344
+ import sys
345
+ sys.path.insert(0, '${INFRA_LIB}')
346
+ from load_config import load_packageconfig
347
+ from pathlib import Path
348
+ print(load_packageconfig(Path('${PC_LOCAL}')).get('name') or 'app')
349
+ "
350
+ )}"
351
+ _REG_ROLE="$ROLE"
352
+ case "$ROLE" in
353
+ backend) _REG_ROLE=api ;;
354
+ workers) _REG_ROLE=nodes ;;
355
+ esac
356
+ # shellcheck source=/dev/null
357
+ if [[ -f "${INFRA_LIB}/update_queue.sh" ]]; then
358
+ source "${INFRA_LIB}/update_queue.sh"
359
+ elif [[ -f "${PACKAGER_RAW}/lib/update_queue.sh" ]]; then
360
+ source "${PACKAGER_RAW}/lib/update_queue.sh"
361
+ fi
362
+ if declare -F vibed_register_app >/dev/null 2>&1; then
363
+ _env_val() {
364
+ local key="$1"
365
+ [[ -f "$DEST/.env" ]] || return 0
366
+ grep -E "^${key}=" "$DEST/.env" 2>/dev/null | head -1 | cut -d= -f2-
367
+ }
368
+ _REG_IMAGE=""
369
+ case "$_REG_ROLE" in
370
+ api) _REG_IMAGE="$(_env_val BACKEND_IMAGE)" ;;
371
+ ui) _REG_IMAGE="$(_env_val UI_IMAGE)" ;;
372
+ nodes) _REG_IMAGE="$(_env_val WORKER_IMAGE)" ;;
373
+ gateway) _REG_IMAGE="$(_env_val NGINX_IMAGE)" ;;
374
+ esac
375
+ vibed_register_app "$PRODUCT_NAME" "$_REG_ROLE" "$DEST" "$_REG_IMAGE" || true
376
+ _HOOK_TOKEN="$(
377
+ python3 -c "
378
+ import sys
379
+ sys.path.insert(0, '${INFRA_LIB}')
380
+ from pathlib import Path
381
+ from load_config import load_packageconfig
382
+ try:
383
+ from webhook import webhook_from_packageconfig
384
+ except ImportError:
385
+ webhook_from_packageconfig = None
386
+ c = load_packageconfig(Path('${PC_LOCAL}'))
387
+ if webhook_from_packageconfig:
388
+ print(webhook_from_packageconfig(c).get('token') or '')
389
+ else:
390
+ print((c.get('webhook') or {}).get('token') or '')
391
+ "
392
+ )"
393
+ if [[ -n "${_HOOK_TOKEN}" ]]; then
394
+ AGENT_HOME="$(vibed_agent_home 2>/dev/null || echo "${VIBED_HOME:-$HOME/services/vibed-infra}/update-agent")"
395
+ mkdir -p "${AGENT_HOME}/tokens"
396
+ printf '%s\n' "$_HOOK_TOKEN" >"${AGENT_HOME}/tokens/${PRODUCT_NAME}"
397
+ echo "webhook token registered for ${PRODUCT_NAME}"
398
+ fi
324
399
  fi
325
400
  if [[ -f "${PACKAGER_RAW}/templates/persist-logs/install-persist-logs.sh" ]]; then
326
401
  PACKAGER_RAW="$PACKAGER_RAW" VIBED_HOME="${VIBED_HOME:-}" bash "${PACKAGER_RAW}/templates/persist-logs/install-persist-logs.sh" || true
@@ -333,67 +408,90 @@ if [[ "$ROLE" == "gateway" ]]; then
333
408
  source "$DEST/lib-env.sh"
334
409
  load_dotenv "$DEST/.env"
335
410
  HG="$(vibed_gateway_home)"
336
- # Prefer host certs
337
- if [[ -f "$HG/.env" ]]; then
338
- load_dotenv "$HG/.env"
339
- fi
340
- CERT_DIR="$(python3 -c "
411
+ mkdir -p "$HG/certs" "$HG/certbot-www"
412
+
413
+ # Propagate public IP / TLS email from packageconfig into host + product .env
414
+ GW_PUBLIC_IP="$(
415
+ python3 -c "
341
416
  import sys
342
417
  sys.path.insert(0, '${INFRA_LIB}')
343
418
  from load_config import load_packageconfig, get_profile
344
419
  from pathlib import Path
345
- p = get_profile(load_packageconfig(Path('${PC_LOCAL}')), '${PROFILE}')
346
- sites = p.get('sites') or []
347
- if not sites:
348
- print('${HG}/certs')
349
- else:
350
- s = sites[0]
351
- print(s.get('tlsCertDir') or ('${HG}/certs'))
352
- ")"
353
- TLS_FULLCHAIN="${TLS_FULLCHAIN:-${CERT_DIR}/fullchain.pem}"
354
- TLS_PRIVKEY="${TLS_PRIVKEY:-${CERT_DIR}/privkey.pem}"
355
- # Prefer writable host certs when packaged tlsCertDir is not writable (e.g. /etc/letsencrypt)
356
- if [[ ! -f "$TLS_FULLCHAIN" ]]; then
357
- HOST_CERT_DIR="${HG}/certs"
358
- mkdir -p "$HOST_CERT_DIR" 2>/dev/null || true
359
- if [[ -x "${PRODUCT_RAW}/gen-dev-certs.sh" ]]; then
360
- APP_HOST="$(python3 -c "
420
+ c = load_packageconfig(Path('${PC_LOCAL}'))
421
+ p = get_profile(c, '${PROFILE}')
422
+ print(p.get('publicIp') or '')
423
+ "
424
+ )"
425
+ GW_TLS_EMAIL="$(
426
+ python3 -c "
361
427
  import sys
362
428
  sys.path.insert(0, '${INFRA_LIB}')
363
429
  from load_config import load_packageconfig, get_profile
364
430
  from pathlib import Path
365
- p = get_profile(load_packageconfig(Path('${PC_LOCAL}')), '${PROFILE}')
366
- sites = p.get('sites') or []
367
- print(sites[0]['host'] if sites else 'localhost')
368
- ")"
369
- if APP_HOST="$APP_HOST" bash "${PRODUCT_RAW}/gen-dev-certs.sh" "$HOST_CERT_DIR" 2>/dev/null; then
370
- TLS_FULLCHAIN="${HOST_CERT_DIR}/fullchain.pem"
371
- TLS_PRIVKEY="${HOST_CERT_DIR}/privkey.pem"
372
- fi
431
+ c = load_packageconfig(Path('${PC_LOCAL}'))
432
+ p = get_profile(c, '${PROFILE}')
433
+ print(p.get('tlsEmail') or '')
434
+ "
435
+ )"
436
+ _patch_env() {
437
+ local file="$1" key="$2" val="$3"
438
+ [[ -n "$val" ]] || return 0
439
+ [[ -f "$file" ]] || return 0
440
+ if grep -q "^${key}=" "$file" 2>/dev/null; then
441
+ sed -i "s|^${key}=.*|${key}=${val}|" "$file"
442
+ else
443
+ echo "${key}=${val}" >>"$file"
373
444
  fi
374
- fi
445
+ }
375
446
  if [[ -f "$HG/.env" ]]; then
376
- sed -i "s|^TLS_FULLCHAIN=.*|TLS_FULLCHAIN=${TLS_FULLCHAIN}|" "$HG/.env" 2>/dev/null || true
377
- sed -i "s|^TLS_PRIVKEY=.*|TLS_PRIVKEY=${TLS_PRIVKEY}|" "$HG/.env" 2>/dev/null || true
447
+ _patch_env "$HG/.env" GATEWAY_PUBLIC_IP "${GATEWAY_PUBLIC_IP:-$GW_PUBLIC_IP}"
448
+ _patch_env "$HG/.env" TLS_EMAIL "${TLS_EMAIL:-$GW_TLS_EMAIL}"
449
+ # Product/install env can force lab (CI) without wiping host LE later
450
+ if [[ -n "${TLS_MODE:-}" ]]; then
451
+ _patch_env "$HG/.env" TLS_MODE "$TLS_MODE"
452
+ fi
378
453
  fi
379
- mapfile -t DOMAIN_ARR < <(python3 -c "
454
+ _patch_env "$DEST/.env" GATEWAY_PUBLIC_IP "${GATEWAY_PUBLIC_IP:-$GW_PUBLIC_IP}"
455
+ _patch_env "$DEST/.env" TLS_EMAIL "${TLS_EMAIL:-$GW_TLS_EMAIL}"
456
+
457
+ # Collect domains for TLS_DOMAINS fallback if apps conf not parseable yet
458
+ TLS_DOMAINS="$(
459
+ python3 -c "
380
460
  import sys
381
461
  sys.path.insert(0, '${INFRA_LIB}')
382
462
  from load_config import load_packageconfig, get_profile
383
463
  from pathlib import Path
384
464
  p = get_profile(load_packageconfig(Path('${PC_LOCAL}')), '${PROFILE}')
465
+ names=[]
385
466
  for s in p.get('sites') or []:
386
- print(s['host'])
387
- for a in s.get('aliases') or []:
388
- print(a)
389
- ")
390
- infra_tls_offer_interactive "$TLS_FULLCHAIN" "$TLS_PRIVKEY" "${DOMAIN_ARR[@]}" || true
467
+ names.append(s['host'])
468
+ names.extend(s.get('aliases') or [])
469
+ print(' '.join(names))
470
+ "
471
+ )"
472
+ export TLS_DOMAINS
473
+ export GATEWAY_PUBLIC_IP="${GATEWAY_PUBLIC_IP:-$GW_PUBLIC_IP}"
474
+ export TLS_EMAIL="${TLS_EMAIL:-$GW_TLS_EMAIL}"
475
+
476
+ if [[ -x "$HG/setup-tls.sh" ]]; then
477
+ echo "Running host gateway TLS setup ..."
478
+ (cd "$HG" && GATEWAY_HOME="$HG" TLS_DOMAINS="$TLS_DOMAINS" \
479
+ GATEWAY_PUBLIC_IP="${GATEWAY_PUBLIC_IP:-}" \
480
+ TLS_EMAIL="${TLS_EMAIL:-}" \
481
+ TLS_MODE="${TLS_MODE:-}" \
482
+ ./setup-tls.sh) || {
483
+ echo "TLS setup did not complete — fix DNS then: cd $HG && ./setup-tls.sh --force" >&2
484
+ }
485
+ else
486
+ echo "warning: $HG/setup-tls.sh missing — re-run gateway install to bootstrap" >&2
487
+ fi
391
488
  fi
392
489
 
393
490
  echo ""
394
491
  if [[ "$ROLE" == "gateway" ]]; then
395
492
  echo "Install complete. Host gateway: $(vibed_gateway_home)"
396
493
  echo "App sites: $(vibed_gateway_home)/apps/${PRODUCT_NAME:-app}/sites.conf"
494
+ echo "TLS: cd $(vibed_gateway_home) && ./setup-tls.sh # re-run if host/IP/domains change (--force)"
397
495
  echo "Start: cd $DEST && ./start-gateway.sh"
398
496
  else
399
497
  echo "Install complete. Start: cd $DEST && ./${START_SCRIPT}"
package/lib/generate.py CHANGED
@@ -59,6 +59,14 @@ server {{
59
59
  set ${be_var}_upstream {backend}:{backend_port};
60
60
  set ${ui_var}_upstream {ui}:{ui_port};
61
61
 
62
+ location /_vibed/hooks/ {{
63
+ proxy_pass http://host.docker.internal:19200;
64
+ proxy_set_header Host $host;
65
+ proxy_set_header X-Real-IP $remote_addr;
66
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
67
+ proxy_set_header X-Forwarded-Proto $scheme;
68
+ }}
69
+
62
70
  location = {health} {{
63
71
  {proxy_headers}
64
72
  }}