@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/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 trimmed = value?.trim();
15
- if (trimmed)
16
- return trimmed;
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 getAdminCandidates = (explicitBase) => {
98
- const baseUrl = resolveAdminApiUrl('', explicitBase).replace(/\/$/, '');
99
- if (isLocalUrl(baseUrl) && baseUrl !== DEFAULT_ADMIN_API_URL) {
100
- return [baseUrl, DEFAULT_ADMIN_API_URL];
101
- }
102
- return [baseUrl];
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
- for (const baseUrl of getAdminCandidates(options.adminApiUrl)) {
111
- const url = `${baseUrl}/tenants/${encodeURIComponent(slug)}?view=${encodeURIComponent(options.view ?? 'composer')}${subtenantQuery}`;
112
- const payload = await fetchJson(url, options);
113
- if (payload)
114
- return payload;
115
- }
116
- return null;
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
- for (const baseUrl of getAdminCandidates(options.adminApiUrl)) {
124
- const urls = slug
125
- ? [
126
- `${baseUrl}/branding/${encodeURIComponent(slug)}${subtenantQuery}`,
127
- `${baseUrl}/platform/branding?tenantId=${encodeURIComponent(slug)}${surfaceContext.subtenantSlug
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 sectionParagraphs = Array.isArray(value.sections)
182
- ? value.sections.flatMap((section) => isRecord(section) && Array.isArray(section.paragraphs)
183
- ? section.paragraphs.filter((entry) => typeof entry === 'string' && entry.trim().length > 0)
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
- return login || landing ? { ...(login ? { login } : {}), ...(landing ? { landing } : {}) } : undefined;
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 normalizePromptPacksByMode = (value) => {
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 entries = [];
231
- for (const [mode, ref] of Object.entries(value)) {
232
- if (!isRecord(ref) || typeof ref.id !== 'string')
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
- manifestPaths: asStringArray(value.manifestPaths),
247
- promptPacksByMode: normalizePromptPacksByMode(value.promptPacksByMode),
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 legacySurfaceConfig = isRecord(document.surfaceConfig) ? document.surfaceConfig : null;
293
- const legacyPromptPacks = isRecord(document.promptPacks) ? document.promptPacks : null;
294
- const legacyPluginRegistry = isRecord(document.pluginRegistry) ? document.pluginRegistry : null;
295
- const runtime = normalizeRuntime(document.runtime) ?? {
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) ?? (legacySurfaceConfig ? asString(legacySurfaceConfig.title) : undefined),
315
- lockedLabel: asString(document.lockedLabel) ?? (legacySurfaceConfig ? asString(legacySurfaceConfig.lockedLabel) : undefined),
316
- navigation: normalizeNavigation(document.navigation) ??
317
- (legacySurfaceConfig
318
- ? {
319
- surfaceTitle: asString(legacySurfaceConfig.surfaceTitle) ??
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: override.modeTabs ?? base.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
- for (const baseUrl of getAdminCandidates(fetchOptions.adminApiUrl)) {
387
- const url = `${baseUrl}/platform/composer-data?tenantId=${encodeURIComponent(surfaceContext.tenantSlug)}${subtenantQuery}&surfaceId=${encodeURIComponent(surfaceContext.surfaceId || 'composer')}`;
388
- const payload = await fetchJson(url, fetchOptions);
389
- const normalized = normalizeSurfaceComposerData(payload);
390
- if (normalized) {
391
- return mergeSurfaceComposerData(fallbackData, normalized);
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 || compatibilityMembership || null;
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 fetchImpl(resolveAdminApiUrl('/auth/validate', adminApiUrl), {
478
- method: 'POST',
479
- headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
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 fetchImpl(resolveAdminApiUrl('/auth/login', adminApiUrl), {
506
- method: 'POST',
507
- headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
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 ? applySurfaceBrandingToManifest(manifest, branding) : null;
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 } = resolveSurfaceRequestContext(request, resolveSurfaceHostContext);
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 NextResponse.next();
1013
+ return nextWithSurfaceContext();
658
1014
  }
659
1015
  const token = readSurfaceAuthToken(request.cookies);
660
- if (pathname === loginPath) {
1016
+ if (loginPaths.has(pathname)) {
661
1017
  if (!token) {
662
- return maybeAttachBrandingCookie(request, NextResponse.next(), surfaceContext);
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(NextResponse.next()), surfaceContext);
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, NextResponse.next(), surfaceContext);
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(NextResponse.next()), surfaceContext);
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, NextResponse.next(), surfaceContext);
1052
+ return maybeAttachBrandingCookie(request, nextWithSurfaceContext(), surfaceContext);
697
1053
  };
698
1054
  };