jssm 5.154.0 → 5.155.1

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/dist/wc/docs.js CHANGED
@@ -2143,10 +2143,10 @@ FslDocs.styles = css `
2143
2143
  .nav { list-style: none; margin: 0.3rem 0; padding: 0; }
2144
2144
  .nav li { margin: 0.1rem 0; }
2145
2145
  .nav a { display: block; padding: 0.35rem 0.25rem; border-radius: 4px; }
2146
- .nav a:hover { background: rgba(127,127,127,0.14); }
2146
+ .nav a:hover { background: color-mix(in srgb, var(--_fsl-text) 12%, transparent); }
2147
2147
  .docs-page { font-size: 0.82rem; line-height: 1.55; }
2148
- .docs-page pre { background: var(--_fsl-surface-alt, rgba(127,127,127,0.1)); padding: 0.5rem 0.6rem; border-radius: 6px; overflow-x: auto; }
2149
- .docs-page pre code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.78rem; }
2148
+ .docs-page pre { background: color-mix(in srgb, var(--_fsl-text) 6%, var(--_fsl-surface)); padding: 0.5rem 0.6rem; border-radius: 6px; overflow-x: auto; }
2149
+ .docs-page pre code { font-family: var(--_fsl-font-mono); font-size: 0.78rem; }
2150
2150
  .docs-page .fsl-tok-comment { color: var(--fsl-tok-comment, #7d8590); font-style: italic; }
2151
2151
  .docs-page .fsl-tok-string { color: var(--fsl-tok-string, #2e9e5b); }
2152
2152
  .docs-page .fsl-tok-action { color: var(--fsl-tok-action, #c2710c); }
@@ -521,7 +521,19 @@ async function deflate_raw(bytes) {
521
521
  return new Uint8Array(await new Response(stream.readable).arrayBuffer());
522
522
  }
523
523
  /**
524
- * Inverse of {@link deflate_raw}.
524
+ * Hard ceiling on the inflated size of a permalink, in bytes. A permalink rides
525
+ * in a URL an attacker can hand a victim, and {@link inflate_raw} runs on it
526
+ * automatically on page load, so an uncapped inflate is a decompression-bomb
527
+ * vector (a tiny `#m=…` could expand to hundreds of MB and OOM the tab). This is
528
+ * generous for real FSL (text — even a vast machine is well under a megabyte).
529
+ */
530
+ const MAX_PERMALINK_INFLATE_BYTES = 5 * 1024 * 1024;
531
+ /**
532
+ * Inverse of {@link deflate_raw}, reading the stream in chunks and aborting once
533
+ * the inflated output would exceed {@link MAX_PERMALINK_INFLATE_BYTES} (a
534
+ * decompression-bomb guard — see that constant).
535
+ *
536
+ * @throws RangeError when the inflated output exceeds the cap.
525
537
  *
526
538
  * @example
527
539
  * new TextDecoder().decode(await inflate_raw(await deflate_raw(new TextEncoder().encode("hi")))); // "hi"
@@ -531,7 +543,30 @@ async function inflate_raw(bytes) {
531
543
  const writer = stream.writable.getWriter();
532
544
  void writer.write(bytes);
533
545
  void writer.close();
534
- return new Uint8Array(await new Response(stream.readable).arrayBuffer());
546
+ // Read incrementally; stopping past the cap leaves the stream half-drained, so
547
+ // backpressure halts further inflation and the abandoned stream is GC'd. We do
548
+ // not cancel (which would abort the writable and leak an unhandled rejection).
549
+ const reader = stream.readable.getReader();
550
+ const chunks = [];
551
+ let total = 0;
552
+ for (;;) {
553
+ const { done, value } = await reader.read();
554
+ if (done) {
555
+ break;
556
+ }
557
+ total += value.length;
558
+ if (total > MAX_PERMALINK_INFLATE_BYTES) {
559
+ throw new RangeError(`permalink inflate exceeded ${MAX_PERMALINK_INFLATE_BYTES} bytes`);
560
+ }
561
+ chunks.push(value);
562
+ }
563
+ const out = new Uint8Array(total);
564
+ let offset = 0;
565
+ for (const chunk of chunks) {
566
+ out.set(chunk, offset);
567
+ offset += chunk.length;
568
+ }
569
+ return out;
535
570
  }
536
571
  /**
537
572
  * Encode FSL to a `<scheme><payload>` segment value (the part after `key=`).
@@ -560,14 +595,28 @@ async function decode_machine(segment) {
560
595
  const plain = scheme === '1' ? await inflate_raw(bytes) : bytes;
561
596
  return new TextDecoder().decode(plain);
562
597
  }
563
- /** Split a fragment (leading `#` optional) into `[key, value]` pairs, dropping empties. */
598
+ /** `decodeURIComponent` that returns its input untouched on a malformed escape,
599
+ * so a hand-mangled fragment never throws out of {@link read_fragment_param}. */
600
+ function safe_decode(text) {
601
+ try {
602
+ return decodeURIComponent(text);
603
+ }
604
+ catch (_a) {
605
+ return text;
606
+ }
607
+ }
608
+ /**
609
+ * Split a fragment (leading `#` optional) into `[key, value]` pairs, dropping
610
+ * empties. Keys are percent-decoded (they are percent-encoded on write by
611
+ * {@link set_fragment_param}); values are the URL-safe base64 payload as-is.
612
+ */
564
613
  function fragment_pairs(hash) {
565
614
  const body = hash.startsWith('#') ? hash.slice(1) : hash;
566
615
  return body.split('&').filter(Boolean).map(seg => {
567
616
  const eq = seg.indexOf('=');
568
617
  return eq === -1
569
- ? [seg, '']
570
- : [seg.slice(0, eq), seg.slice(eq + 1)];
618
+ ? [safe_decode(seg), '']
619
+ : [safe_decode(seg.slice(0, eq)), seg.slice(eq + 1)];
571
620
  });
572
621
  }
573
622
  /**
@@ -598,7 +647,9 @@ function set_fragment_param(hash, key, value) {
598
647
  else {
599
648
  pairs[at] = [key, value];
600
649
  }
601
- return pairs.map(([k, v]) => `${k}=${v}`).join('&');
650
+ // Percent-encode the key so an `id`/`uhash` containing `=`, `&`, or `#` cannot
651
+ // break segmentation or collide with a sibling. Values are URL-safe base64.
652
+ return pairs.map(([k, v]) => `${encodeURIComponent(k)}=${v}`).join('&');
602
653
  }
603
654
  /**
604
655
  * The fragment key an element owns: its `uhash` attribute if set, else its
@@ -666,6 +717,13 @@ class FslPermalinkSync {
666
717
  }
667
718
  try {
668
719
  const fsl = await decode_machine(segment);
720
+ // The decode is async; if the host was disconnected while it ran, drop the
721
+ // result rather than mutating a detached element (and triggering a stray
722
+ // rebuild on a later reconnect). A reconnect runs hostConnected → _restore
723
+ // afresh.
724
+ if (!this.host.isConnected) {
725
+ return;
726
+ }
669
727
  this._last = segment;
670
728
  this.host.fsl = fsl;
671
729
  }
@@ -136,14 +136,28 @@ async function encode_machine(fsl) {
136
136
  const deflated = bytes_to_base64url(await deflate_raw(utf8));
137
137
  return deflated.length < raw.length ? `1${deflated}` : `0${raw}`;
138
138
  }
139
- /** Split a fragment (leading `#` optional) into `[key, value]` pairs, dropping empties. */
139
+ /** `decodeURIComponent` that returns its input untouched on a malformed escape,
140
+ * so a hand-mangled fragment never throws out of {@link read_fragment_param}. */
141
+ function safe_decode(text) {
142
+ try {
143
+ return decodeURIComponent(text);
144
+ }
145
+ catch (_a) {
146
+ return text;
147
+ }
148
+ }
149
+ /**
150
+ * Split a fragment (leading `#` optional) into `[key, value]` pairs, dropping
151
+ * empties. Keys are percent-decoded (they are percent-encoded on write by
152
+ * {@link set_fragment_param}); values are the URL-safe base64 payload as-is.
153
+ */
140
154
  function fragment_pairs(hash) {
141
155
  const body = hash.startsWith('#') ? hash.slice(1) : hash;
142
156
  return body.split('&').filter(Boolean).map(seg => {
143
157
  const eq = seg.indexOf('=');
144
158
  return eq === -1
145
- ? [seg, '']
146
- : [seg.slice(0, eq), seg.slice(eq + 1)];
159
+ ? [safe_decode(seg), '']
160
+ : [safe_decode(seg.slice(0, eq)), seg.slice(eq + 1)];
147
161
  });
148
162
  }
149
163
  /**
@@ -162,7 +176,9 @@ function set_fragment_param(hash, key, value) {
162
176
  else {
163
177
  pairs[at] = [key, value];
164
178
  }
165
- return pairs.map(([k, v]) => `${k}=${v}`).join('&');
179
+ // Percent-encode the key so an `id`/`uhash` containing `=`, `&`, or `#` cannot
180
+ // break segmentation or collide with a sibling. Values are URL-safe base64.
181
+ return pairs.map(([k, v]) => `${encodeURIComponent(k)}=${v}`).join('&');
166
182
  }
167
183
  /**
168
184
  * The fragment key an element owns: its `uhash` attribute if set, else its
@@ -398,13 +414,6 @@ class FslToolbar extends LitElement {
398
414
  return html `
399
415
  <div class="toolbar" part="toolbar" role="toolbar" aria-label="Workbench controls">
400
416
  <span class="spacer"></span>
401
- ${host ? html `
402
- <div class="grp">
403
- ${this.noValidate ? '' : html `
404
- <button class="tb icon" aria-label="Validate" title="Validate" @click=${() => this._fireAction('fsl-validate')}>${ICON_VALIDATE}</button>`}
405
- ${this.noLint ? '' : html `
406
- <button class="tb icon" aria-label="Lint" title="Lint" @click=${() => this._fireAction('fsl-lint')}>${ICON_LINT}</button>`}
407
- </div>` : ''}
408
417
  <div class="grp">
409
418
  ${host
410
419
  ? this._present.map(p => html `
@@ -412,6 +421,13 @@ class FslToolbar extends LitElement {
412
421
  @click=${() => { host.togglePanel(p.slot); this.requestUpdate(); }}>${p.icon}</button>`)
413
422
  : ''}
414
423
  </div>
424
+ ${host ? html `
425
+ <div class="grp">
426
+ ${this.noValidate ? '' : html `
427
+ <button class="tb icon" aria-label="Validate" title="Validate" @click=${() => this._fireAction('fsl-validate')}>${ICON_VALIDATE}</button>`}
428
+ ${this.noLint ? '' : html `
429
+ <button class="tb icon" aria-label="Lint" title="Lint" @click=${() => this._fireAction('fsl-lint')}>${ICON_LINT}</button>`}
430
+ </div>` : ''}
415
431
  <div class="grp">
416
432
  <button class="tb layout" aria-haspopup="true" aria-expanded=${this._openMenu === 'layout'} aria-label="Layout" title="Layout" @click=${() => this._toggleMenu('layout')}>${layoutIcon}<span class="caret">▾</span></button>
417
433
  ${this._openMenu === 'layout' ? html `
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jssm",
3
- "version": "5.154.0",
3
+ "version": "5.155.1",
4
4
  "engines": {
5
5
  "node": ">=10.0.0"
6
6
  },