@f5-sales-demo/docs-theme 4.11.7 → 4.13.0

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/config.ts CHANGED
@@ -23,13 +23,12 @@ import { defaultLocale as f5xcDefaultLocale, f5xcDefaultLocales } from './src/i1
23
23
  import { mobileLabels } from './src/i18n/mega-menu-translations.ts';
24
24
  import { sidebarTranslations } from './src/i18n/translations.ts';
25
25
  import remarkMermaid from './src/plugins/remark-mermaid.mjs';
26
+ import { sharedSearchIntegration } from './src/shared/search-integration.mjs';
27
+ import { sharedMode } from './src/shared/settings.mjs';
26
28
  import canonicalLlms from './src/utils/canonical-llms.mjs';
27
- import { providerMegaMenu } from './src/utils/canonical-provider.ts';
28
29
  import { menuLocalizationPolicy } from './src/utils/menu-localization.ts';
29
- import { stagedProviderSections } from './src/utils/provider-sections.ts';
30
30
  import { publicationSidebar, repositoryPublicationProfile } from './src/utils/publication-profiles.ts';
31
- import { resolveMegaMenuIcon } from './src/utils/resolve-icon.ts';
32
- import { federatedSearchSites } from './src/utils/search-sites';
31
+ import { localSearchSites as federatedSearchSites } from './src/utils/search-sites';
33
32
  import providerSharedAssets from './src/utils/share-provider-assets.mjs';
34
33
  import { statisticsScriptMarkdown } from './src/utils/statistics-script-markdown.mjs';
35
34
  import { buildSubcategorySidebar } from './src/utils/subcategory-sidebar.ts';
@@ -126,333 +125,7 @@ export interface F5xcDocsConfigOptions {
126
125
  };
127
126
  }
128
127
 
129
- export const defaultMegaMenuItems: MegaMenuItem[] = [
130
- {
131
- label: 'Demos',
132
- content: {
133
- layout: 'grid',
134
- columns: 2,
135
- categories: [
136
- {
137
- title: 'Security',
138
- items: [
139
- {
140
- label: 'Web App & API Protection',
141
- description: 'Deploy an application security demo with Terraform and follow its protection walkthroughs.',
142
- href: 'https://f5-sales-demo.github.io/webapp-api-protection/en/',
143
- icon: resolveMegaMenuIcon('f5xc:web-app-and-api-protection'),
144
- },
145
- {
146
- label: 'API Protection',
147
- description: 'Find guides to API discovery, schema validation, and request controls.',
148
- href: 'https://f5-sales-demo.github.io/api-protection/en/',
149
- icon: resolveMegaMenuIcon('f5xc:application-traffic-insight'),
150
- },
151
- {
152
- label: 'Bot Defense Advanced',
153
- description: 'Review behavioral bot detection and mitigation scenarios.',
154
- href: 'https://f5-sales-demo.github.io/bot-advanced/en/',
155
- icon: resolveMegaMenuIcon('f5xc:bot-defense'),
156
- },
157
- {
158
- label: 'Bot Defense Standard',
159
- description: 'Review bot classification, verified crawlers, and blocking policies.',
160
- href: 'https://f5-sales-demo.github.io/bot-standard/en/',
161
- icon: resolveMegaMenuIcon('f5xc:bot-defense'),
162
- },
163
- {
164
- label: 'Client-Side Defense',
165
- description: 'Deploy browser script monitoring with an API or Terraform workflow.',
166
- href: 'https://f5-sales-demo.github.io/csd/en/',
167
- icon: resolveMegaMenuIcon('f5xc:client-side-defense'),
168
- },
169
- {
170
- label: 'DDoS Mitigation',
171
- description: 'Review distributed denial-of-service defenses and demo scenarios.',
172
- href: 'https://f5-sales-demo.github.io/ddos/en/',
173
- icon: resolveMegaMenuIcon('f5xc:ddos-and-transit-services'),
174
- },
175
- {
176
- label: 'Web App Scanning',
177
- description: 'Find web application vulnerability scanning guides.',
178
- href: 'https://f5-sales-demo.github.io/was/en/',
179
- icon: resolveMegaMenuIcon('f5xc:web-app-scanning'),
180
- },
181
- {
182
- label: 'Custom responses',
183
- description: 'Configure application replies, redirects, and security response pages.',
184
- href: 'https://f5-sales-demo.github.io/custom-responses/en/',
185
- icon: resolveMegaMenuIcon('f5xc:web-app-and-api-protection'),
186
- },
187
- ],
188
- },
189
- {
190
- title: 'Networking and performance',
191
- items: [
192
- {
193
- label: 'Multi-Cloud Networking',
194
- description: 'Find site deployment and cross-cloud connectivity guides.',
195
- href: 'https://f5-sales-demo.github.io/multi-cloud-networking/en/',
196
- icon: resolveMegaMenuIcon('f5xc:multi-cloud-network-connect'),
197
- },
198
- {
199
- label: 'Canada topology',
200
- description: 'Deploy Canadian application hosting and demonstrate regional access controls.',
201
- href: 'https://f5-sales-demo.github.io/canada/en/',
202
- icon: {
203
- body: fs
204
- .readFileSync(new URL('./assets/canada-flag.svg', import.meta.url), 'utf8')
205
- .replace(/<svg[^>]*>|<\/svg>/g, ''),
206
- width: 640,
207
- height: 480,
208
- mode: 'original',
209
- },
210
- },
211
- {
212
- label: 'Content Delivery Network',
213
- description: 'Review content delivery network (CDN) caching and origin configuration.',
214
- href: 'https://f5-sales-demo.github.io/cdn/en/',
215
- icon: resolveMegaMenuIcon('f5xc:content-delivery-network'),
216
- },
217
- {
218
- label: 'DNS Management',
219
- description: 'Find Domain Name System (DNS) zone and load balancing guides.',
220
- href: 'https://f5-sales-demo.github.io/dns/en/',
221
- icon: resolveMegaMenuIcon('f5xc:dns-management'),
222
- },
223
- {
224
- label: 'NGINX One',
225
- description: 'Review NGINX instance visibility and configuration management.',
226
- href: 'https://f5-sales-demo.github.io/nginx/en/',
227
- icon: resolveMegaMenuIcon('f5xc:nginx-one'),
228
- },
229
- ],
230
- },
231
- ],
232
- footer: {
233
- label: 'Demo catalog',
234
- href: 'https://f5-sales-demo.github.io/en/demos/',
235
- description: 'All demonstration and capability guides',
236
- },
237
- },
238
- },
239
- {
240
- label: 'Demo environment',
241
- content: {
242
- layout: 'list',
243
- categories: [
244
- {
245
- title: 'Deployment resources',
246
- items: [
247
- {
248
- label: 'Origin server',
249
- description: 'Compare the Azure full-origin stack with the separate AWS Juice Shop deployment.',
250
- href: 'https://f5-sales-demo.github.io/origin-server/en/',
251
- icon: resolveMegaMenuIcon('f5xc:distributed-apps'),
252
- },
253
- {
254
- label: 'Traffic generator',
255
- description:
256
- 'Choose Azure or AWS deployment guides for controlled security traffic and browser scenarios.',
257
- href: 'https://f5-sales-demo.github.io/traffic-generator/en/',
258
- icon: resolveMegaMenuIcon('f5xc:application-traffic-insight'),
259
- },
260
- {
261
- label: 'CDN simulator',
262
- description: 'Deploy an Azure proxy that adds CDN headers to origin requests.',
263
- href: 'https://f5-sales-demo.github.io/cdn-simulator/en/',
264
- icon: resolveMegaMenuIcon('f5xc:content-delivery-network'),
265
- },
266
- ],
267
- },
268
- ],
269
- footer: {
270
- label: 'Demo resource catalog',
271
- href: 'https://f5-sales-demo.github.io/demo-resources/en/',
272
- description: 'Azure and AWS offerings vary by component',
273
- },
274
- },
275
- },
276
- {
277
- label: 'Operations',
278
- content: {
279
- layout: 'list',
280
- categories: [
281
- {
282
- title: 'Queries and administration',
283
- items: [
284
- {
285
- label: 'Statistics',
286
- description: 'Query access logs, application metrics, and security telemetry through the API.',
287
- href: 'https://f5-sales-demo.github.io/statistics/en/',
288
- icon: resolveMegaMenuIcon('f5xc:doc'),
289
- },
290
- {
291
- label: 'Observability',
292
- description: 'Find monitoring, metrics, tracing, and alerting guides.',
293
- href: 'https://f5-sales-demo.github.io/observability/en/',
294
- icon: resolveMegaMenuIcon('f5xc:observability'),
295
- },
296
- {
297
- label: 'Administration',
298
- description: 'Find tenant, namespace, and role management guides.',
299
- href: 'https://f5-sales-demo.github.io/administration/en/',
300
- icon: resolveMegaMenuIcon('f5xc:administration'),
301
- },
302
- ],
303
- },
304
- ],
305
- },
306
- },
307
- {
308
- label: 'Developer tools',
309
- content: {
310
- layout: 'grid',
311
- columns: 2,
312
- categories: [
313
- {
314
- title: 'Tools and automation',
315
- items: [
316
- {
317
- label: 'xcsh',
318
- description:
319
- 'Install the independent terminal assistant for engineering and F5 Distributed Cloud workflows.',
320
- href: 'https://f5-sales-demo.github.io/xcsh/en/',
321
- icon: resolveMegaMenuIcon('carbon:terminal'),
322
- },
323
- {
324
- label: 'Terraform provider',
325
- description: 'Configure F5 Distributed Cloud resources with Terraform.',
326
- href: 'https://f5-sales-demo.github.io/terraform-provider-xcsh/',
327
- icon: resolveMegaMenuIcon('f5xc:doc'),
328
- },
329
- {
330
- label: 'xcsh GitHub Action',
331
- description: 'Run pinned xcsh manifest operations in GitHub Actions.',
332
- href: 'https://f5-sales-demo.github.io/xcsh-action/en/',
333
- icon: resolveMegaMenuIcon('carbon:workflow-automation'),
334
- },
335
- {
336
- label: 'VS Code extension',
337
- description: 'Author manifests and manage F5 Distributed Cloud resources in Visual Studio Code.',
338
- href: 'https://f5-sales-demo.github.io/vscode-xcsh/en/',
339
- icon: resolveMegaMenuIcon('carbon:code'),
340
- },
341
- {
342
- label: 'xcsh Chrome extension',
343
- description: 'Connect xcsh to the F5 Distributed Cloud console through a local browser bridge.',
344
- href: 'https://f5-sales-demo.github.io/xcsh-chrome-extension/en/',
345
- icon: resolveMegaMenuIcon('carbon:application-web'),
346
- },
347
- {
348
- label: 'APT repository',
349
- description: 'Install signed Debian and Ubuntu packages for xcsh and supporting tools.',
350
- href: 'https://f5-sales-demo.github.io/apt-repo/en/',
351
- icon: resolveMegaMenuIcon('f5xc:doc'),
352
- },
353
- ],
354
- },
355
- {
356
- title: 'Specifications and plugins',
357
- items: [
358
- {
359
- label: 'API specifications',
360
- description: 'Find validated OpenAPI specifications and the specification update pipeline.',
361
- href: 'https://f5-sales-demo.github.io/api-specs/en/',
362
- icon: resolveMegaMenuIcon('f5xc:data-intelligence'),
363
- },
364
- {
365
- label: 'Enriched API specifications',
366
- description: 'Browse OpenAPI schemas with additional constraints and examples.',
367
- href: 'https://f5-sales-demo.github.io/api-specs-enriched/en/',
368
- icon: resolveMegaMenuIcon('f5xc:data-intelligence'),
369
- },
370
- {
371
- label: 'xcsh marketplace',
372
- description: 'Find xcsh plugins for documentation, sales, cloud, security, and desktop work.',
373
- href: 'https://f5-sales-demo.github.io/marketplace/en/',
374
- icon: resolveMegaMenuIcon('f5xc:ai_assistant_logo'),
375
- },
376
- {
377
- label: 'Marketplace for Claude Code',
378
- description: 'Find Language Server Protocol integrations and developer tools for Claude Code.',
379
- href: 'https://f5-sales-demo.github.io/marketplace-claude-code/en/',
380
- icon: resolveMegaMenuIcon('f5xc:doc'),
381
- },
382
- {
383
- label: 'Console catalog',
384
- description: 'Browse documented console routes and browser automation workflows.',
385
- href: 'https://f5-sales-demo.github.io/console/en/',
386
- icon: resolveMegaMenuIcon('f5xc:ai_assistant_logo'),
387
- },
388
- ],
389
- },
390
- ],
391
- },
392
- },
393
- {
394
- label: 'Ecosystem',
395
- content: {
396
- layout: 'list',
397
- categories: [
398
- {
399
- title: 'Public projects',
400
- items: [
401
- {
402
- label: 'F5 documentation corpus',
403
- description: 'Browse curated F5 documentation and its Markdown sources.',
404
- href: 'https://f5-sales-demo.github.io/html-to-markdown/',
405
- icon: resolveMegaMenuIcon('f5xc:doc'),
406
- },
407
- {
408
- label: 'Ecosystem directory',
409
- description: 'Documentation, tools, and public project sources',
410
- href: 'https://f5-sales-demo.github.io/en/ecosystem/',
411
- icon: resolveMegaMenuIcon('f5xc:doc'),
412
- },
413
- ],
414
- },
415
- ],
416
- footer: {
417
- label: 'Project sources',
418
- href: 'https://github.com/f5-sales-demo',
419
- description: 'Public repositories in the community organization',
420
- },
421
- },
422
- },
423
- {
424
- label: 'F5 services',
425
- content: {
426
- layout: 'list',
427
- categories: [
428
- {
429
- title: 'Official documentation and services',
430
- items: [
431
- {
432
- label: 'F5 Distributed Cloud console',
433
- description: 'Management service; sign-in required',
434
- href: 'https://console.ves.volterra.io',
435
- icon: resolveMegaMenuIcon('f5xc:platform'),
436
- },
437
- {
438
- label: 'F5 Distributed Cloud documentation',
439
- description: 'Official public product documentation',
440
- href: 'https://docs.cloud.f5.com',
441
- icon: resolveMegaMenuIcon('f5xc:doc'),
442
- },
443
- {
444
- label: 'MyF5 support',
445
- description: 'Support portal; sign-in required for account services',
446
- href: 'https://my.f5.com/manage/s/',
447
- icon: resolveMegaMenuIcon('f5xc:support'),
448
- },
449
- ],
450
- },
451
- ],
452
- },
453
- },
454
- ];
455
-
128
+ export const defaultMegaMenuItems: MegaMenuItem[] = [];
456
129
  const defaultHead: HeadEntry[] = [
457
130
  {
458
131
  tag: 'script',
@@ -528,6 +201,7 @@ export function createF5xcDocsConfig(options: F5xcDocsConfigOptions = {}) {
528
201
  const githubRepository = options.githubRepository || process.env.GITHUB_REPOSITORY || '';
529
202
  const publicationProfile = repositoryPublicationProfile(githubRepository);
530
203
  const favicon = options.favicon || publicationProfile?.favicon;
204
+ if (options.favicon) process.env.DOCS_CUSTOM_FAVICON = options.favicon;
531
205
  let llmsOptionalLinks: Array<{ title: string; url: string }> = options.llmsOptionalLinks || [];
532
206
  if (!options.llmsOptionalLinks && process.env.LLMS_OPTIONAL_LINKS) {
533
207
  try {
@@ -568,19 +242,18 @@ export function createF5xcDocsConfig(options: F5xcDocsConfigOptions = {}) {
568
242
  console.warn('[docs-theme] OPENAPI_SPECS_CONFIG contains invalid JSON; skipping OpenAPI plugin.', e);
569
243
  }
570
244
  }
571
- const megaMenuItems =
572
- options.megaMenuItems ||
573
- (canonicalProvider
574
- ? [...defaultMegaMenuItems, ...providerMegaMenu(base, stagedProviderSections(canonicalProvider.manifest))]
575
- : defaultMegaMenuItems);
245
+ const mode = sharedMode(githubRepository);
246
+ const megaMenuItems = options.megaMenuItems || defaultMegaMenuItems;
576
247
  const head =
577
248
  options.head ||
578
249
  (canonicalProvider || githubRepository === 'f5-sales-demo/f5-sales-demo.github.io' ? [] : defaultHead);
579
- const logo = options.logo || { src: '@f5-sales-demo/docs-theme/assets/f5-distributed-cloud.svg' };
250
+ const logo =
251
+ options.logo ||
252
+ (mode === 'local' ? { src: '@f5-sales-demo/docs-theme/assets/f5-distributed-cloud.svg' } : undefined);
580
253
  const additionalRemarkPlugins = options.additionalRemarkPlugins || [];
581
254
  const additionalIntegrations = options.additionalIntegrations || [];
582
255
 
583
- const federatedSearch = !canonicalProvider && options.federatedSearch !== false;
256
+ const federatedSearch = mode === 'local' && !canonicalProvider && options.federatedSearch !== false;
584
257
  const normalizedBase = base.replace(/\/+$/, '');
585
258
  const repositoryCorpusPolicy = progressiveCorpusPolicy(llmsConfig);
586
259
  const progressiveCorpus =
@@ -610,7 +283,7 @@ export function createF5xcDocsConfig(options: F5xcDocsConfigOptions = {}) {
610
283
  : undefined;
611
284
 
612
285
  const starlightPlugins: StarlightPlugin[] = [
613
- starlightMegaMenu({ items: megaMenuItems, mobileLabels }),
286
+ ...(mode === 'local' ? [starlightMegaMenu({ items: megaMenuItems, mobileLabels })] : []),
614
287
  starlightVideosPlugin(),
615
288
  starlightImageZoom(),
616
289
  f5xcDocsTheme(githubRepository),
@@ -734,7 +407,10 @@ export function createF5xcDocsConfig(options: F5xcDocsConfigOptions = {}) {
734
407
  return defineConfig({
735
408
  site,
736
409
  base,
737
- vite: { plugins: [menuLocalizationPolicy()], ...(canonicalProvider ? { build: { assetsInlineLimit: 0 } } : {}) },
410
+ vite: {
411
+ plugins: mode === 'local' ? [menuLocalizationPolicy()] : [sharedSearchIntegration()],
412
+ ...(canonicalProvider ? { build: { assetsInlineLimit: 0 } } : {}),
413
+ },
738
414
  ...(canonicalProvider
739
415
  ? {
740
416
  experimental: { collectionStorage: { type: 'chunked' as const, chunkSize: 1024 * 1024 } },
package/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { StarlightPlugin } from '@astrojs/starlight/types';
2
2
  import { translations } from './src/i18n/translations.ts';
3
+ import { sharedMode } from './src/shared/settings.mjs';
3
4
 
4
5
  export default function f5xcDocsTheme(repository = process.env.GITHUB_REPOSITORY || ''): StarlightPlugin {
5
6
  return {
@@ -19,12 +20,20 @@ export default function f5xcDocsTheme(repository = process.env.GITHUB_REPOSITORY
19
20
  updateConfig({
20
21
  customCss: [
21
22
  ...(config.customCss ?? []),
22
- '@f5-sales-demo/docs-theme/fonts/font-face.css',
23
- '@f5-sales-demo/docs-theme/styles/custom.css',
23
+ ...(sharedMode(repository) === 'local'
24
+ ? [
25
+ '@f5-sales-demo/docs-theme/fonts/font-face.css',
26
+ '@f5-sales-demo/docs-theme/styles/custom.css',
27
+ '@f5-sales-demo/docs-theme/styles/shell.css',
28
+ ]
29
+ : []),
24
30
  ],
25
31
  components: {
26
32
  ...config.components,
27
- Header: '@f5-sales-demo/docs-theme/components/SharedHeader.astro',
33
+ Header:
34
+ sharedMode(repository) === 'local'
35
+ ? '@f5-sales-demo/docs-theme/components/LocalHeader.astro'
36
+ : '@f5-sales-demo/docs-theme/components/SharedHeader.astro',
28
37
  Head: '@f5-sales-demo/docs-theme/components/Head.astro',
29
38
  ...(process.env.DOCS_PROFILE === 'canonical-provider'
30
39
  ? {
@@ -34,7 +43,9 @@ export default function f5xcDocsTheme(repository = process.env.GITHUB_REPOSITORY
34
43
  ? {
35
44
  Sidebar: '@f5-sales-demo/docs-theme/components/CanadaSidebar.astro',
36
45
  }
37
- : {}),
46
+ : repository.split('/').pop() === 'statistics'
47
+ ? { Sidebar: '@f5-sales-demo/docs-theme/components/StatisticsSidebar.astro' }
48
+ : {}),
38
49
  Banner: '@f5-sales-demo/docs-theme/components/Banner.astro',
39
50
  EditLink: '@f5-sales-demo/docs-theme/components/EditLink.astro',
40
51
  Footer: '@f5-sales-demo/docs-theme/components/Footer.astro',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@f5-sales-demo/docs-theme",
3
- "version": "4.11.7",
3
+ "version": "4.13.0",
4
4
  "description": "F5 Distributed Cloud branded Starlight documentation theme",
5
5
  "type": "module",
6
6
  "engines": {
@@ -48,7 +48,10 @@
48
48
  "./components/SharedHeader.astro": "./components/SharedHeader.astro",
49
49
  "./components/Head.astro": "./components/Head.astro",
50
50
  "./assets/canada-flag.svg": "./assets/canada-flag.svg",
51
- "./assets/canada-favicon.svg": "./assets/canada-favicon.svg"
51
+ "./assets/canada-favicon.svg": "./assets/canada-favicon.svg",
52
+ "./styles/shell.css": "./styles/shell.css",
53
+ "./components/LocalHeader.astro": "./components/LocalHeader.astro",
54
+ "./components/StatisticsSidebar.astro": "./components/StatisticsSidebar.astro"
52
55
  },
53
56
  "files": [
54
57
  "index.ts",
@@ -100,10 +103,11 @@
100
103
  "build": "node scripts/run-astro-with-content.mjs build",
101
104
  "dev": "node scripts/run-astro-with-content.mjs dev",
102
105
  "preview": "astro preview",
103
- "test": "vitest run",
106
+ "test": "vitest run && npm run test:shared",
104
107
  "test:watch": "vitest",
105
108
  "test:visual": "playwright test",
106
- "test:visual:update": "playwright test --update-snapshots"
109
+ "test:visual:update": "playwright test --update-snapshots",
110
+ "test:shared": "node --test tests/shared-shell.node.mjs"
107
111
  },
108
112
  "peerDependencies": {
109
113
  "@astrojs/markdown-remark": "^7.2.4",
@@ -0,0 +1,30 @@
1
+ {
2
+ "pointer": {
3
+ "contract": "v1",
4
+ "release": {
5
+ "url": "https://f5-sales-demo.github.io/shared/v1/releases/e3434cae2b7a79b34171b674e9378cdf3bfe3965071ca9c9b231e2d819f035e4.json",
6
+ "sha256": "e3434cae2b7a79b34171b674e9378cdf3bfe3965071ca9c9b231e2d819f035e4",
7
+ "bytes": 5766
8
+ }
9
+ },
10
+ "assets": {
11
+ "css": {
12
+ "url": "https://f5-sales-demo.github.io/shared/assets/b208db097ace5238f60a621abd4b03965c6c0bb5a35f1d498ea76172b23f6f29.css",
13
+ "sha256": "b208db097ace5238f60a621abd4b03965c6c0bb5a35f1d498ea76172b23f6f29",
14
+ "bytes": 44879,
15
+ "extension": "css"
16
+ },
17
+ "logo": {
18
+ "url": "https://f5-sales-demo.github.io/shared/assets/487f0f7a70ffaa67c7127b2eef2b094c6eae42d8d6e6ba0a14c784013b2c6d48.svg",
19
+ "sha256": "487f0f7a70ffaa67c7127b2eef2b094c6eae42d8d6e6ba0a14c784013b2c6d48",
20
+ "bytes": 2294,
21
+ "extension": "svg"
22
+ },
23
+ "favicon": {
24
+ "url": "https://f5-sales-demo.github.io/shared/assets/3402d9a74d6a627f098da467fda72cde93ee686ecb1ceba2c96d1f4bd18857b3.svg",
25
+ "sha256": "3402d9a74d6a627f098da467fda72cde93ee686ecb1ceba2c96d1f4bd18857b3",
26
+ "bytes": 4282,
27
+ "extension": "svg"
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,135 @@
1
+ import { ROOT, validateConsumer, validateData, validateManifest, validatePointer } from './contract.mjs';
2
+
3
+ const CACHE_KEY = 'f5-shared-v1:last-success';
4
+ const TIMEOUT = 12000;
5
+ export async function verifiedBytes(receipt, fetcher = fetch, cryptoProvider = crypto) {
6
+ const response = await fetcher(receipt.url, { signal: AbortSignal.timeout(TIMEOUT) });
7
+ if (!response.ok) throw new Error(`Shared asset unavailable: ${response.status}`);
8
+ const bytes = await response.arrayBuffer();
9
+ const digest = Array.from(new Uint8Array(await cryptoProvider.subtle.digest('SHA-256', bytes)), (value) =>
10
+ value.toString(16).padStart(2, '0'),
11
+ ).join('');
12
+ if (bytes.byteLength !== receipt.bytes || digest !== receipt.sha256)
13
+ throw new Error('Shared asset integrity mismatch');
14
+ return bytes;
15
+ }
16
+ export async function loadRelease(pointer, fetcher = fetch, cryptoProvider = crypto) {
17
+ validatePointer(pointer);
18
+ const manifest = validateManifest(
19
+ JSON.parse(new TextDecoder().decode(await verifiedBytes(pointer.release, fetcher, cryptoProvider))),
20
+ );
21
+ const entries = Object.entries(manifest.assets);
22
+ const buffers = await Promise.all(entries.map(([, asset]) => verifiedBytes(asset, fetcher, cryptoProvider)));
23
+ const dataIndex = entries.findIndex(([key]) => key === 'data');
24
+ const data = validateData(JSON.parse(new TextDecoder().decode(buffers[dataIndex])));
25
+ return { pointer, manifest, data };
26
+ }
27
+ export async function selectRelease({ baseline, fetcher = fetch, storage, activate, cryptoProvider = crypto }) {
28
+ let cached;
29
+ try {
30
+ cached = JSON.parse(storage?.getItem(CACHE_KEY) || 'null');
31
+ } catch {
32
+ /* Storage may be unavailable. */
33
+ }
34
+ let active;
35
+ try {
36
+ const response = await fetcher(`${ROOT}v1/current.json`, {
37
+ cache: 'no-store',
38
+ signal: AbortSignal.timeout(TIMEOUT),
39
+ });
40
+ if (!response.ok) throw new Error('Active release unavailable');
41
+ active = validatePointer(await response.json());
42
+ } catch {
43
+ /* Try the previously verified release, then the build baseline. */
44
+ }
45
+ const attempted = new Set();
46
+ for (const pointer of [active, cached, baseline]) {
47
+ if (!pointer || attempted.has(pointer.release?.sha256)) continue;
48
+ attempted.add(pointer.release?.sha256);
49
+ try {
50
+ const release = await loadRelease(pointer, fetcher, cryptoProvider);
51
+ await activate(release);
52
+ try {
53
+ storage?.setItem(CACHE_KEY, JSON.stringify(pointer));
54
+ } catch {
55
+ /* Cache is optional. */
56
+ }
57
+ return release;
58
+ } catch {
59
+ /* A partial release must never replace the working shell. */
60
+ }
61
+ }
62
+ return null;
63
+ }
64
+ function integrity(receipt) {
65
+ return `sha256-${btoa(String.fromCharCode(...receipt.sha256.match(/../g).map((pair) => parseInt(pair, 16))))}`;
66
+ }
67
+ async function loadElement(element, parent) {
68
+ await new Promise((resolve, reject) => {
69
+ const timer = setTimeout(() => {
70
+ element.remove();
71
+ reject(new Error('Shared load timed out'));
72
+ }, TIMEOUT);
73
+ element.onload = () => {
74
+ clearTimeout(timer);
75
+ resolve();
76
+ };
77
+ element.onerror = () => {
78
+ clearTimeout(timer);
79
+ element.remove();
80
+ reject(new Error('Shared load failed'));
81
+ };
82
+ parent.append(element);
83
+ });
84
+ }
85
+ export async function startSharedShell(consumer, baseline) {
86
+ validateConsumer(consumer);
87
+ let storage;
88
+ try {
89
+ storage = window.localStorage;
90
+ } catch {
91
+ /* Browser policy can reject even property access. */
92
+ }
93
+ return selectRelease({
94
+ baseline,
95
+ storage,
96
+ activate: async (release) => {
97
+ const { css, runtime } = release.manifest.assets;
98
+ const link = document.createElement('link');
99
+ Object.assign(link, {
100
+ rel: 'stylesheet',
101
+ href: css.url,
102
+ integrity: integrity(css),
103
+ crossOrigin: 'anonymous',
104
+ media: 'not all',
105
+ });
106
+ let prepared;
107
+ try {
108
+ await loadElement(link, document.head);
109
+ window.F5SharedV1 ||= new Map();
110
+ if (!window.F5SharedV1.has(runtime.url)) {
111
+ const script = document.createElement('script');
112
+ Object.assign(script, {
113
+ type: 'module',
114
+ src: runtime.url,
115
+ integrity: integrity(runtime),
116
+ crossOrigin: 'anonymous',
117
+ });
118
+ await loadElement(script, document.head);
119
+ }
120
+ const implementation = window.F5SharedV1.get(runtime.url);
121
+ if (!implementation) throw new Error('Shared runtime did not register');
122
+ prepared = implementation.prepare(consumer, release);
123
+ prepared.commit();
124
+ link.media = 'all';
125
+ document.querySelector('[data-f5-baseline-css]')?.remove();
126
+ document.documentElement.dataset.f5SharedRelease = release.pointer.release.sha256;
127
+ document.dispatchEvent(new CustomEvent('f5:shared-ready', { detail: release.pointer }));
128
+ } catch (error) {
129
+ prepared?.dispose();
130
+ link.remove();
131
+ throw error;
132
+ }
133
+ },
134
+ });
135
+ }