@treasuryspatial/surface-kit 0.1.10 → 0.1.16
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/dist/index.d.ts +162 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +550 -46
- package/dist/server.d.ts +24 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +487 -131
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -4
package/dist/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server.js';
|
|
2
|
-
import { applySurfaceBrandingToManifest, normalizeSurfaceBrandingPayload, } from './index.js';
|
|
2
|
+
import { applySurfaceBrandingToManifest, mapSurfaceBrandingManifestImages, orderSurfaceModeTabs, normalizeSurfaceBrandingPayload, } from './index.js';
|
|
3
3
|
export const SURFACE_AUTH_COOKIE_NAME = 'treasury_auth_token';
|
|
4
4
|
const DEFAULT_ADMIN_API_URL = 'https://admin-api.treasury.space/api';
|
|
5
5
|
const DEFAULT_BRANDING_SCOPE = ['admin'];
|
|
@@ -8,16 +8,26 @@ const DEFAULT_BRANDING_TTL = 600;
|
|
|
8
8
|
const DEFAULT_BRANDING_AUDIENCE = 'assets';
|
|
9
9
|
const DEFAULT_BYPASS_EXACT = new Set(['/favicon.ico', '/robots.txt', '/sitemap.xml']);
|
|
10
10
|
const DEFAULT_BYPASS_PREFIXES = ['/_next', '/fonts', '/textures'];
|
|
11
|
-
const DEFAULT_BYPASS_PATTERN = /\.(?:css|js|map|ico|png|jpg|jpeg|gif|svg|webp|avif|woff2?|ttf|otf|eot|wasm|json|txt|xml|csv)$/i;
|
|
11
|
+
const DEFAULT_BYPASS_PATTERN = /\.(?:css|js|map|ico|png|jpg|jpeg|gif|svg|webp|avif|woff2?|ttf|otf|eot|wasm|json|txt|xml|csv|wav|mp3|m4a|ogg)$/i;
|
|
12
|
+
export const SURFACE_CONTEXT_HEADER = 'x-treasury-surface-context';
|
|
13
|
+
const sanitizeEnvValue = (value) => value?.replace(/[\r\n]+/g, '').trim() || '';
|
|
12
14
|
const readEnv = (...values) => {
|
|
13
15
|
for (const value of values) {
|
|
14
|
-
const
|
|
15
|
-
if (
|
|
16
|
-
return
|
|
16
|
+
const normalized = sanitizeEnvValue(value);
|
|
17
|
+
if (normalized)
|
|
18
|
+
return normalized;
|
|
17
19
|
}
|
|
18
20
|
return '';
|
|
19
21
|
};
|
|
20
22
|
const normalizeTenant = (value) => String(value || '').trim().toLowerCase();
|
|
23
|
+
const asMembershipStringArray = (value) => Array.isArray(value)
|
|
24
|
+
? value
|
|
25
|
+
.map((entry) => String(entry || '').trim().toLowerCase())
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
: [];
|
|
28
|
+
const membershipArrayKeys = [
|
|
29
|
+
'tenantSurfaceAccess',
|
|
30
|
+
];
|
|
21
31
|
const isLocalUrl = (value) => value.includes('localhost') || value.includes('127.0.0.1');
|
|
22
32
|
const buildServiceAuthHeaders = () => {
|
|
23
33
|
const serviceToken = readEnv(process.env.TREASURY_SERVICE_TOKEN);
|
|
@@ -61,7 +71,7 @@ export const normalizeRequestHost = (value) => (value || '')
|
|
|
61
71
|
.trim()
|
|
62
72
|
.toLowerCase()
|
|
63
73
|
.replace(/:\d+$/, '');
|
|
64
|
-
export const resolveRequestHost = (headers) => normalizeRequestHost(headers.get('x-forwarded-host') || headers.get('host'));
|
|
74
|
+
export const resolveRequestHost = (headers) => normalizeRequestHost(headers.get('x-tenant-host') || headers.get('x-forwarded-host') || headers.get('host'));
|
|
65
75
|
const isHeaderBag = (value) => Boolean(value) && typeof value === 'object' && typeof value.get === 'function';
|
|
66
76
|
const coerceHeaderBag = (requestOrHeaders) => {
|
|
67
77
|
if (isHeaderBag(requestOrHeaders)) {
|
|
@@ -75,12 +85,101 @@ const coerceHeaderBag = (requestOrHeaders) => {
|
|
|
75
85
|
}
|
|
76
86
|
throw new TypeError('Surface request context requires a headers-like object');
|
|
77
87
|
};
|
|
88
|
+
const serializeSurfaceContext = (surfaceContext) => encodeURIComponent(JSON.stringify({
|
|
89
|
+
publicHost: surfaceContext.publicHost,
|
|
90
|
+
hostSlug: surfaceContext.hostSlug,
|
|
91
|
+
tenantSlug: surfaceContext.tenantSlug,
|
|
92
|
+
effectiveTenantSlug: surfaceContext.effectiveTenantSlug,
|
|
93
|
+
subtenantSlug: surfaceContext.subtenantSlug,
|
|
94
|
+
surfaceId: surfaceContext.surfaceId,
|
|
95
|
+
brandProfile: surfaceContext.brandProfile,
|
|
96
|
+
compatibilityMode: surfaceContext.compatibilityMode,
|
|
97
|
+
isMarketingHost: surfaceContext.isMarketingHost,
|
|
98
|
+
}));
|
|
99
|
+
const parseSurfaceContext = (value) => {
|
|
100
|
+
if (!value)
|
|
101
|
+
return null;
|
|
102
|
+
try {
|
|
103
|
+
const parsed = JSON.parse(decodeURIComponent(value));
|
|
104
|
+
if (typeof parsed.publicHost !== 'string' ||
|
|
105
|
+
typeof parsed.hostSlug !== 'string' ||
|
|
106
|
+
typeof parsed.tenantSlug !== 'string' ||
|
|
107
|
+
typeof parsed.effectiveTenantSlug !== 'string' ||
|
|
108
|
+
typeof parsed.surfaceId !== 'string' ||
|
|
109
|
+
typeof parsed.brandProfile !== 'string' ||
|
|
110
|
+
typeof parsed.compatibilityMode !== 'string' ||
|
|
111
|
+
typeof parsed.isMarketingHost !== 'boolean') {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
publicHost: parsed.publicHost,
|
|
116
|
+
hostSlug: parsed.hostSlug,
|
|
117
|
+
tenantSlug: parsed.tenantSlug,
|
|
118
|
+
effectiveTenantSlug: parsed.effectiveTenantSlug,
|
|
119
|
+
subtenantSlug: typeof parsed.subtenantSlug === 'string' ? parsed.subtenantSlug : null,
|
|
120
|
+
surfaceId: parsed.surfaceId,
|
|
121
|
+
brandProfile: parsed.brandProfile,
|
|
122
|
+
compatibilityMode: parsed.compatibilityMode,
|
|
123
|
+
isMarketingHost: parsed.isMarketingHost,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
export const readSurfaceContextHeader = (headers) => parseSurfaceContext(headers.get(SURFACE_CONTEXT_HEADER));
|
|
131
|
+
export const writeSurfaceContextHeaders = (headers, surfaceContext) => {
|
|
132
|
+
headers.set(SURFACE_CONTEXT_HEADER, serializeSurfaceContext(surfaceContext));
|
|
133
|
+
return headers;
|
|
134
|
+
};
|
|
135
|
+
const buildSurfaceContextRequestHeaders = (request, surfaceContext) => writeSurfaceContextHeaders(new Headers(request.headers), surfaceContext);
|
|
136
|
+
const isNextRequestLike = (value) => Boolean(value &&
|
|
137
|
+
typeof value === 'object' &&
|
|
138
|
+
'nextUrl' in value &&
|
|
139
|
+
value.nextUrl &&
|
|
140
|
+
typeof value.nextUrl.searchParams?.get === 'function');
|
|
141
|
+
const readSurfaceRequestResolutionHints = (requestOrHeaders) => {
|
|
142
|
+
if (!isNextRequestLike(requestOrHeaders))
|
|
143
|
+
return {};
|
|
144
|
+
const searchParams = requestOrHeaders.nextUrl.searchParams;
|
|
145
|
+
return {
|
|
146
|
+
requestedSubtenantSlug: searchParams.get('subtenant') ||
|
|
147
|
+
searchParams.get('subtenantSlug') ||
|
|
148
|
+
searchParams.get('requestedSubtenantSlug'),
|
|
149
|
+
requestedSubtenantId: searchParams.get('subtenantId') ||
|
|
150
|
+
searchParams.get('requestedSubtenantId'),
|
|
151
|
+
surfaceId: searchParams.get('surfaceId'),
|
|
152
|
+
pathname: requestOrHeaders.nextUrl.pathname,
|
|
153
|
+
};
|
|
154
|
+
};
|
|
78
155
|
export const resolveSurfaceRequestContext = (requestOrHeaders, resolveSurfaceHostContext) => {
|
|
79
156
|
const headers = coerceHeaderBag(requestOrHeaders);
|
|
80
157
|
const host = resolveRequestHost(headers);
|
|
158
|
+
const stampedSurfaceContext = readSurfaceContextHeader(headers);
|
|
159
|
+
if (stampedSurfaceContext) {
|
|
160
|
+
return {
|
|
161
|
+
host,
|
|
162
|
+
surfaceContext: stampedSurfaceContext,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
81
165
|
return {
|
|
82
166
|
host,
|
|
83
|
-
surfaceContext: resolveSurfaceHostContext(host),
|
|
167
|
+
surfaceContext: resolveSurfaceHostContext(host, readSurfaceRequestResolutionHints(requestOrHeaders)),
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
export const resolveSurfaceRequestContextAsync = async (requestOrHeaders, resolveSurfaceHostContext) => {
|
|
171
|
+
const headers = coerceHeaderBag(requestOrHeaders);
|
|
172
|
+
const host = resolveRequestHost(headers);
|
|
173
|
+
const stampedSurfaceContext = readSurfaceContextHeader(headers);
|
|
174
|
+
if (stampedSurfaceContext) {
|
|
175
|
+
return {
|
|
176
|
+
host,
|
|
177
|
+
surfaceContext: stampedSurfaceContext,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
host,
|
|
182
|
+
surfaceContext: await resolveSurfaceHostContext(host, readSurfaceRequestResolutionHints(requestOrHeaders)),
|
|
84
183
|
};
|
|
85
184
|
};
|
|
86
185
|
export const resolveAdminApiUrl = (pathname, explicitBase) => {
|
|
@@ -94,12 +193,13 @@ export const resolveAdminApiUrl = (pathname, explicitBase) => {
|
|
|
94
193
|
return `${base}${pathname}`;
|
|
95
194
|
return `${base}/api${pathname}`;
|
|
96
195
|
};
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
196
|
+
const postJsonCandidates = async (pathname, body, { fetchImpl = fetch, adminApiUrl, } = {}) => {
|
|
197
|
+
return fetchImpl(resolveAdminApiUrl(pathname, adminApiUrl), {
|
|
198
|
+
method: 'POST',
|
|
199
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
200
|
+
body: JSON.stringify(body),
|
|
201
|
+
cache: 'no-store',
|
|
202
|
+
});
|
|
103
203
|
};
|
|
104
204
|
export const resolveTenantLookupSlug = (surfaceContext) => surfaceContext.subtenantSlug ? surfaceContext.tenantSlug : surfaceContext.effectiveTenantSlug;
|
|
105
205
|
export const fetchSurfaceTenant = async (surfaceContext, options = {}) => {
|
|
@@ -107,55 +207,166 @@ export const fetchSurfaceTenant = async (surfaceContext, options = {}) => {
|
|
|
107
207
|
const subtenantQuery = surfaceContext.subtenantSlug
|
|
108
208
|
? `&subtenantSlug=${encodeURIComponent(surfaceContext.subtenantSlug)}`
|
|
109
209
|
: '';
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
210
|
+
const baseUrl = resolveAdminApiUrl('', options.adminApiUrl).replace(/\/$/, '');
|
|
211
|
+
const url = `${baseUrl}/tenants/${encodeURIComponent(slug)}?view=${encodeURIComponent(options.view ?? 'composer')}${subtenantQuery}`;
|
|
212
|
+
return fetchJson(url, options);
|
|
213
|
+
};
|
|
214
|
+
export const fetchSurfaceTenantBySlug = async (slug, options = {}) => {
|
|
215
|
+
const normalizedSlug = normalizeTenant(slug);
|
|
216
|
+
if (!normalizedSlug)
|
|
217
|
+
return null;
|
|
218
|
+
const baseUrl = resolveAdminApiUrl('', options.adminApiUrl).replace(/\/$/, '');
|
|
219
|
+
const url = `${baseUrl}/tenants/${encodeURIComponent(normalizedSlug)}?view=${encodeURIComponent(options.view ?? 'composer')}`;
|
|
220
|
+
return fetchJson(url, options);
|
|
117
221
|
};
|
|
118
222
|
export const fetchSurfaceBranding = async (surfaceContext, options = {}) => {
|
|
119
223
|
const slug = resolveTenantLookupSlug(surfaceContext);
|
|
120
224
|
const subtenantQuery = surfaceContext.subtenantSlug
|
|
121
225
|
? `?subtenantSlug=${encodeURIComponent(surfaceContext.subtenantSlug)}`
|
|
122
226
|
: '';
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
? `&subtenantSlug=${encodeURIComponent(surfaceContext.subtenantSlug)}`
|
|
129
|
-
: ''}`,
|
|
130
|
-
]
|
|
131
|
-
: [`${baseUrl}/branding`];
|
|
132
|
-
for (const url of urls) {
|
|
133
|
-
const payload = await fetchJson(url, options);
|
|
134
|
-
if (payload)
|
|
135
|
-
return payload;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return null;
|
|
227
|
+
const baseUrl = resolveAdminApiUrl('', options.adminApiUrl).replace(/\/$/, '');
|
|
228
|
+
const url = slug
|
|
229
|
+
? `${baseUrl}/branding/${encodeURIComponent(slug)}${subtenantQuery}`
|
|
230
|
+
: `${baseUrl}/branding`;
|
|
231
|
+
return fetchJson(url, options);
|
|
139
232
|
};
|
|
140
233
|
const isRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
141
234
|
const asString = (value) => (typeof value === 'string' && value.trim() ? value.trim() : undefined);
|
|
142
235
|
const asStringArray = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === 'string' && entry.trim().length > 0) : undefined;
|
|
143
236
|
const joinNonEmpty = (...values) => values.filter(Boolean).join(' ').trim() || undefined;
|
|
237
|
+
const normalizeManifestImage = (value, fallbackAlt) => {
|
|
238
|
+
if (typeof value === 'string' && value.trim()) {
|
|
239
|
+
return { src: value.trim(), alt: fallbackAlt };
|
|
240
|
+
}
|
|
241
|
+
if (!isRecord(value))
|
|
242
|
+
return undefined;
|
|
243
|
+
const src = asString(value.src);
|
|
244
|
+
if (!src)
|
|
245
|
+
return undefined;
|
|
246
|
+
return {
|
|
247
|
+
src,
|
|
248
|
+
alt: asString(value.alt) ?? fallbackAlt,
|
|
249
|
+
};
|
|
250
|
+
};
|
|
251
|
+
const normalizeTopbarLinks = (value) => {
|
|
252
|
+
if (!Array.isArray(value))
|
|
253
|
+
return undefined;
|
|
254
|
+
const links = value
|
|
255
|
+
.map((entry) => {
|
|
256
|
+
if (!isRecord(entry))
|
|
257
|
+
return null;
|
|
258
|
+
const label = asString(entry.label);
|
|
259
|
+
const route = entry.route === 'intro' || entry.route === 'compose'
|
|
260
|
+
? entry.route
|
|
261
|
+
: undefined;
|
|
262
|
+
const href = asString(entry.href);
|
|
263
|
+
if (!label || (!route && !href))
|
|
264
|
+
return null;
|
|
265
|
+
const target = entry.target === '_blank' || entry.target === '_self' ? entry.target : undefined;
|
|
266
|
+
return {
|
|
267
|
+
label,
|
|
268
|
+
...(route ? { route } : {}),
|
|
269
|
+
...(href ? { href } : {}),
|
|
270
|
+
...(target ? { target } : {}),
|
|
271
|
+
};
|
|
272
|
+
})
|
|
273
|
+
.filter((entry) => entry !== null);
|
|
274
|
+
return links.length > 0 ? links : undefined;
|
|
275
|
+
};
|
|
276
|
+
const TOPBAR_CONTEXTS = ['landing', 'login', 'composer', 'admin'];
|
|
277
|
+
const normalizeTopbarContext = (value) => {
|
|
278
|
+
if (!isRecord(value))
|
|
279
|
+
return undefined;
|
|
280
|
+
const title = asString(value.title);
|
|
281
|
+
const subtitle = asString(value.subtitle);
|
|
282
|
+
const productLabel = asString(value.productLabel);
|
|
283
|
+
const productSuffixLabel = asString(value.productSuffixLabel);
|
|
284
|
+
const links = normalizeTopbarLinks(value.links);
|
|
285
|
+
if (!title && !subtitle && !productLabel && !productSuffixLabel && !links) {
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
...(title ? { title } : {}),
|
|
290
|
+
...(subtitle ? { subtitle } : {}),
|
|
291
|
+
...(productLabel ? { productLabel } : {}),
|
|
292
|
+
...(productSuffixLabel ? { productSuffixLabel } : {}),
|
|
293
|
+
...(links ? { links } : {}),
|
|
294
|
+
};
|
|
295
|
+
};
|
|
296
|
+
const normalizeTopbarContexts = (value) => {
|
|
297
|
+
if (!isRecord(value))
|
|
298
|
+
return undefined;
|
|
299
|
+
const contexts = TOPBAR_CONTEXTS.reduce((acc, context) => {
|
|
300
|
+
const normalized = normalizeTopbarContext(value[context]);
|
|
301
|
+
if (normalized) {
|
|
302
|
+
acc[context] = normalized;
|
|
303
|
+
}
|
|
304
|
+
return acc;
|
|
305
|
+
}, {});
|
|
306
|
+
return Object.keys(contexts).length > 0 ? contexts : undefined;
|
|
307
|
+
};
|
|
144
308
|
const normalizeNavigation = (value) => {
|
|
145
309
|
if (!isRecord(value))
|
|
146
310
|
return undefined;
|
|
147
311
|
const surfaceTitle = asString(value.surfaceTitle);
|
|
148
312
|
const introHref = asString(value.introHref);
|
|
149
313
|
const composeHref = asString(value.composeHref);
|
|
314
|
+
const introTitle = asString(value.introTitle);
|
|
315
|
+
const composeTitle = asString(value.composeTitle);
|
|
150
316
|
return surfaceTitle && introHref && composeHref
|
|
151
317
|
? {
|
|
152
318
|
surfaceTitle,
|
|
153
319
|
introHref,
|
|
154
320
|
composeHref,
|
|
321
|
+
...(introTitle ? { introTitle } : {}),
|
|
322
|
+
...(composeTitle ? { composeTitle } : {}),
|
|
155
323
|
searchEnabled: typeof value.searchEnabled === 'boolean' ? value.searchEnabled : undefined,
|
|
156
324
|
}
|
|
157
325
|
: undefined;
|
|
158
326
|
};
|
|
327
|
+
const normalizeTopbar = (value) => {
|
|
328
|
+
if (!isRecord(value))
|
|
329
|
+
return undefined;
|
|
330
|
+
const title = asString(value.title);
|
|
331
|
+
const subtitle = asString(value.subtitle);
|
|
332
|
+
const productLabel = asString(value.productLabel);
|
|
333
|
+
const productSuffixLabel = asString(value.productSuffixLabel);
|
|
334
|
+
const secondaryLogo = normalizeManifestImage(value.secondaryLogo, productLabel || 'Secondary');
|
|
335
|
+
const links = normalizeTopbarLinks(value.links);
|
|
336
|
+
const contexts = normalizeTopbarContexts(value.contexts);
|
|
337
|
+
if (!title && !subtitle && !productLabel && !productSuffixLabel && !secondaryLogo && !links && !contexts) {
|
|
338
|
+
return undefined;
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
...(secondaryLogo ? { secondaryLogo } : {}),
|
|
342
|
+
...(title ? { title } : {}),
|
|
343
|
+
...(subtitle ? { subtitle } : {}),
|
|
344
|
+
...(productLabel ? { productLabel } : {}),
|
|
345
|
+
...(productSuffixLabel ? { productSuffixLabel } : {}),
|
|
346
|
+
...(links ? { links } : {}),
|
|
347
|
+
...(contexts ? { contexts } : {}),
|
|
348
|
+
};
|
|
349
|
+
};
|
|
350
|
+
const mergeTopbar = (base, override) => {
|
|
351
|
+
const contexts = base?.contexts || override?.contexts
|
|
352
|
+
? TOPBAR_CONTEXTS.reduce((acc, context) => {
|
|
353
|
+
const mergedContext = {
|
|
354
|
+
...(base?.contexts?.[context] ?? {}),
|
|
355
|
+
...(override?.contexts?.[context] ?? {}),
|
|
356
|
+
};
|
|
357
|
+
if (Object.keys(mergedContext).length > 0) {
|
|
358
|
+
acc[context] = mergedContext;
|
|
359
|
+
}
|
|
360
|
+
return acc;
|
|
361
|
+
}, {})
|
|
362
|
+
: undefined;
|
|
363
|
+
const merged = {
|
|
364
|
+
...(base ?? {}),
|
|
365
|
+
...(override ?? {}),
|
|
366
|
+
...(contexts && Object.keys(contexts).length > 0 ? { contexts } : {}),
|
|
367
|
+
};
|
|
368
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
369
|
+
};
|
|
159
370
|
const normalizeLoginContent = (value) => {
|
|
160
371
|
if (!isRecord(value))
|
|
161
372
|
return undefined;
|
|
@@ -178,29 +389,82 @@ const normalizeLandingContent = (value) => {
|
|
|
178
389
|
const header = asString(value.header) ?? asString(value.hero?.title);
|
|
179
390
|
const strap = asString(value.strap) ?? asString(value.hero?.subtitle);
|
|
180
391
|
const heroDescription = asString(value.hero?.description);
|
|
181
|
-
const
|
|
182
|
-
? value.sections.flatMap((section) =>
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
392
|
+
const sections = Array.isArray(value.sections)
|
|
393
|
+
? value.sections.flatMap((section) => {
|
|
394
|
+
if (!isRecord(section))
|
|
395
|
+
return [];
|
|
396
|
+
const title = asString(section.title);
|
|
397
|
+
const paragraphs = asStringArray(section.paragraphs);
|
|
398
|
+
const bullets = asStringArray(section.bullets);
|
|
399
|
+
return title || (paragraphs?.length ?? 0) > 0 || (bullets?.length ?? 0) > 0
|
|
400
|
+
? [
|
|
401
|
+
{
|
|
402
|
+
...(title ? { title } : {}),
|
|
403
|
+
...(paragraphs?.length ? { paragraphs } : {}),
|
|
404
|
+
...(bullets?.length ? { bullets } : {}),
|
|
405
|
+
},
|
|
406
|
+
]
|
|
407
|
+
: [];
|
|
408
|
+
})
|
|
409
|
+
: undefined;
|
|
410
|
+
const sectionParagraphs = sections?.flatMap((section) => section.paragraphs ?? []) ?? [];
|
|
186
411
|
const bodyParagraphs = asStringArray(value.bodyParagraphs) ?? [
|
|
187
412
|
...(heroDescription ? [heroDescription] : []),
|
|
188
413
|
...sectionParagraphs,
|
|
189
414
|
];
|
|
190
|
-
return header && strap && bodyParagraphs.length > 0
|
|
415
|
+
return header && strap && (bodyParagraphs.length > 0 || (sections?.length ?? 0) > 0)
|
|
191
416
|
? {
|
|
192
417
|
header,
|
|
193
418
|
strap,
|
|
194
419
|
bodyParagraphs,
|
|
420
|
+
...(sections?.length ? { sections } : {}),
|
|
195
421
|
}
|
|
196
422
|
: undefined;
|
|
197
423
|
};
|
|
198
424
|
const normalizeComposerContent = (value) => {
|
|
199
425
|
if (!isRecord(value))
|
|
200
426
|
return undefined;
|
|
427
|
+
const subtitle = asString(value.subtitle);
|
|
201
428
|
const login = normalizeLoginContent(value.login);
|
|
202
429
|
const landing = normalizeLandingContent(value.landing);
|
|
203
|
-
|
|
430
|
+
const legacyStraps = [login?.strap, landing?.strap]
|
|
431
|
+
.map((entry) => entry?.trim())
|
|
432
|
+
.filter((entry) => Boolean(entry));
|
|
433
|
+
const sharedLegacySubtitle = legacyStraps.length > 0 && legacyStraps.every((entry) => entry === legacyStraps[0])
|
|
434
|
+
? legacyStraps[0]
|
|
435
|
+
: undefined;
|
|
436
|
+
const resolvedSubtitle = subtitle ?? sharedLegacySubtitle;
|
|
437
|
+
return resolvedSubtitle || login || landing
|
|
438
|
+
? {
|
|
439
|
+
...(resolvedSubtitle ? { subtitle: resolvedSubtitle } : {}),
|
|
440
|
+
...(login ? { login } : {}),
|
|
441
|
+
...(landing ? { landing } : {}),
|
|
442
|
+
}
|
|
443
|
+
: undefined;
|
|
444
|
+
};
|
|
445
|
+
const buildComposerContentWarnings = (content) => {
|
|
446
|
+
if (!content)
|
|
447
|
+
return [];
|
|
448
|
+
const warnings = [];
|
|
449
|
+
const pageStraps = [
|
|
450
|
+
['content.login.strap', content.login?.strap],
|
|
451
|
+
['content.landing.strap', content.landing?.strap],
|
|
452
|
+
].filter((entry) => typeof entry[1] === 'string' && entry[1].trim().length > 0);
|
|
453
|
+
const sharedSubtitle = content.subtitle?.trim();
|
|
454
|
+
if (sharedSubtitle) {
|
|
455
|
+
for (const [path, strap] of pageStraps) {
|
|
456
|
+
if (strap.trim() !== sharedSubtitle) {
|
|
457
|
+
warnings.push(`${path} differs from canonical content.subtitle; login and landing subtitles must share one data source.`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
else if (pageStraps.length > 1) {
|
|
462
|
+
const firstStrap = pageStraps[0]?.[1].trim();
|
|
463
|
+
if (firstStrap && pageStraps.some(([, strap]) => strap.trim() !== firstStrap)) {
|
|
464
|
+
warnings.push('content.subtitle is missing and legacy login/landing straps disagree; add canonical content.subtitle.');
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return warnings;
|
|
204
468
|
};
|
|
205
469
|
const normalizeModeTabs = (value) => {
|
|
206
470
|
if (!Array.isArray(value))
|
|
@@ -210,13 +474,16 @@ const normalizeModeTabs = (value) => {
|
|
|
210
474
|
return false;
|
|
211
475
|
return typeof entry.id === 'string' && typeof entry.label === 'string' && typeof entry.href === 'string';
|
|
212
476
|
});
|
|
213
|
-
return tabs.length > 0 ? tabs : undefined;
|
|
477
|
+
return tabs.length > 0 ? orderSurfaceModeTabs(tabs) : undefined;
|
|
214
478
|
};
|
|
215
479
|
const normalizeRoutePolicy = (value) => {
|
|
216
480
|
if (!isRecord(value))
|
|
217
481
|
return undefined;
|
|
218
482
|
return {
|
|
219
483
|
homeRoute: asString(value.homeRoute),
|
|
484
|
+
landingMode: value.landingMode === 'composer' || value.landingMode === 'product'
|
|
485
|
+
? value.landingMode
|
|
486
|
+
: undefined,
|
|
220
487
|
publicRoutePrefixes: asStringArray(value.publicRoutePrefixes),
|
|
221
488
|
adminRoutePrefixes: asStringArray(value.adminRoutePrefixes),
|
|
222
489
|
gatedModeIds: asStringArray(value.gatedModeIds),
|
|
@@ -224,27 +491,41 @@ const normalizeRoutePolicy = (value) => {
|
|
|
224
491
|
privateApiPrefixes: asStringArray(value.privateApiPrefixes),
|
|
225
492
|
};
|
|
226
493
|
};
|
|
227
|
-
const
|
|
494
|
+
const normalizeRuntimeRecordRef = (value) => {
|
|
495
|
+
if (typeof value === 'string') {
|
|
496
|
+
const id = asString(value);
|
|
497
|
+
return id ? { id } : undefined;
|
|
498
|
+
}
|
|
228
499
|
if (!isRecord(value))
|
|
229
500
|
return undefined;
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
continue;
|
|
234
|
-
entries.push([mode, { id: ref.id, version: asString(ref.version) }]);
|
|
235
|
-
}
|
|
236
|
-
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
501
|
+
const id = asString(value.id) ?? asString(value.slug);
|
|
502
|
+
const version = asString(value.version);
|
|
503
|
+
return id ? { id, version } : undefined;
|
|
237
504
|
};
|
|
238
505
|
const normalizeRuntime = (value) => {
|
|
239
506
|
if (!isRecord(value))
|
|
240
507
|
return undefined;
|
|
508
|
+
const surfacePackage = normalizeRuntimeRecordRef(value.surfacePackage ?? value.surfacePackageId);
|
|
509
|
+
const skinProfile = normalizeRuntimeRecordRef(value.skinProfile ?? value.skinProfileId);
|
|
510
|
+
const promptPolicy = normalizeRuntimeRecordRef(value.promptPolicy ?? value.promptPolicyId);
|
|
511
|
+
const toolCatalogSource = value.toolCatalogSource === 'registry-live' || value.toolCatalogSource === 'toolset'
|
|
512
|
+
? value.toolCatalogSource
|
|
513
|
+
: undefined;
|
|
514
|
+
const toolset = toolCatalogSource === 'registry-live'
|
|
515
|
+
? undefined
|
|
516
|
+
: normalizeRuntimeRecordRef(value.toolset ?? value.toolsetId);
|
|
241
517
|
return {
|
|
518
|
+
surfacePackage,
|
|
519
|
+
skinProfile,
|
|
520
|
+
promptPolicy,
|
|
521
|
+
toolCatalogSource,
|
|
522
|
+
toolset,
|
|
242
523
|
defaultToolId: asString(value.defaultToolId),
|
|
243
|
-
promptPolicyId: asString(value.promptPolicyId),
|
|
244
|
-
toolsetId: asString(value.toolsetId),
|
|
524
|
+
promptPolicyId: asString(value.promptPolicyId) ?? promptPolicy?.id,
|
|
525
|
+
toolsetId: toolCatalogSource === 'registry-live' ? undefined : (asString(value.toolsetId) ?? toolset?.id),
|
|
245
526
|
customModeIds: asStringArray(value.customModeIds),
|
|
246
|
-
|
|
247
|
-
|
|
527
|
+
visibleToolIds: asStringArray(value.visibleToolIds),
|
|
528
|
+
hiddenToolIds: asStringArray(value.hiddenToolIds),
|
|
248
529
|
promptPackCatalogMode: value.promptPackCatalogMode === 'all' || value.promptPackCatalogMode === 'surface'
|
|
249
530
|
? value.promptPackCatalogMode
|
|
250
531
|
: undefined,
|
|
@@ -273,6 +554,42 @@ const normalizeStudio = (value) => {
|
|
|
273
554
|
shotTemplates: Array.isArray(value.shotTemplates) ? value.shotTemplates : undefined,
|
|
274
555
|
};
|
|
275
556
|
};
|
|
557
|
+
const normalizeFeedbackRoadmapItems = (value) => {
|
|
558
|
+
if (!Array.isArray(value))
|
|
559
|
+
return undefined;
|
|
560
|
+
const items = value
|
|
561
|
+
.filter(isRecord)
|
|
562
|
+
.map((entry) => ({
|
|
563
|
+
id: asString(entry.id)?.toLowerCase() ?? '',
|
|
564
|
+
name: asString(entry.name) ?? asString(entry.label) ?? asString(entry.title) ?? '',
|
|
565
|
+
description: asString(entry.description) ?? asString(entry.summary),
|
|
566
|
+
category: asString(entry.category)?.toLowerCase(),
|
|
567
|
+
status: asString(entry.status)?.toLowerCase(),
|
|
568
|
+
enabled: typeof entry.enabled === 'boolean' ? entry.enabled : undefined,
|
|
569
|
+
sortOrder: Number.isFinite(Number(entry.sortOrder)) ? Number(entry.sortOrder) : undefined,
|
|
570
|
+
}))
|
|
571
|
+
.filter((entry) => entry.id && entry.name);
|
|
572
|
+
return items.length > 0 ? items : undefined;
|
|
573
|
+
};
|
|
574
|
+
const normalizeFeedbackPanel = (value) => {
|
|
575
|
+
if (!isRecord(value))
|
|
576
|
+
return undefined;
|
|
577
|
+
const roadmap = isRecord(value.roadmap)
|
|
578
|
+
? {
|
|
579
|
+
enabled: typeof value.roadmap.enabled === 'boolean' ? value.roadmap.enabled : undefined,
|
|
580
|
+
title: asString(value.roadmap.title),
|
|
581
|
+
description: asString(value.roadmap.description),
|
|
582
|
+
rankingMode: asString(value.roadmap.rankingMode),
|
|
583
|
+
items: normalizeFeedbackRoadmapItems(value.roadmap.items),
|
|
584
|
+
}
|
|
585
|
+
: undefined;
|
|
586
|
+
return {
|
|
587
|
+
enabled: typeof value.enabled === 'boolean' ? value.enabled : undefined,
|
|
588
|
+
title: asString(value.title),
|
|
589
|
+
description: asString(value.description),
|
|
590
|
+
roadmap,
|
|
591
|
+
};
|
|
592
|
+
};
|
|
276
593
|
const resolveComposerDocument = (raw) => {
|
|
277
594
|
if (!isRecord(raw))
|
|
278
595
|
return null;
|
|
@@ -289,52 +606,26 @@ export const normalizeSurfaceComposerData = (raw) => {
|
|
|
289
606
|
const surfaceId = asString(document.surfaceId);
|
|
290
607
|
if (!tenantSlug || !surfaceId)
|
|
291
608
|
return null;
|
|
292
|
-
const
|
|
293
|
-
const
|
|
294
|
-
const
|
|
295
|
-
const
|
|
296
|
-
defaultToolId: legacySurfaceConfig ? asString(legacySurfaceConfig.defaultToolId) : undefined,
|
|
297
|
-
promptPolicyId: asString(document.promptPolicyId),
|
|
298
|
-
toolsetId: asString(document.toolsetId),
|
|
299
|
-
manifestPaths: legacyPluginRegistry ? asStringArray(legacyPluginRegistry.manifestPaths) : undefined,
|
|
300
|
-
promptPacksByMode: legacyPromptPacks ? normalizePromptPacksByMode(legacyPromptPacks.byMode) : undefined,
|
|
301
|
-
};
|
|
302
|
-
const routePolicy = normalizeRoutePolicy(document.routePolicy) ?? {
|
|
303
|
-
homeRoute: legacySurfaceConfig ? asString(legacySurfaceConfig.homeRoute) : undefined,
|
|
304
|
-
publicRoutePrefixes: legacySurfaceConfig ? asStringArray(legacySurfaceConfig.publicRoutePrefixes) : undefined,
|
|
305
|
-
adminRoutePrefixes: legacySurfaceConfig ? asStringArray(legacySurfaceConfig.adminRoutePrefixes) : undefined,
|
|
306
|
-
gatedModeIds: legacySurfaceConfig ? asStringArray(legacySurfaceConfig.gatedModeIds) : undefined,
|
|
307
|
-
privateRoutePrefixes: legacySurfaceConfig ? asStringArray(legacySurfaceConfig.privateRoutePrefixes) : undefined,
|
|
308
|
-
privateApiPrefixes: legacySurfaceConfig ? asStringArray(legacySurfaceConfig.privateApiPrefixes) : undefined,
|
|
309
|
-
};
|
|
609
|
+
const runtime = normalizeRuntime(document.runtime);
|
|
610
|
+
const routePolicy = normalizeRoutePolicy(document.routePolicy);
|
|
611
|
+
const content = normalizeComposerContent(document.content);
|
|
612
|
+
const controlPlaneWarnings = buildComposerContentWarnings(content);
|
|
310
613
|
return {
|
|
311
614
|
tenantSlug,
|
|
312
615
|
subtenantSlug: asString(document.subtenantSlug) ?? null,
|
|
313
616
|
surfaceId,
|
|
314
|
-
title: asString(document.title)
|
|
315
|
-
lockedLabel: asString(document.lockedLabel)
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
asString(document.title) ??
|
|
321
|
-
asString(legacySurfaceConfig.title) ??
|
|
322
|
-
'Composer',
|
|
323
|
-
introHref: asString(legacySurfaceConfig.introHref) ?? '/',
|
|
324
|
-
composeHref: asString(legacySurfaceConfig.composeHref) ??
|
|
325
|
-
asString(legacySurfaceConfig.homeRoute) ??
|
|
326
|
-
'/compose',
|
|
327
|
-
searchEnabled: typeof legacySurfaceConfig.searchEnabled === 'boolean'
|
|
328
|
-
? legacySurfaceConfig.searchEnabled
|
|
329
|
-
: undefined,
|
|
330
|
-
}
|
|
331
|
-
: undefined),
|
|
332
|
-
content: normalizeComposerContent(document.content),
|
|
333
|
-
modeTabs: normalizeModeTabs(document.modeTabs) ?? (legacySurfaceConfig ? normalizeModeTabs(legacySurfaceConfig.modeTabs) : undefined),
|
|
617
|
+
title: asString(document.title),
|
|
618
|
+
lockedLabel: asString(document.lockedLabel),
|
|
619
|
+
topbar: normalizeTopbar(document.topbar),
|
|
620
|
+
navigation: normalizeNavigation(document.navigation),
|
|
621
|
+
content,
|
|
622
|
+
modeTabs: normalizeModeTabs(document.modeTabs),
|
|
334
623
|
routePolicy,
|
|
335
624
|
runtime,
|
|
336
625
|
featureFlags: normalizeFeatureFlags(document.featureFlags),
|
|
337
626
|
studio: normalizeStudio(document.studio),
|
|
627
|
+
feedbackPanel: normalizeFeedbackPanel(document.feedbackPanel),
|
|
628
|
+
controlPlaneWarnings: controlPlaneWarnings.length > 0 ? controlPlaneWarnings : undefined,
|
|
338
629
|
};
|
|
339
630
|
};
|
|
340
631
|
export const mergeSurfaceComposerData = (base, override) => {
|
|
@@ -344,30 +635,53 @@ export const mergeSurfaceComposerData = (base, override) => {
|
|
|
344
635
|
return override ?? null;
|
|
345
636
|
if (!override)
|
|
346
637
|
return base;
|
|
638
|
+
const mergedModeTabs = override.modeTabs ?? base.modeTabs;
|
|
639
|
+
const preserveRegistryLiveRuntime = base.runtime?.toolCatalogSource === 'registry-live';
|
|
640
|
+
const mergedToolCatalogSource = preserveRegistryLiveRuntime
|
|
641
|
+
? 'registry-live'
|
|
642
|
+
: (override.runtime?.toolCatalogSource ?? base.runtime?.toolCatalogSource);
|
|
643
|
+
const mergedRuntime = base.runtime || override.runtime
|
|
644
|
+
? {
|
|
645
|
+
...(base.runtime ?? {}),
|
|
646
|
+
...(override.runtime ?? {}),
|
|
647
|
+
surfacePackage: override.runtime?.surfacePackage ?? base.runtime?.surfacePackage,
|
|
648
|
+
skinProfile: override.runtime?.skinProfile ?? base.runtime?.skinProfile,
|
|
649
|
+
promptPolicy: override.runtime?.promptPolicy ?? base.runtime?.promptPolicy,
|
|
650
|
+
toolCatalogSource: mergedToolCatalogSource,
|
|
651
|
+
toolset: mergedToolCatalogSource === 'registry-live'
|
|
652
|
+
? undefined
|
|
653
|
+
: (override.runtime?.toolset ?? base.runtime?.toolset),
|
|
654
|
+
defaultToolId: override.runtime?.defaultToolId ?? base.runtime?.defaultToolId,
|
|
655
|
+
promptPolicyId: override.runtime?.promptPolicyId ?? base.runtime?.promptPolicyId,
|
|
656
|
+
toolsetId: mergedToolCatalogSource === 'registry-live'
|
|
657
|
+
? undefined
|
|
658
|
+
: (override.runtime?.toolsetId ?? base.runtime?.toolsetId),
|
|
659
|
+
customModeIds: override.runtime?.customModeIds ?? base.runtime?.customModeIds,
|
|
660
|
+
visibleToolIds: override.runtime?.visibleToolIds ?? base.runtime?.visibleToolIds,
|
|
661
|
+
hiddenToolIds: override.runtime?.hiddenToolIds ?? base.runtime?.hiddenToolIds,
|
|
662
|
+
promptPackCatalogMode: override.runtime?.promptPackCatalogMode ?? base.runtime?.promptPackCatalogMode,
|
|
663
|
+
moduleCatalogMode: override.runtime?.moduleCatalogMode ?? base.runtime?.moduleCatalogMode,
|
|
664
|
+
}
|
|
665
|
+
: undefined;
|
|
347
666
|
return {
|
|
348
667
|
tenantSlug: override.tenantSlug || base.tenantSlug,
|
|
349
668
|
subtenantSlug: override.subtenantSlug ?? base.subtenantSlug,
|
|
350
669
|
surfaceId: override.surfaceId || base.surfaceId,
|
|
351
670
|
title: override.title ?? base.title,
|
|
352
671
|
lockedLabel: override.lockedLabel ?? base.lockedLabel,
|
|
672
|
+
topbar: mergeTopbar(base.topbar, override.topbar),
|
|
353
673
|
navigation: override.navigation ?? base.navigation,
|
|
354
674
|
content: {
|
|
675
|
+
subtitle: override.content?.subtitle ?? base.content?.subtitle,
|
|
355
676
|
login: override.content?.login ?? base.content?.login,
|
|
356
677
|
landing: override.content?.landing ?? base.content?.landing,
|
|
357
678
|
},
|
|
358
|
-
modeTabs:
|
|
679
|
+
modeTabs: mergedModeTabs ? orderSurfaceModeTabs(mergedModeTabs) : undefined,
|
|
359
680
|
routePolicy: {
|
|
360
681
|
...base.routePolicy,
|
|
361
682
|
...override.routePolicy,
|
|
362
683
|
},
|
|
363
|
-
runtime:
|
|
364
|
-
...base.runtime,
|
|
365
|
-
...override.runtime,
|
|
366
|
-
promptPacksByMode: {
|
|
367
|
-
...(base.runtime?.promptPacksByMode ?? {}),
|
|
368
|
-
...(override.runtime?.promptPacksByMode ?? {}),
|
|
369
|
-
},
|
|
370
|
-
},
|
|
684
|
+
runtime: mergedRuntime,
|
|
371
685
|
featureFlags: {
|
|
372
686
|
...base.featureFlags,
|
|
373
687
|
...override.featureFlags,
|
|
@@ -376,6 +690,7 @@ export const mergeSurfaceComposerData = (base, override) => {
|
|
|
376
690
|
...base.studio,
|
|
377
691
|
...override.studio,
|
|
378
692
|
},
|
|
693
|
+
feedbackPanel: override.feedbackPanel ?? base.feedbackPanel,
|
|
379
694
|
};
|
|
380
695
|
};
|
|
381
696
|
export const fetchSurfaceComposerData = async (surfaceContext, options = {}) => {
|
|
@@ -383,13 +698,12 @@ export const fetchSurfaceComposerData = async (surfaceContext, options = {}) =>
|
|
|
383
698
|
const subtenantQuery = surfaceContext.subtenantSlug
|
|
384
699
|
? `&subtenantSlug=${encodeURIComponent(surfaceContext.subtenantSlug)}`
|
|
385
700
|
: '';
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
}
|
|
701
|
+
const baseUrl = resolveAdminApiUrl('', fetchOptions.adminApiUrl).replace(/\/$/, '');
|
|
702
|
+
const url = `${baseUrl}/platform/composer-data?tenantId=${encodeURIComponent(surfaceContext.tenantSlug)}${subtenantQuery}&surfaceId=${encodeURIComponent(surfaceContext.surfaceId || 'composer')}`;
|
|
703
|
+
const payload = await fetchJson(url, fetchOptions);
|
|
704
|
+
const normalized = normalizeSurfaceComposerData(payload);
|
|
705
|
+
if (normalized) {
|
|
706
|
+
return mergeSurfaceComposerData(fallbackData, normalized);
|
|
393
707
|
}
|
|
394
708
|
if (fallbackData || !strict) {
|
|
395
709
|
return fallbackData;
|
|
@@ -418,9 +732,34 @@ export const findSurfaceMembership = (memberships = [], tenantSlug) => {
|
|
|
418
732
|
return membershipTenant === target;
|
|
419
733
|
});
|
|
420
734
|
};
|
|
735
|
+
export const resolveMembershipProducts = (membership) => {
|
|
736
|
+
if (!membership || typeof membership !== 'object')
|
|
737
|
+
return [];
|
|
738
|
+
for (const key of membershipArrayKeys) {
|
|
739
|
+
if (Object.prototype.hasOwnProperty.call(membership, key)) {
|
|
740
|
+
return asMembershipStringArray(membership[key]);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return [];
|
|
744
|
+
};
|
|
421
745
|
const isPrivilegedUser = (user) => Boolean(user?.permissions?.isSuperAdmin ||
|
|
422
746
|
user?.permissions?.canManageTenants ||
|
|
423
747
|
user?.permissions?.canManageUsers);
|
|
748
|
+
const buildSubtenantOnlyMembership = (user, surfaceContext) => {
|
|
749
|
+
const role = normalizeTenant(user?.subtenant?.role);
|
|
750
|
+
if (!role)
|
|
751
|
+
return null;
|
|
752
|
+
return {
|
|
753
|
+
tenantId: surfaceContext.tenantSlug,
|
|
754
|
+
tenant_id: surfaceContext.tenantSlug,
|
|
755
|
+
tenantSlug: surfaceContext.tenantSlug,
|
|
756
|
+
tenantName: surfaceContext.tenantSlug,
|
|
757
|
+
membershipType: role,
|
|
758
|
+
membership_type: role,
|
|
759
|
+
roles: [role],
|
|
760
|
+
tenantSurfaceAccess: ['composer'],
|
|
761
|
+
};
|
|
762
|
+
};
|
|
424
763
|
export const buildAdminAuthSelection = (surfaceContext) => {
|
|
425
764
|
const tenantId = surfaceContext.subtenantSlug
|
|
426
765
|
? surfaceContext.tenantSlug
|
|
@@ -443,7 +782,9 @@ export const resolveSurfaceAccess = (user, surfaceContext) => {
|
|
|
443
782
|
return { authorized: Boolean(membership), membership };
|
|
444
783
|
}
|
|
445
784
|
if (resolvedSubtenantSlug && resolvedSubtenantSlug === requestedSubtenantSlug) {
|
|
446
|
-
const membership = canonicalMembership ||
|
|
785
|
+
const membership = canonicalMembership ||
|
|
786
|
+
compatibilityMembership ||
|
|
787
|
+
buildSubtenantOnlyMembership(user, surfaceContext);
|
|
447
788
|
return { authorized: Boolean(membership), membership };
|
|
448
789
|
}
|
|
449
790
|
if (compatibilityMembership) {
|
|
@@ -474,11 +815,9 @@ export const clearSurfaceAuthCookie = (cookies, path = '/') => {
|
|
|
474
815
|
export const resolveSecureCookie = (forwardedProto, protocol) => forwardedProto ? forwardedProto === 'https' : protocol === 'https:';
|
|
475
816
|
export const validateSurfaceBrowserSession = async ({ token, surfaceContext, fetchImpl = fetch, adminApiUrl, }) => {
|
|
476
817
|
try {
|
|
477
|
-
const response = await
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
body: JSON.stringify({ token, ...buildAdminAuthSelection(surfaceContext) }),
|
|
481
|
-
cache: 'no-store',
|
|
818
|
+
const response = await postJsonCandidates('/auth/validate', { token, ...buildAdminAuthSelection(surfaceContext) }, {
|
|
819
|
+
fetchImpl,
|
|
820
|
+
adminApiUrl,
|
|
482
821
|
});
|
|
483
822
|
const payload = (await response.json().catch(() => null));
|
|
484
823
|
const valid = Boolean(payload?.valid ?? payload?.success);
|
|
@@ -502,11 +841,9 @@ export const validateSurfaceBrowserSession = async ({ token, surfaceContext, fet
|
|
|
502
841
|
export const exchangeSurfaceBrowserSession = async ({ email, password, surfaceContext, fetchImpl = fetch, adminApiUrl, }) => {
|
|
503
842
|
let response;
|
|
504
843
|
try {
|
|
505
|
-
response = await
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
body: JSON.stringify({ email, password, ...buildAdminAuthSelection(surfaceContext) }),
|
|
509
|
-
cache: 'no-store',
|
|
844
|
+
response = await postJsonCandidates('/auth/login', { email, password, ...buildAdminAuthSelection(surfaceContext) }, {
|
|
845
|
+
fetchImpl,
|
|
846
|
+
adminApiUrl,
|
|
510
847
|
});
|
|
511
848
|
}
|
|
512
849
|
catch (error) {
|
|
@@ -585,7 +922,11 @@ export const buildSurfaceBrandingAssetCookie = async ({ headers, surfaceContext,
|
|
|
585
922
|
export const bootstrapSurfaceBranding = async ({ headers, surfaceContext, manifest = null, brandingPayload, requestAssetToken, assetScope, assetPrefix, assetTtl, assetAudience, ...fetchOptions }) => {
|
|
586
923
|
const brandingSource = brandingPayload ?? (await fetchSurfaceBranding(surfaceContext, fetchOptions));
|
|
587
924
|
const branding = normalizeSurfaceBrandingPayload(brandingSource);
|
|
588
|
-
const brandedManifest = manifest
|
|
925
|
+
const brandedManifest = manifest
|
|
926
|
+
? mapSurfaceBrandingManifestImages(applySurfaceBrandingToManifest(manifest, branding), (src) => shouldSignSurfaceBrandingAsset(src)
|
|
927
|
+
? `/api/assets/hdr?src=${encodeURIComponent(src)}`
|
|
928
|
+
: src)
|
|
929
|
+
: null;
|
|
589
930
|
const assetCookie = requestAssetToken && branding?.logoUrl
|
|
590
931
|
? await buildBrandingAssetCookie({
|
|
591
932
|
headers,
|
|
@@ -610,7 +951,8 @@ export const isSurfaceStaticBypassPath = (pathname, extraPrefixes = []) => {
|
|
|
610
951
|
return true;
|
|
611
952
|
return DEFAULT_BYPASS_PATTERN.test(pathname);
|
|
612
953
|
};
|
|
613
|
-
export const createSurfaceMiddleware = ({ resolveSurfaceHostContext, requestAssetToken, bypass, allowUnauthenticated, attachBrandingCookie, loginPath = '/login', clearCookieNames = [], redirectAuthenticatedFromLogin, redirectUnauthenticated, redirectUnauthorized, onAuthorized, ...fetchOptions }) => {
|
|
954
|
+
export const createSurfaceMiddleware = ({ resolveSurfaceHostContext, requestAssetToken, bypass, allowUnauthenticated, attachBrandingCookie, loginPath = '/login', loginPathAliases = [], clearCookieNames = [], redirectAuthenticatedFromLogin, redirectUnauthenticated, redirectUnauthorized, onAuthorized, ...fetchOptions }) => {
|
|
955
|
+
const loginPaths = new Set([loginPath, ...loginPathAliases]);
|
|
614
956
|
const defaultRedirectToLogin = (request) => {
|
|
615
957
|
const redirectUrl = request.nextUrl.clone();
|
|
616
958
|
redirectUrl.pathname = loginPath;
|
|
@@ -652,14 +994,28 @@ export const createSurfaceMiddleware = ({ resolveSurfaceHostContext, requestAsse
|
|
|
652
994
|
};
|
|
653
995
|
return async (request) => {
|
|
654
996
|
const { pathname } = request.nextUrl;
|
|
655
|
-
const { surfaceContext } =
|
|
997
|
+
const { surfaceContext } = await resolveSurfaceRequestContextAsync(request, resolveSurfaceHostContext);
|
|
998
|
+
const nextWithSurfaceContext = () => NextResponse.next({
|
|
999
|
+
request: {
|
|
1000
|
+
headers: buildSurfaceContextRequestHeaders(request, surfaceContext),
|
|
1001
|
+
},
|
|
1002
|
+
});
|
|
1003
|
+
const rewriteToLoginWithSurfaceContext = () => {
|
|
1004
|
+
const rewriteUrl = request.nextUrl.clone();
|
|
1005
|
+
rewriteUrl.pathname = loginPath;
|
|
1006
|
+
return NextResponse.rewrite(rewriteUrl, {
|
|
1007
|
+
request: {
|
|
1008
|
+
headers: buildSurfaceContextRequestHeaders(request, surfaceContext),
|
|
1009
|
+
},
|
|
1010
|
+
});
|
|
1011
|
+
};
|
|
656
1012
|
if (isSurfaceStaticBypassPath(pathname) || bypass?.(pathname, surfaceContext) || pathname.startsWith('/api/')) {
|
|
657
|
-
return
|
|
1013
|
+
return nextWithSurfaceContext();
|
|
658
1014
|
}
|
|
659
1015
|
const token = readSurfaceAuthToken(request.cookies);
|
|
660
|
-
if (pathname
|
|
1016
|
+
if (loginPaths.has(pathname)) {
|
|
661
1017
|
if (!token) {
|
|
662
|
-
return maybeAttachBrandingCookie(request,
|
|
1018
|
+
return maybeAttachBrandingCookie(request, pathname === loginPath ? nextWithSurfaceContext() : rewriteToLoginWithSurfaceContext(), surfaceContext);
|
|
663
1019
|
}
|
|
664
1020
|
const validation = await validateSurfaceBrowserSession({
|
|
665
1021
|
token,
|
|
@@ -669,12 +1025,12 @@ export const createSurfaceMiddleware = ({ resolveSurfaceHostContext, requestAsse
|
|
|
669
1025
|
if (validation.valid) {
|
|
670
1026
|
return NextResponse.redirect(redirectAuthenticatedFromLogin?.(request, surfaceContext) ?? new URL('/', request.url));
|
|
671
1027
|
}
|
|
672
|
-
return maybeAttachBrandingCookie(request, clearCookies(
|
|
1028
|
+
return maybeAttachBrandingCookie(request, clearCookies(pathname === loginPath ? nextWithSurfaceContext() : rewriteToLoginWithSurfaceContext()), surfaceContext);
|
|
673
1029
|
}
|
|
674
1030
|
const isPublic = allowUnauthenticated?.(pathname, surfaceContext) ?? false;
|
|
675
1031
|
if (!token) {
|
|
676
1032
|
if (isPublic) {
|
|
677
|
-
return maybeAttachBrandingCookie(request,
|
|
1033
|
+
return maybeAttachBrandingCookie(request, nextWithSurfaceContext(), surfaceContext);
|
|
678
1034
|
}
|
|
679
1035
|
return NextResponse.redirect(redirectUnauthenticated?.(request, surfaceContext) ?? defaultRedirectToLogin(request));
|
|
680
1036
|
}
|
|
@@ -685,7 +1041,7 @@ export const createSurfaceMiddleware = ({ resolveSurfaceHostContext, requestAsse
|
|
|
685
1041
|
});
|
|
686
1042
|
if (!validation.valid) {
|
|
687
1043
|
if (isPublic) {
|
|
688
|
-
return maybeAttachBrandingCookie(request, clearCookies(
|
|
1044
|
+
return maybeAttachBrandingCookie(request, clearCookies(nextWithSurfaceContext()), surfaceContext);
|
|
689
1045
|
}
|
|
690
1046
|
return clearCookies(NextResponse.redirect(redirectUnauthorized?.(request, surfaceContext) ?? defaultRedirectToLogin(request)));
|
|
691
1047
|
}
|
|
@@ -693,6 +1049,6 @@ export const createSurfaceMiddleware = ({ resolveSurfaceHostContext, requestAsse
|
|
|
693
1049
|
if (authorizedResponse) {
|
|
694
1050
|
return authorizedResponse;
|
|
695
1051
|
}
|
|
696
|
-
return maybeAttachBrandingCookie(request,
|
|
1052
|
+
return maybeAttachBrandingCookie(request, nextWithSurfaceContext(), surfaceContext);
|
|
697
1053
|
};
|
|
698
1054
|
};
|