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.
@@ -8,6 +8,21 @@
8
8
 
9
9
  /* Auth config */
10
10
 
11
+ // Refuse strings that still contain an unresolved ${VAR} reference. The loader
12
+ // runs window.ManifestDataConfig.interpolateManifest at manifest-load time, so
13
+ // by the time we read these fields the env-var substitution has already been
14
+ // applied. Anything still matching ${VAR} is an undefined env var — passing it
15
+ // to Appwrite would either silently fail or, worse, be sent verbatim as an
16
+ // HTTP header value, leaking the env var name. Loud-fail instead.
17
+ function resolvedOrNull(value, fieldName) {
18
+ if (typeof value !== 'string') return value;
19
+ if (/\$\{[^}]+\}/.test(value)) {
20
+ console.error(`[Manifest Auth] manifest.appwrite.${fieldName} references an undefined env var (${value}). Auth disabled.`);
21
+ return null;
22
+ }
23
+ return value;
24
+ }
25
+
11
26
  // Load manifest if not already loaded (loader may set __manifestLoaded / registry.manifest)
12
27
  async function ensureManifest() {
13
28
  if (window.ManifestComponentsRegistry?.manifest) {
@@ -34,13 +49,22 @@ async function getAppwriteConfig() {
34
49
  }
35
50
 
36
51
  const appwriteConfig = manifest.appwrite;
37
- const endpoint = appwriteConfig.endpoint;
38
- const projectId = appwriteConfig.projectId;
39
- const devKey = appwriteConfig.devKey; // Optional dev key to bypass rate limits in development
52
+ const endpoint = resolvedOrNull(appwriteConfig.endpoint, 'endpoint');
53
+ const projectId = resolvedOrNull(appwriteConfig.projectId, 'projectId');
54
+ // Optional dev key to bypass rate limits in development. The schema
55
+ // documents `${VAR_NAME}` interpolation for this field specifically —
56
+ // refuse to forward a literal placeholder as an HTTP header.
57
+ const devKey = appwriteConfig.devKey ? resolvedOrNull(appwriteConfig.devKey, 'devKey') : undefined;
40
58
 
41
59
  if (!endpoint || !projectId) {
42
60
  return null;
43
61
  }
62
+ // devKey is optional: if the user supplied one but it failed to resolve,
63
+ // resolvedOrNull returned null (and logged) — drop the field rather than
64
+ // initialize Appwrite with a literal `${VAR}` header.
65
+ if (appwriteConfig.devKey && devKey === null) {
66
+ return null;
67
+ }
44
68
 
45
69
  // Get auth methods from config (defaults to ["magic", "oauth"] if not specified)
46
70
  const authMethods = appwriteConfig.auth?.methods || ["magic", "oauth"];
@@ -164,6 +188,27 @@ function initializeAuthStore() {
164
188
  // Cross-tab synchronization using localStorage events
165
189
  const STORAGE_KEY = 'manifest:auth:state';
166
190
 
191
+ // Whitelist of Appwrite Session fields safe to mirror across tabs.
192
+ // CRITICALLY excludes `secret` (the bearer credential), `providerAccessToken`,
193
+ // `providerRefreshToken`, and `providerAccessTokenExpiry`. The cookie set
194
+ // on the Appwrite domain is the actual auth of record; this localStorage
195
+ // copy only supports UI cross-tab sync ("someone just logged in here").
196
+ // An XSS on this origin must not be able to lift session secrets out.
197
+ const SAFE_SESSION_FIELDS = [
198
+ '$id', 'userId', 'provider', 'expire', 'current',
199
+ 'clientName', 'osName', 'osCode', 'deviceName',
200
+ 'deviceBrand', 'deviceModel', 'countryCode', 'countryName'
201
+ ];
202
+
203
+ function sanitizeSessionForStorage(session) {
204
+ if (!session || typeof session !== 'object') return session;
205
+ const safe = {};
206
+ for (const f of SAFE_SESSION_FIELDS) {
207
+ if (f in session) safe[f] = session[f];
208
+ }
209
+ return safe;
210
+ }
211
+
167
212
  // Listen for storage events from other tabs
168
213
  window.addEventListener('storage', (e) => {
169
214
  if (e.key === STORAGE_KEY && e.newValue) {
@@ -193,7 +238,7 @@ function initializeAuthStore() {
193
238
  isAuthenticated: store.isAuthenticated,
194
239
  isAnonymous: store.isAnonymous,
195
240
  user: store.user,
196
- session: store.session,
241
+ session: sanitizeSessionForStorage(store.session),
197
242
  magicLinkSent: store.magicLinkSent,
198
243
  magicLinkExpired: store.magicLinkExpired,
199
244
  error: store.error
@@ -5769,13 +5814,11 @@ function initializeMagicLinks() {
5769
5814
  );
5770
5815
 
5771
5816
  if (isRateLimit) {
5772
- // Store callback for retry
5773
- try {
5774
- sessionStorage.setItem('manifest:magic-link:callback', JSON.stringify({ userId, secret }));
5775
- } catch (e) {
5776
- // Ignore
5777
- }
5778
- this.error = 'Rate limit exceeded. Please wait a moment and refresh the page.';
5817
+ // Don't persist {userId, secret} for retry — the secret is a
5818
+ // bearer credential that any same-origin XSS could lift. On
5819
+ // rate limit the user must re-request the magic link from
5820
+ // their email (cleanupUrl has already stripped URL params).
5821
+ this.error = 'Rate limit exceeded. Please wait a moment and request a new magic link.';
5779
5822
  this.isAuthenticated = false;
5780
5823
  this.isAnonymous = false;
5781
5824
  this.magicLinkExpired = false;
@@ -5901,15 +5944,10 @@ function handleMagicLinkCallbacks() {
5901
5944
 
5902
5945
  const callbackInfo = event.detail;
5903
5946
 
5904
- // Store callback for retry if rate limited
5905
- try {
5906
- sessionStorage.setItem('manifest:magic-link:callback', JSON.stringify({
5907
- userId: callbackInfo.userId,
5908
- secret: callbackInfo.secret
5909
- }));
5910
- } catch (e) {
5911
- console.warn('[Manifest Appwrite Auth] Could not store callback:', e);
5912
- }
5947
+ // Note: we deliberately do NOT persist {userId, secret} to sessionStorage
5948
+ // as a retry safety net. The secret is a single-use bearer credential
5949
+ // that any same-origin XSS could read; UX of "refresh to retry on rate
5950
+ // limit" isn't worth the credential-exfil risk.
5913
5951
 
5914
5952
  // Handle the callback
5915
5953
  await store.handleMagicLinkCallback(callbackInfo.userId, callbackInfo.secret);
@@ -6247,16 +6285,11 @@ function initializeCallbacks() {
6247
6285
  const secret = urlParams.get('secret');
6248
6286
  const expire = urlParams.get('expire');
6249
6287
 
6250
- // Check for stored callback (from rate limit retry)
6251
- let storedCallback = null;
6252
- try {
6253
- const stored = sessionStorage.getItem('manifest:magic-link:callback');
6254
- if (stored) {
6255
- storedCallback = JSON.parse(stored);
6256
- }
6257
- } catch (e) {
6258
- // Ignore parse errors
6259
- }
6288
+ // Eagerly clear any stale magic-link credentials left in sessionStorage
6289
+ // by an earlier (pre-fix) Manifest version. We no longer persist them,
6290
+ // and reading stale ones could resurrect a credential that should have
6291
+ // been forgotten.
6292
+ try { sessionStorage.removeItem('manifest:magic-link:callback'); } catch (e) {}
6260
6293
 
6261
6294
  // Check OAuth redirect flag
6262
6295
  const isOAuthCallback = sessionStorage.getItem('manifest:oauth:redirect') === 'true';
@@ -6267,14 +6300,14 @@ function initializeCallbacks() {
6267
6300
  const isTeamInvite = !!(teamId && membershipId && userId && secret);
6268
6301
 
6269
6302
  const callbackInfo = {
6270
- userId: userId || storedCallback?.userId,
6271
- secret: secret || storedCallback?.secret,
6303
+ userId: userId,
6304
+ secret: secret,
6272
6305
  expire: expire,
6273
6306
  teamId: teamId,
6274
6307
  membershipId: membershipId,
6275
6308
  isOAuth: isOAuthCallback,
6276
6309
  isTeamInvite: isTeamInvite,
6277
- hasCallback: !!(userId || storedCallback?.userId) && !!(secret || storedCallback?.secret),
6310
+ hasCallback: !!userId && !!secret,
6278
6311
  hasExpired: !!expire && !userId && !secret
6279
6312
  };
6280
6313
 
@@ -228,7 +228,7 @@
228
228
  color: var(--color-content-neutral, oklch(48.26% 0.0365 255.09));
229
229
  background-color: var(--color-page, oklch(100% 0 0));
230
230
  border: 1px solid var(--color-field-surface, oklch(91.79% 0.0029 264.26));
231
- border-radius: 1rem;
231
+ border-radius: calc(var(--radius, 0.5rem) * 2);
232
232
  tab-size: 4;
233
233
  white-space: pre;
234
234
  white-space-collapse: preserve
@@ -261,7 +261,7 @@
261
261
  line-height: 1.5;
262
262
  background: color-mix(in oklch, var(--color-field-surface, oklch(91.79% 0.0029 264.26)) 45%, transparent);
263
263
  border: 1px solid var(--color-field-surface, oklch(91.79% 0.0029 264.26));
264
- border-radius: 1rem;
264
+ border-radius: calc(var(--radius, 0.5rem) * 2);
265
265
  overflow: hidden;
266
266
 
267
267
  &:has(> header) {
@@ -346,6 +346,8 @@
346
346
  text-align: right;
347
347
  color: var(--color-content-subtle, oklch(67.4% 0.0318 251.27));
348
348
  background: var(--color-page, oklch(100% 0 0));
349
+ border: 1px solid var(--color-field-surface, oklch(91.79% 0.0029 264.26));
350
+ border-inline-end: 0 none;
349
351
  pointer-events: none;
350
352
  user-select: none;
351
353
  }
@@ -364,13 +366,18 @@
364
366
  border-radius: 0.875rem
365
367
  }
366
368
 
369
+ &:not(> header) pre {
370
+ border: 0 none
371
+ }
372
+
367
373
  &[numbers] pre {
374
+ border-inline-start: 0 none;
368
375
  border-start-start-radius: 0;
369
376
  border-end-start-radius: 0
370
377
  }
371
378
 
372
379
  /* Copy button */
373
- & .copy {
380
+ & button.copy {
374
381
  position: absolute;
375
382
  top: 0.3rem;
376
383
  right: 0.5rem;
@@ -378,13 +385,18 @@
378
385
  min-width: 0;
379
386
  height: 1.75rem;
380
387
  padding: 0;
388
+ background: none;
389
+
390
+ &:hover {
391
+ background: var(--color-field-surface, silver)
392
+ }
381
393
 
382
394
  &::after {
383
395
  content: '';
384
396
  display: block;
385
397
  width: 0.8125rem;
386
398
  height: 0.8125rem;
387
- background-color: var(--color-field-inverse, oklch(16.6% 0.026 267));
399
+ background-color: var(--color-content-neutral, gray);
388
400
  mask-image: var(--icon-copy-code);
389
401
  mask-size: 1rem;
390
402
  mask-size: contain;
@@ -398,7 +410,7 @@
398
410
  &.copied {
399
411
  --color-field-surface: var(--color-positive-surface);
400
412
  --color-field-surface-hover: var(--color-positive-surface-hover);
401
- --color-field-inverse: var(--color-positive-inverse);
413
+ --color-content-neutral: var(--color-positive-inverse);
402
414
  }
403
415
  }
404
416
 
@@ -408,14 +420,14 @@
408
420
  border: 0 none;
409
421
  overflow: visible;
410
422
 
411
- & .copy {
423
+ & button.copy {
412
424
  top: -2.25rem
413
425
  }
414
426
  }
415
427
  }
416
428
 
417
429
  /* RTL support */
418
- [dir=rtl] :where(x-code-group, x-code, [x-code]) .copy {
430
+ [dir=rtl] :where(x-code-group, x-code, [x-code]) button.copy {
419
431
  right: auto;
420
432
  left: 0.5rem
421
433
  }
@@ -1 +1 @@
1
- @import url("https://fonts.googleapis.com/css2?family=Gabarito:wght@400..900&family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&family=Lexend+Exa&display=swap");:root{--icon-copy-code:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Crect width='14' height='14' x='8' y='8' rx='2' ry='2'/%3E%3Cpath d='M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2'/%3E%3C/g%3E%3C/svg%3E");--icon-copied-code:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");--color-code-keyword:#b8860b;--color-code-string:#8b4513;--color-code-comment:grey;--color-code-function:peru;--color-code-number:sienna;--color-code-operator:#2f4f4f;--color-code-class-name:#daa520;--color-code-tag:#4682b4;--color-code-attr-name:#ff8c00;--color-code-attr-value:#8b4513;--color-code-property:sienna;--color-code-selector:#4682b4;--color-code-punctuation:#2f4f4f;--color-code-builtin:#b8860b;--color-code-constant:sienna;--color-code-boolean:sienna;--color-code-regex:#8b4513;--color-code-symbol:#daa520;--color-code-entity:#daa520;--color-code-url:sienna;--color-code-atrule:#b8860b;--color-code-rule:#4682b4;--color-code-doctype:grey;--color-code-cdata:grey;--color-code-prolog:grey;--color-code-namespace:grey;--color-code-important:#b8860b;--color-code-inserted:#228b22;--color-code-deleted:#dc143c;--color-code-char:#8b4513}.dark{--color-code-keyword:#f4a460;--color-code-string:#deb887;--color-code-comment:#8b8b8b;--color-code-function:#daa520;--color-code-number:tan;--color-code-operator:wheat;--color-code-class-name:peru;--color-code-tag:#87ceeb;--color-code-attr-name:gold;--color-code-attr-value:#deb887;--color-code-property:tan;--color-code-selector:#87ceeb;--color-code-punctuation:wheat;--color-code-builtin:#f4a460;--color-code-constant:tan;--color-code-boolean:tan;--color-code-regex:#deb887;--color-code-symbol:peru;--color-code-entity:peru;--color-code-url:tan;--color-code-atrule:#f4a460;--color-code-rule:#98fb98;--color-code-doctype:#8b8b8b;--color-code-cdata:#8b8b8b;--color-code-prolog:#8b8b8b;--color-code-namespace:#8b8b8b;--color-code-important:#f4a460;--color-code-inserted:#98fb98;--color-code-deleted:#f08080;--color-code-char:#deb887}@layer utilities{.hljs-comment{color:var(--color-code-comment)!important}.hljs-keyword{color:var(--color-code-keyword)!important}.hljs-string{color:var(--color-code-string)!important}.hljs-number{color:var(--color-code-number)!important}.hljs-literal{color:var(--color-code-constant)!important}.hljs-type{color:var(--color-code-class-name)!important}.hljs-variable{color:var(--color-code-property)!important}.hljs-variable.language_{color:var(--color-code-keyword)!important}.hljs-variable.constant_{color:var(--color-code-constant)!important}.hljs-title{color:var(--color-code-function)!important}.hljs-title.class_.inherited__{color:var(--color-code-class-name)!important}.hljs-title.function_.invoke__{color:var(--color-code-function)!important}.hljs-params{color:var(--color-code-property)!important}.hljs-doctag{color:var(--color-code-keyword)!important;font-weight:600!important}.hljs-meta{color:var(--color-code-comment)!important}.hljs-meta.keyword_,.hljs-meta.prompt_{color:var(--color-code-keyword)!important}.hljs-meta.string_{color:var(--color-code-string)!important}.hljs-section{color:var(--color-code-keyword)!important;font-weight:600!important}.hljs-name{color:var(--color-code-tag)!important}.hljs-attribute{color:var(--color-code-attr-name)!important}.hljs-bullet{color:var(--color-code-punctuation)!important}.hljs-code{color:var(--color-code-property)!important}.hljs-formula{color:var(--color-code-number)!important}.hljs-quote{color:var(--color-code-string)!important}.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-selector-pseudo{color:var(--color-code-selector)!important}.hljs-template-tag{color:var(--color-code-tag)!important}.hljs-template-variable{color:var(--color-code-property)!important}.hljs-subst{color:var(--color-code-string)!important}}@layer components{:where(pre):not(.unstyle){display:flex;flex:1;-ms-overflow-style:scrollbar;background-color:var(--color-page,oklch(100% 0 0));border:1px solid var(--color-field-surface,oklch(91.79% .0029 264.26));border-radius:1rem;color:var(--color-content-neutral,oklch(48.26% .0365 255.09));font-family:IBM Plex Mono,monospace;font-size:.8125rem;line-height:1.5;overflow-x:auto;padding:calc(var(--spacing, .25rem)*4);tab-size:4;text-indent:0;white-space:pre;white-space-collapse:preserve;width:auto}:where(pre code):not(.unstyle){background-color:transparent;border:0;color:inherit;display:block;font-family:inherit;font-size:inherit;font-weight:inherit;line-height:inherit;padding:0;width:100%;& span{vertical-align:initial}}:where(x-code-group,x-code,[x-code]){background:color-mix(in oklch,var(--color-field-surface,oklch(91.79% .0029 264.26)) 45%,transparent);border:1px solid var(--color-field-surface,oklch(91.79% .0029 264.26));border-radius:1rem;display:flex;flex-flow:row wrap;font-family:IBM Plex Mono,monospace;font-size:.8125rem;line-height:1.5;overflow:hidden;position:relative;&:has(>header){padding:.125rem}&>header{align-items:center;border-bottom:none;border-radius:.8125rem .8125rem 0 0;color:var(--color-content-subtle,oklch(67.4% .0318 251.27));display:flex;font-family:var(--font-sans,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-size:.8125rem;font-weight:500;gap:calc(var(--spacing, .25rem)*2);overflow-x:auto;padding:.5rem 4rem .5rem 1rem;width:100%;-ms-overflow-style:none;scrollbar-width:none;&::-webkit-scrollbar{display:none}& button[role=tab]{background:transparent;border-radius:var(--radius,.5rem);color:inherit;cursor:pointer;flex-shrink:0;font-family:inherit;font-size:inherit;height:fit-content;padding:var(--spacing,.25rem) calc(var(--spacing, .25rem)*2);transition:var(--transition,all .05s ease-in-out);&:hover{background-color:var(--color-surface-3,oklch(91.79% .0029 264.26));color:var(--color-content-neutral,oklch(48.26% .0365 255.09))}&.selected{background:transparent;color:var(--color-brand-content,#de6618);position:relative;&:hover{background-color:var(--color-surface-3,oklch(91.79% .0029 264.26))}&:after{background:color-mix(in oklch,var(--color-brand-content,#de6618) 50%,transparent);border-radius:.5rem;bottom:-.5rem;content:"";height:2px;left:50%;position:absolute;transform:translateX(-50%);width:calc(100% - var(--spacing, .25rem)*4)}}}}& .lines{background:var(--color-page,oklch(100% 0 0));color:var(--color-content-subtle,oklch(67.4% .0318 251.27));display:flex;flex-direction:column;font-family:inherit;font-size:inherit;line-height:inherit;min-width:2.5rem;padding:calc(var(--spacing, .25rem)*4) .5rem calc(var(--spacing, .25rem)*4) .5rem;pointer-events:none;text-align:right;user-select:none;width:fit-content}&:has(>header) .lines{border-end-start-radius:.875rem;border-start-start-radius:.875rem}& pre{border:0;margin-top:0}&:has(>header) pre{border-radius:.875rem}&[numbers] pre{border-end-start-radius:0;border-start-start-radius:0}& .copy{height:1.75rem;min-width:0;padding:0;position:absolute;right:.5rem;top:.3rem;width:1.75rem;&:after{background-color:var(--color-field-inverse,oklch(16.6% .026 267));content:"";display:block;height:.8125rem;mask-image:var(--icon-copy-code);mask-repeat:no-repeat;mask-size:1rem;mask-size:contain;width:.8125rem}&.copied:after{mask-image:var(--icon-copied-code)}&.copied{--color-field-surface:var(--color-positive-surface);--color-field-surface-hover:var(--color-positive-surface-hover);--color-field-inverse:var(--color-positive-inverse)}}:where(x-code){border:0;margin-top:0;overflow:visible;width:100%;& .copy{top:-2.25rem}}}[dir=rtl] :where(x-code-group,x-code,[x-code]) .copy{left:.5rem;right:auto}}@layer utilities{.prose aside.frame{background:color-mix(in oklch,var(--color-field-surface,oklch(91.79% .0029 264.26)) 35%,transparent);border:1px solid var(--color-field-surface,oklch(91.79% .0029 264.26));border-radius:calc(var(--radius, .5rem)*2);display:flex;gap:calc(var(--spacing, .25rem)*4)}.prose aside.frame:has(+x-code-group,+x-code){border-bottom:0;border-bottom-left-radius:0;border-bottom-right-radius:0}.prose aside.frame+x-code,.prose aside.frame+x-code-group{border-top-left-radius:0;border-top-right-radius:0;margin-top:0}.prose aside.frame+x-code pre{border-top-left-radius:0;border-top-right-radius:0}}
1
+ @import url("https://fonts.googleapis.com/css2?family=Gabarito:wght@400..900&family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&family=Lexend+Exa&display=swap");:root{--icon-copy-code:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Crect width='14' height='14' x='8' y='8' rx='2' ry='2'/%3E%3Cpath d='M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2'/%3E%3C/g%3E%3C/svg%3E");--icon-copied-code:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");--color-code-keyword:#b8860b;--color-code-string:#8b4513;--color-code-comment:gray;--color-code-function:peru;--color-code-number:sienna;--color-code-operator:#2f4f4f;--color-code-class-name:#daa520;--color-code-tag:#4682b4;--color-code-attr-name:#ff8c00;--color-code-attr-value:#8b4513;--color-code-property:sienna;--color-code-selector:#4682b4;--color-code-punctuation:#2f4f4f;--color-code-builtin:#b8860b;--color-code-constant:sienna;--color-code-boolean:sienna;--color-code-regex:#8b4513;--color-code-symbol:#daa520;--color-code-entity:#daa520;--color-code-url:sienna;--color-code-atrule:#b8860b;--color-code-rule:#4682b4;--color-code-doctype:gray;--color-code-cdata:gray;--color-code-prolog:gray;--color-code-namespace:gray;--color-code-important:#b8860b;--color-code-inserted:#228b22;--color-code-deleted:#dc143c;--color-code-char:#8b4513}.dark{--color-code-keyword:#f4a460;--color-code-string:#deb887;--color-code-comment:#8b8b8b;--color-code-function:#daa520;--color-code-number:tan;--color-code-operator:wheat;--color-code-class-name:peru;--color-code-tag:#87ceeb;--color-code-attr-name:gold;--color-code-attr-value:#deb887;--color-code-property:tan;--color-code-selector:#87ceeb;--color-code-punctuation:wheat;--color-code-builtin:#f4a460;--color-code-constant:tan;--color-code-boolean:tan;--color-code-regex:#deb887;--color-code-symbol:peru;--color-code-entity:peru;--color-code-url:tan;--color-code-atrule:#f4a460;--color-code-rule:#98fb98;--color-code-doctype:#8b8b8b;--color-code-cdata:#8b8b8b;--color-code-prolog:#8b8b8b;--color-code-namespace:#8b8b8b;--color-code-important:#f4a460;--color-code-inserted:#98fb98;--color-code-deleted:#f08080;--color-code-char:#deb887}@layer utilities{.hljs-comment{color:var(--color-code-comment)!important}.hljs-keyword{color:var(--color-code-keyword)!important}.hljs-string{color:var(--color-code-string)!important}.hljs-number{color:var(--color-code-number)!important}.hljs-literal{color:var(--color-code-constant)!important}.hljs-type{color:var(--color-code-class-name)!important}.hljs-variable{color:var(--color-code-property)!important}.hljs-variable.language_{color:var(--color-code-keyword)!important}.hljs-variable.constant_{color:var(--color-code-constant)!important}.hljs-title{color:var(--color-code-function)!important}.hljs-title.class_.inherited__{color:var(--color-code-class-name)!important}.hljs-title.function_.invoke__{color:var(--color-code-function)!important}.hljs-params{color:var(--color-code-property)!important}.hljs-doctag{color:var(--color-code-keyword)!important;font-weight:600!important}.hljs-meta{color:var(--color-code-comment)!important}.hljs-meta.keyword_,.hljs-meta.prompt_{color:var(--color-code-keyword)!important}.hljs-meta.string_{color:var(--color-code-string)!important}.hljs-section{color:var(--color-code-keyword)!important;font-weight:600!important}.hljs-name{color:var(--color-code-tag)!important}.hljs-attribute{color:var(--color-code-attr-name)!important}.hljs-bullet{color:var(--color-code-punctuation)!important}.hljs-code{color:var(--color-code-property)!important}.hljs-formula{color:var(--color-code-number)!important}.hljs-quote{color:var(--color-code-string)!important}.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-selector-pseudo{color:var(--color-code-selector)!important}.hljs-template-tag{color:var(--color-code-tag)!important}.hljs-template-variable{color:var(--color-code-property)!important}.hljs-subst{color:var(--color-code-string)!important}}@layer components{:where(pre):not(.unstyle){display:flex;flex:1;-ms-overflow-style:scrollbar;background-color:var(--color-page,oklch(100% 0 0));border:1px solid var(--color-field-surface,oklch(91.79% .0029 264.26));border-radius:calc(var(--radius, .5rem)*2);color:var(--color-content-neutral,oklch(48.26% .0365 255.09));font-family:IBM Plex Mono,monospace;font-size:.8125rem;line-height:1.5;overflow-x:auto;padding:calc(var(--spacing, .25rem)*4);tab-size:4;text-indent:0;white-space:pre;white-space-collapse:preserve;width:auto}:where(pre code):not(.unstyle){background-color:transparent;border:0;color:inherit;display:block;font-family:inherit;font-size:inherit;font-weight:inherit;line-height:inherit;padding:0;width:100%;& span{vertical-align:initial}}:where(x-code-group,x-code,[x-code]){background:color-mix(in oklch,var(--color-field-surface,oklch(91.79% .0029 264.26)) 45%,transparent);border:1px solid var(--color-field-surface,oklch(91.79% .0029 264.26));border-radius:calc(var(--radius, .5rem)*2);display:flex;flex-flow:row wrap;font-family:IBM Plex Mono,monospace;font-size:.8125rem;line-height:1.5;overflow:hidden;position:relative;&:has(>header){padding:.125rem}&>header{align-items:center;border-bottom:none;border-radius:.8125rem .8125rem 0 0;color:var(--color-content-subtle,oklch(67.4% .0318 251.27));display:flex;font-family:var(--font-sans,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-size:.8125rem;font-weight:500;gap:calc(var(--spacing, .25rem)*2);overflow-x:auto;padding:.5rem 4rem .5rem 1rem;width:100%;-ms-overflow-style:none;scrollbar-width:none;&::-webkit-scrollbar{display:none}& button[role=tab]{background:transparent;border-radius:var(--radius,.5rem);color:inherit;cursor:pointer;flex-shrink:0;font-family:inherit;font-size:inherit;height:fit-content;padding:var(--spacing,.25rem) calc(var(--spacing, .25rem)*2);transition:var(--transition,all .05s ease-in-out);&:hover{background-color:var(--color-surface-3,oklch(91.79% .0029 264.26));color:var(--color-content-neutral,oklch(48.26% .0365 255.09))}&.selected{background:transparent;color:var(--color-brand-content,#de6618);position:relative;&:hover{background-color:var(--color-surface-3,oklch(91.79% .0029 264.26))}&:after{background:color-mix(in oklch,var(--color-brand-content,#de6618) 50%,transparent);border-radius:.5rem;bottom:-.5rem;content:"";height:2px;left:50%;position:absolute;transform:translateX(-50%);width:calc(100% - var(--spacing, .25rem)*4)}}}}& .lines{background:var(--color-page,oklch(100% 0 0));border:1px solid var(--color-field-surface,oklch(91.79% .0029 264.26));border-inline-end:0 none;color:var(--color-content-subtle,oklch(67.4% .0318 251.27));display:flex;flex-direction:column;font-family:inherit;font-size:inherit;line-height:inherit;min-width:2.5rem;padding:calc(var(--spacing, .25rem)*4) .5rem calc(var(--spacing, .25rem)*4) .5rem;pointer-events:none;text-align:right;user-select:none;width:fit-content}&:has(>header) .lines{border-end-start-radius:.875rem;border-start-start-radius:.875rem}& pre{border:0;margin-top:0}&:has(>header) pre{border-radius:.875rem}&:not(>header) pre{border:0}&[numbers] pre{border-end-start-radius:0;border-inline-start:0 none;border-start-start-radius:0}& button.copy{background:none;height:1.75rem;min-width:0;padding:0;position:absolute;right:.5rem;top:.3rem;width:1.75rem;&:hover{background:var(--color-field-surface,silver)}&:after{background-color:var(--color-content-neutral,gray);content:"";display:block;height:.8125rem;mask-image:var(--icon-copy-code);mask-repeat:no-repeat;mask-size:1rem;mask-size:contain;width:.8125rem}&.copied:after{mask-image:var(--icon-copied-code)}&.copied{--color-field-surface:var(--color-positive-surface);--color-field-surface-hover:var(--color-positive-surface-hover);--color-content-neutral:var(--color-positive-inverse)}}:where(x-code){border:0;margin-top:0;overflow:visible;width:100%;& button.copy{top:-2.25rem}}}[dir=rtl] :where(x-code-group,x-code,[x-code]) button.copy{left:.5rem;right:auto}}@layer utilities{.prose aside.frame{background:color-mix(in oklch,var(--color-field-surface,oklch(91.79% .0029 264.26)) 35%,transparent);border:1px solid var(--color-field-surface,oklch(91.79% .0029 264.26));border-radius:calc(var(--radius, .5rem)*2);display:flex;gap:calc(var(--spacing, .25rem)*4)}.prose aside.frame:has(+x-code-group,+x-code){border-bottom:0;border-bottom-left-radius:0;border-bottom-right-radius:0}.prose aside.frame+x-code,.prose aside.frame+x-code-group{border-top-left-radius:0;border-top-right-radius:0;margin-top:0}.prose aside.frame+x-code pre{border-top-left-radius:0;border-top-right-radius:0}}
@@ -8,7 +8,7 @@ window.getManifestBase = function getManifestBase() {
8
8
  };
9
9
 
10
10
  // Absolute pathname prefix for the app root (e.g. "/src/dist"). Used by router for links and route matching.
11
- // Prerender injects <meta name="manifest:router-base" content="/path"> from manifest.prerender.routerBase or root+output. If present, use it; else fall back to depth or manifest link.
11
+ // Prerender injects <meta name="manifest:router-base" content="/path"> from manifest.render.routerBase or root+output. If present, use it; else fall back to depth or manifest link.
12
12
  window.getManifestBasePath = function getManifestBasePath() {
13
13
  const baseMeta = document.querySelector('meta[name="manifest:router-base"]');
14
14
  const content = baseMeta?.getAttribute('content');
@@ -149,6 +149,28 @@ window.ManifestComponentsLoader = {
149
149
  };
150
150
 
151
151
  // Components processor
152
+
153
+ // Escape a string so it's safe to interpolate inside a single-quoted JS
154
+ // string AND inside backtick template literals. The original escape covered
155
+ // the single-quote + whitespace cases but missed three vectors that matter
156
+ // when component templates use backtick literals (e.g. x-text="`Hi $modify('n')`"):
157
+ // - `\` → a trailing backslash would escape the closing quote
158
+ // - `` ` `` → terminates a backtick template literal
159
+ // - `${` → opens an interpolation that Alpine evaluates as JS
160
+ // Without escaping those, a bound value like `${alert(1)}` from a data
161
+ // source becomes code execution inside the wrapping template literal.
162
+ // Backslash must be escaped FIRST so the other replacements don't compound.
163
+ function escapeForSingleQuotedJsString(s) {
164
+ return String(s)
165
+ .replace(/\\/g, '\\\\')
166
+ .replace(/'/g, "\\'")
167
+ .replace(/`/g, '\\`')
168
+ .replace(/\$\{/g, '\\${')
169
+ .replace(/\r/g, '\\r')
170
+ .replace(/\n/g, '\\n')
171
+ .replace(/\t/g, '\\t');
172
+ }
173
+
152
174
  window.ManifestComponentsProcessor = {
153
175
  async processComponent(element, instanceId) {
154
176
  const name = element.tagName.toLowerCase().replace('x-', '');
@@ -295,7 +317,7 @@ window.ManifestComponentsProcessor = {
295
317
  return val;
296
318
  }
297
319
  // Always quote string values to ensure they're treated as strings, not variables
298
- return `'${val.replace(/'/g, "\\'").replace(/\r/g, '\\r').replace(/\n/g, '\\n').replace(/\t/g, '\\t')}'`;
320
+ return `'${escapeForSingleQuotedJsString(val)}'`;
299
321
  }
300
322
  );
301
323
  el.setAttribute(attr.name, newValue);
@@ -310,7 +332,7 @@ window.ManifestComponentsProcessor = {
310
332
  el.setAttribute(attr.name, propValue);
311
333
  } else {
312
334
  // Always quote string values and escape special characters
313
- const quotedValue = `'${propValue.replace(/'/g, "\\'").replace(/\r/g, '\\r').replace(/\n/g, '\\n').replace(/\t/g, '\\t')}'`;
335
+ const quotedValue = `'${escapeForSingleQuotedJsString(propValue)}'`;
314
336
  el.setAttribute(attr.name, quotedValue);
315
337
  }
316
338
  }
@@ -776,158 +798,6 @@ window.ManifestComponentsMutation = {
776
798
  }
777
799
  };
778
800
 
779
- // Components — route-level prefetch.
780
- //
781
- // Two enhancements that run on top of the existing on-encounter loader:
782
- //
783
- // 1. Parallel batch on route change. When manifest:route-change fires,
784
- // scan the [x-route] subtrees that match the new route and call
785
- // loadComponent() on every <x-*> tag inside them. The loader
786
- // deduplicates fetches, so calling it for components that the
787
- // regular swapping logic is already mounting is harmless — but
788
- // pre-issuing in parallel saves 50–200 ms vs. one-by-one fetches.
789
- //
790
- // 2. Prefetch on hover. When the pointer enters an internal <a href>,
791
- // derive the target pathname, find the [x-route] subtree(s) that
792
- // would match it, and prefetch their components. By the time the
793
- // user clicks the link, the components are warm in the loader's
794
- // cache and navigation feels instant.
795
- //
796
- // Both phases require zero author configuration. Manifest auto-discovers
797
- // what to prefetch from the existing [x-route] DOM structure.
798
-
799
- (function () {
800
- 'use strict';
801
-
802
- // <x-*> tag pattern — lowercase, hyphenated.
803
- const TAG_RE = /^x-[a-z][a-z0-9-]*$/;
804
-
805
- // Framework-provided web components (registered by Manifest plugins
806
- // themselves, not as project components in manifest.json). Skip these
807
- // when scanning for project components to prefetch.
808
- const FRAMEWORK_TAGS = new Set(['code', 'code-group']);
809
-
810
- // Anchors we've already issued a hover-prefetch for. WeakSet so DOM
811
- // garbage-collects naturally as elements leave the tree.
812
- const prefetchedAnchors = new WeakSet();
813
-
814
- function loader() { return window.ManifestComponentsLoader; }
815
-
816
- // Match a single route pattern against a normalized pathname (no
817
- // leading/trailing slashes, '/' represented as '/'). Mirrors the
818
- // router visibility logic so prefetch targets the same subtrees.
819
- function routeMatches(routeValue, pathname) {
820
- const pieces = String(routeValue || '').split(',').map((s) => s.trim()).filter(Boolean);
821
- let matched = false;
822
- let negated = false;
823
- for (const piece of pieces) {
824
- if (piece === '!*') continue; // catch-all only handled by visibility plugin
825
- if (piece.startsWith('!')) {
826
- if (piece.slice(1) === pathname) negated = true;
827
- continue;
828
- }
829
- if (piece.startsWith('=')) {
830
- if (piece.slice(1) === pathname) matched = true;
831
- continue;
832
- }
833
- if (piece.endsWith('/*')) {
834
- const prefix = piece.slice(0, -2);
835
- if (pathname === prefix || pathname.startsWith(prefix + '/')) matched = true;
836
- continue;
837
- }
838
- if (piece === pathname) { matched = true; continue; }
839
- if (pathname.startsWith(piece + '/')) matched = true;
840
- }
841
- return matched && !negated;
842
- }
843
-
844
- function findRouteSubtrees(pathname) {
845
- const normalized = (pathname || '/') === '/' ? '/' : pathname.replace(/^\/|\/$/g, '');
846
- const out = [];
847
- document.querySelectorAll('[x-route]').forEach((el) => {
848
- const value = el.getAttribute('x-route') || '';
849
- if (routeMatches(value, normalized)) out.push(el);
850
- });
851
- return out;
852
- }
853
-
854
- function discoverComponentNames(root) {
855
- const names = new Set();
856
- if (!root || !root.querySelectorAll) return names;
857
- // querySelectorAll('*') is the fastest path for "every descendant".
858
- // We filter by tag name in JS — there's no CSS selector for "tag
859
- // name starts with x-". A page typically has a few thousand nodes,
860
- // which scans in well under a millisecond.
861
- root.querySelectorAll('*').forEach((el) => {
862
- const tag = el.tagName.toLowerCase();
863
- if (!tag.startsWith('x-') || !TAG_RE.test(tag)) return;
864
- const name = tag.slice(2);
865
- if (!FRAMEWORK_TAGS.has(name)) names.add(name);
866
- });
867
- return names;
868
- }
869
-
870
- function prefetchForRoute(pathname) {
871
- const L = loader();
872
- if (!L || typeof L.loadComponent !== 'function') return;
873
- const subtrees = findRouteSubtrees(pathname);
874
- if (!subtrees.length) return;
875
- const names = new Set();
876
- for (const subtree of subtrees) {
877
- discoverComponentNames(subtree).forEach((n) => names.add(n));
878
- }
879
- names.forEach((name) => {
880
- try { L.loadComponent(name); } catch { /* swallow — dedup is internal */ }
881
- });
882
- }
883
-
884
- function hrefToPathname(href) {
885
- if (!href) return null;
886
- if (/^(#|mailto:|tel:|javascript:)/i.test(href)) return null;
887
- try {
888
- const url = new URL(href, window.location.href);
889
- if (url.origin !== window.location.origin) return null;
890
- return url.pathname || '/';
891
- } catch {
892
- return null;
893
- }
894
- }
895
-
896
- function initialize() {
897
- // 1) Parallel batch on route change.
898
- window.addEventListener('manifest:route-change', (event) => {
899
- const detail = (event && event.detail) || {};
900
- const path = detail.normalizedPath || detail.to || '/';
901
- const pathname = String(path).startsWith('/') ? String(path) : '/' + String(path);
902
- prefetchForRoute(pathname);
903
- });
904
-
905
- // 2) Hover prefetch. Use pointerover (bubbles) and check the closest
906
- // anchor on each event so we get a single trigger per anchor entry
907
- // without needing pointerenter (which doesn't bubble). Dedup via
908
- // a WeakSet so repeat moves within the anchor don't re-scan.
909
- document.addEventListener('pointerover', (e) => {
910
- if (!e.target || !e.target.closest) return;
911
- const a = e.target.closest('a[href]');
912
- if (!a || prefetchedAnchors.has(a)) return;
913
- // Author opt-out: `data-no-prefetch` skips this anchor.
914
- if (a.hasAttribute('data-no-prefetch')) return;
915
- const href = a.getAttribute('href');
916
- const pathname = hrefToPathname(href);
917
- if (!pathname) return;
918
- prefetchedAnchors.add(a);
919
- prefetchForRoute(pathname);
920
- });
921
- }
922
-
923
- if (document.readyState === 'loading') {
924
- document.addEventListener('DOMContentLoaded', initialize);
925
- } else {
926
- initialize();
927
- }
928
- })();
929
-
930
-
931
801
  // Main initialization for Manifest Components
932
802
  function initializeComponents() {
933
803
  if (window.ManifestComponentsRegistry) window.ManifestComponentsRegistry.initialize();