unoverse 0.1.138 → 0.1.140
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/operator/infra/aws/main.tf +18 -4
- package/operator/infra/aws/terraform.tfvars.example +8 -0
- package/operator/infra/aws/variables.tf +18 -0
- package/operator/infra/digitalocean/main.tf +15 -8
- package/operator/infra/digitalocean/outputs.tf +2 -2
- package/operator/infra/digitalocean/terraform.tfvars.example +8 -0
- package/operator/infra/digitalocean/variables.tf +28 -0
- package/package.json +1 -1
|
@@ -400,8 +400,18 @@ data "archive_file" "pretoken" {
|
|
|
400
400
|
output_path = "${path.module}/pretoken/pretoken.zip"
|
|
401
401
|
}
|
|
402
402
|
|
|
403
|
+
# THE ROLE IS OURS TO CREATE ONLY WHERE IAM WRITE IS DELEGATED. Where it is not, the
|
|
404
|
+
# account's own pipeline creates it and hands us the ARN, and we must reference it rather
|
|
405
|
+
# than manage it: two states owning one role fight, and our destroy would take theirs with
|
|
406
|
+
# it. See variables.tf, `pretoken_role_arn`.
|
|
407
|
+
locals {
|
|
408
|
+
pretoken_role_arn = var.pretoken_role_arn != "" ? var.pretoken_role_arn : one(aws_iam_role.pretoken[*].arn)
|
|
409
|
+
pretoken_role_ours = var.pretoken_role_arn == ""
|
|
410
|
+
}
|
|
411
|
+
|
|
403
412
|
resource "aws_iam_role" "pretoken" {
|
|
404
|
-
|
|
413
|
+
count = local.pretoken_role_ours ? 1 : 0
|
|
414
|
+
name = "${var.name}-pretoken"
|
|
405
415
|
assume_role_policy = jsonencode({
|
|
406
416
|
Version = "2012-10-17"
|
|
407
417
|
Statement = [{
|
|
@@ -413,7 +423,8 @@ resource "aws_iam_role" "pretoken" {
|
|
|
413
423
|
}
|
|
414
424
|
|
|
415
425
|
resource "aws_iam_role_policy_attachment" "pretoken_logs" {
|
|
416
|
-
|
|
426
|
+
count = local.pretoken_role_ours ? 1 : 0
|
|
427
|
+
role = aws_iam_role.pretoken[0].name
|
|
417
428
|
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
|
418
429
|
}
|
|
419
430
|
|
|
@@ -422,8 +433,11 @@ resource "aws_iam_role_policy_attachment" "pretoken_logs" {
|
|
|
422
433
|
# assumed by Lambda" — the role is correct and simply not visible yet. Terraform's
|
|
423
434
|
# dependency graph is satisfied because the role exists; AWS's own propagation is not.
|
|
424
435
|
#
|
|
425
|
-
# Ten seconds is the usual advice and it costs ten seconds on a first apply only.
|
|
436
|
+
# Ten seconds is the usual advice and it costs ten seconds on a first apply only. A role we
|
|
437
|
+
# were handed was created by another pipeline long before this apply, so there is nothing to
|
|
438
|
+
# outrun and the wait is skipped with the role.
|
|
426
439
|
resource "time_sleep" "iam_propagation" {
|
|
440
|
+
count = local.pretoken_role_ours ? 1 : 0
|
|
427
441
|
depends_on = [aws_iam_role.pretoken, aws_iam_role_policy_attachment.pretoken_logs]
|
|
428
442
|
create_duration = "10s"
|
|
429
443
|
}
|
|
@@ -435,7 +449,7 @@ resource "aws_lambda_function" "pretoken" {
|
|
|
435
449
|
variables = { ROLE_PERMISSIONS = jsonencode(local.all_roles) }
|
|
436
450
|
}
|
|
437
451
|
function_name = "${var.name}-pretoken"
|
|
438
|
-
role =
|
|
452
|
+
role = local.pretoken_role_arn
|
|
439
453
|
runtime = "nodejs20.x"
|
|
440
454
|
handler = "index.handler"
|
|
441
455
|
filename = data.archive_file.pretoken.output_path
|
|
@@ -57,3 +57,11 @@ roles = [
|
|
|
57
57
|
# Marketplace catalogue. Defaults to the official one — set this only to point somewhere
|
|
58
58
|
# else, or to "" for local items only.
|
|
59
59
|
# marketplace_url = "https://your-own-marketplace.example.com"
|
|
60
|
+
|
|
61
|
+
# An account that will not let you create IAM roles. Leave both of these alone unless your
|
|
62
|
+
# deploy fails with AccessDenied on iam:CreateRole or iam:CreateUser, which is what a
|
|
63
|
+
# customer-owned account governed by a permission set (AWS's PowerUserAccess excludes IAM
|
|
64
|
+
# by design) looks like. Then: their team creates the Lambda execution role, you paste the
|
|
65
|
+
# ARN here, and you skip the Bedrock user and paste its keys into a credential by hand.
|
|
66
|
+
# pretoken_role_arn = "arn:aws:iam::000000000000:role/unoverse-pretoken-role"
|
|
67
|
+
# bedrock_credentials = false
|
|
@@ -130,3 +130,21 @@ variable "bedrock_credentials" {
|
|
|
130
130
|
default = true
|
|
131
131
|
description = "Create an IAM user and access key for the Bedrock nodes. Set false where IAM write is not delegated."
|
|
132
132
|
}
|
|
133
|
+
|
|
134
|
+
# The pre-token Lambda's execution role (main.tf, "pretoken"), when the account will not let
|
|
135
|
+
# us create it.
|
|
136
|
+
#
|
|
137
|
+
# EMPTY IS EVERY DEPLOYMENT WE HAVE. The module creates the role, attaches
|
|
138
|
+
# AWSLambdaBasicExecutionRole, and nothing changes. Set this only where `iam:CreateRole` is
|
|
139
|
+
# not delegated, which is the normal shape of a customer-owned account governed by a
|
|
140
|
+
# permission set: their pipeline creates the role, we reference the ARN it produces.
|
|
141
|
+
#
|
|
142
|
+
# Creating the Lambda still needs `iam:PassRole` on that role, conditioned on
|
|
143
|
+
# lambda.amazonaws.com, because AWS asks that of whoever attaches the role, not whoever made
|
|
144
|
+
# it. Handing us an ARN does not remove it. The role needs only lambda.amazonaws.com in its
|
|
145
|
+
# trust policy and AWSLambdaBasicExecutionRole attached, which is log writes and nothing else.
|
|
146
|
+
variable "pretoken_role_arn" {
|
|
147
|
+
type = string
|
|
148
|
+
default = ""
|
|
149
|
+
description = "ARN of an existing Lambda execution role for the pre-token trigger. Empty = the module creates it."
|
|
150
|
+
}
|
|
@@ -146,9 +146,9 @@ resource "digitalocean_firewall" "app" {
|
|
|
146
146
|
# NOTE (INFRASTRUCTURE.md § Ingress): DO caps http idle timeout at 600s — set to
|
|
147
147
|
# the cap; long-quiet SSE/WS sessions must survive a reconnect (smoke-test item).
|
|
148
148
|
resource "digitalocean_certificate" "api" {
|
|
149
|
-
count
|
|
150
|
-
name
|
|
151
|
-
type
|
|
149
|
+
count = local.has_domain ? 1 : 0
|
|
150
|
+
name = "${var.name}-api"
|
|
151
|
+
type = "lets_encrypt"
|
|
152
152
|
# canvas.<domain> rides the SAME certificate as a SAN (and the same LB): the
|
|
153
153
|
# clean hostname costs nothing — only the PORT cannot be dropped, because DO
|
|
154
154
|
# LBs cannot host-route (DECIDED 2026-07-29: one LB, port in the URL).
|
|
@@ -156,11 +156,11 @@ resource "digitalocean_certificate" "api" {
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
resource "digitalocean_loadbalancer" "public" {
|
|
159
|
-
name
|
|
160
|
-
region
|
|
161
|
-
droplet_ids
|
|
162
|
-
redirect_http_to_https
|
|
163
|
-
http_idle_timeout_seconds = 600
|
|
159
|
+
name = "${var.name}-lb"
|
|
160
|
+
region = var.region
|
|
161
|
+
droplet_ids = [digitalocean_droplet.app.id]
|
|
162
|
+
redirect_http_to_https = local.has_domain # domainless serves HTTP itself, nothing to redirect to
|
|
163
|
+
http_idle_timeout_seconds = 600 # DO's maximum
|
|
164
164
|
|
|
165
165
|
# With a domain: HTTPS 443 with the managed cert. Without: plain HTTP 80 on
|
|
166
166
|
# the LB IP. Same port meanings for the developer either way (API on the root).
|
|
@@ -367,6 +367,13 @@ resource "random_password" "credential_key" {
|
|
|
367
367
|
special = false
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
+
# The key that actually ships: the operator's when they brought one (a database that
|
|
371
|
+
# already holds credentials keeps the key that encrypted them), otherwise the generated
|
|
372
|
+
# one above. See variables.tf § credential master key.
|
|
373
|
+
locals {
|
|
374
|
+
credential_encryption_key = var.credential_encryption_key != "" ? var.credential_encryption_key : random_password.credential_key.result
|
|
375
|
+
}
|
|
376
|
+
|
|
370
377
|
# ── One project, so a universe looks like one thing ───────────────────────────
|
|
371
378
|
#
|
|
372
379
|
# Without this, a universe's parts scatter through the DigitalOcean default project among
|
|
@@ -23,7 +23,7 @@ locals {
|
|
|
23
23
|
# spends it on one GRANT, and it goes no further.
|
|
24
24
|
pg_admin_user = local.pg_adopt ? data.digitalocean_database_cluster.existing_pg[0].user : (local.provision_pg ? digitalocean_database_cluster.pg[0].user : "")
|
|
25
25
|
pg_admin_pass = local.pg_adopt ? data.digitalocean_database_cluster.existing_pg[0].password : (local.provision_pg ? digitalocean_database_cluster.pg[0].password : "")
|
|
26
|
-
pg_admin_url
|
|
26
|
+
pg_admin_url = local.pg_managed ? "postgresql://${local.pg_admin_user}:${local.pg_admin_pass}@${local.pg_cluster_host}:${local.pg_cluster_port}/${digitalocean_database_db.universe[0].name}?sslmode=require" : ""
|
|
27
27
|
|
|
28
28
|
# Redis is always ours — provisioned above, no BYO branch.
|
|
29
29
|
redis_host = digitalocean_database_cluster.redis.private_host
|
|
@@ -100,7 +100,7 @@ output "env_production" {
|
|
|
100
100
|
|
|
101
101
|
# Credential encryption at rest — per-deployment, generated by Terraform.
|
|
102
102
|
# Back this up with the database (a DB backup is unreadable without it).
|
|
103
|
-
CREDENTIAL_ENCRYPTION_KEY=${
|
|
103
|
+
CREDENTIAL_ENCRYPTION_KEY=${local.credential_encryption_key}
|
|
104
104
|
|
|
105
105
|
# Redis (managed, TLS)
|
|
106
106
|
REDIS_HOST=${local.redis_host}
|
|
@@ -43,6 +43,14 @@ openai_api_key = "sk-..." # memory server + OpenAI nodes
|
|
|
43
43
|
# Or a fully external database, used verbatim:
|
|
44
44
|
# byo_postgres_url = "postgresql://user:pass@host:5432/db?sslmode=require"
|
|
45
45
|
|
|
46
|
+
# BRINGING A DATABASE MEANS BRINGING ITS KEY. Credentials are encrypted at rest, and the
|
|
47
|
+
# key belongs to the data: reuse a database that already holds credentials without it and
|
|
48
|
+
# every one of them fails to decrypt. Leave this empty for a FRESH database (Terraform
|
|
49
|
+
# generates one, the common case); set it to the existing key when the database is not
|
|
50
|
+
# new. The same applies to a local .env pointed at this universe's database — one key,
|
|
51
|
+
# everywhere that reads those rows.
|
|
52
|
+
# credential_encryption_key = "the key that already encrypted those rows"
|
|
53
|
+
|
|
46
54
|
# Redis: always provisioned by Terraform (Managed Redis, TLS). Not configurable.
|
|
47
55
|
|
|
48
56
|
# The rendered output (terraform output -raw env_production > ../../.env.production)
|
|
@@ -141,3 +141,31 @@ variable "marketplace_url" {
|
|
|
141
141
|
type = string
|
|
142
142
|
default = "https://unoverse-marketplace-4hlb9.ondigitalocean.app"
|
|
143
143
|
}
|
|
144
|
+
|
|
145
|
+
# ── The credential master key: BRING YOUR OWN when you bring your own database ──
|
|
146
|
+
#
|
|
147
|
+
# THE KEY BELONGS TO THE DATA, NOT TO THE DEPLOYMENT. It decrypts the credential rows in
|
|
148
|
+
# the database it is paired with, so any two environments sharing a database must share
|
|
149
|
+
# this key. Terraform generating a fresh one per deployment is right for a fresh database
|
|
150
|
+
# and WRONG for byo_postgres_url / existing_pg_cluster_name pointing at a database that
|
|
151
|
+
# already holds credentials: the deploy renders a new key and every stored credential
|
|
152
|
+
# fails with OpenSSL "bad decrypt". The same trap catches a local .env pointed at this
|
|
153
|
+
# universe's database (SECURITY.md § Credential encryption at rest).
|
|
154
|
+
#
|
|
155
|
+
# DIGITALOCEAN ONLY, deliberately. AWS always provisions its own RDS instance, so its
|
|
156
|
+
# database is always new and a generated key is always right; a variable there would be
|
|
157
|
+
# a way to get it wrong with no case that needs it.
|
|
158
|
+
#
|
|
159
|
+
# Leave it empty for a fresh database and Terraform generates one, which is the common
|
|
160
|
+
# case. Set it to the key that already encrypted those rows when the database is not new,
|
|
161
|
+
# and keep the two in step from then on.
|
|
162
|
+
variable "credential_encryption_key" {
|
|
163
|
+
description = "Master key for credentials at rest. Empty = generate one (fresh database). Set it to the EXISTING key when reusing a database that already holds credentials."
|
|
164
|
+
type = string
|
|
165
|
+
sensitive = true
|
|
166
|
+
default = ""
|
|
167
|
+
validation {
|
|
168
|
+
condition = var.credential_encryption_key == "" || length(var.credential_encryption_key) >= 32
|
|
169
|
+
error_message = "credential_encryption_key must be at least 32 characters (openssl rand -base64 32)."
|
|
170
|
+
}
|
|
171
|
+
}
|
package/package.json
CHANGED