mnfst 0.5.79 → 0.5.81

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/lib/manifest.css CHANGED
@@ -2416,7 +2416,7 @@
2416
2416
 
2417
2417
  @layer components {
2418
2418
 
2419
- :where(aside[popover]) {
2419
+ :where(nav[popover]) {
2420
2420
  inset-inline-start: auto;
2421
2421
  inset-inline-end: 0;
2422
2422
  width: fit-content;
@@ -2456,7 +2456,7 @@
2456
2456
  }
2457
2457
  }
2458
2458
 
2459
- :where(aside[popover].appear-start) {
2459
+ :where(nav[popover].appear-start) {
2460
2460
  inset-inline-start: 0;
2461
2461
  inset-inline-end: auto;
2462
2462
 
@@ -2481,7 +2481,7 @@
2481
2481
  }
2482
2482
 
2483
2483
  /* Dark mode override */
2484
- .dark :where(aside[popover]) {
2484
+ .dark :where(nav[popover]) {
2485
2485
  background-color: var(--color-surface-1, oklch(98.17% 0.0005 95.87))
2486
2486
  }
2487
2487
  }
@@ -2951,18 +2951,10 @@
2951
2951
  .h4,
2952
2952
  .h5,
2953
2953
  .h6 {
2954
- font-weight: bolder;
2954
+ font-weight: 550;
2955
2955
  word-wrap: break-word
2956
2956
  }
2957
2957
 
2958
- :where(h1, h2, h3, h4):not(.unstyle),
2959
- .h1,
2960
- .h2,
2961
- .h3,
2962
- .h4 {
2963
- font-weight: 700;
2964
- }
2965
-
2966
2958
  :where(h1, h2, h3):not(.unstyle),
2967
2959
  .h1,
2968
2960
  .h2,
@@ -2972,18 +2964,18 @@
2972
2964
 
2973
2965
  :where(h1):not(.unstyle),
2974
2966
  .h1 {
2975
- font-size: 2.25rem;
2967
+ font-size: 3rem;
2976
2968
  line-height: 1.25
2977
2969
  }
2978
2970
 
2979
2971
  :where(h2):not(.unstyle),
2980
2972
  .h2 {
2981
- font-size: 1.5rem
2973
+ font-size: 2.25rem
2982
2974
  }
2983
2975
 
2984
2976
  :where(h3):not(.unstyle),
2985
2977
  .h3 {
2986
- font-size: 1.25rem;
2978
+ font-size: 1.75rem;
2987
2979
  line-height: 1.4
2988
2980
  }
2989
2981
 
@@ -2994,18 +2986,21 @@
2994
2986
 
2995
2987
  :where(h5):not(.unstyle),
2996
2988
  .h5 {
2997
- font-weight: 600;
2998
- font-size: .875rem;
2989
+ font-size: 1rem;
2999
2990
  line-height: 1rem;
3000
- color: var(--color-content-neutral, oklch(48.26% 0.0365 255.09))
2991
+ color: var(--color-content-neutral, black);
2992
+
2993
+ & a:hover {
2994
+ color: var(--color-content-stark, black)
2995
+ }
3001
2996
  }
3002
2997
 
3003
2998
  :where(h6):not(.unstyle),
3004
2999
  .h6 {
3005
- font-weight: 600;
3000
+ font-family: var(--font-mono);
3006
3001
  font-size: 0.8125rem;
3007
3002
  line-height: 1.4;
3008
- text-transform: uppercase;
3003
+ color: var(--color-brand-content, black)
3009
3004
  }
3010
3005
 
3011
3006
  /* Paragraphs */
@@ -3016,18 +3011,15 @@
3016
3011
 
3017
3012
  /* Links */
3018
3013
  :where(a:not([role=button]), button.link):not(.unstyle) {
3019
- text-decoration: underline;
3020
- text-underline-offset: 2px;
3014
+ text-align: unset;
3015
+ text-decoration: none;
3021
3016
  cursor: pointer;
3022
- transition: var(--transition, all .05s ease-in-out);
3023
-
3024
- &:hover {
3025
- color: var(--color-content-neutral, oklch(48.26% 0.0365 255.09))
3026
- }
3017
+ transition: var(--transition, all .05s ease-in-out)
3018
+ }
3027
3019
 
3028
- &:active {
3029
- color: var(--color-content-neutral, oklch(48.26% 0.0365 255.09))
3030
- }
3020
+ :where(abbr, address, blockquote, code, del, figcaption, h1, h2, h3, h4, h5, h6, ins, legend, p, small, cite, q, .h1, .h2, .h3, .h4, .h5, .h6, .paragraph, .small, .caption, .label):not(.unstyle)>a {
3021
+ text-decoration: underline;
3022
+ text-underline-offset: 2px
3031
3023
  }
3032
3024
 
3033
3025
  /* Blockquotes */
@@ -3037,8 +3029,8 @@
3037
3029
  max-width: 100%;
3038
3030
  margin: calc(var(--spacing, 0.25rem) * 4) 0;
3039
3031
  padding: 0 calc(var(--spacing, 0.25rem) * 4);
3040
- color: var(--color-content-stark, oklch(16.6% 0.026 267));
3041
- border-inline-start: 0.25rem solid color-mix(in oklch, var(--color-content-stark, oklch(16.6% 0.026 267)) 25%, transparent);
3032
+ color: var(--color-content-stark, black);
3033
+ border-inline-start: 0.25rem solid color-mix(in oklch, var(--color-content-stark, black) 25%, transparent);
3042
3034
  border-inline-end: none;
3043
3035
 
3044
3036
  & * {
@@ -3051,12 +3043,13 @@
3051
3043
  display: inline-block;
3052
3044
  width: fit-content;
3053
3045
  height: fit-content;
3054
- padding: 0.1ch 0.5ch;
3046
+ padding: 0 0.7ch;
3055
3047
  font-size: 82.5%;
3048
+ line-height: 1.4;
3056
3049
  word-wrap: break-word;
3057
- color: var(--color-content-neutral, oklch(48.26% 0.0365 255.09));
3058
- background-color: color-mix(in oklch, var(--color-content-neutral, oklch(48.26% 0.0365 255.09)) 10%, transparent);
3059
- border: 1px solid color-mix(in oklch, var(--color-content-subtle, oklch(67.4% 0.0318 251.27)) 10%, transparent);
3050
+ color: var(--color-content-neutral, grey);
3051
+ background-color: color-mix(in oklch, var(--color-content-neutral, grey) 10%, transparent);
3052
+ border: 1px solid color-mix(in oklch, var(--color-content-subtle, silver) 10%, transparent);
3060
3053
  border-radius: var(--radius, 0.5rem)
3061
3054
  }
3062
3055
 
@@ -3064,11 +3057,10 @@
3064
3057
  :where(figcaption):not(.unstyle),
3065
3058
  .caption {
3066
3059
  font-size: 0.8125rem;
3067
- color: var(--color-content-neutral, oklch(48.26% 0.0365 255.09));
3060
+ color: var(--color-content-neutral, grey);
3068
3061
 
3069
- & a {
3070
- font-weight: inherit;
3071
- color: inherit
3062
+ & a:hover {
3063
+ color: var(--color-content-stark, black)
3072
3064
  }
3073
3065
  }
3074
3066
 
@@ -3081,7 +3073,11 @@
3081
3073
  :where(small):not(.unstyle),
3082
3074
  .small {
3083
3075
  font-size: 0.875rem;
3084
- color: var(--color-content-neutral, oklch(48.26% 0.0365 255.09))
3076
+ color: var(--color-content-neutral, grey);
3077
+
3078
+ & a:hover {
3079
+ color: var(--color-content-stark, black)
3080
+ }
3085
3081
  }
3086
3082
 
3087
3083
  /* Icons */
@@ -3108,8 +3104,8 @@
3108
3104
  font-weight: 600;
3109
3105
  line-height: 1;
3110
3106
  text-align: center;
3111
- color: var(--color-content-neutral, oklch(48.26% 0.0365 255.09));
3112
- background-color: color-mix(in oklch, var(--color-content-neutral, oklch(48.26% 0.0365 255.09)) 10%, transparent);
3107
+ color: var(--color-content-neutral, grey);
3108
+ background-color: color-mix(in oklch, var(--color-content-neutral, grey) 10%, transparent);
3113
3109
  border-radius: calc(var(--radius, 0.5rem) / 1.5);
3114
3110
 
3115
3111
  &:not(:last-of-type) {
@@ -3152,7 +3148,7 @@
3152
3148
  font-weight: 500;
3153
3149
  font-size: 0.75rem;
3154
3150
  line-height: 1;
3155
- color: var(--color-field-inverse, oklch(16.6% 0.026 267));
3151
+ color: var(--color-field-inverse, black);
3156
3152
  background-color: var(--color-field-surface, oklch(91.79% 0.0029 264.26));
3157
3153
  border-radius: 100px;
3158
3154
 
@@ -3473,7 +3469,7 @@
3473
3469
 
3474
3470
  /* Prose styles for long-form content */
3475
3471
  :where(.prose, .prose details) {
3476
- width: 65ch;
3472
+ width: 42rem;
3477
3473
  max-width: 100%;
3478
3474
 
3479
3475
  /* Asides inside a prose element are used as callouts */
@@ -3544,6 +3540,10 @@
3544
3540
  }
3545
3541
  }
3546
3542
 
3543
+ &>h1 {
3544
+ font-size: 2.25rem
3545
+ }
3546
+
3547
3547
  &>h1+p {
3548
3548
  margin-top: 0.625rem;
3549
3549
  font-size: 1.125rem;
@@ -3551,11 +3551,13 @@
3551
3551
  }
3552
3552
 
3553
3553
  &>h2 {
3554
+ font-size: 1.75rem;
3554
3555
  margin-top: 1rem;
3555
3556
  margin-bottom: calc(1rem * 0.6667)
3556
3557
  }
3557
3558
 
3558
3559
  &>h3 {
3560
+ font-size: 1.25rem;
3559
3561
  margin-top: calc(1rem * 2.4)
3560
3562
  }
3561
3563
 
@@ -12,7 +12,9 @@ async function ensureManifest() {
12
12
  try {
13
13
  const manifestUrl = (document.querySelector('link[rel="manifest"]')?.getAttribute('href')) || '/manifest.json';
14
14
  const response = await fetch(manifestUrl);
15
- return await response.json();
15
+ const manifest = await response.json();
16
+ interpolateManifest(manifest);
17
+ return manifest;
16
18
  } catch (error) {
17
19
  console.error('[Manifest Data] Failed to load manifest:', error);
18
20
  return null;
@@ -36,6 +38,33 @@ function interpolateEnvVars(str) {
36
38
  });
37
39
  }
38
40
 
41
+ // Recursively walk a manifest object and interpolate every string value in
42
+ // place. Object keys are left untouched. Called once at manifest-load time so
43
+ // downstream consumers (auth, data, appwrite) read already-resolved values.
44
+ function interpolateManifest(obj) {
45
+ if (obj === null || typeof obj !== 'object') return obj;
46
+ if (Array.isArray(obj)) {
47
+ for (let i = 0; i < obj.length; i++) {
48
+ const v = obj[i];
49
+ if (typeof v === 'string') {
50
+ obj[i] = interpolateEnvVars(v);
51
+ } else if (v !== null && typeof v === 'object') {
52
+ interpolateManifest(v);
53
+ }
54
+ }
55
+ return obj;
56
+ }
57
+ for (const key of Object.keys(obj)) {
58
+ const v = obj[key];
59
+ if (typeof v === 'string') {
60
+ obj[key] = interpolateEnvVars(v);
61
+ } else if (v !== null && typeof v === 'object') {
62
+ interpolateManifest(v);
63
+ }
64
+ }
65
+ return obj;
66
+ }
67
+
39
68
  // Helper to get nested value from object
40
69
  function getNestedValue(obj, path) {
41
70
  return path.split('.').reduce((current, key) => {
@@ -179,6 +208,7 @@ function getQueries(dataSource) {
179
208
  window.ManifestDataConfig = {
180
209
  ensureManifest,
181
210
  interpolateEnvVars,
211
+ interpolateManifest,
182
212
  getNestedValue,
183
213
  getDefaultLocale,
184
214
  parseContentPath,
@@ -1202,6 +1232,13 @@ window.ManifestDataStore = {
1202
1232
 
1203
1233
  /* Manifest Data Sources - File Loaders */
1204
1234
 
1235
+ // Key names that would walk into Object's prototype chain if used as nested-
1236
+ // path segments. Rejecting them in setNestedValue and deepMergeWithFallback
1237
+ // prevents a CSV row like `__proto__.polluted, true` (or a malicious JSON
1238
+ // locale file containing `{"__proto__": {...}}`) from polluting
1239
+ // Object.prototype and silently affecting every plain object on the page.
1240
+ const POLLUTING_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
1241
+
1205
1242
  // Dynamic js-yaml loader
1206
1243
  let jsyaml = null;
1207
1244
  let yamlLoadingPromise = null;
@@ -1336,6 +1373,7 @@ function deepMergeWithFallback(currentData, fallbackData) {
1336
1373
  !Array.isArray(currentData) && !Array.isArray(fallbackData)) {
1337
1374
  const merged = { ...fallbackData };
1338
1375
  for (const key in currentData) {
1376
+ if (POLLUTING_KEYS.has(key)) continue;
1339
1377
  if (key.startsWith('_')) {
1340
1378
  // Preserve metadata from current locale
1341
1379
  merged[key] = currentData[key];
@@ -1363,6 +1401,9 @@ function deepMergeWithFallback(currentData, fallbackData) {
1363
1401
  // Numeric path segments (e.g. cards.0.title) create real arrays so x-for="card in $x....cards" works.
1364
1402
  function setNestedValue(obj, path, value) {
1365
1403
  const keys = path.split('.');
1404
+ // Drop the whole row if any segment would walk into the prototype chain.
1405
+ // `foo.constructor.prototype.polluted` is just as dangerous as `__proto__.polluted`.
1406
+ if (keys.some(k => POLLUTING_KEYS.has(k))) return;
1366
1407
  let current = obj;
1367
1408
 
1368
1409
  for (let i = 0; i < keys.length - 1; i++) {
@@ -0,0 +1,26 @@
1
+ {
2
+ "manifest.appwrite.auth.js": "sha384-to37ssZJXGeOS6+rf2VI47ox2mEqgsi5oQ1E5vv8XU/lDspbDFE1KHEMm8TxBhxW",
3
+ "manifest.appwrite.data.js": "sha384-00ulLT+GAIuPHA/rRT9p98vYlsyDzkyKXtg86BDQ6FGQa5vVVN+W6kuforniBAsz",
4
+ "manifest.appwrite.presence.js": "sha384-uxRpx9/Jj0kGtklH5QmUlAzD3zdSvFRfK6bcJQqxl+Bsf5tOo4zgwqJTQgtZoHQP",
5
+ "manifest.code.js": "sha384-e6s5v78qYi7GgrXqXRAtWerh7g5f2m9k8bPZnxk+CeBWXOImWvZFWpT7uZUcWHqT",
6
+ "manifest.color.js": "sha384-Z9G/lzt0vVMxjz4wkPuGG1X9mmQAJR15aOoGX3ephf7r2wnlUWet5GLgkUMtT4vt",
7
+ "manifest.colorpicker.js": "sha384-0EVn+Ha06h7FIvOxc6WjZYnKYXzi+zba08yKvczSEGTRkWRxyKN2TFrZHI1SDCXu",
8
+ "manifest.colors.js": "sha384-u8iD6kapVj4OjeCILxBkYQKgXtDQ7LdEodILkQuknzPMwzSMBmDHN25UuzxepHby",
9
+ "manifest.components.js": "sha384-3dCTD5EwCZTiX+1obYtDNM3WWwPh2JDQUQQsdRUUK3gs6FXjse1ShkKaT/2jsNaI",
10
+ "manifest.data.js": "sha384-dLvmRs4rC/s3tg4lePwJgKb1Q/cAKc7uFTjHzpusnQgcnfXahmGdAW4g5MOF2V8M",
11
+ "manifest.dropdowns.js": "sha384-WMrFoSpKfJuo81dyrwhVrDO8rq+rDwh2x8x4nH01BY5ZHkvjE+/SaT2gWCI0zOn+",
12
+ "manifest.icons.js": "sha384-uOkboYrovjCpl22eey3Jaxpey+pOnot5NDnRRumcRxiR7IOVaRh1i20gYnWXR5dW",
13
+ "manifest.localization.js": "sha384-eKdBIMEAwsugPP2p2fuPzQUkU44f1+Y0JgukMJ1KXLQY1/AYvpcGsEiritVDElsN",
14
+ "manifest.markdown.js": "sha384-ykEREUXsGW26uQcPfTy05lXjaBdNPduUsiTOsr9btQ4NiOy5dMgEk2fMD669p9Pn",
15
+ "manifest.resize.js": "sha384-Ak5gf44ERfh9pOSAD1qZzJSysslpwBCkevIlz7R3dszTUyzUKGKGF4pn5arOtgG0",
16
+ "manifest.router.js": "sha384-n6xmIfWnYzd/0kkVTFuHhFzHuxiDgZ1Lg1W0yB6/w3Myw5pQ6PgE6SJBHfVsO7/D",
17
+ "manifest.slides.js": "sha384-3uRTkyK9XPLmnxI2+igZlpi4EyPlU/7IHj5j3BZJJ2KN455vXyk99fiXV3feO/XY",
18
+ "manifest.svg.js": "sha384-gulBJnHAlmmBo9nIxMNO+4rjF6Xdvrv8la8PPYAC5vOA2uAou/twI6rXrp+vOKIC",
19
+ "manifest.tabs.js": "sha384-v6Ti0zHfdLhkFHbTMg0FH6uMrThuBvZrL2PQgVBeeXhDjuN7x4MtoNWogPbAQTaD",
20
+ "manifest.tailwind.js": "sha384-aHLvl2oSuUgy06VaBqhhByn5wWxqvnqxw6KCwehakKUS00F/s/Nb62umeASS6Y4P",
21
+ "manifest.toasts.js": "sha384-ytd5rDbax/Ou9z23uedFXPZbxDPsk2E/pxCTq4WLvfv+os1qTI6kELp0kPp07g24",
22
+ "manifest.tooltips.js": "sha384-ppK+/G3UH8/J1Dn48comIwXcPjVVKiCNEJr/L1B+6eZQr2DLoBohwbIUR7w40jZf",
23
+ "manifest.url.parameters.js": "sha384-FIufiClqDx1rJpU/QUc9z/D43qClQ6Qm8rBahipbJl9BDHUvhrOsUDegmTWW7Tuf",
24
+ "manifest.utilities.js": "sha384-Q98oZClq/iRKFmuwHolisLgEitsTZiEPHxUW29liKlnL1Gx+YGq8MMivYbDlGDD6",
25
+ "manifest.js": "sha384-KyKrxpjCNpSBv30BPYliAWOonAQ6cKW+IHF6+twjmyiGGx6UNV9YVDqGZmjtr/i/"
26
+ }
package/lib/manifest.js CHANGED
@@ -175,6 +175,41 @@
175
175
  const DEFAULT_VERSION = 'latest';
176
176
  const ALPINE_CDN_URL = 'https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js';
177
177
 
178
+ // SRI integrity map: { 'manifest.foo.min.js': 'sha384-...', ... }
179
+ // Inlined by build.mjs's emitIntegrityMap() step. Empty in source so the
180
+ // unbuilt loader works in dev (where files are served from the local
181
+ // project, same-origin, no SRI needed). Built artifact in lib/ carries
182
+ // the populated map. Looked up by the filename suffix of every script URL
183
+ // addScript() injects — when the file is in the map, the matching
184
+ // integrity + crossorigin attributes are set, and the browser refuses to
185
+ // execute the script if the bytes don't match (defends against CDN
186
+ // poisoning / npm hijack).
187
+ const INTEGRITY = {
188
+ "manifest.appwrite.auth.js": "sha384-to37ssZJXGeOS6+rf2VI47ox2mEqgsi5oQ1E5vv8XU/lDspbDFE1KHEMm8TxBhxW",
189
+ "manifest.appwrite.data.js": "sha384-00ulLT+GAIuPHA/rRT9p98vYlsyDzkyKXtg86BDQ6FGQa5vVVN+W6kuforniBAsz",
190
+ "manifest.appwrite.presence.js": "sha384-uxRpx9/Jj0kGtklH5QmUlAzD3zdSvFRfK6bcJQqxl+Bsf5tOo4zgwqJTQgtZoHQP",
191
+ "manifest.code.js": "sha384-e6s5v78qYi7GgrXqXRAtWerh7g5f2m9k8bPZnxk+CeBWXOImWvZFWpT7uZUcWHqT",
192
+ "manifest.color.js": "sha384-Z9G/lzt0vVMxjz4wkPuGG1X9mmQAJR15aOoGX3ephf7r2wnlUWet5GLgkUMtT4vt",
193
+ "manifest.colorpicker.js": "sha384-0EVn+Ha06h7FIvOxc6WjZYnKYXzi+zba08yKvczSEGTRkWRxyKN2TFrZHI1SDCXu",
194
+ "manifest.colors.js": "sha384-u8iD6kapVj4OjeCILxBkYQKgXtDQ7LdEodILkQuknzPMwzSMBmDHN25UuzxepHby",
195
+ "manifest.components.js": "sha384-3dCTD5EwCZTiX+1obYtDNM3WWwPh2JDQUQQsdRUUK3gs6FXjse1ShkKaT/2jsNaI",
196
+ "manifest.data.js": "sha384-dLvmRs4rC/s3tg4lePwJgKb1Q/cAKc7uFTjHzpusnQgcnfXahmGdAW4g5MOF2V8M",
197
+ "manifest.dropdowns.js": "sha384-WMrFoSpKfJuo81dyrwhVrDO8rq+rDwh2x8x4nH01BY5ZHkvjE+/SaT2gWCI0zOn+",
198
+ "manifest.icons.js": "sha384-uOkboYrovjCpl22eey3Jaxpey+pOnot5NDnRRumcRxiR7IOVaRh1i20gYnWXR5dW",
199
+ "manifest.localization.js": "sha384-eKdBIMEAwsugPP2p2fuPzQUkU44f1+Y0JgukMJ1KXLQY1/AYvpcGsEiritVDElsN",
200
+ "manifest.markdown.js": "sha384-ykEREUXsGW26uQcPfTy05lXjaBdNPduUsiTOsr9btQ4NiOy5dMgEk2fMD669p9Pn",
201
+ "manifest.resize.js": "sha384-Ak5gf44ERfh9pOSAD1qZzJSysslpwBCkevIlz7R3dszTUyzUKGKGF4pn5arOtgG0",
202
+ "manifest.router.js": "sha384-n6xmIfWnYzd/0kkVTFuHhFzHuxiDgZ1Lg1W0yB6/w3Myw5pQ6PgE6SJBHfVsO7/D",
203
+ "manifest.slides.js": "sha384-3uRTkyK9XPLmnxI2+igZlpi4EyPlU/7IHj5j3BZJJ2KN455vXyk99fiXV3feO/XY",
204
+ "manifest.svg.js": "sha384-gulBJnHAlmmBo9nIxMNO+4rjF6Xdvrv8la8PPYAC5vOA2uAou/twI6rXrp+vOKIC",
205
+ "manifest.tabs.js": "sha384-v6Ti0zHfdLhkFHbTMg0FH6uMrThuBvZrL2PQgVBeeXhDjuN7x4MtoNWogPbAQTaD",
206
+ "manifest.tailwind.js": "sha384-aHLvl2oSuUgy06VaBqhhByn5wWxqvnqxw6KCwehakKUS00F/s/Nb62umeASS6Y4P",
207
+ "manifest.toasts.js": "sha384-ytd5rDbax/Ou9z23uedFXPZbxDPsk2E/pxCTq4WLvfv+os1qTI6kELp0kPp07g24",
208
+ "manifest.tooltips.js": "sha384-ppK+/G3UH8/J1Dn48comIwXcPjVVKiCNEJr/L1B+6eZQr2DLoBohwbIUR7w40jZf",
209
+ "manifest.url.parameters.js": "sha384-FIufiClqDx1rJpU/QUc9z/D43qClQ6Qm8rBahipbJl9BDHUvhrOsUDegmTWW7Tuf",
210
+ "manifest.utilities.js": "sha384-Q98oZClq/iRKFmuwHolisLgEitsTZiEPHxUW29liKlnL1Gx+YGq8MMivYbDlGDD6"
211
+ };
212
+
178
213
  // Get base URL for a given version
179
214
  function getBaseUrl(version = DEFAULT_VERSION) {
180
215
  return `https://cdn.jsdelivr.net/npm/mnfst@${version}/lib`;
@@ -199,9 +234,7 @@
199
234
  'slides',
200
235
  'resize',
201
236
  'colorpicker',
202
- 'url-parameters',
203
- 'virtual',
204
- 'export'
237
+ 'url-parameters'
205
238
  ];
206
239
 
207
240
  // Appwrite integration plugins (opt-in only, never auto-loaded)
@@ -211,10 +244,16 @@
211
244
  'appwrite-presence'
212
245
  ];
213
246
 
214
- // Plugin dependencies: plugins that require other plugins to be loaded first
247
+ // Plugin dependencies: plugins that require other plugins to be loaded first.
248
+ // All Appwrite plugins depend on `data` for env-var interpolation at
249
+ // manifest-load time (window.ManifestDataConfig.interpolateManifest); auth
250
+ // and presence also share the data plugin's manifest-fetch plumbing.
251
+ // Localization reads data-source URLs that may carry ${VAR} references.
215
252
  const PLUGIN_DEPENDENCIES = {
253
+ 'appwrite-auth': ['data'],
216
254
  'appwrite-data': ['data'],
217
- 'appwrite-presence': ['data']
255
+ 'appwrite-presence': ['data'],
256
+ 'localization': ['data']
218
257
  };
219
258
 
220
259
  // Derive default plugin list from manifest (only load data/localization/components when manifest needs them)
@@ -315,6 +354,15 @@
315
354
  const script = document.createElement('script');
316
355
  script.src = url;
317
356
  script.async = false; // Ensure scripts execute in order
357
+ // Apply SRI when the file is in the inlined integrity map. The map
358
+ // keys files by basename, so it works whether the URL points at
359
+ // jsDelivr or a user-configured pluginBase mirror — as long as the
360
+ // served bytes match what this loader version was built against.
361
+ const fileName = url.split('/').pop().split('?')[0];
362
+ if (INTEGRITY[fileName]) {
363
+ script.integrity = INTEGRITY[fileName];
364
+ script.crossOrigin = 'anonymous';
365
+ }
318
366
  script.onload = () => resolve();
319
367
  script.onerror = () => reject(new Error(`Failed to load ${pluginName} from ${url}`));
320
368
  document.head.appendChild(script);
@@ -522,6 +570,13 @@
522
570
  manifest = await manifestPromise;
523
571
  }
524
572
  if (manifest && typeof window !== 'undefined') {
573
+ // Resolve ${VAR} env-var references once, at the canonical load
574
+ // point, so downstream plugins (auth, data, appwrite) read
575
+ // already-interpolated values instead of each handling env-var
576
+ // substitution themselves.
577
+ if (window.ManifestDataConfig?.interpolateManifest) {
578
+ window.ManifestDataConfig.interpolateManifest(manifest);
579
+ }
525
580
  window.__manifestLoaded = manifest;
526
581
  if (window.ManifestComponentsRegistry) {
527
582
  window.ManifestComponentsRegistry.manifest = manifest;
@@ -17,6 +17,68 @@ if (typeof window !== 'undefined') {
17
17
  });
18
18
  }
19
19
 
20
+ // Cache for DOMPurify loading (only fetched when the .safe modifier is used)
21
+ let purifyPromise = null;
22
+
23
+ // DOMPurify config tuned for Manifest's markdown output. The markdown
24
+ // extensions emit <x-code>, <x-icon>, and other x-* custom elements that must
25
+ // survive sanitization, so custom-element handling is enabled with a
26
+ // tag-name allowlist (x-*) and an attribute filter that rejects event
27
+ // handlers (on*). DOMPurify's defaults handle <script>, javascript: URLs,
28
+ // srcdoc, and the usual XSS vectors for standard HTML tags.
29
+ const MARKDOWN_PURIFY_CONFIG = {
30
+ CUSTOM_ELEMENT_HANDLING: {
31
+ tagNameCheck: /^x-[a-z][\w-]*$/,
32
+ attributeNameCheck: /^(?!on)[a-z][\w\-:]*$/i,
33
+ allowCustomizedBuiltInElements: false
34
+ }
35
+ };
36
+
37
+ async function loadDOMPurify() {
38
+ if (typeof window.DOMPurify !== 'undefined') return window.DOMPurify;
39
+ if (purifyPromise) return purifyPromise;
40
+ purifyPromise = new Promise((resolve, reject) => {
41
+ const script = document.createElement('script');
42
+ script.src = 'https://cdn.jsdelivr.net/npm/dompurify@latest/dist/purify.min.js';
43
+ script.onload = () => {
44
+ if (typeof window.DOMPurify !== 'undefined') {
45
+ resolve(window.DOMPurify);
46
+ } else {
47
+ console.error('[Manifest Markdown] DOMPurify failed to load — DOMPurify is undefined');
48
+ purifyPromise = null;
49
+ reject(new Error('DOMPurify failed to load'));
50
+ }
51
+ };
52
+ script.onerror = (err) => {
53
+ console.error('[Manifest Markdown] DOMPurify script failed to load:', err);
54
+ purifyPromise = null;
55
+ reject(err);
56
+ };
57
+ document.head.appendChild(script);
58
+ });
59
+ return purifyPromise;
60
+ }
61
+
62
+ // Sanitize HTML if the .safe modifier was used; pass-through otherwise.
63
+ // Manifest's default is unsanitized so authors can render arbitrary HTML and
64
+ // the markdown custom-element extensions work — but the .safe opt-in lets
65
+ // authors render data-source content (e.g. user-submitted markdown from
66
+ // Appwrite) without an XSS sink.
67
+ async function maybeSanitizeMarkdownHtml(html, safe) {
68
+ if (!safe) return html;
69
+ try {
70
+ const DOMPurify = await loadDOMPurify();
71
+ return DOMPurify.sanitize(html, MARKDOWN_PURIFY_CONFIG);
72
+ } catch {
73
+ // Loader failure — fall back to escaping rather than silently emitting
74
+ // un-sanitized HTML. The author asked for safe; honour that.
75
+ const escaped = String(html)
76
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
77
+ console.warn('[Manifest Markdown] x-markdown.safe: DOMPurify unavailable — emitting escaped text.');
78
+ return escaped;
79
+ }
80
+ }
81
+
20
82
  // Load marked.js from CDN
21
83
  async function loadMarkedJS() {
22
84
  if (typeof marked !== 'undefined') {
@@ -52,6 +114,18 @@ async function loadMarkedJS() {
52
114
  return markedPromise;
53
115
  }
54
116
 
117
+ // HTML-escape a string for safe interpolation inside an attribute value.
118
+ // Used by the code-fence renderer below — title/language strings come from
119
+ // the markdown source, so without escaping a fence like ```js " onclick=alert(1) x="
120
+ // could inject arbitrary attributes onto the <x-code> element.
121
+ function escapeForAttribute(s) {
122
+ return String(s)
123
+ .replace(/&/g, '&amp;')
124
+ .replace(/"/g, '&quot;')
125
+ .replace(/</g, '&lt;')
126
+ .replace(/>/g, '&gt;');
127
+ }
128
+
55
129
  // Configure marked to preserve full language strings
56
130
  async function configureMarked(marked) {
57
131
  marked.use({
@@ -67,10 +141,10 @@ async function configureMarked(marked) {
67
141
  // Build attributes for the x-code element
68
142
  let xCodeAttributes = '';
69
143
  if (attributes.title) {
70
- xCodeAttributes += ` name="${attributes.title}"`;
144
+ xCodeAttributes += ` name="${escapeForAttribute(attributes.title)}"`;
71
145
  }
72
146
  if (attributes.language) {
73
- xCodeAttributes += ` language="${attributes.language}"`;
147
+ xCodeAttributes += ` language="${escapeForAttribute(attributes.language)}"`;
74
148
  }
75
149
  if (attributes.numbers) {
76
150
  xCodeAttributes += ' numbers';
@@ -158,7 +232,7 @@ async function configureMarked(marked) {
158
232
  parsedContent = marked.parse(token.text);
159
233
  }
160
234
 
161
- const iconHtml = iconValue ? `<span x-icon="${iconValue}"></span>` : '';
235
+ const iconHtml = iconValue ? `<span x-icon="${escapeForAttribute(iconValue)}"></span>` : '';
162
236
 
163
237
  // Create a temporary div to count top-level elements
164
238
  const temp = document.createElement('div');
@@ -337,6 +411,15 @@ async function initializeMarkdownPlugin() {
337
411
  return;
338
412
  }
339
413
 
414
+ // Opt-in sanitization. When `.safe` is on the directive
415
+ // (`x-markdown.safe="$x.user.bio"`), parsed HTML is run through
416
+ // DOMPurify before injection. Default is unsanitized — Manifest's
417
+ // design lets authors render raw HTML and custom-element extensions
418
+ // (x-code, x-icon, callouts) freely. Use .safe when the markdown
419
+ // source can contain content from untrusted parties (Appwrite
420
+ // collections, API responses, crowdsourced translations, etc.).
421
+ const safe = Array.isArray(modifiers) && modifiers.includes('safe');
422
+
340
423
  // Prerender idempotency: if the page is a prerendered MPA and this
341
424
  // element already has rendered HTML children, the content was baked
342
425
  // at build time and is authoritative for SEO + no-JS users. Skip
@@ -394,6 +477,9 @@ async function initializeMarkdownPlugin() {
394
477
  // Post-process HTML to enable checkboxes (remove disabled attribute)
395
478
  html = enableCheckboxes(html);
396
479
 
480
+ // Apply opt-in DOMPurify sanitization for x-markdown.safe
481
+ html = await maybeSanitizeMarkdownHtml(html, safe);
482
+
397
483
  // Only update if content has changed and isn't empty
398
484
  if (element.innerHTML !== html && html.trim() !== '') {
399
485
  // Create a temporary container to hold the HTML
@@ -567,6 +653,9 @@ async function initializeMarkdownPlugin() {
567
653
  // Post-process HTML to enable checkboxes (remove disabled attribute)
568
654
  html = enableCheckboxes(html);
569
655
 
656
+ // Apply opt-in DOMPurify sanitization for x-markdown.safe
657
+ html = await maybeSanitizeMarkdownHtml(html, safe);
658
+
570
659
  // Only update DOM if HTML actually changed
571
660
  if (el.innerHTML !== html) {
572
661
  // Create temporary container
@@ -649,6 +738,9 @@ async function initializeMarkdownPlugin() {
649
738
  // Post-process HTML to enable checkboxes (remove disabled attribute)
650
739
  html = html.replace(/<input type="checkbox"([^>]*?)disabled([^>]*?)>/g, '<input type="checkbox"$1$2>');
651
740
 
741
+ // Apply opt-in DOMPurify sanitization for x-markdown.safe
742
+ html = await maybeSanitizeMarkdownHtml(html, safe);
743
+
652
744
  // Create temporary container
653
745
  const temp = document.createElement('div');
654
746
  temp.innerHTML = html;