@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.
- package/apps/agents-server/playwright.config.ts +2 -1
- package/apps/agents-server/src/app/admin/cli-access/CliAccessClient.tsx +99 -0
- package/apps/agents-server/src/app/admin/cli-access/page.tsx +14 -0
- package/apps/agents-server/src/app/admin/code-runners/CodeRunnersClient.tsx +124 -34
- package/apps/agents-server/src/app/admin/servers/CreateServerDialog.tsx +46 -505
- package/apps/agents-server/src/app/admin/servers/ServersClient.tsx +23 -11
- package/apps/agents-server/src/app/admin/servers/ServersRegistryApi.ts +5 -0
- package/apps/agents-server/src/app/admin/servers/ServersRegistryDnsTypes.ts +87 -0
- package/apps/agents-server/src/app/admin/servers/ServersRegistryTable.tsx +258 -128
- package/apps/agents-server/src/app/admin/servers/useCreateServerWizard.ts +46 -334
- package/apps/agents-server/src/app/admin/servers/useServersRegistryState.ts +26 -2
- package/apps/agents-server/src/app/admin/update/UpdateClient.tsx +435 -0
- package/apps/agents-server/src/app/admin/update/page.tsx +14 -0
- package/apps/agents-server/src/app/agents/[agentName]/chat/CanonicalAgentChatSurface.tsx +24 -0
- package/apps/agents-server/src/app/api/admin/cli-access/route.ts +137 -0
- package/apps/agents-server/src/app/api/admin/code-runners/authentication/route.ts +140 -0
- package/apps/agents-server/src/app/api/admin/code-runners/route.ts +4 -35
- package/apps/agents-server/src/app/api/admin/servers/[serverId]/route.ts +7 -2
- package/apps/agents-server/src/app/api/admin/servers/route.ts +95 -4
- package/apps/agents-server/src/app/api/admin/update/route.ts +52 -0
- package/apps/agents-server/src/app/api/auth/login/route.ts +8 -0
- package/apps/agents-server/src/app/api/auth/logout/route.ts +10 -2
- package/apps/agents-server/src/app/api/chat/export/pdf/route.ts +63 -0
- package/apps/agents-server/src/app/page.tsx +10 -0
- package/apps/agents-server/src/components/AdminTerminal/AdminTerminalCard.tsx +279 -0
- package/apps/agents-server/src/components/AdminTerminal/useAdminTerminalSession.ts +336 -0
- package/apps/agents-server/src/components/Header/buildHeaderSystemMenuItems.ts +10 -0
- package/apps/agents-server/src/languages/ServerTranslationKeys.ts +2 -0
- package/apps/agents-server/src/languages/translations/czech.yaml +2 -0
- package/apps/agents-server/src/languages/translations/english.yaml +2 -0
- package/apps/agents-server/src/middleware.ts +32 -0
- package/apps/agents-server/src/tools/BrowserConnectionProvider.ts +1 -1
- package/apps/agents-server/src/utils/chatExport/downloadChatPdfFromServer.ts +59 -0
- package/apps/agents-server/src/utils/chatExport/renderHtmlToPdfOnServer.ts +37 -0
- package/apps/agents-server/src/utils/codeRunnerAuthentication.ts +234 -0
- package/apps/agents-server/src/utils/codeRunnerConfiguration.ts +67 -0
- package/apps/agents-server/src/utils/createInteractiveTerminalEventStream.ts +84 -0
- package/apps/agents-server/src/utils/interactiveTerminalSession.ts +442 -0
- package/apps/agents-server/src/utils/serverCliAccess.ts +221 -0
- package/apps/agents-server/src/utils/serverManagement/standaloneVpsServerMetadata.ts +145 -0
- package/apps/agents-server/src/utils/serverRegistry.ts +3 -2
- package/apps/agents-server/src/utils/session.ts +37 -9
- package/apps/agents-server/src/utils/shibboleth/createShibbolethAuthenticationLogPayload.ts +173 -0
- package/apps/agents-server/src/utils/shibboleth/writeShibbolethAuthenticationLog.ts +27 -0
- package/apps/agents-server/src/utils/standaloneVpsDnsDiagnostics.ts +258 -0
- package/apps/agents-server/src/utils/standaloneVpsRawIpBootstrap.ts +87 -0
- package/apps/agents-server/src/utils/vpsConfiguration.ts +87 -13
- package/apps/agents-server/src/utils/vpsSelfUpdate.ts +664 -0
- package/esm/apps/agents-server/src/utils/serverRegistry.d.ts +1 -1
- package/esm/index.es.js +7 -5
- package/esm/index.es.js.map +1 -1
- package/esm/src/book-components/Chat/Chat/ChatActionsBar.d.ts +2 -0
- package/esm/src/book-components/Chat/Chat/ChatProps.d.ts +6 -0
- package/esm/src/book-components/Chat/save/_common/ChatSaveFormatHandler.d.ts +35 -0
- package/esm/src/book-components/Chat/save/_common/createChatExportFilename.d.ts +11 -0
- package/esm/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/book-components/Chat/Chat/Chat.tsx +2 -0
- package/src/book-components/Chat/Chat/ChatActionsBar.tsx +17 -9
- package/src/book-components/Chat/Chat/ChatProps.tsx +7 -0
- package/src/book-components/Chat/save/_common/ChatSaveFormatHandler.ts +40 -0
- package/src/book-components/Chat/save/_common/createChatExportFilename.ts +20 -0
- package/src/book-components/Chat/utils/renderMarkdown.ts +1 -3
- package/src/other/templates/getTemplatesPipelineCollection.ts +718 -790
- package/src/scrapers/document/DocumentScraper.ts +1 -1
- package/src/scrapers/document-legacy/LegacyDocumentScraper.ts +1 -1
- package/src/version.ts +2 -2
- package/src/versions.txt +2 -0
- package/umd/apps/agents-server/src/utils/serverRegistry.d.ts +1 -1
- package/umd/index.umd.js +7 -5
- package/umd/index.umd.js.map +1 -1
- package/umd/src/book-components/Chat/Chat/ChatActionsBar.d.ts +2 -0
- package/umd/src/book-components/Chat/Chat/ChatProps.d.ts +6 -0
- package/umd/src/book-components/Chat/save/_common/ChatSaveFormatHandler.d.ts +35 -0
- package/umd/src/book-components/Chat/save/_common/createChatExportFilename.d.ts +11 -0
- package/umd/src/version.d.ts +1 -1
- package/src/conversion/validation/_importPipeline.ts +0 -88
- /package/esm/src/conversion/validation/{_importPipeline.d.ts → _importPipeline.test.d.ts} +0 -0
- /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
|
|
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 (
|
|
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
|
|
165
|
+
const headerStore = await headers();
|
|
166
|
+
const cookieStore = await cookies();
|
|
167
|
+
const secure = shouldUseSecureSessionCookieForHeaders(headerStore);
|
|
154
168
|
|
|
155
|
-
(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|