@epiccontext/mcp 0.1.41 → 0.1.43

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.
@@ -0,0 +1,991 @@
1
+ /**
2
+ * Storybook Template
3
+ *
4
+ * This file contains the complete Storybook template with EpicContext authentication middleware.
5
+ * The template is bundled as string constants to ensure it's included in the npm package.
6
+ */
7
+ /**
8
+ * Get all Storybook template files
9
+ * Files are organized to be written to CONTEXT/design-system/storybook/
10
+ */
11
+ export function getStorybookTemplateFiles() {
12
+ return [
13
+ // Package configuration
14
+ {
15
+ path: "package.json",
16
+ content: `{
17
+ "name": "@epiccontext/storybook-template",
18
+ "version": "1.0.0",
19
+ "private": true,
20
+ "description": "EpicContext Storybook template with authentication middleware",
21
+ "scripts": {
22
+ "dev": "storybook dev -p 6006",
23
+ "build": "storybook build && node scripts/postbuild.mjs",
24
+ "preview": "npx serve storybook-static"
25
+ },
26
+ "dependencies": {
27
+ "class-variance-authority": "^0.7.0",
28
+ "clsx": "^2.1.1",
29
+ "lucide-react": "^0.460.0",
30
+ "react": "^18.2.0",
31
+ "react-dom": "^18.2.0",
32
+ "tailwind-merge": "^2.5.2",
33
+ "tailwindcss-animate": "^1.0.7"
34
+ },
35
+ "devDependencies": {
36
+ "@storybook/addon-essentials": "^8.4.0",
37
+ "@storybook/addon-interactions": "^8.4.0",
38
+ "@storybook/addon-links": "^8.4.0",
39
+ "@storybook/blocks": "^8.4.0",
40
+ "@storybook/react": "^8.4.0",
41
+ "@storybook/react-vite": "^8.4.0",
42
+ "@storybook/test": "^8.4.0",
43
+ "@types/node": "^20.0.0",
44
+ "@types/react": "^18.2.0",
45
+ "@types/react-dom": "^18.2.0",
46
+ "autoprefixer": "^10.4.20",
47
+ "postcss": "^8.4.47",
48
+ "storybook": "^8.4.0",
49
+ "tailwindcss": "^3.4.14",
50
+ "typescript": "^5.0.0",
51
+ "vite": "^5.0.0"
52
+ }
53
+ }
54
+ `,
55
+ },
56
+ // TypeScript configuration
57
+ {
58
+ path: "tsconfig.json",
59
+ content: `{
60
+ "compilerOptions": {
61
+ "target": "ES2020",
62
+ "useDefineForClassFields": true,
63
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
64
+ "module": "ESNext",
65
+ "skipLibCheck": true,
66
+ "moduleResolution": "bundler",
67
+ "allowImportingTsExtensions": true,
68
+ "resolveJsonModule": true,
69
+ "isolatedModules": true,
70
+ "noEmit": true,
71
+ "jsx": "react-jsx",
72
+ "strict": true,
73
+ "noUnusedLocals": true,
74
+ "noUnusedParameters": true,
75
+ "noFallthroughCasesInSwitch": true,
76
+ "baseUrl": ".",
77
+ "paths": {
78
+ "@/*": ["./*"]
79
+ }
80
+ },
81
+ "include": ["**/*.ts", "**/*.tsx"],
82
+ "exclude": ["node_modules"]
83
+ }
84
+ `,
85
+ },
86
+ // Tailwind configuration
87
+ {
88
+ path: "tailwind.config.ts",
89
+ content: `import type { Config } from "tailwindcss";
90
+
91
+ const config: Config = {
92
+ darkMode: ["class"],
93
+ content: [
94
+ "./stories/**/*.{js,ts,jsx,tsx,mdx}",
95
+ "./components/**/*.{js,ts,jsx,tsx,mdx}",
96
+ ],
97
+ theme: {
98
+ extend: {
99
+ colors: {
100
+ background: "hsl(var(--background))",
101
+ foreground: "hsl(var(--foreground))",
102
+ card: {
103
+ DEFAULT: "hsl(var(--card))",
104
+ foreground: "hsl(var(--card-foreground))",
105
+ },
106
+ popover: {
107
+ DEFAULT: "hsl(var(--popover))",
108
+ foreground: "hsl(var(--popover-foreground))",
109
+ },
110
+ primary: {
111
+ DEFAULT: "hsl(var(--primary))",
112
+ foreground: "hsl(var(--primary-foreground))",
113
+ },
114
+ secondary: {
115
+ DEFAULT: "hsl(var(--secondary))",
116
+ foreground: "hsl(var(--secondary-foreground))",
117
+ },
118
+ muted: {
119
+ DEFAULT: "hsl(var(--muted))",
120
+ foreground: "hsl(var(--muted-foreground))",
121
+ },
122
+ accent: {
123
+ DEFAULT: "hsl(var(--accent))",
124
+ foreground: "hsl(var(--accent-foreground))",
125
+ },
126
+ destructive: {
127
+ DEFAULT: "hsl(var(--destructive))",
128
+ foreground: "hsl(var(--destructive-foreground))",
129
+ },
130
+ border: "hsl(var(--border))",
131
+ input: "hsl(var(--input))",
132
+ ring: "hsl(var(--ring))",
133
+ chart: {
134
+ "1": "hsl(var(--chart-1))",
135
+ "2": "hsl(var(--chart-2))",
136
+ "3": "hsl(var(--chart-3))",
137
+ "4": "hsl(var(--chart-4))",
138
+ "5": "hsl(var(--chart-5))",
139
+ },
140
+ },
141
+ borderRadius: {
142
+ lg: "var(--radius)",
143
+ md: "calc(var(--radius) - 2px)",
144
+ sm: "calc(var(--radius) - 4px)",
145
+ },
146
+ },
147
+ },
148
+ plugins: [require("tailwindcss-animate")],
149
+ };
150
+
151
+ export default config;
152
+ `,
153
+ },
154
+ // PostCSS configuration
155
+ {
156
+ path: "postcss.config.js",
157
+ content: `module.exports = {
158
+ plugins: {
159
+ tailwindcss: {},
160
+ autoprefixer: {},
161
+ },
162
+ };
163
+ `,
164
+ },
165
+ // Vercel configuration
166
+ {
167
+ path: "vercel.json",
168
+ content: `{
169
+ "$schema": "https://openapi.vercel.sh/vercel.json",
170
+ "buildCommand": "npm run build",
171
+ "installCommand": "npm install",
172
+ "framework": null
173
+ }
174
+ `,
175
+ },
176
+ // Git ignore
177
+ {
178
+ path: ".gitignore",
179
+ content: `# Dependencies
180
+ node_modules/
181
+
182
+ # Build output
183
+ storybook-static/
184
+ dist/
185
+ .vercel/
186
+
187
+ # Environment
188
+ .env
189
+ .env.local
190
+ .env.*.local
191
+
192
+ # IDE
193
+ .vscode/
194
+ .idea/
195
+ *.swp
196
+ *.swo
197
+
198
+ # OS
199
+ .DS_Store
200
+ Thumbs.db
201
+
202
+ # Logs
203
+ *.log
204
+ npm-debug.log*
205
+
206
+ # Cache
207
+ .cache/
208
+ `,
209
+ },
210
+ // README
211
+ {
212
+ path: "README.md",
213
+ content: `# Design System Storybook
214
+
215
+ This is your project's design system, powered by Storybook with EpicContext authentication.
216
+
217
+ ## Quick Start
218
+
219
+ \`\`\`bash
220
+ # Install dependencies
221
+ npm install
222
+
223
+ # Start Storybook (development)
224
+ npm run dev
225
+
226
+ # Build for production
227
+ npm run build
228
+ \`\`\`
229
+
230
+ ## Adding Components
231
+
232
+ 1. Create a \`components/ui/\` folder
233
+ 2. Add your component (e.g., \`Button.tsx\`)
234
+ 3. Add a story file (e.g., \`Button.stories.tsx\`)
235
+ 4. Components will appear in Storybook automatically
236
+
237
+ ## Deploying to Vercel
238
+
239
+ 1. Push this folder to a GitHub repository
240
+ 2. Connect it to Vercel
241
+ 3. Add environment variables:
242
+ - \`EPICCONTEXT_STORYBOOK_SECRET\` - Get from EpicContext settings
243
+ - \`EPICCONTEXT_APP_URL\` - Your EpicContext instance URL
244
+ 4. Deploy!
245
+
246
+ ## Authentication
247
+
248
+ When deployed with the environment variables set, your Storybook is protected.
249
+ Only team members logged into EpicContext can access it.
250
+
251
+ During local development, authentication is bypassed.
252
+
253
+ ## Customization
254
+
255
+ - **Design tokens**: Edit \`styles/globals.css\`
256
+ - **Tailwind config**: Edit \`tailwind.config.ts\`
257
+ - **Storybook config**: Edit \`.storybook/main.ts\` and \`.storybook/preview.ts\`
258
+
259
+ ## Structure
260
+
261
+ \`\`\`
262
+ storybook/
263
+ ├── .storybook/ # Storybook configuration
264
+ ├── components/ # Your components (create this)
265
+ ├── lib/ # Utilities
266
+ ├── stories/ # Documentation pages
267
+ ├── styles/ # CSS and design tokens
268
+ ├── scripts/ # Build scripts
269
+ └── middleware.js # EpicContext auth
270
+ \`\`\`
271
+ `,
272
+ },
273
+ // Storybook main config
274
+ {
275
+ path: ".storybook/main.ts",
276
+ content: `import type { StorybookConfig } from "@storybook/react-vite";
277
+
278
+ const config: StorybookConfig = {
279
+ stories: [
280
+ "../stories/**/*.mdx",
281
+ "../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)",
282
+ "../components/**/*.stories.@(js|jsx|mjs|ts|tsx)",
283
+ ],
284
+ addons: [
285
+ "@storybook/addon-links",
286
+ "@storybook/addon-essentials",
287
+ "@storybook/addon-interactions",
288
+ ],
289
+ framework: {
290
+ name: "@storybook/react-vite",
291
+ options: {},
292
+ },
293
+ docs: {
294
+ autodocs: "tag",
295
+ },
296
+ staticDirs: ["../public"],
297
+ viteFinal: async (config) => {
298
+ // Add path aliases to match component imports
299
+ config.resolve = config.resolve || {};
300
+ config.resolve.alias = {
301
+ ...config.resolve.alias,
302
+ // Local paths - create components folder and add components here
303
+ "@/components": new URL("../components", import.meta.url).pathname,
304
+ "@/lib": new URL("../lib", import.meta.url).pathname,
305
+ // Next.js mocks for Storybook (allows using Next.js components)
306
+ "next/navigation": new URL("../lib/next-mocks.ts", import.meta.url).pathname,
307
+ "next/link": new URL("../lib/next-mocks.ts", import.meta.url).pathname,
308
+ "next/dynamic": new URL("../lib/next-mocks.ts", import.meta.url).pathname,
309
+ };
310
+ return config;
311
+ },
312
+ };
313
+
314
+ export default config;
315
+ `,
316
+ },
317
+ // Storybook preview config
318
+ {
319
+ path: ".storybook/preview.ts",
320
+ content: `import type { Preview } from "@storybook/react";
321
+ import "../styles/globals.css";
322
+
323
+ const preview: Preview = {
324
+ parameters: {
325
+ controls: {
326
+ matchers: {
327
+ color: /(background|color)$/i,
328
+ date: /Date$/i,
329
+ },
330
+ },
331
+ docs: {
332
+ toc: true,
333
+ },
334
+ },
335
+ decorators: [],
336
+ };
337
+
338
+ export default preview;
339
+ `,
340
+ },
341
+ // Global styles
342
+ {
343
+ path: "styles/globals.css",
344
+ content: `@tailwind base;
345
+ @tailwind components;
346
+ @tailwind utilities;
347
+
348
+ @layer base {
349
+ :root {
350
+ --background: 0 0% 100%;
351
+ --foreground: 222.2 84% 4.9%;
352
+ --card: 0 0% 100%;
353
+ --card-foreground: 222.2 84% 4.9%;
354
+ --popover: 0 0% 100%;
355
+ --popover-foreground: 222.2 84% 4.9%;
356
+ --primary: 222.2 47.4% 11.2%;
357
+ --primary-foreground: 210 40% 98%;
358
+ --secondary: 210 40% 96.1%;
359
+ --secondary-foreground: 222.2 47.4% 11.2%;
360
+ --muted: 210 40% 96.1%;
361
+ --muted-foreground: 215.4 16.3% 46.9%;
362
+ --accent: 210 40% 96.1%;
363
+ --accent-foreground: 222.2 47.4% 11.2%;
364
+ --destructive: 0 84.2% 60.2%;
365
+ --destructive-foreground: 210 40% 98%;
366
+ --border: 214.3 31.8% 91.4%;
367
+ --input: 214.3 31.8% 91.4%;
368
+ --ring: 222.2 84% 4.9%;
369
+ --radius: 0.5rem;
370
+ --chart-1: 12 76% 61%;
371
+ --chart-2: 173 58% 39%;
372
+ --chart-3: 197 37% 24%;
373
+ --chart-4: 43 74% 66%;
374
+ --chart-5: 27 87% 67%;
375
+ }
376
+
377
+ .dark {
378
+ --background: 222.2 84% 4.9%;
379
+ --foreground: 210 40% 98%;
380
+ --card: 222.2 84% 4.9%;
381
+ --card-foreground: 210 40% 98%;
382
+ --popover: 222.2 84% 4.9%;
383
+ --popover-foreground: 210 40% 98%;
384
+ --primary: 210 40% 98%;
385
+ --primary-foreground: 222.2 47.4% 11.2%;
386
+ --secondary: 217.2 32.6% 17.5%;
387
+ --secondary-foreground: 210 40% 98%;
388
+ --muted: 217.2 32.6% 17.5%;
389
+ --muted-foreground: 215 20.2% 65.1%;
390
+ --accent: 217.2 32.6% 17.5%;
391
+ --accent-foreground: 210 40% 98%;
392
+ --destructive: 0 62.8% 30.6%;
393
+ --destructive-foreground: 210 40% 98%;
394
+ --border: 217.2 32.6% 17.5%;
395
+ --input: 217.2 32.6% 17.5%;
396
+ --ring: 212.7 26.8% 83.9%;
397
+ --chart-1: 220 70% 50%;
398
+ --chart-2: 160 60% 45%;
399
+ --chart-3: 30 80% 55%;
400
+ --chart-4: 280 65% 60%;
401
+ --chart-5: 340 75% 55%;
402
+ }
403
+ }
404
+
405
+ @layer base {
406
+ * {
407
+ @apply border-border;
408
+ }
409
+ body {
410
+ @apply bg-background text-foreground;
411
+ }
412
+ }
413
+
414
+ /* Utility classes for Storybook stories */
415
+ .story-container {
416
+ @apply p-6;
417
+ }
418
+
419
+ .story-grid {
420
+ @apply grid gap-4;
421
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
422
+ }
423
+
424
+ .story-row {
425
+ @apply flex gap-4 flex-wrap items-center;
426
+ }
427
+
428
+ .story-stack {
429
+ @apply flex flex-col gap-4;
430
+ }
431
+ `,
432
+ },
433
+ // Lib utilities
434
+ {
435
+ path: "lib/utils.ts",
436
+ content: `import { type ClassValue, clsx } from "clsx";
437
+ import { twMerge } from "tailwind-merge";
438
+
439
+ export function cn(...inputs: ClassValue[]) {
440
+ return twMerge(clsx(inputs));
441
+ }
442
+ `,
443
+ },
444
+ // Next.js mocks for Storybook
445
+ {
446
+ path: "lib/next-mocks.ts",
447
+ content: `// Mock implementations for Next.js features in Storybook
448
+ // These mocks allow components that use Next.js features to render in Storybook
449
+
450
+ import * as React from "react";
451
+
452
+ // next/navigation mocks
453
+ export const useRouter = () => ({
454
+ push: () => {},
455
+ replace: () => {},
456
+ prefetch: () => {},
457
+ back: () => {},
458
+ forward: () => {},
459
+ refresh: () => {},
460
+ });
461
+
462
+ export const usePathname = () => "/";
463
+ export const useSearchParams = () => new URLSearchParams();
464
+ export const useParams = () => ({});
465
+
466
+ // next/link mock - just renders an anchor tag
467
+ interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
468
+ href: string;
469
+ children?: React.ReactNode;
470
+ }
471
+
472
+ export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
473
+ ({ href, children, ...props }, ref) => {
474
+ return React.createElement("a", { ...props, href, ref }, children);
475
+ }
476
+ );
477
+ Link.displayName = "Link";
478
+
479
+ // next/dynamic mock - returns the component directly
480
+ function dynamicImpl<T>(
481
+ loader: () => Promise<{ default: React.ComponentType<T> }>,
482
+ options?: { loading?: () => React.ReactNode; ssr?: boolean }
483
+ ) {
484
+ const LazyComponent = React.lazy(loader);
485
+
486
+ return function DynamicComponent(props: T) {
487
+ return React.createElement(
488
+ React.Suspense,
489
+ { fallback: options?.loading?.() ?? null },
490
+ React.createElement(LazyComponent, props as React.Attributes & T)
491
+ );
492
+ };
493
+ }
494
+
495
+ export { dynamicImpl as dynamic };
496
+ export default dynamicImpl;
497
+ `,
498
+ },
499
+ // Introduction story
500
+ {
501
+ path: "stories/Introduction.mdx",
502
+ content: `{/* Introduction.mdx */}
503
+ import { Meta } from "@storybook/blocks";
504
+
505
+ <Meta title="Introduction" />
506
+
507
+ # Welcome to Your Design System
508
+
509
+ This is your project's Storybook - a living design system that documents your UI components.
510
+
511
+ ## Getting Started
512
+
513
+ 1. **Install dependencies**: Run \`npm install\` in this folder
514
+ 2. **Add your first component** in the \`components/\` folder
515
+ 3. **Create a story** in the same folder to document it
516
+ 4. **Run Storybook** with \`npm run dev\`
517
+
518
+ ## How to Create Components
519
+
520
+ Create a component file like \`components/ui/Button.tsx\`:
521
+
522
+ \`\`\`tsx
523
+ import * as React from "react";
524
+ import { cn } from "@/lib/utils";
525
+
526
+ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
527
+ variant?: "default" | "destructive" | "outline";
528
+ }
529
+
530
+ export function Button({ className, variant = "default", ...props }: ButtonProps) {
531
+ return (
532
+ <button
533
+ className={cn(
534
+ "px-4 py-2 rounded-md font-medium transition-colors",
535
+ variant === "default" && "bg-primary text-primary-foreground hover:bg-primary/90",
536
+ variant === "destructive" && "bg-destructive text-destructive-foreground hover:bg-destructive/90",
537
+ variant === "outline" && "border border-input bg-background hover:bg-accent",
538
+ className
539
+ )}
540
+ {...props}
541
+ />
542
+ );
543
+ }
544
+ \`\`\`
545
+
546
+ Then create a story file \`components/ui/Button.stories.tsx\`:
547
+
548
+ \`\`\`tsx
549
+ import type { Meta, StoryObj } from "@storybook/react";
550
+ import { Button } from "./Button";
551
+
552
+ const meta: Meta<typeof Button> = {
553
+ title: "Components/Button",
554
+ component: Button,
555
+ tags: ["autodocs"],
556
+ argTypes: {
557
+ variant: {
558
+ control: "select",
559
+ options: ["default", "destructive", "outline"],
560
+ },
561
+ },
562
+ };
563
+
564
+ export default meta;
565
+ type Story = StoryObj<typeof Button>;
566
+
567
+ export const Default: Story = {
568
+ args: {
569
+ children: "Click me",
570
+ variant: "default",
571
+ },
572
+ };
573
+
574
+ export const Destructive: Story = {
575
+ args: {
576
+ children: "Delete",
577
+ variant: "destructive",
578
+ },
579
+ };
580
+
581
+ export const Outline: Story = {
582
+ args: {
583
+ children: "Cancel",
584
+ variant: "outline",
585
+ },
586
+ };
587
+ \`\`\`
588
+
589
+ ## Project Structure
590
+
591
+ \`\`\`
592
+ storybook/
593
+ ├── .storybook/ # Storybook configuration
594
+ │ ├── main.ts # Addons and build config
595
+ │ └── preview.ts # Global decorators and parameters
596
+ ├── components/ # Your components (create this folder)
597
+ │ └── ui/ # UI components + stories
598
+ ├── lib/
599
+ │ ├── utils.ts # Utility functions (cn)
600
+ │ └── next-mocks.ts # Next.js mocks for Storybook
601
+ ├── stories/
602
+ │ └── Introduction.mdx # This file
603
+ ├── styles/
604
+ │ └── globals.css # Tailwind + design tokens
605
+ ├── middleware.js # EpicContext authentication
606
+ ├── tailwind.config.ts # Tailwind configuration
607
+ └── package.json
608
+ \`\`\`
609
+
610
+ ## Design Tokens
611
+
612
+ All tokens are defined in \`styles/globals.css\` using CSS custom properties:
613
+
614
+ - **Colors**: Primary, secondary, muted, accent, destructive
615
+ - **Backgrounds**: background, card, popover
616
+ - **Border**: border, input, ring
617
+ - **Radius**: lg, md, sm
618
+
619
+ Light and dark themes are fully supported. Customize these tokens to match your brand.
620
+
621
+ ## Authentication
622
+
623
+ This Storybook is protected by EpicContext authentication. Users must be logged into your EpicContext organization to view it when deployed.
624
+
625
+ ### Local Development
626
+
627
+ During local development (\`npm run dev\`), authentication is bypassed so you can work freely.
628
+
629
+ ### Deploying to Vercel
630
+
631
+ 1. Push this folder to a GitHub repository
632
+ 2. Connect it to Vercel
633
+ 3. Set environment variables:
634
+ - \`EPICCONTEXT_STORYBOOK_SECRET\` - Get from EpicContext settings
635
+ - \`EPICCONTEXT_APP_URL\` - Your EpicContext instance URL
636
+ 4. Deploy!
637
+
638
+ The middleware will automatically protect your Storybook and redirect unauthenticated users to log in.
639
+
640
+ ## Next Steps
641
+
642
+ - [ ] Create a \`components/ui\` folder
643
+ - [ ] Add your first component (Button is a great start)
644
+ - [ ] Document it with a story file
645
+ - [ ] Deploy to Vercel
646
+ - [ ] Link the URL in EpicContext Design System section
647
+
648
+ ---
649
+
650
+ Built with [Storybook](https://storybook.js.org) | Secured by [EpicContext](https://epiccontext.com)
651
+ `,
652
+ },
653
+ // Post-build script for Vercel
654
+ {
655
+ path: "scripts/postbuild.mjs",
656
+ content: `/**
657
+ * Post-build script for Vercel Build Output API v3
658
+ *
659
+ * This script restructures the Storybook build output into Vercel's
660
+ * Build Output API format, enabling Edge Middleware for authentication.
661
+ */
662
+
663
+ import fs from "fs";
664
+ import path from "path";
665
+ import { fileURLToPath } from "url";
666
+
667
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
668
+ const rootDir = path.resolve(__dirname, "..");
669
+ const storybookOutput = path.join(rootDir, "storybook-static");
670
+ const vercelOutput = path.join(rootDir, ".vercel", "output");
671
+
672
+ console.log("Setting up Vercel Build Output API v3...");
673
+
674
+ // Clean and create output directory
675
+ if (fs.existsSync(vercelOutput)) {
676
+ fs.rmSync(vercelOutput, { recursive: true });
677
+ }
678
+ fs.mkdirSync(path.join(vercelOutput, "static"), { recursive: true });
679
+ fs.mkdirSync(path.join(vercelOutput, "functions", "middleware.func"), {
680
+ recursive: true,
681
+ });
682
+
683
+ // Copy static files recursively
684
+ console.log("Copying static files...");
685
+ const copyRecursive = (src, dest) => {
686
+ if (!fs.existsSync(src)) return;
687
+
688
+ if (fs.statSync(src).isDirectory()) {
689
+ fs.mkdirSync(dest, { recursive: true });
690
+ for (const file of fs.readdirSync(src)) {
691
+ copyRecursive(path.join(src, file), path.join(dest, file));
692
+ }
693
+ } else {
694
+ fs.copyFileSync(src, dest);
695
+ }
696
+ };
697
+
698
+ copyRecursive(storybookOutput, path.join(vercelOutput, "static"));
699
+
700
+ // Copy middleware function
701
+ console.log("Setting up Edge Middleware...");
702
+ fs.copyFileSync(
703
+ path.join(rootDir, "middleware.js"),
704
+ path.join(vercelOutput, "functions", "middleware.func", "index.js")
705
+ );
706
+
707
+ // Write the function config
708
+ fs.writeFileSync(
709
+ path.join(vercelOutput, "functions", "middleware.func", ".vc-config.json"),
710
+ JSON.stringify(
711
+ {
712
+ runtime: "edge",
713
+ entrypoint: "index.js",
714
+ },
715
+ null,
716
+ 2
717
+ )
718
+ );
719
+
720
+ // Create the main config.json
721
+ console.log("Writing config.json...");
722
+ fs.writeFileSync(
723
+ path.join(vercelOutput, "config.json"),
724
+ JSON.stringify(
725
+ {
726
+ version: 3,
727
+ routes: [
728
+ // Static assets - bypass middleware, serve directly
729
+ {
730
+ src: "^/assets/.*",
731
+ headers: { "Cache-Control": "public, max-age=31536000, immutable" },
732
+ continue: true,
733
+ },
734
+ {
735
+ src: "^/sb-.*",
736
+ continue: true,
737
+ },
738
+ {
739
+ src: ".*\\\\.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot|json|map)$",
740
+ continue: true,
741
+ },
742
+ // HTML requests go through middleware
743
+ {
744
+ src: "^/(?!assets|sb-).*$",
745
+ middlewarePath: "middleware",
746
+ continue: true,
747
+ },
748
+ // Serve static files
749
+ { handle: "filesystem" },
750
+ // Fallback to index.html for SPA routing
751
+ {
752
+ src: "/(.*)",
753
+ dest: "/index.html",
754
+ },
755
+ ],
756
+ },
757
+ null,
758
+ 2
759
+ )
760
+ );
761
+
762
+ console.log("Vercel Build Output API setup complete!");
763
+ console.log(" Static files: .vercel/output/static");
764
+ console.log(" Middleware: .vercel/output/functions/middleware.func");
765
+ console.log(" Config: .vercel/output/config.json");
766
+ `,
767
+ },
768
+ // Authentication middleware
769
+ {
770
+ path: "middleware.js",
771
+ content: `/**
772
+ * EpicContext Storybook Authentication Middleware
773
+ *
774
+ * Vercel Edge Middleware for non-Next.js projects.
775
+ * Protects Storybook by validating JWT tokens from EpicContext.
776
+ */
777
+
778
+ export const config = {
779
+ // Match all paths - we'll filter inside the middleware
780
+ matcher: ["/(.*)", "/"],
781
+ };
782
+
783
+ const COOKIE_NAME = "epiccontext_storybook_token";
784
+ const COOKIE_MAX_AGE = 60 * 60 * 24; // 24 hours
785
+
786
+ function base64UrlDecode(data) {
787
+ const padded = data + "=".repeat((4 - (data.length % 4)) % 4);
788
+ return atob(padded.replace(/-/g, "+").replace(/_/g, "/"));
789
+ }
790
+
791
+ async function createSignature(data, secret) {
792
+ const encoder = new TextEncoder();
793
+ const keyData = encoder.encode(secret);
794
+ const messageData = encoder.encode(data);
795
+
796
+ const cryptoKey = await crypto.subtle.importKey(
797
+ "raw",
798
+ keyData,
799
+ { name: "HMAC", hash: "SHA-256" },
800
+ false,
801
+ ["sign"]
802
+ );
803
+
804
+ const signature = await crypto.subtle.sign("HMAC", cryptoKey, messageData);
805
+ const base64 = btoa(String.fromCharCode(...new Uint8Array(signature)));
806
+ return base64.replace(/\\+/g, "-").replace(/\\//g, "_").replace(/=/g, "");
807
+ }
808
+
809
+ async function deriveSecret(secret) {
810
+ const encoder = new TextEncoder();
811
+ const data = encoder.encode(\`epiccontext-storybook:\${secret}\`);
812
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
813
+ const hashArray = new Uint8Array(hashBuffer);
814
+ return Array.from(hashArray)
815
+ .map((b) => b.toString(16).padStart(2, "0"))
816
+ .join("");
817
+ }
818
+
819
+ async function validateToken(token, secret, expectedAudience) {
820
+ try {
821
+ const parts = token.split(".");
822
+ if (parts.length !== 3) {
823
+ return { valid: false, error: "Invalid token format" };
824
+ }
825
+
826
+ const [encodedHeader, encodedPayload, signature] = parts;
827
+ const derivedSecret = await deriveSecret(secret);
828
+
829
+ const expectedSignature = await createSignature(
830
+ \`\${encodedHeader}.\${encodedPayload}\`,
831
+ derivedSecret
832
+ );
833
+
834
+ if (signature !== expectedSignature) {
835
+ return { valid: false, error: "Invalid signature" };
836
+ }
837
+
838
+ const payload = JSON.parse(base64UrlDecode(encodedPayload));
839
+
840
+ if (payload.iss !== "epiccontext") {
841
+ return { valid: false, error: "Invalid issuer" };
842
+ }
843
+
844
+ const now = Math.floor(Date.now() / 1000);
845
+ if (payload.exp < now) {
846
+ return { valid: false, error: "Token expired" };
847
+ }
848
+
849
+ if (expectedAudience && payload.aud !== expectedAudience) {
850
+ return { valid: false, error: "Invalid audience" };
851
+ }
852
+
853
+ return { valid: true, payload };
854
+ } catch (e) {
855
+ return { valid: false, error: "Token parsing failed" };
856
+ }
857
+ }
858
+
859
+ function getEpicContextUrl() {
860
+ try {
861
+ return process.env.EPICCONTEXT_APP_URL || "https://epic-context.vercel.app";
862
+ } catch {
863
+ return "https://epic-context.vercel.app";
864
+ }
865
+ }
866
+
867
+ function getSecret() {
868
+ try {
869
+ return process.env.EPICCONTEXT_STORYBOOK_SECRET || null;
870
+ } catch {
871
+ return null;
872
+ }
873
+ }
874
+
875
+ function parseCookies(cookieHeader) {
876
+ if (!cookieHeader) return {};
877
+ return Object.fromEntries(
878
+ cookieHeader.split(";").map((cookie) => {
879
+ const [key, ...val] = cookie.trim().split("=");
880
+ return [key, val.join("=")];
881
+ })
882
+ );
883
+ }
884
+
885
+ // Static file extensions that should bypass authentication
886
+ const STATIC_EXTENSIONS = [
887
+ '.js', '.css', '.json', '.png', '.jpg', '.jpeg', '.gif', '.svg',
888
+ '.ico', '.woff', '.woff2', '.ttf', '.eot', '.map', '.txt'
889
+ ];
890
+
891
+ // Paths that should bypass authentication
892
+ const BYPASS_PATHS = [
893
+ '/assets/', '/sb-', '/static/', '/_next/', '/favicon'
894
+ ];
895
+
896
+ function shouldBypassAuth(pathname) {
897
+ const hasStaticExtension = STATIC_EXTENSIONS.some(ext => pathname.endsWith(ext));
898
+ if (hasStaticExtension) return true;
899
+
900
+ const isBypassPath = BYPASS_PATHS.some(path => pathname.startsWith(path));
901
+ if (isBypassPath) return true;
902
+
903
+ return false;
904
+ }
905
+
906
+ export default async function middleware(request) {
907
+ const url = new URL(request.url);
908
+ const pathname = url.pathname;
909
+
910
+ // Skip authentication for static files and internal Storybook paths
911
+ if (shouldBypassAuth(pathname)) {
912
+ return undefined;
913
+ }
914
+
915
+ const secret = getSecret();
916
+
917
+ // If no secret configured, allow access (development mode)
918
+ if (!secret) {
919
+ console.warn(
920
+ "EPICCONTEXT_STORYBOOK_SECRET not set - Storybook is unprotected"
921
+ );
922
+ return undefined;
923
+ }
924
+
925
+ const hostname = url.hostname;
926
+
927
+ // Check for token in URL params
928
+ const tokenFromUrl = url.searchParams.get("token");
929
+
930
+ // Check for token in cookie
931
+ const cookieHeader = request.headers.get("cookie");
932
+ const cookies = parseCookies(cookieHeader);
933
+ const tokenFromCookie = cookies[COOKIE_NAME];
934
+
935
+ const token = tokenFromUrl || tokenFromCookie;
936
+
937
+ if (!token) {
938
+ // No token - redirect to EpicContext login
939
+ const epicContextUrl = getEpicContextUrl();
940
+ const redirectUrl = new URL("/auth/storybook", epicContextUrl);
941
+ redirectUrl.searchParams.set("redirect", request.url);
942
+
943
+ return Response.redirect(redirectUrl.toString(), 302);
944
+ }
945
+
946
+ // Validate the token
947
+ const result = await validateToken(token, secret, hostname);
948
+
949
+ if (!result.valid) {
950
+ // Invalid token - redirect to EpicContext with error
951
+ const epicContextUrl = getEpicContextUrl();
952
+ const errorUrl = new URL("/auth/storybook/error", epicContextUrl);
953
+ errorUrl.searchParams.set("error", result.error || "Invalid token");
954
+ errorUrl.searchParams.set("redirect", request.url);
955
+
956
+ return Response.redirect(errorUrl.toString(), 302);
957
+ }
958
+
959
+ // Token is valid
960
+ // If token came from URL, set cookie and redirect to clean URL
961
+ if (tokenFromUrl && !tokenFromCookie) {
962
+ const cleanUrl = new URL(request.url);
963
+ cleanUrl.searchParams.delete("token");
964
+
965
+ const isSecure = url.protocol === "https:";
966
+ const cookieValue = \`\${COOKIE_NAME}=\${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=\${COOKIE_MAX_AGE}\${isSecure ? "; Secure" : ""}\`;
967
+
968
+ return new Response(null, {
969
+ status: 302,
970
+ headers: {
971
+ "Location": cleanUrl.toString(),
972
+ "Set-Cookie": cookieValue,
973
+ },
974
+ });
975
+ }
976
+
977
+ // Continue to static files
978
+ return undefined;
979
+ }
980
+ `,
981
+ },
982
+ // Empty public folder placeholder
983
+ {
984
+ path: "public/.gitkeep",
985
+ content: `# This folder contains static assets for Storybook
986
+ # Add images, fonts, or other assets here
987
+ `,
988
+ },
989
+ ];
990
+ }
991
+ //# sourceMappingURL=storybook.js.map