@promptbook/cli 0.112.0-96 → 0.112.0-98

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 (79) hide show
  1. package/apps/agents-server/playwright.config.ts +2 -1
  2. package/apps/agents-server/src/app/admin/cli-access/CliAccessClient.tsx +99 -0
  3. package/apps/agents-server/src/app/admin/cli-access/page.tsx +14 -0
  4. package/apps/agents-server/src/app/admin/code-runners/CodeRunnersClient.tsx +124 -34
  5. package/apps/agents-server/src/app/admin/servers/CreateServerDialog.tsx +46 -505
  6. package/apps/agents-server/src/app/admin/servers/ServersClient.tsx +23 -11
  7. package/apps/agents-server/src/app/admin/servers/ServersRegistryApi.ts +5 -0
  8. package/apps/agents-server/src/app/admin/servers/ServersRegistryDnsTypes.ts +87 -0
  9. package/apps/agents-server/src/app/admin/servers/ServersRegistryTable.tsx +258 -128
  10. package/apps/agents-server/src/app/admin/servers/useCreateServerWizard.ts +46 -334
  11. package/apps/agents-server/src/app/admin/servers/useServersRegistryState.ts +26 -2
  12. package/apps/agents-server/src/app/admin/update/UpdateClient.tsx +435 -0
  13. package/apps/agents-server/src/app/admin/update/page.tsx +14 -0
  14. package/apps/agents-server/src/app/agents/[agentName]/chat/CanonicalAgentChatSurface.tsx +24 -0
  15. package/apps/agents-server/src/app/api/admin/cli-access/route.ts +137 -0
  16. package/apps/agents-server/src/app/api/admin/code-runners/authentication/route.ts +140 -0
  17. package/apps/agents-server/src/app/api/admin/code-runners/route.ts +4 -35
  18. package/apps/agents-server/src/app/api/admin/servers/[serverId]/route.ts +7 -2
  19. package/apps/agents-server/src/app/api/admin/servers/route.ts +95 -4
  20. package/apps/agents-server/src/app/api/admin/update/route.ts +52 -0
  21. package/apps/agents-server/src/app/api/auth/login/route.ts +8 -0
  22. package/apps/agents-server/src/app/api/auth/logout/route.ts +10 -2
  23. package/apps/agents-server/src/app/api/chat/export/pdf/route.ts +63 -0
  24. package/apps/agents-server/src/app/page.tsx +10 -0
  25. package/apps/agents-server/src/components/AdminTerminal/AdminTerminalCard.tsx +279 -0
  26. package/apps/agents-server/src/components/AdminTerminal/useAdminTerminalSession.ts +336 -0
  27. package/apps/agents-server/src/components/Header/buildHeaderSystemMenuItems.ts +10 -0
  28. package/apps/agents-server/src/languages/ServerTranslationKeys.ts +2 -0
  29. package/apps/agents-server/src/languages/translations/czech.yaml +2 -0
  30. package/apps/agents-server/src/languages/translations/english.yaml +2 -0
  31. package/apps/agents-server/src/middleware.ts +32 -0
  32. package/apps/agents-server/src/tools/BrowserConnectionProvider.ts +1 -1
  33. package/apps/agents-server/src/utils/chatExport/downloadChatPdfFromServer.ts +59 -0
  34. package/apps/agents-server/src/utils/chatExport/renderHtmlToPdfOnServer.ts +37 -0
  35. package/apps/agents-server/src/utils/codeRunnerAuthentication.ts +234 -0
  36. package/apps/agents-server/src/utils/codeRunnerConfiguration.ts +67 -0
  37. package/apps/agents-server/src/utils/createInteractiveTerminalEventStream.ts +84 -0
  38. package/apps/agents-server/src/utils/interactiveTerminalSession.ts +442 -0
  39. package/apps/agents-server/src/utils/serverCliAccess.ts +221 -0
  40. package/apps/agents-server/src/utils/serverManagement/standaloneVpsServerMetadata.ts +145 -0
  41. package/apps/agents-server/src/utils/serverRegistry.ts +3 -2
  42. package/apps/agents-server/src/utils/session.ts +37 -9
  43. package/apps/agents-server/src/utils/shibboleth/createShibbolethAuthenticationLogPayload.ts +173 -0
  44. package/apps/agents-server/src/utils/shibboleth/writeShibbolethAuthenticationLog.ts +27 -0
  45. package/apps/agents-server/src/utils/standaloneVpsDnsDiagnostics.ts +258 -0
  46. package/apps/agents-server/src/utils/standaloneVpsRawIpBootstrap.ts +87 -0
  47. package/apps/agents-server/src/utils/vpsConfiguration.ts +87 -13
  48. package/apps/agents-server/src/utils/vpsSelfUpdate.ts +664 -0
  49. package/esm/apps/agents-server/src/utils/serverRegistry.d.ts +1 -1
  50. package/esm/index.es.js +7 -5
  51. package/esm/index.es.js.map +1 -1
  52. package/esm/src/book-components/Chat/Chat/ChatActionsBar.d.ts +2 -0
  53. package/esm/src/book-components/Chat/Chat/ChatProps.d.ts +6 -0
  54. package/esm/src/book-components/Chat/save/_common/ChatSaveFormatHandler.d.ts +35 -0
  55. package/esm/src/book-components/Chat/save/_common/createChatExportFilename.d.ts +11 -0
  56. package/esm/src/version.d.ts +1 -1
  57. package/package.json +1 -1
  58. package/src/book-components/Chat/Chat/Chat.tsx +2 -0
  59. package/src/book-components/Chat/Chat/ChatActionsBar.tsx +17 -9
  60. package/src/book-components/Chat/Chat/ChatProps.tsx +7 -0
  61. package/src/book-components/Chat/save/_common/ChatSaveFormatHandler.ts +40 -0
  62. package/src/book-components/Chat/save/_common/createChatExportFilename.ts +20 -0
  63. package/src/book-components/Chat/utils/renderMarkdown.ts +1 -3
  64. package/src/other/templates/getTemplatesPipelineCollection.ts +718 -790
  65. package/src/scrapers/document/DocumentScraper.ts +1 -1
  66. package/src/scrapers/document-legacy/LegacyDocumentScraper.ts +1 -1
  67. package/src/version.ts +2 -2
  68. package/src/versions.txt +2 -0
  69. package/umd/apps/agents-server/src/utils/serverRegistry.d.ts +1 -1
  70. package/umd/index.umd.js +7 -5
  71. package/umd/index.umd.js.map +1 -1
  72. package/umd/src/book-components/Chat/Chat/ChatActionsBar.d.ts +2 -0
  73. package/umd/src/book-components/Chat/Chat/ChatProps.d.ts +6 -0
  74. package/umd/src/book-components/Chat/save/_common/ChatSaveFormatHandler.d.ts +35 -0
  75. package/umd/src/book-components/Chat/save/_common/createChatExportFilename.d.ts +11 -0
  76. package/umd/src/version.d.ts +1 -1
  77. package/src/conversion/validation/_importPipeline.ts +0 -88
  78. /package/esm/src/conversion/validation/{_importPipeline.d.ts → _importPipeline.test.d.ts} +0 -0
  79. /package/umd/src/conversion/validation/{_importPipeline.d.ts → _importPipeline.test.d.ts} +0 -0
@@ -0,0 +1,145 @@
1
+ import type { SupabaseClient } from '@supabase/supabase-js';
2
+ import { spaceTrim } from 'spacetrim';
3
+ import { DatabaseError } from '../../../../../src/errors/DatabaseError';
4
+ import { $provideSupabaseForServer } from '../../database/$provideSupabaseForServer';
5
+ import type { ServerRecord } from '../serverRegistry';
6
+
7
+ /**
8
+ * Metadata key storing the human-facing server name.
9
+ *
10
+ * @private constant of standalone VPS server management.
11
+ */
12
+ const SERVER_NAME_METADATA_KEY = 'SERVER_NAME';
13
+
14
+ /**
15
+ * Metadata keys that use the uploaded server icon.
16
+ *
17
+ * @private constant of standalone VPS server management.
18
+ */
19
+ const SERVER_ICON_METADATA_KEYS = ['SERVER_LOGO_URL', 'SERVER_FAVICON_URL'] as const;
20
+
21
+ /**
22
+ * Minimal metadata row used by standalone VPS server setup.
23
+ *
24
+ * @private type of standalone VPS server management.
25
+ */
26
+ type StandaloneVpsMetadataRow = {
27
+ /**
28
+ * Metadata key.
29
+ */
30
+ readonly key: string;
31
+
32
+ /**
33
+ * Metadata value.
34
+ */
35
+ readonly value: string;
36
+
37
+ /**
38
+ * Optional admin-facing note.
39
+ */
40
+ readonly note: string | null;
41
+
42
+ /**
43
+ * Last update timestamp.
44
+ */
45
+ readonly updatedAt: string;
46
+ };
47
+
48
+ /**
49
+ * Applies visible standalone VPS setup values into the prefixed metadata table.
50
+ *
51
+ * @param input - Server prefix and optional metadata values.
52
+ *
53
+ * @private internal standalone VPS server management helper.
54
+ */
55
+ export async function applyStandaloneVpsServerMetadata(input: {
56
+ readonly tablePrefix: string;
57
+ readonly name?: string | null;
58
+ readonly iconUrl?: string | null;
59
+ }): Promise<void> {
60
+ const metadataRows = createStandaloneVpsMetadataRows(input);
61
+ if (metadataRows.length === 0) {
62
+ return;
63
+ }
64
+
65
+ const metadataTableName = `${input.tablePrefix}Metadata`;
66
+ const supabase = $provideSupabaseForServer() as SupabaseClient;
67
+ const { error } = await supabase.from(metadataTableName).upsert([...metadataRows], {
68
+ onConflict: 'key',
69
+ });
70
+
71
+ if (error) {
72
+ throw new DatabaseError(
73
+ spaceTrim(`
74
+ Failed to update standalone VPS server metadata.
75
+
76
+ ${error.message}
77
+ `),
78
+ );
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Resolves the configured display name for a standalone VPS virtual server.
84
+ *
85
+ * @param server - Virtual server row derived from the `SERVERS` environment variable.
86
+ * @returns Metadata server name or the virtual row name when metadata is missing.
87
+ *
88
+ * @private internal standalone VPS server management helper.
89
+ */
90
+ export async function resolveStandaloneVpsServerDisplayName(server: ServerRecord): Promise<string> {
91
+ const metadataTableName = `${server.tablePrefix}Metadata`;
92
+ const supabase = $provideSupabaseForServer() as SupabaseClient;
93
+ const { data, error } = await supabase
94
+ .from(metadataTableName)
95
+ .select('value')
96
+ .eq('key', SERVER_NAME_METADATA_KEY)
97
+ .maybeSingle<{ readonly value: string | null }>();
98
+
99
+ if (error) {
100
+ return server.name;
101
+ }
102
+
103
+ const metadataName = typeof data?.value === 'string' ? data.value.trim() : '';
104
+ return metadataName || server.name;
105
+ }
106
+
107
+ /**
108
+ * Creates metadata rows from visible setup fields.
109
+ *
110
+ * @param input - Raw setup values.
111
+ * @returns Metadata rows ready for upsert.
112
+ *
113
+ * @private function of standalone VPS server management.
114
+ */
115
+ function createStandaloneVpsMetadataRows(input: {
116
+ readonly name?: string | null;
117
+ readonly iconUrl?: string | null;
118
+ }): ReadonlyArray<StandaloneVpsMetadataRow> {
119
+ const updatedAt = new Date().toISOString();
120
+ const rows: Array<StandaloneVpsMetadataRow> = [];
121
+ const name = typeof input.name === 'string' ? input.name.trim() : '';
122
+ const iconUrl = typeof input.iconUrl === 'string' ? input.iconUrl.trim() : '';
123
+
124
+ if (name) {
125
+ rows.push({
126
+ key: SERVER_NAME_METADATA_KEY,
127
+ value: name,
128
+ note: null,
129
+ updatedAt,
130
+ });
131
+ }
132
+
133
+ if (iconUrl) {
134
+ for (const key of SERVER_ICON_METADATA_KEYS) {
135
+ rows.push({
136
+ key,
137
+ value: iconUrl,
138
+ note: null,
139
+ updatedAt,
140
+ });
141
+ }
142
+ }
143
+
144
+ return rows;
145
+ }
@@ -171,13 +171,14 @@ export async function listRegisteredServersUsingServiceRole(options?: {
171
171
  /**
172
172
  * Loads virtual server records from the comma-separated `SERVERS` environment variable.
173
173
  *
174
- * @returns Server records with deterministic table prefixes derived from normalized domains.
174
+ * @returns Server records with deterministic table prefixes from the configured server prefix or normalized domains.
175
175
  */
176
176
  export function listEnvironmentRegisteredServers(): Array<ServerRecord> {
177
177
  const rawServers = process.env[SERVERS_ENV_NAME];
178
178
  if (!rawServers) {
179
179
  return [];
180
180
  }
181
+ const configuredTablePrefix = process.env.SUPABASE_TABLE_PREFIX?.trim() || '';
181
182
 
182
183
  const normalizedDomains = uniqueStrings(
183
184
  rawServers
@@ -191,7 +192,7 @@ export function listEnvironmentRegisteredServers(): Array<ServerRecord> {
191
192
  name: domain,
192
193
  environment: SERVER_ENVIRONMENT.PRODUCTION,
193
194
  domain,
194
- tablePrefix: buildEnvironmentServerTablePrefix(domain),
195
+ tablePrefix: configuredTablePrefix || buildEnvironmentServerTablePrefix(domain),
195
196
  createdAt: ENVIRONMENT_SERVER_TIMESTAMP,
196
197
  updatedAt: ENVIRONMENT_SERVER_TIMESTAMP,
197
198
  }));
@@ -1,6 +1,8 @@
1
1
  import { createHmac } from 'crypto';
2
2
  import { cookies, headers } from 'next/headers';
3
3
  import { cache } from 'react';
4
+ import { isStandaloneVpsRawIpBootstrapActive } from './standaloneVpsRawIpBootstrap';
5
+ import { writeShibbolethAuthenticationLog } from './shibboleth/writeShibbolethAuthenticationLog';
4
6
 
5
7
  /**
6
8
  * Cookie name used to store the signed user session.
@@ -50,6 +52,10 @@ export type SessionCookieSecurityContext = {
50
52
  * Raw forwarded protocol header emitted by the reverse proxy.
51
53
  */
52
54
  readonly forwardedProto: string | null;
55
+ /**
56
+ * Canonical public site URL from `NEXT_PUBLIC_SITE_URL`.
57
+ */
58
+ readonly nextPublicSiteUrl: string | null | undefined;
53
59
  /**
54
60
  * Comma-separated configured domain list from `SERVERS`.
55
61
  */
@@ -121,7 +127,13 @@ export function shouldUseSecureSessionCookieForRequest(context: SessionCookieSec
121
127
  return false;
122
128
  }
123
129
 
124
- if (parseConfiguredServers(context.configuredServers).length > 0) {
130
+ if (
131
+ parseConfiguredServers(context.configuredServers).length > 0 &&
132
+ !isStandaloneVpsRawIpBootstrapActive({
133
+ nextPublicSiteUrl: context.nextPublicSiteUrl,
134
+ publicIpAddress: context.publicIpAddress,
135
+ })
136
+ ) {
125
137
  return true;
126
138
  }
127
139
 
@@ -150,9 +162,17 @@ export function shouldUseSecureSessionCookieForRequest(context: SessionCookieSec
150
162
  */
151
163
  export async function setSession(user: SessionUser) {
152
164
  const token = serializeSessionToken(user);
153
- const secure = await shouldUseSecureSessionCookie();
165
+ const headerStore = await headers();
166
+ const cookieStore = await cookies();
167
+ const secure = shouldUseSecureSessionCookieForHeaders(headerStore);
154
168
 
155
- (await cookies()).set(SESSION_COOKIE_NAME, token, {
169
+ writeShibbolethAuthenticationLog(headerStore, {
170
+ event: 'session-set',
171
+ hasSessionCookie: cookieStore.has(SESSION_COOKIE_NAME),
172
+ isSecureSessionCookie: secure,
173
+ });
174
+
175
+ cookieStore.set(SESSION_COOKIE_NAME, token, {
156
176
  httpOnly: true,
157
177
  secure,
158
178
  path: '/',
@@ -164,9 +184,17 @@ export async function setSession(user: SessionUser) {
164
184
  * Clears the current authenticated session cookie.
165
185
  */
166
186
  export async function clearSession() {
167
- (await cookies()).delete(SESSION_COOKIE_NAME);
187
+ const headerStore = await headers();
188
+ const cookieStore = await cookies();
189
+
190
+ writeShibbolethAuthenticationLog(headerStore, {
191
+ event: 'session-cleared',
192
+ hasSessionCookie: cookieStore.has(SESSION_COOKIE_NAME),
193
+ });
194
+
195
+ cookieStore.delete(SESSION_COOKIE_NAME);
168
196
  // Also clear legacy adminToken
169
- (await cookies()).delete('adminToken');
197
+ cookieStore.delete('adminToken');
170
198
  }
171
199
 
172
200
  /**
@@ -189,18 +217,18 @@ export async function getSession(): Promise<SessionUser | null> {
189
217
  }
190
218
 
191
219
  /**
192
- * Resolves the runtime cookie security decision from the current request headers.
220
+ * Resolves the runtime cookie security decision from one request header snapshot.
193
221
  *
222
+ * @param headerStore - Request headers from the active request.
194
223
  * @returns `true` when the session cookie should keep the `Secure` flag.
195
224
  */
196
- async function shouldUseSecureSessionCookie(): Promise<boolean> {
197
- const headerStore = await headers();
198
-
225
+ function shouldUseSecureSessionCookieForHeaders(headerStore: Pick<Headers, 'get'>): boolean {
199
226
  return shouldUseSecureSessionCookieForRequest({
200
227
  isProduction: process.env.NODE_ENV === 'production',
201
228
  host: headerStore.get('host'),
202
229
  forwardedHost: headerStore.get('x-forwarded-host'),
203
230
  forwardedProto: headerStore.get('x-forwarded-proto'),
231
+ nextPublicSiteUrl: process.env.NEXT_PUBLIC_SITE_URL,
204
232
  configuredServers: process.env.SERVERS,
205
233
  publicIpAddress: process.env.PTBK_PUBLIC_IP_ADDRESS,
206
234
  });
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Minimal read-only header accessor shared by middleware, route handlers, and server utilities.
3
+ *
4
+ * @private Internal helper of the Shibboleth authentication diagnostics.
5
+ */
6
+ export type ReadonlyHeadersLike = Pick<Headers, 'get'>;
7
+
8
+ /**
9
+ * One resolved Shibboleth-related attribute included in the diagnostic payload.
10
+ *
11
+ * @private Internal helper of the Shibboleth authentication diagnostics.
12
+ */
13
+ export type ShibbolethAuthenticationLogAttribute = {
14
+ readonly fieldName: string;
15
+ readonly headerName: string;
16
+ readonly fingerprint: string;
17
+ readonly valueLength: number;
18
+ };
19
+
20
+ /**
21
+ * Structured, privacy-preserving diagnostic payload for one Shibboleth-related request event.
22
+ *
23
+ * @private Internal helper of the Agents Server authentication diagnostics.
24
+ */
25
+ export type ShibbolethAuthenticationLogPayload = {
26
+ readonly event: string;
27
+ readonly pathname?: string;
28
+ readonly method?: string;
29
+ readonly hasSessionCookie?: boolean;
30
+ readonly isSecureSessionCookie?: boolean;
31
+ readonly headerNames: ReadonlyArray<string>;
32
+ readonly attributeFields: ReadonlyArray<string>;
33
+ readonly attributes: ReadonlyArray<ShibbolethAuthenticationLogAttribute>;
34
+ };
35
+
36
+ /**
37
+ * Context describing where the Shibboleth diagnostic event happened.
38
+ *
39
+ * @private Internal helper of the Shibboleth authentication diagnostics.
40
+ */
41
+ export type CreateShibbolethAuthenticationLogPayloadOptions = {
42
+ readonly event: string;
43
+ readonly pathname?: string;
44
+ readonly method?: string;
45
+ readonly hasSessionCookie?: boolean;
46
+ readonly isSecureSessionCookie?: boolean;
47
+ };
48
+
49
+ type ShibbolethHeaderDefinition = {
50
+ readonly fieldName: string;
51
+ readonly headerNames: ReadonlyArray<string>;
52
+ };
53
+
54
+ const SHIBBOLETH_HEADER_DEFINITIONS: ReadonlyArray<ShibbolethHeaderDefinition> = [
55
+ { fieldName: 'sessionId', headerNames: ['shib-session-id', 'x-shib-session-id'] },
56
+ { fieldName: 'sessionIndex', headerNames: ['shib-session-index', 'x-shib-session-index'] },
57
+ { fieldName: 'sessionExpires', headerNames: ['shib-session-expires', 'x-shib-session-expires'] },
58
+ { fieldName: 'applicationId', headerNames: ['shib-application-id', 'x-shib-application-id'] },
59
+ { fieldName: 'remoteUser', headerNames: ['remote-user', 'x-remote-user'] },
60
+ { fieldName: 'displayName', headerNames: ['displayname', 'x-displayname'] },
61
+ { fieldName: 'mail', headerNames: ['mail', 'x-mail'] },
62
+ { fieldName: 'unstructuredName', headerNames: ['unstructuredname', 'x-unstructuredname'] },
63
+ {
64
+ fieldName: 'eduPersonPrincipalName',
65
+ headerNames: ['edupersonprincipalname', 'eppn', 'x-edupersonprincipalname', 'x-eppn'],
66
+ },
67
+ { fieldName: 'persistentId', headerNames: ['persistent-id', 'x-persistent-id'] },
68
+ { fieldName: 'nameId', headerNames: ['name-id', 'x-name-id'] },
69
+ ];
70
+
71
+ /**
72
+ * Builds a privacy-preserving Shibboleth diagnostic payload from incoming request headers.
73
+ *
74
+ * Only the presence of relevant headers plus short fingerprints of their values are logged,
75
+ * never the raw personally identifiable header contents.
76
+ *
77
+ * @param headers - Request headers or another read-only header accessor.
78
+ * @param options - Event metadata describing where the diagnostic event originated.
79
+ * @returns Structured log payload, or `null` when the request does not look Shibboleth-related.
80
+ *
81
+ * @private Internal helper of the Agents Server authentication diagnostics.
82
+ */
83
+ export function createShibbolethAuthenticationLogPayload(
84
+ headers: ReadonlyHeadersLike,
85
+ options: CreateShibbolethAuthenticationLogPayloadOptions,
86
+ ): ShibbolethAuthenticationLogPayload | null {
87
+ const attributes = SHIBBOLETH_HEADER_DEFINITIONS.map((definition) => resolveShibbolethHeader(headers, definition)).filter(
88
+ (attribute): attribute is ShibbolethAuthenticationLogAttribute => attribute !== null,
89
+ );
90
+
91
+ if (attributes.length === 0) {
92
+ return null;
93
+ }
94
+
95
+ return {
96
+ event: options.event,
97
+ pathname: options.pathname,
98
+ method: options.method,
99
+ hasSessionCookie: options.hasSessionCookie,
100
+ isSecureSessionCookie: options.isSecureSessionCookie,
101
+ headerNames: attributes.map(({ headerName }) => headerName),
102
+ attributeFields: attributes.map(({ fieldName }) => fieldName),
103
+ attributes,
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Resolves one Shibboleth attribute from any of its expected header aliases.
109
+ *
110
+ * @param headers - Request header accessor.
111
+ * @param definition - Logical Shibboleth field with supported header aliases.
112
+ * @returns Sanitized attribute snapshot or `null` when absent.
113
+ */
114
+ function resolveShibbolethHeader(
115
+ headers: ReadonlyHeadersLike,
116
+ definition: ShibbolethHeaderDefinition,
117
+ ): ShibbolethAuthenticationLogAttribute | null {
118
+ for (const headerName of definition.headerNames) {
119
+ const rawValue = headers.get(headerName);
120
+ const value = normalizeHeaderValue(rawValue);
121
+
122
+ if (!value) {
123
+ continue;
124
+ }
125
+
126
+ return {
127
+ fieldName: definition.fieldName,
128
+ headerName,
129
+ fingerprint: createHeaderValueFingerprint(value),
130
+ valueLength: value.length,
131
+ };
132
+ }
133
+
134
+ return null;
135
+ }
136
+
137
+ /**
138
+ * Normalizes one header value before diagnostic processing.
139
+ *
140
+ * @param value - Raw header value.
141
+ * @returns Trimmed non-empty value or `null`.
142
+ */
143
+ function normalizeHeaderValue(value: string | null): string | null {
144
+ const normalizedValue = value?.trim() || '';
145
+ return normalizedValue === '' ? null : normalizedValue;
146
+ }
147
+
148
+ /**
149
+ * Creates a short stable fingerprint so logs can correlate the same identity/session
150
+ * without storing the raw Shibboleth attribute value.
151
+ *
152
+ * @param value - Raw Shibboleth header value.
153
+ * @returns Short deterministic fingerprint.
154
+ */
155
+ function createHeaderValueFingerprint(value: string): string {
156
+ let hashPrimary = 0xdeadbeef ^ value.length;
157
+ let hashSecondary = 0x41c6ce57 ^ value.length;
158
+
159
+ for (const character of value) {
160
+ const codePoint = character.codePointAt(0) || 0;
161
+ hashPrimary = Math.imul(hashPrimary ^ codePoint, 2654435761);
162
+ hashSecondary = Math.imul(hashSecondary ^ codePoint, 1597334677);
163
+ }
164
+
165
+ hashPrimary =
166
+ Math.imul(hashPrimary ^ (hashPrimary >>> 16), 2246822507) ^ Math.imul(hashSecondary ^ (hashSecondary >>> 13), 3266489909);
167
+ hashSecondary =
168
+ Math.imul(hashSecondary ^ (hashSecondary >>> 16), 2246822507) ^
169
+ Math.imul(hashPrimary ^ (hashPrimary >>> 13), 3266489909);
170
+
171
+ const combinedHash = 4294967296 * (2097151 & hashSecondary) + (hashPrimary >>> 0);
172
+ return combinedHash.toString(16).padStart(14, '0').slice(0, 12);
173
+ }
@@ -0,0 +1,27 @@
1
+ import {
2
+ createShibbolethAuthenticationLogPayload,
3
+ type CreateShibbolethAuthenticationLogPayloadOptions,
4
+ type ReadonlyHeadersLike,
5
+ } from './createShibbolethAuthenticationLogPayload';
6
+
7
+ /**
8
+ * Writes one sanitized Shibboleth diagnostic event to the application logs when
9
+ * the current request carries Shibboleth-related headers.
10
+ *
11
+ * @param headers - Request headers or another read-only header accessor.
12
+ * @param options - Event metadata describing where the diagnostic event originated.
13
+ *
14
+ * @private Internal helper of the Agents Server authentication diagnostics.
15
+ */
16
+ export function writeShibbolethAuthenticationLog(
17
+ headers: ReadonlyHeadersLike,
18
+ options: CreateShibbolethAuthenticationLogPayloadOptions,
19
+ ): void {
20
+ const payload = createShibbolethAuthenticationLogPayload(headers, options);
21
+
22
+ if (!payload) {
23
+ return;
24
+ }
25
+
26
+ console.info(`[auth][shibboleth] ${JSON.stringify(payload)}`);
27
+ }