unoverse 0.1.69 → 0.1.71
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/bin/unoverse.mjs +1 -1
- package/lib/create/credential.mjs +90 -0
- package/lib/create/download.mjs +19 -0
- package/lib/create/index.mjs +141 -0
- package/lib/create/target.mjs +40 -0
- package/lib/ui.mjs +119 -0
- package/lib/urls.mjs +1 -7
- package/operator/infra/aws/main.tf +639 -0
- package/operator/infra/aws/outputs.tf +139 -0
- package/operator/infra/aws/pretoken/index.mjs +37 -0
- package/operator/infra/aws/prices.tf +53 -0
- package/operator/infra/aws/terraform.tfvars.example +55 -0
- package/operator/infra/aws/variables.tf +92 -0
- package/operator/infra/digitalocean/main.tf +297 -0
- package/operator/infra/digitalocean/outputs.tf +96 -0
- package/operator/infra/digitalocean/prices.tf +49 -0
- package/operator/infra/digitalocean/terraform.tfvars.example +52 -0
- package/operator/infra/digitalocean/variables.tf +125 -0
- package/operator/lib/deploy.sh +151 -160
- package/operator/lib/init.sh +2 -9
- package/operator/operator.sh +17 -0
- package/package.json +1 -1
- package/lib/create.mjs +0 -369
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Everything .env.production needs — the rendered file is COMPLETE, nothing
|
|
2
|
+
# is hand-edited afterwards:
|
|
3
|
+
# terraform output -raw env_production > ../../.env.production
|
|
4
|
+
# unoverse deploy
|
|
5
|
+
|
|
6
|
+
locals {
|
|
7
|
+
# Pooled URL for the services (transaction-mode PgBouncer; prepared statements off),
|
|
8
|
+
# direct URL for migrations. BYO: your URL is used verbatim for both — you own the
|
|
9
|
+
# pooling story against your own max_connections (INFRASTRUCTURE.md § BYO).
|
|
10
|
+
# Managed modes (provisioned OR adopted existing cluster): the universe's own
|
|
11
|
+
# user/db/pool were created on local.pg_cluster_id either way.
|
|
12
|
+
pg_pooled = local.pg_managed ? "postgresql://${digitalocean_database_user.universe[0].name}:${digitalocean_database_user.universe[0].password}@${digitalocean_database_connection_pool.universe[0].private_host}:${digitalocean_database_connection_pool.universe[0].port}/${digitalocean_database_connection_pool.universe[0].name}?sslmode=require&pgbouncer=true" : var.byo_postgres_url
|
|
13
|
+
pg_direct = local.pg_managed ? "postgresql://${digitalocean_database_user.universe[0].name}:${digitalocean_database_user.universe[0].password}@${local.pg_cluster_host}:${local.pg_cluster_port}/${digitalocean_database_db.universe[0].name}?sslmode=require" : var.byo_postgres_url
|
|
14
|
+
|
|
15
|
+
# Redis is always ours — provisioned above, no BYO branch.
|
|
16
|
+
redis_host = digitalocean_database_cluster.redis.private_host
|
|
17
|
+
redis_port = digitalocean_database_cluster.redis.port
|
|
18
|
+
redis_password = digitalocean_database_cluster.redis.password
|
|
19
|
+
redis_tls = true
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
output "deploy_host" {
|
|
23
|
+
description = "The droplet's public IP (.env.production DEPLOY_HOST). Point api.<domain> at the LB IP, not this."
|
|
24
|
+
value = digitalocean_droplet.app.ipv4_address
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
output "lb_ip" {
|
|
28
|
+
description = "The load balancer's IP — api.<domain>'s A record target (created automatically when manage_dns = true)."
|
|
29
|
+
value = digitalocean_loadbalancer.public.ip
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
output "canvas_url" {
|
|
33
|
+
description = "Public Canvas URL (canvas_public = true only) — add it to the IdP's allowed origins."
|
|
34
|
+
value = var.canvas_public ? (local.has_domain ? "https://canvas.${var.domain}:3001" : "http://${digitalocean_loadbalancer.public.ip}:3001") : "canvas is admin-only (direct http://<droplet-ip>:3001 from admin_cidr)"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
output "api_url" {
|
|
38
|
+
description = "The API base URL as deployed — https://api.<domain> with a domain, http://<lb-ip> without one."
|
|
39
|
+
value = local.has_domain ? "https://${local.api_host}" : "http://${digitalocean_loadbalancer.public.ip}"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
output "env_production" {
|
|
43
|
+
sensitive = true
|
|
44
|
+
description = "Rendered .env.production — complete, write to a file and deploy"
|
|
45
|
+
value = <<-ENV
|
|
46
|
+
# UNIVERSE (DigitalOcean) — generated by infra/digitalocean. COMPLETE:
|
|
47
|
+
# do not hand-edit; change terraform.tfvars and re-render instead.
|
|
48
|
+
|
|
49
|
+
# Deploy target
|
|
50
|
+
DEPLOY_HOST=${digitalocean_droplet.app.ipv4_address}
|
|
51
|
+
DEPLOY_USER=root
|
|
52
|
+
|
|
53
|
+
# Container registry
|
|
54
|
+
DOCR_TOKEN=${var.docr_token}
|
|
55
|
+
|
|
56
|
+
# Database (pooled via managed PgBouncer, transaction mode — the Postgres law).
|
|
57
|
+
# Migrations use the DIRECT url (db-setup prefers DATABASE_URL_DIRECT).
|
|
58
|
+
DATABASE_URL=${local.pg_pooled}
|
|
59
|
+
DATABASE_URL_DIRECT=${local.pg_direct}
|
|
60
|
+
|
|
61
|
+
# Connection budget (INFRASTRUCTURE.md, size = ${var.size})
|
|
62
|
+
DB_POOL_ENGINE=${local.s.pool_engine}
|
|
63
|
+
DB_POOL_ENGINE_LEGACY=${local.s.pool_legacy}
|
|
64
|
+
DB_POOL_MEMORY=${local.s.pool_memory}
|
|
65
|
+
|
|
66
|
+
# Credential encryption at rest — per-deployment, generated by Terraform.
|
|
67
|
+
# Back this up with the database (a DB backup is unreadable without it).
|
|
68
|
+
CREDENTIAL_ENCRYPTION_KEY=${random_password.credential_key.result}
|
|
69
|
+
|
|
70
|
+
# Redis (managed, TLS)
|
|
71
|
+
REDIS_HOST=${local.redis_host}
|
|
72
|
+
REDIS_PORT=${local.redis_port}
|
|
73
|
+
REDIS_PASSWORD=${local.redis_password}
|
|
74
|
+
REDIS_TLS=${local.redis_tls}
|
|
75
|
+
REDIS_NAMESPACE=universe
|
|
76
|
+
|
|
77
|
+
# OIDC — byo (Auth0 today): the tenant is authoritative for roles/permissions.
|
|
78
|
+
AUTH_ISSUER=${var.auth_issuer}
|
|
79
|
+
AUTH_CLIENT_ID=${var.auth_client_id}
|
|
80
|
+
AUTH_AUDIENCE=${var.auth_audience}
|
|
81
|
+
|
|
82
|
+
# Service keys
|
|
83
|
+
OPENAI_API_KEY=${var.openai_api_key}
|
|
84
|
+
HYPERBROWSER_API_KEY=${var.hyperbrowser_api_key}
|
|
85
|
+
|
|
86
|
+
# Ingress. With a domain: DOMAIN drives every URL (compose derives
|
|
87
|
+
# https://api.<domain>). Without one (POC): DOMAIN stays empty and the
|
|
88
|
+
# explicit URLs below point at the load balancer's IP over plain HTTP.
|
|
89
|
+
# To upgrade later: set domain in terraform.tfvars, terraform apply,
|
|
90
|
+
# re-render this file, unoverse deploy. Nothing is destroyed.
|
|
91
|
+
DOMAIN=${var.domain}
|
|
92
|
+
${local.has_domain ? "" : "API_URL=http://${digitalocean_loadbalancer.public.ip}"}
|
|
93
|
+
${local.has_domain ? "" : "VITE_SERVER_WS_URL=ws://${digitalocean_loadbalancer.public.ip}"}
|
|
94
|
+
${local.has_domain ? "" : "UNOVERSE_URL=http://${digitalocean_loadbalancer.public.ip}"}
|
|
95
|
+
ENV
|
|
96
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# What this ground costs to run, priced where its sizes are chosen.
|
|
2
|
+
#
|
|
3
|
+
# ONE HOME FOR THE NUMBERS. These used to live in the CLI's plan summariser and again in
|
|
4
|
+
# the docs, so a size change here left two stale copies behind and the deploy screen
|
|
5
|
+
# disagreed with the page describing it. The ground owns its sizes, so the ground prices
|
|
6
|
+
# them: `unoverse deploy` reads this map out of the plan JSON, and the docs quote the
|
|
7
|
+
# same output rather than a table somebody keeps in step by hand.
|
|
8
|
+
#
|
|
9
|
+
# NOT A COST ORACLE. DigitalOcean's published monthly rates for exactly the slugs this
|
|
10
|
+
# ground uses, so a developer sees an order of magnitude instead of a surprise. Rates
|
|
11
|
+
# move; the bill is the provider's.
|
|
12
|
+
|
|
13
|
+
locals {
|
|
14
|
+
# Monthly USD, keyed by the size slug it prices. A slug absent here is simply not
|
|
15
|
+
# priced (the summary shows the resource with no figure) rather than guessed at.
|
|
16
|
+
prices = {
|
|
17
|
+
# Droplets
|
|
18
|
+
"s-4vcpu-16gb-amd" = 96 # small: the POC box
|
|
19
|
+
"g-8vcpu-32gb" = 252 # medium
|
|
20
|
+
"m-8vcpu-64gb" = 386 # large
|
|
21
|
+
# Managed Postgres and Redis
|
|
22
|
+
"db-s-1vcpu-1gb" = 15
|
|
23
|
+
"db-s-1vcpu-2gb" = 30
|
|
24
|
+
"db-s-2vcpu-4gb" = 60
|
|
25
|
+
"db-s-4vcpu-8gb" = 120
|
|
26
|
+
# Fixed-price resources, keyed by what they are rather than a size
|
|
27
|
+
"lb" = 12
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# The steady-state bill for THIS universe, as configured. Adopted or bring-your-own
|
|
31
|
+
# Postgres is not in it: that cluster is somebody else's line item, and pricing it here
|
|
32
|
+
# would invent a charge this stack does not create.
|
|
33
|
+
monthly_estimate = sum([
|
|
34
|
+
lookup(local.prices, local.s.droplet, 0),
|
|
35
|
+
local.provision_pg ? lookup(local.prices, local.s.pg, 0) : 0,
|
|
36
|
+
lookup(local.prices, local.s.redis, 0),
|
|
37
|
+
local.prices["lb"],
|
|
38
|
+
])
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
output "prices" {
|
|
42
|
+
description = "Monthly USD by size slug. The CLI's plan summary reads this, so the deploy screen and the docs cannot drift apart."
|
|
43
|
+
value = local.prices
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
output "monthly_estimate" {
|
|
47
|
+
description = "Roughly what this universe costs a month at list prices, for the size and Postgres mode configured."
|
|
48
|
+
value = local.monthly_estimate
|
|
49
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Copy to terraform.tfvars and fill in — or let `./unoverse ground do` write it
|
|
2
|
+
# for you, prefilled from doctl (your IP, SSH key, existing Postgres clusters).
|
|
3
|
+
# terraform.tfvars is gitignored.
|
|
4
|
+
# Token: set DIGITALOCEAN_TOKEN in the environment (preferred) or do_token here.
|
|
5
|
+
|
|
6
|
+
region = "lon1"
|
|
7
|
+
name = "universe-poc"
|
|
8
|
+
size = "small" # small (POC) | medium | large
|
|
9
|
+
admin_cidr = "203.0.113.7/32" # YOUR IP — SSH + Dozzle only
|
|
10
|
+
ssh_key_name = "your-do-ssh-key-name" # must already exist in the DO account
|
|
11
|
+
# Domain is OPTIONAL. Leave it empty and the universe comes up on the load
|
|
12
|
+
# balancer's IP over plain HTTP (POC mode) — `terraform output api_url` tells
|
|
13
|
+
# you the address. When you have a domain, fill it in and `terraform apply`
|
|
14
|
+
# again: the SAME deployment upgrades in place to TLS at api.<domain>.
|
|
15
|
+
domain = "" # empty = HTTP on the LB IP; "yourdomain.com" = TLS at api.yourdomain.com
|
|
16
|
+
manage_dns = false # true only if the domain's DNS is on DO
|
|
17
|
+
|
|
18
|
+
# ⚠ With a domain set, the managed Let's Encrypt certificate requires the
|
|
19
|
+
# domain's DNS to be HOSTED on DigitalOcean (registrar can stay GoDaddy etc. —
|
|
20
|
+
# point the nameservers at ns1/ns2/ns3.digitalocean.com and add the domain
|
|
21
|
+
# under Networking). Then manage_dns = true also creates the records for you.
|
|
22
|
+
|
|
23
|
+
# POC ONLY: public Canvas at https://api.<domain>:3001 (a second port on the ONE LB).
|
|
24
|
+
# Add that URL to the IdP's allowed origins. Default false = admin-only.
|
|
25
|
+
canvas_public = true
|
|
26
|
+
|
|
27
|
+
# Auth — byo-oidc (the standing choice: DO universes use the existing Auth0
|
|
28
|
+
# tenant; roles/permissions live THERE, Terraform doesn't touch them).
|
|
29
|
+
auth_issuer = "https://your-tenant.auth0.com"
|
|
30
|
+
auth_client_id = "your-spa-client-id"
|
|
31
|
+
auth_audience = "gravity-api"
|
|
32
|
+
|
|
33
|
+
# Service secrets — with these set, the rendered .env.production is COMPLETE.
|
|
34
|
+
docr_token = "dop_v1_..." # registry token (pulls platform images)
|
|
35
|
+
openai_api_key = "sk-..." # memory server + OpenAI nodes
|
|
36
|
+
# hyperbrowser_api_key = "" # optional — page intelligence
|
|
37
|
+
|
|
38
|
+
# Postgres — three OPTIONAL modes (leave all unset = provision a fresh cluster):
|
|
39
|
+
# Reuse YOUR existing DO Postgres (the usual case) — Terraform adds the
|
|
40
|
+
# universe's own db/user/pool to it, touching nothing else. Name from
|
|
41
|
+
# `doctl databases list`:
|
|
42
|
+
# existing_pg_cluster_name = "your-existing-cluster-name"
|
|
43
|
+
# Or a fully external database, used verbatim:
|
|
44
|
+
# byo_postgres_url = "postgresql://user:pass@host:5432/db?sslmode=require"
|
|
45
|
+
|
|
46
|
+
# Redis: always provisioned by Terraform (Managed Redis, TLS). Not configurable.
|
|
47
|
+
|
|
48
|
+
# The rendered output (terraform output -raw env_production > ../../.env.production)
|
|
49
|
+
# IS your universe's secrets, including the MASTER KEY (CREDENTIAL_ENCRYPTION_KEY)
|
|
50
|
+
# that locks every credential users save in the platform. If it's lost, no backup
|
|
51
|
+
# can bring them back. Never commit it; keep a safe copy with your database backups.
|
|
52
|
+
# To change any value: edit this file and re-render. Never hand-edit the output.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
variable "do_token" {
|
|
2
|
+
description = "DigitalOcean API token (or set DIGITALOCEAN_TOKEN in the environment and leave this empty)."
|
|
3
|
+
type = string
|
|
4
|
+
default = ""
|
|
5
|
+
sensitive = true
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
variable "region" {
|
|
9
|
+
description = "DigitalOcean region for everything."
|
|
10
|
+
type = string
|
|
11
|
+
default = "lon1"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
variable "name" {
|
|
15
|
+
description = "Prefix for every resource."
|
|
16
|
+
type = string
|
|
17
|
+
default = "universe-poc"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
variable "size" {
|
|
21
|
+
description = "Deployment size (INFRASTRUCTURE.md size table): small = POC box."
|
|
22
|
+
type = string
|
|
23
|
+
default = "small"
|
|
24
|
+
validation {
|
|
25
|
+
condition = contains(["small", "medium", "large"], var.size)
|
|
26
|
+
error_message = "size must be small, medium or large."
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
variable "admin_cidr" {
|
|
31
|
+
description = "The ONLY CIDR allowed to SSH (22) and view logs (Dozzle 8080). Your IP as x.x.x.x/32."
|
|
32
|
+
type = string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
variable "ssh_key_name" {
|
|
36
|
+
description = "Name of an SSH key already uploaded to the DigitalOcean account."
|
|
37
|
+
type = string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
variable "domain" {
|
|
41
|
+
description = "OPTIONAL. Empty (the default) = no certificate and no hostnames: the LB serves plain HTTP on its IP — the zero-friction first apply. Set it later and re-apply to upgrade IN PLACE to TLS at api.<domain> (the DNS A record is created only when manage_dns = true). Nothing is destroyed by the upgrade."
|
|
42
|
+
type = string
|
|
43
|
+
default = ""
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
variable "manage_dns" {
|
|
47
|
+
description = "true = the domain's DNS is hosted on DigitalOcean and Terraform manages the records. false = you point api.<domain> at the LB IP yourself (the LB cert still needs the record in place before it can issue)."
|
|
48
|
+
type = bool
|
|
49
|
+
default = false
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# ── Auth: DigitalOcean universes run byo-oidc (Auth0 today) — the tenant is
|
|
53
|
+
# authoritative for roles/permissions; Terraform only passes the pointers through
|
|
54
|
+
# (INFRASTRUCTURE.md: roles are PROVISIONED only under Cognito on AWS).
|
|
55
|
+
variable "auth_issuer" {
|
|
56
|
+
description = "OIDC issuer, e.g. https://your-tenant.auth0.com"
|
|
57
|
+
type = string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
variable "auth_client_id" {
|
|
61
|
+
description = "OIDC SPA client id (public by definition)."
|
|
62
|
+
type = string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
variable "auth_audience" {
|
|
66
|
+
description = "OIDC audience (the Auth0 API identifier, e.g. gravity-api)."
|
|
67
|
+
type = string
|
|
68
|
+
default = "gravity-api"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# ── Postgres: three modes, first match wins ───────────────────────────────────
|
|
72
|
+
# 1. byo_postgres_url — fully external DB, URL used verbatim (you own
|
|
73
|
+
# pooling, backups, reachability).
|
|
74
|
+
# 2. existing_pg_cluster_name — an EXISTING DO managed cluster (the usual case:
|
|
75
|
+
# "I already have Postgres in DO"): Terraform creates
|
|
76
|
+
# the universe's OWN database, user, transaction
|
|
77
|
+
# pool, and firewall rule ON that cluster. Your
|
|
78
|
+
# existing databases are untouched — but the
|
|
79
|
+
# cluster's max_connections is now SHARED between
|
|
80
|
+
# your existing workload and this universe's pools;
|
|
81
|
+
# on the 1GB tier (~22 connections) watch the sum.
|
|
82
|
+
# 3. neither set — Terraform provisions a fresh cluster + pool.
|
|
83
|
+
variable "byo_postgres_url" {
|
|
84
|
+
description = "Mode 1: fully external Postgres URL (PG 14+, include sslmode=require). Empty = see existing_pg_cluster_id / provision."
|
|
85
|
+
type = string
|
|
86
|
+
default = ""
|
|
87
|
+
sensitive = true
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
variable "existing_pg_cluster_name" {
|
|
91
|
+
description = "Mode 2: the NAME of an EXISTING DO managed Postgres cluster (doctl databases list). Terraform adds the universe's db/user/pool/firewall to it and creates no cluster."
|
|
92
|
+
type = string
|
|
93
|
+
default = ""
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
variable "canvas_public" {
|
|
97
|
+
description = "POC ONLY (decided 2026-07-29): Canvas publicly at https://api.<domain>:3001 — a second port on the ONE load balancer (no second LB, no extra cost; DO LBs cannot host-route, and a clean hostname would cost a second LB). false = Canvas stays admin-only direct :3001 (the standing posture). Add https://api.<domain>:3001 to the IdP's allowed origins."
|
|
98
|
+
type = bool
|
|
99
|
+
default = false
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# Redis is NOT flexible: always provisioned by Terraform (Managed Redis, TLS).
|
|
103
|
+
# It is the platform's shared-state backbone (and the active/active future) —
|
|
104
|
+
# the stack owns its version and locality. Only Postgres is BYO-able.
|
|
105
|
+
|
|
106
|
+
# ── Service secrets: taken here so the rendered .env.production is COMPLETE —
|
|
107
|
+
# the operator fills terraform.tfvars once and never hand-edits the env file.
|
|
108
|
+
variable "docr_token" {
|
|
109
|
+
description = "Container registry token — pulls the platform images."
|
|
110
|
+
type = string
|
|
111
|
+
sensitive = true
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
variable "openai_api_key" {
|
|
115
|
+
description = "OpenAI API key — memory server + OpenAI nodes."
|
|
116
|
+
type = string
|
|
117
|
+
sensitive = true
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
variable "hyperbrowser_api_key" {
|
|
121
|
+
description = "Optional — page-intelligence features. Empty = feature off."
|
|
122
|
+
type = string
|
|
123
|
+
default = ""
|
|
124
|
+
sensitive = true
|
|
125
|
+
}
|