1import * as Data from "effect/Data";
2import * as Effect from "effect/Effect";
3import * as Redacted from "effect/Redacted";
4import { createPhysicalName } from "../PhysicalName.ts";
5import type { ResourceBinding } from "../Resource.ts";
6import type { FunctionBinding, FunctionProps } from "./Function.ts";
7
8export class FunctionConfigurationError extends Data.TaggedError(
9 "FunctionConfigurationError",
10)<{ message: string; cause?: unknown }> {}
11
12const injected = new Set([
13 "NEON_API_KEY",
14 "DATABASE_URL",
15 "DATABASE_URL_UNPOOLED",
16 "PGHOST",
17 "PGHOST_UNPOOLED",
18 "PGUSER",
19 "PGPASSWORD",
20 "PGDATABASE",
21 "AWS_ACCESS_KEY_ID",
22 "AWS_SECRET_ACCESS_KEY",
23 "AWS_ENDPOINT_URL_S3",
24 "AWS_REGION",
25 "NEON_AUTH_BASE_URL",
26 "NEON_AUTH_URL",
27 "NEON_AUTH_JWKS_URL",
28 "NEON_DATA_API_URL",
29 "NEON_AI_GATEWAY_TOKEN",
30 "NEON_AI_GATEWAY_BASE_URL",
31 "NEON_BRANCH",
32 "AI_GATEWAY_API_KEY",
33 "AI_GATEWAY_BASE_URL",
34 "NEON_PROJECT_ID",
35 "NEON_BRANCH_ID",
36]);
37
38const checkedSlug = Effect.fn(function* (id: string, slug?: string) {
39 const value =
40 slug ??
41 (yield* createPhysicalName({
42 id,
43 maxLength: 20,
44 delimiter: "",
45 lowercase: true,
46 })).replaceAll(/[^a-z0-9]/g, "");
47 if (!/^[a-z0-9]{1,20}$/.test(value))
48 return yield* new FunctionConfigurationError({
49 message: "Function slug must match ^[a-z0-9]{1,20}$",
50 });
51 return value;
52});
53
54export function functionSlug(
55 id: string,
56 slug: string,
57): Effect.Effect<string, FunctionConfigurationError>;
58export function functionSlug(
59 id: string,
60 slug?: string,
61): ReturnType<typeof checkedSlug>;
62export function functionSlug(id: string, slug?: string) {
63 return checkedSlug(id, slug);
64}
65
66export const functionEnvironment = Effect.fn(function* (
67 props: FunctionProps,
68 bindings: ResourceBinding<FunctionBinding>[],
69) {
70 const env: Record<string, string> = {};
71 for (const source of [
72 props.env ?? {},
73 ...bindings.map((binding) => binding.data.env ?? {}),
74 ]) {
75 for (const [key, value] of Object.entries(source)) {
76 if (value === undefined) continue;
77 if (injected.has(key) || key.startsWith("ALCHEMY_"))
78 return yield* new FunctionConfigurationError({
79 message: `Cannot override injected Function environment key ${key}`,
80 });
81 if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
82 return yield* new FunctionConfigurationError({
83 message: `Invalid Function environment key ${key}`,
84 });
85 const plain = Redacted.isRedacted(value) ? Redacted.value(value) : value;
86 if (key in env && env[key] !== plain)
87 return yield* new FunctionConfigurationError({
88 message: `Conflicting Function environment key ${key}`,
89 });
90 env[key] = plain;
91 }
92 }
93 return env;
94});