forked from CopilotKit/CopilotKit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathruntime-config.ts
More file actions
145 lines (135 loc) · 6.14 KB
/
Copy pathruntime-config.ts
File metadata and controls
145 lines (135 loc) · 6.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
// Server-only runtime config reader. Reads from process.env at REQUEST
// time (not at module load) so a single built artifact can serve
// different URL values across staging vs prod by changing the Railway
// service's env vars — no rebuild required.
//
// `unstable_noStore()` opts the calling segment out of Next.js's static
// cache so reads always reflect the live env. Without it, a server
// component that uses this could be statically rendered at build time
// and freeze the URLs back into the artifact — defeating the runtime
// switch. See Next.js App Router docs on Dynamic Rendering.
//
// This module MUST NOT be imported from client components. The matching
// client-side reader lives in runtime-config.client.ts and reads from
// window.__SHOWCASE_CONFIG__ which the root layout injects.
import { unstable_noStore as noStore } from "next/cache";
export interface RuntimeConfig {
/** PocketBase backend used by the Status tab live-readers. */
pocketbaseUrl: string;
/** Showcase shell host — used to build Demo / Code / docs-shell links. */
shellUrl: string;
/**
* Client DIRECT ops base URL — an opt-in escape hatch for direct
* cross-origin calls (e.g. local dev hitting a remote harness),
* sourced from `NEXT_PUBLIC_OPS_DIRECT_BASE_URL`. Defaults to "" so
* `ops-api.ts:resolveBaseUrl()` falls through to the same-origin
* `/api/ops` proxy.
*
* This is DISTINCT from the server proxy target `OPS_BASE_URL`, which
* is read directly (server-only) by the Route Handler at
* `src/app/api/ops/[...path]/route.ts` and is intentionally NEVER
* injected into the client config — leaking it makes the browser fetch
* the harness cross-origin (CORS-blocked, wrong path).
*/
opsBaseUrl: string;
}
const PROD_INVALID_POCKETBASE_URL = "http://pocketbase.invalid";
const PROD_INVALID_SHELL_URL = "about:blank#shell-url-missing";
/**
* Resolve the runtime config for shell-dashboard. Called once per request
* by the root layout and by any other server component that needs it.
*
* Fail-loud strategy mirrors the prior build-time pb.ts logic: in
* production, missing env vars produce sentinel URLs (visible breakage)
* AND a console.error; in dev, we fall back to localhost so iteration is
* frictionless.
*
* `opts.noStore` (default `true`) controls whether to call
* `unstable_noStore()`. The Node.js server runtime needs the opt-out so
* Next.js does not statically prerender callers and freeze the URLs into
* the build artifact. The Edge runtime (middleware) MUST pass
* `{ noStore: false }` — `unstable_noStore()` is unavailable there, and
* middleware always runs per-request by definition so there is no
* static cache to opt out of. The thin `getRuntimeConfigForMiddleware()` wrapper
* below makes this explicit at the call site.
*/
export function getRuntimeConfig(
opts: { noStore?: boolean } = {},
): RuntimeConfig {
if (opts.noStore !== false) noStore();
const isProd = process.env.NODE_ENV === "production";
const pocketbaseUrl = readUrl(
"POCKETBASE_URL",
isProd ? PROD_INVALID_POCKETBASE_URL : "http://127.0.0.1:8090",
isProd,
);
const shellUrl = readUrl(
"SHELL_URL",
isProd ? PROD_INVALID_SHELL_URL : "http://localhost:3000",
isProd,
);
// Client DIRECT ops override — opt-in escape hatch for direct
// cross-origin calls. Sourced ONLY from the NEXT_PUBLIC_-prefixed
// client-intended name (NOT the bare server proxy target OPS_BASE_URL,
// which the Route Handler reads server-side). Defaults to "" in every
// environment so the client falls through to the same-origin /api/ops
// proxy unless a developer explicitly opts in. No sentinel and no
// FATAL-CONFIG: an unset override is the normal production case, not a
// misconfiguration.
const opsBaseUrl = (process.env.NEXT_PUBLIC_OPS_DIRECT_BASE_URL ?? "")
.trim()
.replace(/\/+$/, "");
return { pocketbaseUrl, shellUrl, opsBaseUrl };
}
/**
* Edge-runtime variant. Identical semantics to `getRuntimeConfig()`
* except `unstable_noStore()` is skipped — `next/cache`'s no-store
* helper is not available in the Edge runtime, and middleware always
* runs per-request by definition so there is no static cache to opt
* out of. Thin wrapper to keep the body single-sourced.
*
* Middleware (`src/middleware.ts`) MUST import this rather than
* `getRuntimeConfig` — otherwise the Edge bundle pulls in `next/cache`
* and the build fails with "module not found in edge runtime."
*/
export function getRuntimeConfigForMiddleware(): RuntimeConfig {
return getRuntimeConfig({ noStore: false });
}
// Env-name tolerance: deploy configs in the wild use either the bare
// name (e.g. `OPS_BASE_URL`) or the `NEXT_PUBLIC_*`-prefixed name. We
// accept either — the primary (passed-in) name wins, with transparent
// fallback to the alternate so a Railway service variable set under
// the "wrong" name still works without redeploy.
function altEnvName(envKey: string): string {
return envKey.startsWith("NEXT_PUBLIC_")
? envKey.slice("NEXT_PUBLIC_".length)
: `NEXT_PUBLIC_${envKey}`;
}
// Length-aware env coalesce: a deliberately-empty primary (e.g. an
// operator clearing `OPS_BASE_URL=""` on a Railway service) must NOT
// mask a populated alternate. Treat empty-string as "unset" and fall
// through to the alternate.
function readEnvPair(envKey: string): string | undefined {
const primary = process.env[envKey];
if (primary && primary.length > 0) return primary;
const alt = process.env[altEnvName(envKey)];
if (alt && alt.length > 0) return alt;
return undefined;
}
function readUrl(envKey: string, fallback: string, isProd: boolean): string {
const value = readEnvPair(envKey);
if (value !== undefined) return value.replace(/\/+$/, "");
if (isProd) {
// eslint-disable-next-line no-console
console.error(
`[shell-dashboard runtime-config] FATAL-CONFIG: ${envKey} is unset in a production deploy; ` +
`using sentinel ${fallback}. Set the env var on the Railway service.`,
);
} else {
// eslint-disable-next-line no-console
console.warn(
`[shell-dashboard runtime-config] ${envKey} unset; using dev fallback ${fallback}`,
);
}
return fallback.replace(/\/+$/, "");
}