Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions documentor/FORK.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,15 @@ because those files still type-check against it.

## Settings

| Setting | Meaning |
| --------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `OPENCODE_DATABASE_URL` | Required. A host that is not loopback must carry `sslmode=verify-full` |
| `OPENCODE_DATABASE_SCHEMA` | The scope's schema, sent as the search path. 1 to 63 characters of `a-z`, `0-9`, `_` |
| `OPENCODE_DATABASE_POOL_MAX` | Connections per process, 1 to 100, default 4 |
| `OPENCODE_DATABASE_ROLE_PASSWORD` | Read by `db provision` when it has to create the role |
| `OPENCODE_DATABASE_EPHEMERAL=1` | Tests only: each database layer creates a schema `t_<32 hex>`, migrates it, and drops it when it closes |
| `OPENCODE_TEST_DATABASE_URL` | Tests only: a superuser on a Postgres 18 whose database collation is `C.UTF-8` |
| Setting | Meaning |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `OPENCODE_DATABASE_URL` | Required. A host that is not loopback must carry `sslmode=verify-full`, but for the one exception below |
| `OPENCODE_DATABASE_PLAINTEXT_HOST` | `documentor-dev-agentstate` lets that host, DocuMentor's development Postgres, connect with `sslmode=disable`; any other value relaxes nothing |
| `OPENCODE_DATABASE_SCHEMA` | The scope's schema, sent as the search path. 1 to 63 characters of `a-z`, `0-9`, `_` |
| `OPENCODE_DATABASE_POOL_MAX` | Connections per process, 1 to 100, default 4 |
| `OPENCODE_DATABASE_ROLE_PASSWORD` | Read by `db provision` when it has to create the role |
| `OPENCODE_DATABASE_EPHEMERAL=1` | Tests only: each database layer creates a schema `t_<32 hex>`, migrates it, and drops it when it closes |
| `OPENCODE_TEST_DATABASE_URL` | Tests only: a superuser on a Postgres 18 whose database collation is `C.UTF-8` |

The database must use byte-order collation (`C`, `POSIX` or `C.UTF-8`).
opencode pages by comparing text ids, and a locale collation reorders them.
Expand Down
19 changes: 18 additions & 1 deletion packages/core/src/database/target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,23 @@ export interface Target {
const LOOPBACK = new Set(["127.0.0.1", "localhost", "[::1]", "::1"])
const IDENTIFIER = /^[a-z_][a-z0-9_]{0,62}$/

/** DocuMentor's development Postgres on a dev host's Docker network; it holds development data only. */
const DEV_PLAINTEXT_HOST = "documentor-dev-agentstate"

/**
* A remote host is reached only over verified TLS, with one exception: the
* development Postgres, which has no certificate. The host is fixed here;
* OPENCODE_DATABASE_PLAINTEXT_HOST only switches the exception on, so no
* setting can downgrade another host. The URL must say `sslmode=disable` itself.
*/
function plaintextAllowed(url: URL, env: Record<string, string | undefined>): boolean {
return (
env.OPENCODE_DATABASE_PLAINTEXT_HOST?.trim() === DEV_PLAINTEXT_HOST &&
url.hostname === DEV_PLAINTEXT_HOST &&
url.searchParams.get("sslmode") === "disable"
)
}

/** Reads where this process connects. Throws `InvalidTargetError` naming the setting at fault. */
export function read(env: Record<string, string | undefined> = process.env): Target {
const address = env.OPENCODE_DATABASE_URL
Expand All @@ -27,7 +44,7 @@ export function read(env: Record<string, string | undefined> = process.env): Tar
throw new InvalidTargetError("OPENCODE_DATABASE_URL is not a valid URL")
}
})()
if (!LOOPBACK.has(url.hostname) && url.searchParams.get("sslmode") !== "verify-full")
if (!LOOPBACK.has(url.hostname) && url.searchParams.get("sslmode") !== "verify-full" && !plaintextAllowed(url, env))
throw new InvalidTargetError(
`OPENCODE_DATABASE_URL names the remote host ${url.hostname} without sslmode=verify-full`,
)
Expand Down
18 changes: 18 additions & 0 deletions packages/core/test/pg/target.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,24 @@ describe("database target", () => {
)
})

test("accepts the development Postgres without TLS only where switched on, by name, with sslmode=disable", () => {
const dev = "postgresql://u@documentor-dev-agentstate:5432/db?sslmode=disable"
expect(() => read({ OPENCODE_DATABASE_URL: dev })).toThrow("sslmode=verify-full")
expect(
read({ OPENCODE_DATABASE_URL: dev, OPENCODE_DATABASE_PLAINTEXT_HOST: "documentor-dev-agentstate" }).url,
).toContain("documentor-dev-agentstate")
// The switch names no host of its own choosing, and never relaxes the URL's own mode.
for (const [url, host] of [
["postgresql://u@db.internal:5432/db?sslmode=disable", "db.internal"],
["postgresql://u@db.internal:5432/db?sslmode=disable", "documentor-dev-agentstate"],
["postgresql://u@documentor-dev-agentstate:5432/db", "documentor-dev-agentstate"],
["postgresql://u@documentor-dev-agentstate:5432/db?sslmode=require", "documentor-dev-agentstate"],
])
expect(() => read({ OPENCODE_DATABASE_URL: url, OPENCODE_DATABASE_PLAINTEXT_HOST: host })).toThrow(
"sslmode=verify-full",
)
})

test("accepts a remote host with sslmode=verify-full", () => {
expect(read({ OPENCODE_DATABASE_URL: "postgresql://u@db.internal:5432/db?sslmode=verify-full" }).url).toContain(
"sslmode=verify-full",
Expand Down
Loading