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: 10 additions & 7 deletions apps/cli/src/command-internal/stack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {
import { RuntimeInfo } from "../shared/runtime/runtime-info.service.ts";
import { CLI_VERSION } from "../shared/cli/version.ts";
import { decryptSecretMap } from "../shared/config/vault-decrypt.ts";
import { toEdgeRuntimeSecretsEnv } from "../shared/functions/functions-config.ts";
import {
describeConfigLoadFailure,
loadResolvedConfigContext,
Expand Down Expand Up @@ -467,20 +468,22 @@ export const loadStackConfig = Effect.fn("StackConfig.load")(
),
catch: (cause) => new StackConfigError({ message: String(cause) }),
});
const resolvedSecrets = yield* resolveConfigSubtree(
resolvedConfig,
validatedConfig.edge_runtime.secrets ?? {},
"edge_runtime.secrets",
).pipe(Effect.mapError(toStackConfigError));
const functionsEnvResult = decryptSecretMap(
Object.fromEntries(
Object.entries(validatedConfig.edge_runtime.secrets ?? {}).map(([key, value]) => [
key,
value ?? "",
]),
),
toEdgeRuntimeSecretsEnv(resolvedSecrets),
resolvedConfig.dotenvPrivateKeys,
);
if (!functionsEnvResult.ok)
return yield* new StackConfigError({
message: `failed to parse config: ${functionsEnvResult.error}`,
});
const functionsEnv = functionsEnvResult.value;
const functionsEnv = Object.fromEntries(
Object.entries(functionsEnvResult.value).filter(([name]) => !name.startsWith("SUPABASE_")),
);
const storageFileSizeLimit = yield* Effect.try({
try: () => String(parseFileSizeLimit(validatedConfig.storage.file_size_limit)),
catch: (cause) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,28 @@ SHARED = "encrypted:BOsrXIZY2BNTW43BeRhMbfvlOIUjwI7GCyFHxJD/Ik+UQ4mqkgVl2+61WWhE
}).pipe(Effect.provide(layer)),
);

it.effect("uppercases edge runtime secret names and drops unset or SUPABASE_ ones", () =>
Effect.gen(function* () {
const root = yield* createStackConfigProject(
`project_id = "functions-secret-names"
[edge_runtime.secrets]
my_lower = "lower-value"
FROM_ENV = "env(STACK_SECRET_SOURCE)"
MISSING = "env(STACK_SECRET_UNSET)"
EMPTY = ""
supabase_internal_wallclock_limit_sec = "7"
`,
{ rootEnv: "STACK_SECRET_SOURCE=from-env\n" },
);
const config = yield* loadStackConfig(root);
const services = yield* config.creations("functions-secret-names");
expect(services.find((service) => service.service === "functions")?.config.env).toEqual({
MY_LOWER: "lower-value",
FROM_ENV: "from-env",
});
}).pipe(Effect.provide(layer)),
);

it.effect("leaves empty configured secrets for the stack to normalize", () =>
Effect.gen(function* () {
const root = yield* createStackConfigProject(`project_id = "empty-secrets"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ S3 protocol/vector controls are forwarded to their services. Storage receives th
keys and region, and uses the gateway's `/storage/v1` prefix to verify S3 signatures and to build
resumable upload URLs. `analytics.vector_port` and `SUPABASE_ANALYTICS_VECTOR_PORT` are accepted
and ignored: the stack runs no Vector service.
Encrypted JWT secrets are decrypted before shared credentials are derived, and encrypted
`[edge_runtime.secrets]` values are decrypted before they reach the Functions service. `db.health_timeout`
Encrypted JWT secrets are decrypted before shared credentials are derived. `[edge_runtime.secrets]`
names are uppercased, empty or unresolved `env()` values and `SUPABASE_`-prefixed names are dropped,
and encrypted values are decrypted before they reach the Functions service. `db.health_timeout`
controls database readiness; package JWT and PostgreSQL root-key defaults apply when omitted, and
the effective root key is supplied through a stack-owned key file.
Studio receives the database connection, the Functions management directory/URL, and Analytics
Expand Down
6 changes: 4 additions & 2 deletions apps/cli/src/commands/functions/serve/SIDE_EFFECTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,10 @@ when the owner remains reachable. Import-map and inspector flags are rejected be
mutation. Stack mode writes no project files and does not stop or restart the whole composition.
It reads the shared Functions `.env` when creating a temporary service, or the explicit `--env-file`;
per-function `.env` files are not read, and reserved `SUPABASE_*` entries are ignored.
For Stack mode, TOML `edge_runtime.secrets` overrides the shared `.env`; an explicit
`--env-file` overrides matching shared secrets while retaining other saved values.
For Stack mode, TOML `edge_runtime.secrets` names are uppercased, empty or unresolved `env()`
values and `SUPABASE_`-prefixed names are dropped, encrypted values are decrypted, and the result
overrides the shared `.env`; an explicit `--env-file` overrides matching shared secrets while
retaining other saved values.
Per-function TOML `env` remains the final override for that function.
Without a configured `import_map`, a function uses its own `deno.json` or `deno.jsonc`; a
per-function `import_map.json` is not read and does not fail the command.
Expand Down
19 changes: 18 additions & 1 deletion apps/cli/src/shared/functions/functions-config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Crypto, Effect, type FileSystem, Option, type Path } from "effect";
import { Crypto, Effect, type FileSystem, Option, type Path, Redacted } from "effect";
import type { RuntimeInfo } from "../runtime/runtime-info.service.ts";
import type { LoadedCliConfig } from "@supabase/config/effect";
import { CliConfigValues, type ResolvedCliConfig } from "../../config/cli-config-values.service.ts";
Expand Down Expand Up @@ -74,3 +74,20 @@ export const loadFunctionsCliConfig = Effect.fn("FunctionsConfig.load")(function
denoVersion: context.denoVersion,
} satisfies FunctionsCliConfigContext;
});

/**
* `[edge_runtime.secrets]` as the Functions container env: names uppercased, only resolved
* non-empty values kept. An unresolved `env(VAR)` literal stays a plain string, not
* `Redacted`, so it is dropped, the same guard `secrets set` uses.
*/
export function toEdgeRuntimeSecretsEnv(
secrets: Readonly<Record<string, string | Redacted.Redacted<string> | undefined>> | undefined,
): Record<string, string> {
return Object.fromEntries(
Object.entries(secrets ?? {}).flatMap(([name, value]): Array<[string, string]> =>
Redacted.isRedacted(value) && Redacted.value(value).length > 0
? [[name.toUpperCase(), Redacted.value(value)]]
: [],
),
);
}
19 changes: 6 additions & 13 deletions apps/cli/src/shared/functions/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,11 @@ import {
runChildProcess,
toDockerPath,
} from "./functions-docker.ts";
import { loadFunctionsCliConfig, type FunctionsLocalConfigLoader } from "./functions-config.ts";
import {
loadFunctionsCliConfig,
toEdgeRuntimeSecretsEnv,
type FunctionsLocalConfigLoader,
} from "./functions-config.ts";
import { edgeRuntimeImage, resolveEdgeRuntimeVersionPin } from "./functions.shared.ts";
import { slimImagesEnabled } from "../services/slim-images.ts";
import {
Expand Down Expand Up @@ -438,18 +442,7 @@ export function toPlainEdgeRuntimeConfig(
policy: reveal(edgeRuntime.policy) ?? "",
inspector_port: edgeRuntime.inspector_port,
deno_version: edgeRuntime.deno_version,
// Secret names always reach the container env uppercased regardless of
// authored casing. Only resolved (non-empty) values are kept — an
// unresolved `env(VAR)` literal stays a plain string, not `Redacted`, so
// `Redacted.isRedacted` + non-empty filters it out, the same guard
// `secrets set` uses.
secrets: Object.fromEntries(
Object.entries(edgeRuntime.secrets ?? {}).flatMap(([name, value]) =>
Redacted.isRedacted(value) && Redacted.value(value).length > 0
? [[name.toUpperCase(), Redacted.value(value)] as const]
: [],
),
),
secrets: toEdgeRuntimeSecretsEnv(edgeRuntime.secrets),
};
}

Expand Down
Loading