@systemverification/styling-kit 2.0.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.
@@ -0,0 +1,614 @@
1
+ # System Verification — Design System
2
+
3
+ This file defines the visual identity for System Verification products. When building UI, **always follow these conventions** — do not invent new colors, fonts, or spacing values.
4
+
5
+ ---
6
+
7
+ ## Brand Colors (CSS Custom Properties)
8
+
9
+ Use these as CSS custom properties. Define them in `:root` and reference via `var(--token-name)`.
10
+
11
+ ```css
12
+ :root {
13
+ /* ── Core palette ── */
14
+ --color-dark-blue: #0B1B28; /* Primary background, dark surfaces, input backgrounds */
15
+ --color-light-blue: #435364; /* Secondary backgrounds, dividers, card borders */
16
+ --color-sand: #F2F2EA; /* Primary text color on dark backgrounds */
17
+ --color-yellow: #F7F965; /* Accent / CTA — buttons, links, active states, focus rings */
18
+ --color-white: #ffffff; /* Headings text, high-emphasis text */
19
+
20
+ /* ── Semantic colors ── */
21
+ --color-error: #ef4444; /* Error states, destructive actions, unhealthy badges */
22
+ --color-success: #10b981; /* Running/healthy states, positive badges */
23
+ --color-yellow-hover: #f8faaa; /* Primary button hover — lightened yellow */
24
+ --color-yellow-active: #f5f950;/* Primary button active — saturated yellow */
25
+ --color-error-light: #f87171; /* Danger button text, error badge text — lightened red */
26
+
27
+ /* ── Border radius scale ── */
28
+ --radius-sm: 4px; /* Inputs, code blocks, small elements */
29
+ --radius-md: 8px; /* Cards, form sections, panels */
30
+ --radius-pill: 100px; /* Buttons, badges, tags, pills */
31
+
32
+ /* ── Motion ── */
33
+ --transition: 0.3s ease; /* Default transition for all interactive elements */
34
+ }
35
+ ```
36
+
37
+ ### Extended Color Usage
38
+
39
+ These derivative colors are used throughout the UI. Most are `rgba()` variants of the core palette, used contextually. The tokenized ones (marked with `var(--...)`) **must** be referenced via their token — never hardcoded:
40
+
41
+ | Purpose | Value | Base color |
42
+ |---|---|---|
43
+ | Card background | `rgba(67, 83, 100, 0.35)` | light-blue |
44
+ | Card/section border | `rgba(67, 83, 100, 0.5)` | light-blue |
45
+ | Header border | `rgba(67, 83, 100, 0.4)` | light-blue |
46
+ | Form label text | `rgba(242, 242, 234, 0.7)` | sand |
47
+ | Secondary/meta text | `rgba(242, 242, 234, 0.5)` | sand |
48
+ | Muted text (placeholders) | `rgba(242, 242, 234, 0.3)` | sand |
49
+ | Subtle borders (inputs) | `rgba(242, 242, 234, 0.15)` | sand |
50
+ | Subtle button hover bg | `rgba(242, 242, 234, 0.1)` | sand |
51
+ | Button hover (yellow) | `var(--color-yellow-hover)` = `#f8faaa` | yellow lightened |
52
+ | Button active (yellow) | `var(--color-yellow-active)` = `#f5f950` | yellow saturated |
53
+ | Yellow badge background | `rgba(247, 249, 101, 0.15)` | yellow |
54
+ | Yellow focus ring shadow | `rgba(247, 249, 101, 0.15)` | yellow |
55
+ | Green badge text | `#34d399` | success lightened |
56
+ | Green badge background | `rgba(16, 185, 129, 0.15)` | success |
57
+ | Red badge text / danger text | `var(--color-error-light)` = `#f87171` | error lightened |
58
+ | Red badge background | `rgba(239, 68, 68, 0.15)` | error |
59
+ | Warning/branch text | `#fb923c` | orange (warning) |
60
+ | Warning background | `rgba(251, 146, 60, 0.1)` | orange |
61
+
62
+ ### Design Principle
63
+
64
+ The palette is **dark-mode first**. Background is `--color-dark-blue`, primary text is `--color-sand`, headings are `--color-white`, and `--color-yellow` is the singular accent color used for all interactive/CTA elements. Do not introduce additional accent colors.
65
+
66
+ ---
67
+
68
+ ## Typography
69
+
70
+ ### Fonts
71
+
72
+ Two custom fonts, self-hosted as `.woff2` files:
73
+
74
+ | Font | CSS family | Usage | File |
75
+ |---|---|---|---|
76
+ | **BodyFont** | `"BodyFont", sans-serif` | All body text (globally inherited) | `fonts/body-font.woff2`, `fonts/body-font-italic.woff2` |
77
+ | **HeadingFont** | `"HeadingFont", sans-serif` | All headings (`h1`–`h6`) | `fonts/heading-font.woff2` |
78
+ | Monospace | `monospace` | Code blocks, technical values | System default |
79
+
80
+ ### @font-face Declarations
81
+
82
+ ```css
83
+ @font-face {
84
+ font-family: "BodyFont";
85
+ src: url("/fonts/body-font.woff2") format("woff2");
86
+ font-weight: normal;
87
+ font-style: normal;
88
+ font-display: swap;
89
+ }
90
+
91
+ @font-face {
92
+ font-family: "BodyFont";
93
+ src: url("/fonts/body-font-italic.woff2") format("woff2");
94
+ font-weight: normal;
95
+ font-style: italic;
96
+ font-display: swap;
97
+ }
98
+
99
+ @font-face {
100
+ font-family: "HeadingFont";
101
+ src: url("/fonts/heading-font.woff2") format("woff2");
102
+ font-weight: normal;
103
+ font-style: normal;
104
+ font-display: swap;
105
+ }
106
+ ```
107
+
108
+ ### Font Weights
109
+
110
+ | Weight | Usage |
111
+ |---|---|
112
+ | `300` | Body text default |
113
+ | `400` | Meta text, secondary content |
114
+ | `500` | Labels, links, buttons, nav items |
115
+ | `600` | Headings, badges, emphasized labels |
116
+
117
+ ### Font Sizes
118
+
119
+ | Size | Usage |
120
+ |---|---|
121
+ | `1.05rem` | Card titles |
122
+ | `1rem` | Header title, base size |
123
+ | `0.875rem` | Form inputs |
124
+ | `0.85rem` | Section headings, buttons, toasts |
125
+ | `0.8rem` | Form labels, links, user names |
126
+ | `0.78rem` | Meta text, action buttons, banners |
127
+ | `0.75rem` | Status text, small inputs |
128
+ | `0.7rem` | Status badges, health badges |
129
+ | `0.68rem` | Role badges, code text, small UI |
130
+ | `0.65rem` | Tiny labels |
131
+
132
+ ### Text Styling Conventions
133
+
134
+ - Headings, labels, badges, and status indicators use `text-transform: uppercase`
135
+ - Letter spacing: `0.02em` for header/CTA text, `0.04em`–`0.05em` for labels and badges
136
+ - Use `-webkit-font-smoothing: antialiased` on `body`
137
+
138
+ ---
139
+
140
+ ## Logo & Favicon
141
+
142
+ | Asset | File | Format | Notes |
143
+ |---|---|---|---|
144
+ | **Logo** | `logo.svg` | SVG | "System Verification" wordmark in white (`fill: #ffffff`). Display at `height: 28px`. |
145
+ | **Favicon** | `favicon.png` | PNG | "S" symbol in orange/gold tones. ~92 KB. |
146
+
147
+ ### Logo Usage
148
+
149
+ ```html
150
+ <img src="/logo.svg" alt="System Verification" style="height: 28px;">
151
+ ```
152
+
153
+ ```html
154
+ <link rel="icon" type="image/png" href="/favicon.png">
155
+ ```
156
+
157
+ The logo SVG uses `fill: #ffffff` — it is designed for dark backgrounds only. If you need it on a light background, you must create a separate dark variant.
158
+
159
+ ---
160
+
161
+ ## Layout Conventions
162
+
163
+ | Property | Value | Context |
164
+ |---|---|---|
165
+ | Max content width | `1300px` | Header and main content area |
166
+ | Page padding (horizontal) | `2rem` | Header, main |
167
+ | Page padding (vertical) | `2.5rem` top/bottom | Main area |
168
+ | Card grid | `repeat(auto-fill, minmax(340px, 1fr))` | Instance/item cards |
169
+ | Card gap | `1.25rem` | Between grid items |
170
+ | Card padding | `1.5rem` | Inside cards |
171
+
172
+ ### Sticky Header
173
+
174
+ The header uses a frosted-glass effect:
175
+
176
+ ```css
177
+ header {
178
+ position: sticky;
179
+ top: 0;
180
+ z-index: 100;
181
+ background: var(--color-dark-blue);
182
+ backdrop-filter: blur(8px);
183
+ border-bottom: 1px solid rgba(67, 83, 100, 0.4);
184
+ }
185
+ ```
186
+
187
+ ---
188
+
189
+ ## Component Specifications
190
+
191
+ ### Button
192
+
193
+ **Variants:**
194
+ - **Primary (CTA)**: yellow background for calls-to-action
195
+ - **Secondary/Action**: transparent button for secondary actions
196
+ - **Ghost**: minimal styling for tertiary interactions
197
+ - **Danger**: red-tinted for destructive actions
198
+
199
+ **Structure:**
200
+ - Button element with rounded pill shape (`border-radius: var(--radius-pill)`)
201
+ - Padding: `0.5rem 1rem` (adjust for density)
202
+ - Font: `0.85rem`, `font-weight: 500`
203
+ - Always uppercase via `text-transform: uppercase`
204
+
205
+ **Detailed Variants:**
206
+
207
+ | Variant | Background | Text | Border | Hover State |
208
+ |---|---|---|---|---|
209
+ | Primary | `var(--color-yellow)` | `var(--color-dark-blue)` | none | `var(--color-yellow-hover)` |
210
+ | Secondary | transparent | `var(--color-sand)` | `1px solid rgba(242, 242, 234, 0.2)` | `rgba(242, 242, 234, 0.1)` background |
211
+ | Danger | transparent | `var(--color-error-light)` | `rgba(239, 68, 68, 0.3)` | red-tinted background `rgba(239, 68, 68, 0.1)` |
212
+
213
+ **Required States** (all buttons must include):
214
+ - **Default**: base styling as defined
215
+ - **Hover**: background/text shift per variant
216
+ - **Active**: darker variant of hover state
217
+ - **Focus-visible**: `outline: 2px solid var(--color-yellow)`, `outline-offset: 2px`
218
+ - **Disabled**: `opacity: 0.5`, `cursor: not-allowed`, no pointer events
219
+
220
+ **Code Example (Primary):**
221
+ ```css
222
+ button.primary {
223
+ background: var(--color-yellow);
224
+ color: var(--color-dark-blue);
225
+ border: none;
226
+ border-radius: var(--radius-pill);
227
+ padding: 0.5rem 1rem;
228
+ font: 0.85rem;
229
+ font-weight: 500;
230
+ text-transform: uppercase;
231
+ cursor: pointer;
232
+ transition: var(--transition);
233
+ }
234
+
235
+ button.primary:hover {
236
+ background: var(--color-yellow-hover);
237
+ }
238
+
239
+ button.primary:active {
240
+ background: var(--color-yellow-active);
241
+ }
242
+
243
+ button.primary:focus-visible {
244
+ outline: 2px solid var(--color-yellow);
245
+ outline-offset: 2px;
246
+ }
247
+
248
+ button.primary:disabled {
249
+ opacity: 0.5;
250
+ cursor: not-allowed;
251
+ }
252
+ ```
253
+
254
+ ---
255
+
256
+ ### Card
257
+
258
+ **Structure:**
259
+ - Container with border and translucent background
260
+ - Optional title (uppercase, `1.05rem`, `font-weight: 600`)
261
+ - Content area (flexible layout)
262
+ - Optional footer section
263
+
264
+ **Styling:**
265
+ - `background: rgba(67, 83, 100, 0.35)`
266
+ - `border: 1px solid rgba(67, 83, 100, 0.5)`
267
+ - `border-radius: var(--radius-md)`
268
+ - `padding: 1.5rem`
269
+
270
+ **Required States:**
271
+ - **Default**: base styling
272
+ - **Hover**: `transform: translateY(-2px)`, `box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25)`
273
+ - **Focus** (if interactive): highlight border with accent color
274
+ - **Disabled** (if interactive): reduced opacity, no pointer
275
+
276
+ **Code Example:**
277
+ ```css
278
+ .card {
279
+ background: rgba(67, 83, 100, 0.35);
280
+ border: 1px solid rgba(67, 83, 100, 0.5);
281
+ border-radius: var(--radius-md);
282
+ padding: 1.5rem;
283
+ transition: var(--transition);
284
+ }
285
+
286
+ .card:hover {
287
+ transform: translateY(-2px);
288
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
289
+ }
290
+
291
+ .card-title {
292
+ font-family: "HeadingFont", sans-serif;
293
+ font-size: 1.05rem;
294
+ font-weight: 600;
295
+ text-transform: uppercase;
296
+ color: var(--color-white);
297
+ margin-bottom: 0.75rem;
298
+ }
299
+ ```
300
+
301
+ ---
302
+
303
+ ### Badges / Status Pills
304
+
305
+ - Pill shaped (`border-radius: var(--radius-pill)`)
306
+ - Font size: `0.68rem`–`0.7rem`, `font-weight: 600`
307
+ - Letter spacing: `0.04em` (uppercase)
308
+ - Background: `0.12`–`0.15` alpha tint of status color
309
+ - Text: lighter shade of status color
310
+
311
+ **Status Colors:**
312
+ - **Success**: text `#34d399`, background `rgba(16, 185, 129, 0.15)`
313
+ - **Error**: text `#f87171`, background `rgba(239, 68, 68, 0.15)`
314
+ - **Warning**: text `#fb923c`, background `rgba(251, 146, 60, 0.1)`
315
+ - **Yellow/Accent**: text `var(--color-yellow)`, background `rgba(247, 249, 101, 0.15)`
316
+
317
+ ---
318
+
319
+ ### Inputs
320
+
321
+ - `background: var(--color-dark-blue)`
322
+ - `border: 1px solid rgba(242, 242, 234, 0.15)`
323
+ - `border-radius: var(--radius-sm)`
324
+ - `padding: 0.5rem 0.75rem`
325
+ - `font-size: 0.875rem`
326
+ - `color: var(--color-sand)`
327
+
328
+ **Required States:**
329
+ - **Default**: as above
330
+ - **Focus**: `border-color: var(--color-yellow)` with `box-shadow: 0 0 0 2px rgba(247, 249, 101, 0.15)`
331
+ - **Disabled**: `opacity: 0.5`, `cursor: not-allowed`
332
+ - **Error**: `border-color: rgba(239, 68, 68, 0.5)`
333
+
334
+ ---
335
+
336
+ ### Toasts / Notifications
337
+
338
+ - Floating element with `border-radius: var(--radius-pill)`
339
+ - `box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3)`
340
+ - Padding: `1rem 1.5rem`
341
+
342
+ **Variants:**
343
+ - **Default**: `background: rgba(242, 242, 234, 0.9)`, `color: var(--color-dark-blue)`
344
+ - **Error**: `background: var(--color-error)`, `color: var(--color-white)`
345
+ - **Success**: `background: var(--color-success)`, `color: var(--color-white)`
346
+
347
+ ---
348
+
349
+ ## Common Styling Anti-Patterns
350
+
351
+ **Before writing any styled code, review these patterns to avoid.**
352
+
353
+ ### ❌ Hardcoded Colors
354
+
355
+ ```css
356
+ /* WRONG */
357
+ background: #123456;
358
+ color: #f0f0f0;
359
+ border-color: #ff9900;
360
+ ```
361
+
362
+ ```css
363
+ /* CORRECT */
364
+ background: var(--color-dark-blue);
365
+ color: var(--color-sand);
366
+ border-color: var(--color-yellow);
367
+ ```
368
+
369
+ **Rule:** All colors must use `var(--color-*)` tokens defined in `:root`. Never hardcode hex values.
370
+
371
+ ---
372
+
373
+ ### ❌ Inconsistent or Magic Spacing
374
+
375
+ ```css
376
+ /* WRONG */
377
+ margin: 17px;
378
+ padding: 13px 21px;
379
+ gap: 9px;
380
+ ```
381
+
382
+ ```css
383
+ /* CORRECT */
384
+ margin: 1.5rem; /* Use consistent scale */
385
+ padding: 1rem 1.25rem;
386
+ gap: 0.75rem;
387
+ ```
388
+
389
+ **Rule:** Use the defined spacing scale. No arbitrary pixel values. Use `rem` units for consistency.
390
+
391
+ ---
392
+
393
+ ### ❌ Random Font Sizes (Not on Scale)
394
+
395
+ ```css
396
+ /* WRONG */
397
+ font-size: 15px;
398
+ font-size: 18px;
399
+ font-size: 12px;
400
+ ```
401
+
402
+ ```css
403
+ /* CORRECT */
404
+ font-size: 0.875rem; /* Form inputs */
405
+ font-size: 1rem; /* Base size */
406
+ font-size: 1.05rem; /* Card titles */
407
+ ```
408
+
409
+ **Rule:** Use only the defined typography scale. Do not invent new sizes.
410
+
411
+ ---
412
+
413
+ ### ❌ Accent Color Misuse
414
+
415
+ ```css
416
+ /* WRONG */
417
+ background: var(--color-yellow); /* Large blocks */
418
+ .section { background: var(--color-yellow); } /* Entire panel */
419
+ ```
420
+
421
+ ```css
422
+ /* CORRECT */
423
+ button { background: var(--color-yellow); } /* CTAs */
424
+ a { color: var(--color-yellow); } /* Links */
425
+ input:focus { outline-color: var(--color-yellow); } /* Interactive focus */
426
+ ```
427
+
428
+ **Rule:** Yellow (`--color-yellow`) is **only for interactive/CTA elements**. Never use it for large background areas or non-interactive content.
429
+
430
+ ---
431
+
432
+ ### ❌ Missing Component States
433
+
434
+ ```css
435
+ /* WRONG */
436
+ button {
437
+ background: var(--color-yellow);
438
+ color: var(--color-dark-blue);
439
+ }
440
+ /* No hover, focus, active, or disabled states defined */
441
+ ```
442
+
443
+ ```css
444
+ /* CORRECT */
445
+ button {
446
+ background: var(--color-yellow);
447
+ color: var(--color-dark-blue);
448
+ transition: var(--transition);
449
+ }
450
+
451
+ button:hover {
452
+ background: var(--color-yellow-hover);
453
+ }
454
+
455
+ button:focus-visible {
456
+ outline: 2px solid var(--color-yellow);
457
+ }
458
+
459
+ button:active {
460
+ background: var(--color-yellow-active);
461
+ }
462
+
463
+ button:disabled {
464
+ opacity: 0.5;
465
+ cursor: not-allowed;
466
+ }
467
+ ```
468
+
469
+ **Rule:** All interactive components **must** include `default`, `hover`, `focus-visible`, `active`, and `disabled` states.
470
+
471
+ ---
472
+
473
+ ### ❌ Inline Styles
474
+
475
+ ```css
476
+ /* WRONG */
477
+ <div style="background: red; margin: 10px;">
478
+ ```
479
+
480
+ ```css
481
+ /* CORRECT */
482
+ <div class="my-card">
483
+ /* Then in CSS */
484
+ .my-card { background: var(--color-light-blue); margin: 1rem; }
485
+ ```
486
+
487
+ **Rule:** All styles belong in external stylesheets. Never use `style=""` attributes.
488
+
489
+ ---
490
+
491
+ ### ❌ Non-Semantic Color Values
492
+
493
+ ```css
494
+ /* WRONG */
495
+ .label { color: rgba(200, 200, 200, 0.8); }
496
+ ```
497
+
498
+ ```css
499
+ /* CORRECT */
500
+ .label { color: rgba(242, 242, 234, 0.7); } /* Derived from sand token */
501
+ ```
502
+
503
+ **Rule:** All rgba values must be derived from the core palette tokens documented in this spec.
504
+
505
+ ---
506
+
507
+ ## Box Shadows
508
+
509
+ | Shadow | Usage |
510
+ |---|---|
511
+ | `0 0 0 2px rgba(247, 249, 101, 0.15)` | Input focus ring |
512
+ | `0 8px 24px rgba(0, 0, 0, 0.25)` | Card hover lift |
513
+ | `0 4px 16px rgba(0, 0, 0, 0.3)` | Toast / floating elements |
514
+
515
+ ---
516
+
517
+ ## Animation
518
+
519
+ | Name | Value | Usage |
520
+ |---|---|---|
521
+ | `pulse-badge` | `opacity 1→0.5→1`, `1.5s ease-in-out infinite` | Loading/starting state badges |
522
+ | Default transition | `0.3s ease` (via `var(--transition)`) | All interactive hover/focus states |
523
+
524
+ ---
525
+
526
+ ## CSS Reset
527
+
528
+ Always start with this minimal reset:
529
+
530
+ ```css
531
+ * {
532
+ box-sizing: border-box;
533
+ margin: 0;
534
+ padding: 0;
535
+ }
536
+ ```
537
+
538
+ ---
539
+
540
+ ## Quick Start
541
+
542
+ To apply this design system to a new project:
543
+
544
+ 1. Copy the `design-system/` folder into your project (see folder structure below)
545
+ 2. Add the `@font-face` declarations and `:root` tokens to your main CSS
546
+ 3. Reference `logo.svg` and `favicon.png` in your HTML
547
+ 4. Use `var(--color-*)` and `var(--radius-*)` tokens — never hardcode brand colors
548
+ 5. Follow the typography, component, and layout patterns above
549
+
550
+
551
+ ---
552
+
553
+ # Additions (Normalized for AI usage)
554
+
555
+ ## Additional Semantic Tokens
556
+
557
+ Extend system with these tokens (do NOT remove originals):
558
+
559
+ ```css
560
+ :root {
561
+ --text-primary: var(--color-white);
562
+ --text-secondary: rgba(242, 242, 234, 0.7);
563
+ --text-muted: rgba(242, 242, 234, 0.5);
564
+
565
+ --surface-card: rgba(67, 83, 100, 0.35);
566
+ --surface-hover: rgba(67, 83, 100, 0.5);
567
+
568
+ --border-subtle: rgba(67, 83, 100, 0.4);
569
+ --focus-ring: var(--color-yellow);
570
+ }
571
+ ```
572
+
573
+ ## Typography Scale (Explicit)
574
+
575
+ ```css
576
+ --text-display: 48px;
577
+ --text-h1: 32px;
578
+ --text-h2: 24px;
579
+ --text-h3: 18px;
580
+ --text-body: 14px;
581
+ --text-label: 12px;
582
+ ```
583
+
584
+ ## Component States (Mandatory)
585
+
586
+ All components must define:
587
+
588
+ - default
589
+ - hover
590
+ - active
591
+ - focus-visible
592
+ - disabled
593
+
594
+ Optional but recommended:
595
+ - loading
596
+ - error
597
+ - success
598
+
599
+ ## Layout Patterns
600
+
601
+ Derived from systemverification.com:
602
+
603
+ - Hero (large heading + short intro + CTA)
604
+ - Card grid (3–4 columns desktop)
605
+ - Logo wall (low contrast)
606
+ - Section blocks with clear vertical rhythm
607
+
608
+ ## Key Constraint
609
+
610
+ This design system is **authoritative**.
611
+
612
+ AI must:
613
+ - extend it
614
+ - not override it
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@systemverification/styling-kit",
3
+ "version": "2.0.0",
4
+ "description": "System Verification design system CLI and MCP validation server",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=18"
8
+ },
9
+ "bin": {
10
+ "sv-style": "bin/sv-style.js"
11
+ },
12
+ "scripts": {
13
+ "test": "node --test src/config.test.js src/bundle.test.js src/assets.test.js src/init.test.js src/update.test.js src/shutdown.test.js src/mcp.test.js",
14
+ "build:test-bundle": "node scripts/build-test-bundle.js"
15
+ },
16
+ "dependencies": {
17
+ "@azure/identity": "^4.10.0",
18
+ "@azure/storage-blob": "^12.27.0",
19
+ "@modelcontextprotocol/sdk": "^1.12.0",
20
+ "adm-zip": "^0.5.16"
21
+ }
22
+ }
package/src/assets.js ADDED
@@ -0,0 +1,67 @@
1
+ import { DefaultAzureCredential } from '@azure/identity';
2
+ import { BlobClient } from '@azure/storage-blob';
3
+
4
+ const BUNDLE_BLOB_PATH = 'sv-style/assets/latest/asset-bundle.zip';
5
+
6
+ /**
7
+ * Download the private asset bundle zip from Azure Blob storage.
8
+ *
9
+ * @param {{ accountName: string, containerName: string }} blobConfig
10
+ * @returns {Promise<Buffer>}
11
+ * @throws with a `blobErrorType` property identifying the failure category
12
+ */
13
+ export async function downloadBundle(blobConfig) {
14
+ const { accountName, containerName } = blobConfig;
15
+ const url = `https://${accountName}.blob.core.windows.net/${containerName}/${BUNDLE_BLOB_PATH}`;
16
+
17
+ const credential = new DefaultAzureCredential();
18
+ const client = new BlobClient(url, credential);
19
+
20
+ let response;
21
+ try {
22
+ response = await client.download();
23
+ } catch (err) {
24
+ const classified = new Error(userMessage(err));
25
+ classified.blobErrorType = classifyBlobError(err);
26
+ classified.cause = err;
27
+ throw classified;
28
+ }
29
+
30
+ const chunks = [];
31
+ for await (const chunk of response.readableStreamBody) {
32
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
33
+ }
34
+ return Buffer.concat(chunks);
35
+ }
36
+
37
+ /**
38
+ * Classify an Azure Blob error into a coarse category.
39
+ *
40
+ * @param {Error & { statusCode?: number, code?: string }} err
41
+ * @returns {'not-logged-in' | 'unauthorized' | 'not-found' | 'network-error' | 'transient'}
42
+ */
43
+ export function classifyBlobError(err) {
44
+ const status = err.statusCode ?? err?.response?.status;
45
+ if (status === 401) return 'not-logged-in';
46
+ if (status === 403) return 'unauthorized';
47
+ if (status === 404) return 'not-found';
48
+ if (err.code === 'ENOTFOUND' || err.code === 'ECONNREFUSED' || err.code === 'EAI_AGAIN') {
49
+ return 'network-error';
50
+ }
51
+ return 'transient';
52
+ }
53
+
54
+ function userMessage(err) {
55
+ switch (classifyBlobError(err)) {
56
+ case 'not-logged-in':
57
+ return 'Not authenticated. Run `az login` (or sign in via VS Code / PowerShell) and try again.';
58
+ case 'unauthorized':
59
+ return 'Access denied. Your account does not have read access to the asset bundle. Contact your administrator.';
60
+ case 'not-found':
61
+ return 'Asset bundle not found in Azure Blob. Check blob.accountName and blob.containerName in sv-style.json.';
62
+ case 'network-error':
63
+ return 'Network error reaching Azure Blob. Check your internet connection and try again.';
64
+ default:
65
+ return `Azure Blob error: ${err.message || err}`;
66
+ }
67
+ }