@promptbook/cli 0.112.0-93 → 0.112.0-95

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.
Files changed (40) hide show
  1. package/apps/agents-server/src/app/admin/_components/AdminConfigurationShell.tsx +13 -7
  2. package/apps/agents-server/src/app/admin/code-runners/CodeRunnersClient.tsx +225 -0
  3. package/apps/agents-server/src/app/admin/code-runners/page.tsx +14 -0
  4. package/apps/agents-server/src/app/admin/environment/EnvironmentVariablesClient.tsx +259 -0
  5. package/apps/agents-server/src/app/admin/environment/page.tsx +21 -0
  6. package/apps/agents-server/src/app/admin/logs/LogsClient.tsx +78 -0
  7. package/apps/agents-server/src/app/admin/logs/page.tsx +14 -0
  8. package/apps/agents-server/src/app/admin/servers/ServersClient.tsx +64 -33
  9. package/apps/agents-server/src/app/admin/servers/ServersRegistryApi.ts +5 -0
  10. package/apps/agents-server/src/app/admin/servers/ServersRegistryTable.tsx +15 -2
  11. package/apps/agents-server/src/app/admin/servers/page.tsx +3 -3
  12. package/apps/agents-server/src/app/admin/servers/useServersRegistryState.ts +12 -2
  13. package/apps/agents-server/src/app/api/admin/code-runners/route.ts +104 -0
  14. package/apps/agents-server/src/app/api/admin/environment/route.ts +65 -0
  15. package/apps/agents-server/src/app/api/admin/logs/route.ts +24 -0
  16. package/apps/agents-server/src/app/api/admin/servers/[serverId]/route.ts +79 -3
  17. package/apps/agents-server/src/app/api/admin/servers/route.ts +36 -1
  18. package/apps/agents-server/src/app/page.tsx +101 -1
  19. package/apps/agents-server/src/components/Header/buildHeaderSystemMenuItems.ts +23 -4
  20. package/apps/agents-server/src/languages/ServerTranslationKeys.ts +4 -0
  21. package/apps/agents-server/src/languages/translations/czech.yaml +4 -0
  22. package/apps/agents-server/src/languages/translations/english.yaml +4 -0
  23. package/apps/agents-server/src/tools/$provideServer.ts +27 -0
  24. package/apps/agents-server/src/utils/serverRegistry.ts +20 -1
  25. package/apps/agents-server/src/utils/session.ts +123 -2
  26. package/apps/agents-server/src/utils/vpsConfiguration.ts +550 -0
  27. package/esm/index.es.js +1 -1
  28. package/esm/index.es.js.map +1 -1
  29. package/esm/src/book-components/Chat/utils/renderMarkdown.test.d.ts +1 -0
  30. package/esm/src/version.d.ts +1 -1
  31. package/package.json +2 -1
  32. package/src/book-components/Chat/MarkdownContent/MarkdownContent.tsx +9 -398
  33. package/src/book-components/Chat/utils/renderMarkdown.ts +323 -8
  34. package/src/other/templates/getTemplatesPipelineCollection.ts +683 -879
  35. package/src/version.ts +2 -2
  36. package/src/versions.txt +2 -0
  37. package/umd/index.umd.js +1 -1
  38. package/umd/index.umd.js.map +1 -1
  39. package/umd/src/book-components/Chat/utils/renderMarkdown.test.d.ts +1 -0
  40. package/umd/src/version.d.ts +1 -1
@@ -1,12 +1,24 @@
1
1
  import { NextResponse } from 'next/server';
2
+ import { isAgentsServerSqliteMode } from '../../../../database/agentsServerDatabaseMode';
2
3
  import { resolveCurrentServerRegistryContext } from '../../../../utils/currentServerRegistryContext';
4
+ import { isUserAdmin } from '../../../../utils/isUserAdmin';
3
5
  import { isUserGlobalAdmin } from '../../../../utils/isUserGlobalAdmin';
6
+ import {
7
+ createServerPublicUrl,
8
+ listEnvironmentRegisteredServers,
9
+ normalizeServerDomain,
10
+ } from '../../../../utils/serverRegistry';
4
11
  import {
5
12
  assertGlobalAdminAccess,
6
13
  createManagedServer,
7
14
  resolveManagedServerErrorStatus,
8
15
  type CreateServerInput,
9
16
  } from '../../../../utils/serverManagement';
17
+ import {
18
+ applyVpsRuntimeConfiguration,
19
+ listConfiguredVpsDomains,
20
+ updateConfiguredVpsDomains,
21
+ } from '../../../../utils/vpsConfiguration';
10
22
 
11
23
  /**
12
24
  * Lists all registered servers together with the server resolved from the current domain.
@@ -15,12 +27,15 @@ import {
15
27
  */
16
28
  export async function GET() {
17
29
  try {
18
- assertGlobalAdminAccess(await isUserGlobalAdmin());
30
+ if (!(await isUserAdmin())) {
31
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
32
+ }
19
33
 
20
34
  const context = await resolveCurrentServerRegistryContext();
21
35
  return NextResponse.json({
22
36
  servers: context.registeredServers,
23
37
  currentServerId: context.currentServer?.id ?? null,
38
+ canEdit: await isUserGlobalAdmin(),
24
39
  });
25
40
  } catch (error) {
26
41
  return NextResponse.json(
@@ -43,6 +58,26 @@ export async function POST(request: Request) {
43
58
  assertGlobalAdminAccess(await isUserGlobalAdmin());
44
59
 
45
60
  const body = (await request.json()) as CreateServerInput;
61
+ if (isAgentsServerSqliteMode()) {
62
+ const normalizedDomain = normalizeServerDomain(body.domain);
63
+ if (!normalizedDomain) {
64
+ return NextResponse.json({ error: 'A valid domain is required.' }, { status: 400 });
65
+ }
66
+
67
+ const existingDomains = await listConfiguredVpsDomains();
68
+ await updateConfiguredVpsDomains([...existingDomains, normalizedDomain]);
69
+ await applyVpsRuntimeConfiguration();
70
+ const createdServer = listEnvironmentRegisteredServers().find((server) => server.domain === normalizedDomain);
71
+
72
+ return NextResponse.json(
73
+ {
74
+ server: createdServer ?? null,
75
+ publicUrl: createServerPublicUrl(normalizedDomain).href,
76
+ },
77
+ { status: 201 },
78
+ );
79
+ }
80
+
46
81
  const result = await createManagedServer(body);
47
82
 
48
83
  if (!result.ok) {
@@ -1,15 +1,39 @@
1
1
  import { headers } from 'next/headers';
2
+ import Link from 'next/link';
3
+ import { redirect } from 'next/navigation';
2
4
  import { $sideEffect } from '../../../../src/utils/organization/$sideEffect';
5
+ import { ForbiddenPage } from '../components/ForbiddenPage/ForbiddenPage';
3
6
  import { HomepagePrimarySections } from '../components/Homepage/HomepagePrimarySections';
4
7
  import { $provideServer } from '../tools/$provideServer';
5
8
  import { isUserAdmin } from '../utils/isUserAdmin';
9
+ import { isUserGlobalAdmin } from '../utils/isUserGlobalAdmin';
10
+ import {
11
+ createServerPublicUrl,
12
+ listRegisteredServersUsingServiceRole,
13
+ resolveRegisteredServerByHost,
14
+ type ServerRecord,
15
+ } from '../utils/serverRegistry';
6
16
  import { getHomePageAgents } from './_data/getHomePageAgents';
7
17
 
8
18
  /**
9
19
  * Renders the simplified agents home page with local and federated agents.
10
20
  */
11
21
  export default async function HomePage() {
12
- $sideEffect(/* Note: [??] This will ensure dynamic rendering of page and avoid Next.js pre-render */ headers());
22
+ const headerStore = await headers();
23
+ $sideEffect(/* Note: [??] This will ensure dynamic rendering of page and avoid Next.js pre-render */ headerStore);
24
+
25
+ const ipAddressRouting = await resolveIpAddressRouting(headerStore.get('host'));
26
+ if (ipAddressRouting === 'LOGIN') {
27
+ return <ForbiddenPage />;
28
+ }
29
+
30
+ if (ipAddressRouting === 'CONFIGURE') {
31
+ redirect('/admin/servers?setup=1');
32
+ }
33
+
34
+ if (Array.isArray(ipAddressRouting)) {
35
+ return <IpAddressDomainChooser servers={ipAddressRouting} />;
36
+ }
13
37
 
14
38
  const [{ publicUrl }, isAdmin, { agents, folders, homepageMessage, currentUser }] = await Promise.all([
15
39
  $provideServer(),
@@ -32,3 +56,79 @@ export default async function HomePage() {
32
56
  </div>
33
57
  );
34
58
  }
59
+
60
+ /**
61
+ * Resolves special raw-IP behavior for standalone VPS access.
62
+ *
63
+ * @param host - Request host header.
64
+ * @returns Routing instruction or `null` when normal homepage rendering should continue.
65
+ */
66
+ async function resolveIpAddressRouting(host: string | null): Promise<'LOGIN' | 'CONFIGURE' | ReadonlyArray<ServerRecord> | null> {
67
+ if (!host || !isIpAddressHost(host)) {
68
+ return null;
69
+ }
70
+
71
+ const registeredServers = await listRegisteredServersUsingServiceRole().catch(() => []);
72
+ if (resolveRegisteredServerByHost(host, registeredServers)) {
73
+ return null;
74
+ }
75
+
76
+ if (registeredServers.length === 0) {
77
+ return (await isUserGlobalAdmin()) ? 'CONFIGURE' : 'LOGIN';
78
+ }
79
+
80
+ if (registeredServers.length === 1) {
81
+ redirect(createServerPublicUrl(registeredServers[0]!.domain).href);
82
+ }
83
+
84
+ return registeredServers;
85
+ }
86
+
87
+ /**
88
+ * Checks whether the request host is a raw IPv4 or IPv6 address.
89
+ *
90
+ * @param host - Raw host header.
91
+ * @returns `true` for IP-address access.
92
+ */
93
+ function isIpAddressHost(host: string): boolean {
94
+ const hostname = host
95
+ .trim()
96
+ .replace(/^\[(.+)\](?::\d+)?$/u, '$1')
97
+ .replace(/:\d+$/u, '');
98
+
99
+ return /^\d{1,3}(?:\.\d{1,3}){3}$/u.test(hostname) || hostname.includes(':');
100
+ }
101
+
102
+ /**
103
+ * Simple domain chooser shown when a raw IP has multiple configured domains.
104
+ *
105
+ * @param props - Configured server list.
106
+ */
107
+ function IpAddressDomainChooser(props: { readonly servers: ReadonlyArray<ServerRecord> }) {
108
+ return (
109
+ <div className="min-h-screen bg-slate-50 px-4 py-24">
110
+ <div className="mx-auto max-w-2xl space-y-6">
111
+ <div>
112
+ <h1 className="text-3xl font-light text-slate-900">Choose a domain</h1>
113
+ <p className="mt-2 text-sm text-slate-600">
114
+ This VPS has multiple Agents Server domains configured. Open the domain you want to use.
115
+ </p>
116
+ </div>
117
+ <div className="grid gap-3">
118
+ {props.servers.map((server) => {
119
+ const url = createServerPublicUrl(server.domain).href;
120
+ return (
121
+ <Link
122
+ key={server.id}
123
+ href={url}
124
+ className="rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm font-semibold text-slate-800 shadow-sm transition hover:border-blue-300 hover:text-blue-700"
125
+ >
126
+ {server.domain}
127
+ </Link>
128
+ );
129
+ })}
130
+ </div>
131
+ </div>
132
+ </div>
133
+ );
134
+ }
@@ -28,6 +28,7 @@ type HeaderTranslate = (key: ServerTranslationKey, variables?: Readonly<Record<s
28
28
  type SystemCategoryLabel =
29
29
  | 'My Account'
30
30
  | 'Utilities'
31
+ | 'Super Admin'
31
32
  | 'Administration'
32
33
  | 'Monitoring & Usage'
33
34
  | 'Integrations & Keys'
@@ -54,6 +55,7 @@ type BuildHeaderSystemMenuItemsOptions = {
54
55
  const SYSTEM_CATEGORY_ICON_MAP: Record<SystemCategoryLabel, LucideIcon> = {
55
56
  'My Account': UserRound,
56
57
  Utilities: Wrench,
58
+ 'Super Admin': Settings2,
57
59
  Administration: Settings2,
58
60
  'Monitoring & Usage': BarChart3,
59
61
  'Integrations & Keys': KeyRound,
@@ -67,6 +69,7 @@ const SYSTEM_CATEGORY_ICON_MAP: Record<SystemCategoryLabel, LucideIcon> = {
67
69
  const SYSTEM_CATEGORY_TRANSLATION_KEY_MAP: Record<SystemCategoryLabel, ServerTranslationKey> = {
68
70
  'My Account': 'header.myAccount',
69
71
  Utilities: 'header.utilities',
72
+ 'Super Admin': 'header.superAdmin',
70
73
  Administration: 'header.administration',
71
74
  'Monitoring & Usage': 'header.monitoringAndUsage',
72
75
  'Integrations & Keys': 'header.integrationsAndKeys',
@@ -187,16 +190,31 @@ export function buildHeaderSystemMenuItems({
187
190
  ];
188
191
  }
189
192
 
190
- const administrationSystemItems: SubMenuItem[] = [
193
+ const superAdminSystemItems: SubMenuItem[] = [
194
+ {
195
+ label: translate('header.servers'),
196
+ href: '/admin/servers',
197
+ isBold: true,
198
+ },
199
+ {
200
+ label: translate('header.environmentVariables'),
201
+ href: '/admin/environment',
202
+ },
191
203
  ...(isGlobalAdmin
192
204
  ? [
193
205
  {
194
- label: translate('header.servers'),
195
- href: '/admin/servers',
196
- isBold: true,
206
+ label: translate('header.logs'),
207
+ href: '/admin/logs',
208
+ } as SubMenuItem,
209
+ {
210
+ label: translate('header.codeRunners'),
211
+ href: '/admin/code-runners',
197
212
  } as SubMenuItem,
198
213
  ]
199
214
  : []),
215
+ ];
216
+
217
+ const administrationSystemItems: SubMenuItem[] = [
200
218
  {
201
219
  label: translate('header.models'),
202
220
  href: '/admin/models',
@@ -311,6 +329,7 @@ export function buildHeaderSystemMenuItems({
311
329
  return [
312
330
  ...createSystemCategory('My Account', userAccountSystemItems, translate),
313
331
  ...createSystemCategory('Utilities', utilitiesSystemItems, translate),
332
+ ...createSystemCategory('Super Admin', superAdminSystemItems, translate),
314
333
  ...createSystemCategory('Administration', administrationSystemItems, translate),
315
334
  ...createSystemCategory('Monitoring & Usage', monitoringAndUsageSystemItems, translate),
316
335
  ...createSystemCategory('Integrations & Keys', integrationsAndKeysSystemItems, translate),
@@ -126,6 +126,7 @@ export const SERVER_TRANSLATION_KEYS = [
126
126
  'header.changePassword',
127
127
  'header.menuLabel',
128
128
  'header.myAccount',
129
+ 'header.superAdmin',
129
130
  'header.administration',
130
131
  'header.monitoringAndUsage',
131
132
  'header.integrationsAndKeys',
@@ -140,6 +141,9 @@ export const SERVER_TRANSLATION_KEYS = [
140
141
  'header.viewAllUsers',
141
142
  'header.createNewUser',
142
143
  'header.servers',
144
+ 'header.environmentVariables',
145
+ 'header.logs',
146
+ 'header.codeRunners',
143
147
  'header.models',
144
148
  'header.openApiDocumentation',
145
149
  'header.apiTokens',
@@ -120,6 +120,7 @@ header.logOut: Odhlásit se
120
120
  header.changePassword: Změnit heslo
121
121
  header.menuLabel: Nabídka
122
122
  header.myAccount: Můj účet
123
+ header.superAdmin: Super administrace
123
124
  header.administration: Administrace
124
125
  header.monitoringAndUsage: Monitoring a využití
125
126
  header.integrationsAndKeys: Integrace a klíče
@@ -134,6 +135,9 @@ header.mockedChats: Ukázkové chaty
134
135
  header.viewAllUsers: Zobrazit všechny uživatele
135
136
  header.createNewUser: Vytvořit nového uživatele
136
137
  header.servers: Servery
138
+ header.environmentVariables: Proměnné prostředí
139
+ header.logs: Logy
140
+ header.codeRunners: Code runnery
137
141
  header.models: Modely
138
142
  header.openApiDocumentation: Dokumentace OpenAPI
139
143
  header.apiTokens: API tokeny
@@ -122,6 +122,7 @@ header.logOut: Log out
122
122
  header.changePassword: Change Password
123
123
  header.menuLabel: Menu
124
124
  header.myAccount: My Account
125
+ header.superAdmin: Super Admin
125
126
  header.administration: Administration
126
127
  header.monitoringAndUsage: Monitoring & Usage
127
128
  header.integrationsAndKeys: Integrations & Keys
@@ -136,6 +137,9 @@ header.mockedChats: Mocked Chats
136
137
  header.viewAllUsers: View all users
137
138
  header.createNewUser: Create new user
138
139
  header.servers: Servers
140
+ header.environmentVariables: Environment variables
141
+ header.logs: Logs
142
+ header.codeRunners: Code runners
139
143
  header.models: Models
140
144
  header.openApiDocumentation: OpenAPI Documentation
141
145
  header.apiTokens: API Tokens
@@ -55,6 +55,14 @@ const getCachedProvidedServer = cache(async (): Promise<ProvidedServer> => {
55
55
  });
56
56
 
57
57
  if (!resolvedSqliteServer) {
58
+ if (isIpAddressHost(requestHost)) {
59
+ return {
60
+ id: null,
61
+ publicUrl: resolveFallbackPublicUrl(requestHost),
62
+ tablePrefix: SUPABASE_TABLE_PREFIX,
63
+ };
64
+ }
65
+
58
66
  throw new Error(`Server with host "${requestHost}" is not registered in SERVERS`);
59
67
  }
60
68
 
@@ -156,3 +164,22 @@ function isLocalDevelopmentHost(host: string | null): boolean {
156
164
  normalizedHost.startsWith('[::1]:')
157
165
  );
158
166
  }
167
+
168
+ /**
169
+ * Checks whether the current request host is a raw IP address.
170
+ *
171
+ * @param host - Raw request host.
172
+ * @returns `true` when the host is IPv4 or IPv6.
173
+ */
174
+ function isIpAddressHost(host: string | null): boolean {
175
+ if (!host) {
176
+ return false;
177
+ }
178
+
179
+ const hostname = host
180
+ .trim()
181
+ .replace(/^\[(.+)\](?::\d+)?$/u, '$1')
182
+ .replace(/:\d+$/u, '');
183
+
184
+ return /^\d{1,3}(?:\.\d{1,3}){3}$/u.test(hostname) || hostname.includes(':');
185
+ }
@@ -241,7 +241,11 @@ export function createServerPublicUrl(domain: string): URL {
241
241
  }
242
242
 
243
243
  const protocol =
244
- normalizedDomain.startsWith('localhost') || normalizedDomain.startsWith('127.0.0.1') ? 'http' : 'https';
244
+ normalizedDomain.startsWith('localhost') ||
245
+ normalizedDomain.startsWith('127.0.0.1') ||
246
+ isIpAddressHost(normalizedDomain)
247
+ ? 'http'
248
+ : 'https';
245
249
  return new URL(`${protocol}://${normalizedDomain}`);
246
250
  }
247
251
 
@@ -435,6 +439,21 @@ function hasHttpProtocol(value: string): boolean {
435
439
  return value.startsWith('http://') || value.startsWith('https://');
436
440
  }
437
441
 
442
+ /**
443
+ * Checks whether a normalized host is a raw IPv4 or IPv6 address.
444
+ *
445
+ * @param host - Normalized host or host with port.
446
+ * @returns `true` for raw IP hosts.
447
+ */
448
+ function isIpAddressHost(host: string): boolean {
449
+ const hostname = host
450
+ .trim()
451
+ .replace(/^\[(.+)\](?::\d+)?$/u, '$1')
452
+ .replace(/:\d+$/u, '');
453
+
454
+ return /^\d{1,3}(?:\.\d{1,3}){3}$/u.test(hostname) || hostname.includes(':');
455
+ }
456
+
438
457
  /**
439
458
  * Checks whether a port is implicit for the given protocol and can be omitted.
440
459
  *
@@ -1,5 +1,5 @@
1
1
  import { createHmac } from 'crypto';
2
- import { cookies } from 'next/headers';
2
+ import { cookies, headers } from 'next/headers';
3
3
  import { cache } from 'react';
4
4
 
5
5
  /**
@@ -30,6 +30,36 @@ export type SessionUser = {
30
30
  readonly isGlobalAdmin?: boolean;
31
31
  };
32
32
 
33
+ /**
34
+ * Request details used to decide whether the auth cookie may require HTTPS.
35
+ */
36
+ export type SessionCookieSecurityContext = {
37
+ /**
38
+ * Current deployment mode.
39
+ */
40
+ readonly isProduction: boolean;
41
+ /**
42
+ * Raw `Host` header.
43
+ */
44
+ readonly host: string | null;
45
+ /**
46
+ * Raw forwarded host header emitted by the reverse proxy.
47
+ */
48
+ readonly forwardedHost: string | null;
49
+ /**
50
+ * Raw forwarded protocol header emitted by the reverse proxy.
51
+ */
52
+ readonly forwardedProto: string | null;
53
+ /**
54
+ * Comma-separated configured domain list from `SERVERS`.
55
+ */
56
+ readonly configuredServers: string | null | undefined;
57
+ /**
58
+ * Known standalone VPS public IP address.
59
+ */
60
+ readonly publicIpAddress: string | null | undefined;
61
+ };
62
+
33
63
  /**
34
64
  * Signs a session payload and serializes it into the cookie token format.
35
65
  *
@@ -77,6 +107,42 @@ export function parseSessionToken(token: string | null | undefined): SessionUser
77
107
  }
78
108
  }
79
109
 
110
+ /**
111
+ * Decides whether the session cookie should keep the `Secure` flag for the current request.
112
+ *
113
+ * This keeps production-domain logins protected by HTTPS while allowing the standalone
114
+ * VPS bootstrap flow to authenticate over `http://<IP_ADDRESS>` before any domain exists.
115
+ *
116
+ * @param context - Request and deployment details used for the decision.
117
+ * @returns `true` when the cookie should require HTTPS.
118
+ */
119
+ export function shouldUseSecureSessionCookieForRequest(context: SessionCookieSecurityContext): boolean {
120
+ if (!context.isProduction) {
121
+ return false;
122
+ }
123
+
124
+ if (parseConfiguredServers(context.configuredServers).length > 0) {
125
+ return true;
126
+ }
127
+
128
+ const requestHost = normalizeHost(context.forwardedHost ?? context.host);
129
+ if (!requestHost || !isIpAddressHost(requestHost)) {
130
+ return true;
131
+ }
132
+
133
+ const forwardedProtocol = (context.forwardedProto || '').split(',')[0]?.trim().toLowerCase() || '';
134
+ if (forwardedProtocol === 'https') {
135
+ return true;
136
+ }
137
+
138
+ const configuredPublicIpAddress = normalizeHost(context.publicIpAddress || '');
139
+ if (configuredPublicIpAddress && requestHost !== configuredPublicIpAddress) {
140
+ return true;
141
+ }
142
+
143
+ return false;
144
+ }
145
+
80
146
  /**
81
147
  * Persists the provided session payload into the signed session cookie.
82
148
  *
@@ -84,10 +150,11 @@ export function parseSessionToken(token: string | null | undefined): SessionUser
84
150
  */
85
151
  export async function setSession(user: SessionUser) {
86
152
  const token = serializeSessionToken(user);
153
+ const secure = await shouldUseSecureSessionCookie();
87
154
 
88
155
  (await cookies()).set(SESSION_COOKIE_NAME, token, {
89
156
  httpOnly: true,
90
- secure: process.env.NODE_ENV === 'production',
157
+ secure,
91
158
  path: '/',
92
159
  maxAge: 60 * 60 * 24 * 365 * 2, // 2 years
93
160
  });
@@ -120,3 +187,57 @@ const getCachedSession = cache(async (): Promise<SessionUser | null> => {
120
187
  export async function getSession(): Promise<SessionUser | null> {
121
188
  return getCachedSession();
122
189
  }
190
+
191
+ /**
192
+ * Resolves the runtime cookie security decision from the current request headers.
193
+ *
194
+ * @returns `true` when the session cookie should keep the `Secure` flag.
195
+ */
196
+ async function shouldUseSecureSessionCookie(): Promise<boolean> {
197
+ const headerStore = await headers();
198
+
199
+ return shouldUseSecureSessionCookieForRequest({
200
+ isProduction: process.env.NODE_ENV === 'production',
201
+ host: headerStore.get('host'),
202
+ forwardedHost: headerStore.get('x-forwarded-host'),
203
+ forwardedProto: headerStore.get('x-forwarded-proto'),
204
+ configuredServers: process.env.SERVERS,
205
+ publicIpAddress: process.env.PTBK_PUBLIC_IP_ADDRESS,
206
+ });
207
+ }
208
+
209
+ /**
210
+ * Parses the configured `SERVERS` CSV into non-empty entries.
211
+ *
212
+ * @param configuredServers - Raw environment value.
213
+ * @returns Normalized list of configured domains.
214
+ */
215
+ function parseConfiguredServers(configuredServers: string | null | undefined): Array<string> {
216
+ return (configuredServers || '')
217
+ .split(',')
218
+ .map((server) => server.trim())
219
+ .filter(Boolean);
220
+ }
221
+
222
+ /**
223
+ * Checks whether a host string points to a raw IPv4 or IPv6 address.
224
+ *
225
+ * @param host - Host header or hostname.
226
+ * @returns `true` when the host is a raw IP address.
227
+ */
228
+ function isIpAddressHost(host: string): boolean {
229
+ return /^\d{1,3}(?:\.\d{1,3}){3}$/u.test(host) || host.includes(':');
230
+ }
231
+
232
+ /**
233
+ * Removes ports and IPv6 brackets from host-like strings.
234
+ *
235
+ * @param host - Raw host header value.
236
+ * @returns Normalized bare hostname or IP address.
237
+ */
238
+ function normalizeHost(host: string | null | undefined): string {
239
+ return (host || '')
240
+ .trim()
241
+ .replace(/^\[(.+)\](?::\d+)?$/u, '$1')
242
+ .replace(/:\d+$/u, '');
243
+ }