@linxin666/dsh-remote-web-ui 0.1.18 → 0.1.19

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/index.js CHANGED
@@ -483,20 +483,10 @@ function sameStrings(a, b) {
483
483
  return a.length === b.length && a.every((value, index) => value === b[index]);
484
484
  }
485
485
  //#endregion
486
- //#region src/gate.ts
487
- /**
488
- * Whether a normalized URL hostname names the local loopback authority.
489
- * Semantics mirror the connection package's internal predicate (localhost,
490
- * IPv6 loopback, any IPv4 address in 127/8); it is reimplemented here because
491
- * the connection package no longer exports it — the fence now lives inside
492
- * the connection plugin, and external host plugins only need the
493
- * classification, not the whole trust decision.
494
- * @param hostname - WHATWG URL hostname (IPv6 literals retain brackets).
495
- * @returns true for localhost, IPv6 loopback, or any IPv4 address in 127/8.
496
- */
497
- function isLoopbackHostname(hostname) {
498
- if (hostname === "localhost" || hostname === "[::1]") return true;
499
- const parts = hostname.split(".");
486
+ //#region src/loopback.ts
487
+ /** IPv4 127/8 predicate (four decimal octets, first == 127). */
488
+ function isIPv4Loopback(v4) {
489
+ const parts = v4.split(".");
500
490
  return parts.length === 4 && parts[0] === "127" && parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255);
501
491
  }
502
492
  /** Whether a socket remote address names the loopback range (127/8, ::1, IPv4-mapped). */
@@ -507,11 +497,21 @@ function isLoopbackAddress(address) {
507
497
  if (normalized.startsWith("::ffff:")) return isIPv4Loopback(normalized.slice(7));
508
498
  return isIPv4Loopback(normalized);
509
499
  }
510
- /** IPv4 127/8 predicate (four decimal octets, first == 127). */
511
- function isIPv4Loopback(v4) {
512
- const parts = v4.split(".");
513
- return parts.length === 4 && parts[0] === "127" && parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255);
500
+ /** Whether a normalized URL hostname names the loopback authority (localhost, [::1], 127/8). */
501
+ function isLoopbackHostname(hostname) {
502
+ if (hostname === "localhost" || hostname === "[::1]") return true;
503
+ return isIPv4Loopback(hostname);
514
504
  }
505
+ //#endregion
506
+ //#region src/gate.ts
507
+ /**
508
+ * Loopback classification for the desktop client. The predicates now live in
509
+ * the shared synced copy (shared/host/loopback.ts, mirrored to ./loopback.ts
510
+ * by scripts/sync-shared.mjs): localhost, IPv6 loopback, and any IPv4 address
511
+ * in 127/8.
512
+ * @param hostname - WHATWG URL hostname (IPv6 literals retain brackets).
513
+ * @returns true for localhost, IPv6 loopback, or any IPv4 address in 127/8.
514
+ */
515
515
  /**
516
516
  * Read one cookie value from a Cookie header.
517
517
  * @param header - the raw Cookie header value (or undefined).
@@ -67,6 +67,11 @@ export declare class RemoteSettingsCardController {
67
67
  * @returns the card's snapshot and its form actions.
68
68
  */
69
69
  inject(): RemoteSettingsCardFace;
70
+ /**
71
+ * Release the card's scope subscription and bound stores; the slot
72
+ * disposer calls this on teardown.
73
+ */
74
+ dispose(): void;
70
75
  }
71
76
  /** Props the renderer binds for the remote-control card. */
72
77
  export type RemoteSettingsCardProps = PropsRuntime<'web-ui.plugin.item'> & PropsLocale<'remote'> & InjectFace<RemoteSettingsCardFace>;
@@ -1 +1 @@
1
- {"version":3,"file":"RemoteSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/client/RemoteSettingsCard.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAC7F,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,wCAAwC,CAAA;AAE1F,OAAO,EAAkD,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,IAAI,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAExJ,+EAA+E;AAC/E,MAAM,WAAW,cAAc;IAC7B,oCAAoC;IACpC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,iDAAiD;IACjD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,6FAA6F;IAC7F,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,6EAA6E;IAC7E,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED,4CAA4C;AAC5C,MAAM,WAAW,uBAAwB,SAAQ,SAAS;IACxD,qBAAqB;IACrB,OAAO,EAAE,cAAc,CAAA;IACvB,sBAAsB;IACtB,UAAU,EAAE,cAAc,CAAA;IAC1B,gCAAgC;IAChC,cAAc,EAAE,cAAc,CAAA;IAC9B,yBAAyB;IACzB,UAAU,EAAE,cAAc,CAAA;IAC1B,0BAA0B;IAC1B,UAAU,EAAE,cAAc,CAAA;IAC1B,sBAAsB;IACtB,oBAAoB,EAAE,cAAc,CAAA;IACpC,kCAAkC;IAClC,aAAa,EAAE,cAAc,CAAA;IAC7B,iCAAiC;IACjC,UAAU,EAAE,cAAc,CAAA;IAC1B,4CAA4C;IAC5C,iBAAiB,EAAE,cAAc,CAAA;CAClC;AAED,gEAAgE;AAChE,MAAM,WAAW,sBAAuB,SAAQ,WAAW;IACzD,KAAK,EAAE;QACL,oEAAoE;QACpE,kBAAkB,EAAE,aAAa,CAAC,uBAAuB,CAAC,CAAA;KAC3D,CAAA;CACF;AAED,qEAAqE;AACrE,qBAAa,4BAA4B;IACvC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA0B;IAC/C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAwC;IAE9D,iFAAiF;gBACrE,KAAK,EAAE,aAAa,CAAC,cAAc,CAAC;IAehD,OAAO,CAAC,UAAU;IAelB;;;OAGG;IACH,MAAM,IAAI,sBAAsB;CAGjC;AAED,4DAA4D;AAC5D,MAAM,MAAM,uBAAuB,GACjC,YAAY,CAAC,oBAAoB,CAAC,GAChC,WAAW,CAAC,QAAQ,CAAC,GACrB,UAAU,CAAC,sBAAsB,CAAC,CAAA;AAEtC;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,uBAAuB,+BAsHhE"}
1
+ {"version":3,"file":"RemoteSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/client/RemoteSettingsCard.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAC7F,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,wCAAwC,CAAA;AAE1F,OAAO,EAAkD,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,IAAI,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAExJ,+EAA+E;AAC/E,MAAM,WAAW,cAAc;IAC7B,oCAAoC;IACpC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,iDAAiD;IACjD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,6FAA6F;IAC7F,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,6EAA6E;IAC7E,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED,4CAA4C;AAC5C,MAAM,WAAW,uBAAwB,SAAQ,SAAS;IACxD,qBAAqB;IACrB,OAAO,EAAE,cAAc,CAAA;IACvB,sBAAsB;IACtB,UAAU,EAAE,cAAc,CAAA;IAC1B,gCAAgC;IAChC,cAAc,EAAE,cAAc,CAAA;IAC9B,yBAAyB;IACzB,UAAU,EAAE,cAAc,CAAA;IAC1B,0BAA0B;IAC1B,UAAU,EAAE,cAAc,CAAA;IAC1B,sBAAsB;IACtB,oBAAoB,EAAE,cAAc,CAAA;IACpC,kCAAkC;IAClC,aAAa,EAAE,cAAc,CAAA;IAC7B,iCAAiC;IACjC,UAAU,EAAE,cAAc,CAAA;IAC1B,4CAA4C;IAC5C,iBAAiB,EAAE,cAAc,CAAA;CAClC;AAED,gEAAgE;AAChE,MAAM,WAAW,sBAAuB,SAAQ,WAAW;IACzD,KAAK,EAAE;QACL,oEAAoE;QACpE,kBAAkB,EAAE,aAAa,CAAC,uBAAuB,CAAC,CAAA;KAC3D,CAAA;CACF;AAED,qEAAqE;AACrE,qBAAa,4BAA4B;IACvC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA0B;IAC/C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAwC;IAE9D,iFAAiF;gBACrE,KAAK,EAAE,aAAa,CAAC,cAAc,CAAC;IAehD,OAAO,CAAC,UAAU;IAelB;;;OAGG;IACH,MAAM,IAAI,sBAAsB;IAIhC;;;OAGG;IACH,OAAO,IAAI,IAAI;CAGhB;AAED,4DAA4D;AAC5D,MAAM,MAAM,uBAAuB,GACjC,YAAY,CAAC,oBAAoB,CAAC,GAChC,WAAW,CAAC,QAAQ,CAAC,GACrB,UAAU,CAAC,sBAAsB,CAAC,CAAA;AAEtC;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,uBAAuB,+BAsHhE"}
@@ -41,6 +41,13 @@ export class RemoteSettingsCardController {
41
41
  inject() {
42
42
  return { hooks: { remoteSettingsCard: this.store }, ...this.form.actions() };
43
43
  }
44
+ /**
45
+ * Release the card's scope subscription and bound stores; the slot
46
+ * disposer calls this on teardown.
47
+ */
48
+ dispose() {
49
+ this.form.dispose();
50
+ }
44
51
  }
45
52
  /**
46
53
  * Render the remote-control card.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAA;AAa7G,OAAO,EAAU,KAAK,SAAS,EAAE,MAAM,cAAc,CAAA;AAIrD,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACzD,YAAY,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACrE,YAAY,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AACnE,YAAY,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAC7C,YAAY,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAA;AAC/F,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACzD,YAAY,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAErE,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,0CAA0C;QAC1C,MAAM,EAAE,SAAS,CAAA;KAClB;IAED,UAAU,OAAO;QACf;;;;WAIG;QACH,gBAAgB,EAAE;YAAE,IAAI,EAAE,QAAQ,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,uBAAuB,CAAA;SAAE,CAAA;QACnF;;;;;WAKG;QACH,oBAAoB,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,4BAA4B,CAAA;SAAE,CAAA;KAC3F;CACF;AAED,4GAA4G;AAC5G,MAAM,WAAW,uBAAuB;IACtC,oEAAoE;IACpE,IAAI,EAAE,OAAO,CAAA;CACd;AAED,mEAAmE;AACnE,MAAM,WAAW,4BAA4B;IAC3C,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,KAAK,CAAA;CACjB;AAED,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf;;;;WAIG;QACH,aAAa,CAAC,EAAE;YAAE,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAAA;SAAE,CAAA;KAC1E;CACF;AAYD,wCAAwC;AACxC,eAAO,MAAM,MAAM,UAA+D,CAAA;AAElF;;;GAGG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CA0G9C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAA;AAa7G,OAAO,EAAU,KAAK,SAAS,EAAE,MAAM,cAAc,CAAA;AAIrD,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACzD,YAAY,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACrE,YAAY,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AACnE,YAAY,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAC7C,YAAY,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAA;AAC/F,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACzD,YAAY,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAErE,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,0CAA0C;QAC1C,MAAM,EAAE,SAAS,CAAA;KAClB;IAED,UAAU,OAAO;QACf;;;;WAIG;QACH,gBAAgB,EAAE;YAAE,IAAI,EAAE,QAAQ,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,uBAAuB,CAAA;SAAE,CAAA;QACnF;;;;;WAKG;QACH,oBAAoB,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,4BAA4B,CAAA;SAAE,CAAA;KAC3F;CACF;AAED,4GAA4G;AAC5G,MAAM,WAAW,uBAAuB;IACtC,oEAAoE;IACpE,IAAI,EAAE,OAAO,CAAA;CACd;AAED,mEAAmE;AACnE,MAAM,WAAW,4BAA4B;IAC3C,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,KAAK,CAAA;CACjB;AAED,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf;;;;WAIG;QACH,aAAa,CAAC,EAAE;YAAE,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAAA;SAAE,CAAA;KAC1E;CACF;AAYD,wCAAwC;AACxC,eAAO,MAAM,MAAM,UAA+D,CAAA;AAElF;;;GAGG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAgH9C"}
@@ -86,13 +86,19 @@ export function apply(ctx) {
86
86
  // Plugin configuration card: one staged form over the `remote-web-ui`
87
87
  // settings namespace, contributed to the Web UI plugin group.
88
88
  const remoteSettings = new RemoteSettingsCardController(settingsScope);
89
- ctx.slots.inject('web-ui.plugin.item', () => ctx.slots.register({
90
- name: 'web-ui.plugin.item',
91
- id: 'remote-web-ui',
92
- order: 90,
93
- locale: NS,
94
- inject: () => remoteSettings.inject(),
95
- }, RemoteSettingsCard));
89
+ ctx.slots.inject('web-ui.plugin.item', () => {
90
+ const unregister = ctx.slots.register({
91
+ name: 'web-ui.plugin.item',
92
+ id: 'remote-web-ui',
93
+ order: 90,
94
+ locale: NS,
95
+ inject: () => remoteSettings.inject(),
96
+ }, RemoteSettingsCard);
97
+ return () => {
98
+ remoteSettings.dispose();
99
+ unregister();
100
+ };
101
+ });
96
102
  // Phone-side boot flow + heartbeats. Loopback pages (the desktop) never
97
103
  // heartbeat; the server ignores unpaired heartbeats anyway. Both run only
98
104
  // while the plugin is enabled.
@@ -149,11 +149,19 @@ export declare class CardForm<T> {
149
149
  private readonly specs;
150
150
  private readonly staged;
151
151
  private readonly listeners;
152
+ /** The scope subscription installed in the constructor; released by dispose(). */
153
+ private readonly disposeScope;
154
+ private disposed;
152
155
  private saving;
153
156
  private failed;
154
157
  private failedReason;
155
158
  /** @param scope - the bound settings scope for this card's namespace. */
156
159
  constructor(scope: SettingsScope<T>, specs: FieldSpec[]);
160
+ /**
161
+ * Release the scope subscription and every bound store listener. The card
162
+ * must call this on teardown; later calls are no-ops.
163
+ */
164
+ dispose(): void;
157
165
  /** Publish a projection of this form, rebuilt whenever the scope or a draft changes. */
158
166
  bind<S>(project: () => S): SnapshotStore<S>;
159
167
  /** Read the card-level state: what the Host serves, and what a save would do. */
@@ -1 +1 @@
1
- {"version":3,"file":"settings-form.d.ts","sourceRoot":"","sources":["../../../src/client/settings-form.ts"],"names":[],"mappings":"AACA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAyB,aAAa,EAAE,MAAM,wCAAwC,CAAA;AAGjH,yEAAyE;AACzE,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAA;AAErB,0EAA0E;AAC1E,MAAM,WAAW,SAAS;IACxB,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAA;IACb;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,2FAA2F;IAC3F,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAA;IAClC;;;OAGG;IACH,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,UAAU,GAAG,SAAS,CAAA;CAChD;AAED,wCAAwC;AACxC,MAAM,WAAW,UAAU;IACzB,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAA;IACZ,oEAAoE;IACpE,UAAU,EAAE,OAAO,CAAA;IACnB,gFAAgF;IAChF,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,oDAAoD;AACpD,MAAM,WAAW,SAAS;IACxB,4EAA4E;IAC5E,SAAS,EAAE,OAAO,CAAA;IAClB;;;;;;OAMG;IACH,OAAO,EAAE,OAAO,CAAA;IAChB,gDAAgD;IAChD,QAAQ,EAAE,OAAO,CAAA;IACjB,4DAA4D;IAC5D,KAAK,EAAE,OAAO,CAAA;IACd,kEAAkE;IAClE,OAAO,EAAE,OAAO,CAAA;IAChB,2CAA2C;IAC3C,MAAM,EAAE,OAAO,CAAA;IACf,sFAAsF;IACtF,MAAM,EAAE,OAAO,CAAA;IACf;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,uDAAuD;AACvD,MAAM,WAAW,WAAW;IAC1B,sCAAsC;IACtC,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC3C,gFAAgF;IAChF,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC,yEAAyE;IACzE,IAAI,EAAE,MAAM,IAAI,CAAA;IAChB,8BAA8B;IAC9B,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB;AAoBD,2DAA2D;AAC3D,MAAM,WAAW,YAAY;IAC3B,+BAA+B;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,gDAAgD;IAChD,EAAE,EAAE,KAAK,GAAG,OAAO,CAAA;IACnB,2CAA2C;IAC3C,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED,oDAAoD;AACpD,MAAM,WAAW,kBAAkB;IACjC,+BAA+B;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,6EAA6E;IAC7E,MAAM,EAAE,OAAO,CAAA;CAChB;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,6CAA6C;IAC7C,EAAE,EAAE,OAAO,CAAA;IACX,wEAAwE;IACxE,MAAM,EAAE,kBAAkB,EAAE,CAAA;IAC5B,4CAA4C;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,+CAA+C;IAC/C,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAQD,6FAA6F;AAC7F,MAAM,WAAW,iBAAiB;IAChC,iDAAiD;IACjD,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,gDAAgD;IAChD,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAED,6JAA6J;AAC7J,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,GAAE,iBAAsB,GAAG,SAAS,CAezF;AAED,0DAA0D;AAC1D,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CASlD;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAEpD;AAED,6DAA6D;AAC7D,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAYrD;AAED,yGAAyG;AACzG,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAShF;AAED;;;;;;;GAOG;AACH,qBAAa,QAAQ,CAAC,CAAC;IAUnB,OAAO,CAAC,QAAQ,CAAC,KAAK;IATxB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAwB;IAC9C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgC;IACvD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwB;IAClD,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,YAAY,CAAoB;IAExC,yEAAyE;gBAEtD,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,EACxC,KAAK,EAAE,SAAS,EAAE;IAMpB,wFAAwF;IACxF,IAAI,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC;IAM3C,iFAAiF;IACjF,KAAK,IAAI,SAAS;IAelB,8EAA8E;IAC9E,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU;IAchC,wDAAwD;IACxD,OAAO,IAAI,WAAW;IAiBtB;;;;;;;;;;OAUG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAoC3B,0FAA0F;IAC1F,OAAO,CAAC,YAAY;IAKpB;;;;;;OAMG;IACH,OAAO,CAAC,IAAI;YAiBE,KAAK;YAKL,KAAK;IAUnB,OAAO,CAAC,KAAK;IAOb,OAAO,CAAC,MAAM;IAQd,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,MAAM;IAKd,OAAO,CAAC,OAAO;CAGhB"}
1
+ {"version":3,"file":"settings-form.d.ts","sourceRoot":"","sources":["../../../src/client/settings-form.ts"],"names":[],"mappings":"AACA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAyB,aAAa,EAAE,MAAM,wCAAwC,CAAA;AAGjH,yEAAyE;AACzE,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAA;AAErB,0EAA0E;AAC1E,MAAM,WAAW,SAAS;IACxB,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAA;IACb;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,2FAA2F;IAC3F,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAA;IAClC;;;OAGG;IACH,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,UAAU,GAAG,SAAS,CAAA;CAChD;AAED,wCAAwC;AACxC,MAAM,WAAW,UAAU;IACzB,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAA;IACZ,oEAAoE;IACpE,UAAU,EAAE,OAAO,CAAA;IACnB,gFAAgF;IAChF,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,oDAAoD;AACpD,MAAM,WAAW,SAAS;IACxB,4EAA4E;IAC5E,SAAS,EAAE,OAAO,CAAA;IAClB;;;;;;OAMG;IACH,OAAO,EAAE,OAAO,CAAA;IAChB,gDAAgD;IAChD,QAAQ,EAAE,OAAO,CAAA;IACjB,4DAA4D;IAC5D,KAAK,EAAE,OAAO,CAAA;IACd,kEAAkE;IAClE,OAAO,EAAE,OAAO,CAAA;IAChB,2CAA2C;IAC3C,MAAM,EAAE,OAAO,CAAA;IACf,sFAAsF;IACtF,MAAM,EAAE,OAAO,CAAA;IACf;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,uDAAuD;AACvD,MAAM,WAAW,WAAW;IAC1B,sCAAsC;IACtC,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC3C,gFAAgF;IAChF,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC,yEAAyE;IACzE,IAAI,EAAE,MAAM,IAAI,CAAA;IAChB,8BAA8B;IAC9B,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB;AAoBD,2DAA2D;AAC3D,MAAM,WAAW,YAAY;IAC3B,+BAA+B;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,gDAAgD;IAChD,EAAE,EAAE,KAAK,GAAG,OAAO,CAAA;IACnB,2CAA2C;IAC3C,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED,oDAAoD;AACpD,MAAM,WAAW,kBAAkB;IACjC,+BAA+B;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,6EAA6E;IAC7E,MAAM,EAAE,OAAO,CAAA;CAChB;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,6CAA6C;IAC7C,EAAE,EAAE,OAAO,CAAA;IACX,wEAAwE;IACxE,MAAM,EAAE,kBAAkB,EAAE,CAAA;IAC5B,4CAA4C;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,+CAA+C;IAC/C,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAQD,6FAA6F;AAC7F,MAAM,WAAW,iBAAiB;IAChC,iDAAiD;IACjD,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,gDAAgD;IAChD,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAED,6JAA6J;AAC7J,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,GAAE,iBAAsB,GAAG,SAAS,CAezF;AAED,0DAA0D;AAC1D,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CASlD;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAEpD;AAED,6DAA6D;AAC7D,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAYrD;AAED,yGAAyG;AACzG,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAShF;AAED;;;;;;;GAOG;AACH,qBAAa,QAAQ,CAAC,CAAC;IAanB,OAAO,CAAC,QAAQ,CAAC,KAAK;IAZxB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAwB;IAC9C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgC;IACvD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwB;IAClD,kFAAkF;IAClF,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAY;IACzC,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,YAAY,CAAoB;IAExC,yEAAyE;gBAEtD,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,EACxC,KAAK,EAAE,SAAS,EAAE;IAMpB;;;OAGG;IACH,OAAO,IAAI,IAAI;IAOf,wFAAwF;IACxF,IAAI,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC;IAM3C,iFAAiF;IACjF,KAAK,IAAI,SAAS;IAelB,8EAA8E;IAC9E,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU;IAchC,wDAAwD;IACxD,OAAO,IAAI,WAAW;IAiBtB;;;;;;;;;;OAUG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAoC3B,0FAA0F;IAC1F,OAAO,CAAC,YAAY;IAKpB;;;;;;OAMG;IACH,OAAO,CAAC,IAAI;YAiBE,KAAK;YAKL,KAAK;IAUnB,OAAO,CAAC,KAAK;IAOb,OAAO,CAAC,MAAM;IAQd,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,MAAM;IAKd,OAAO,CAAC,OAAO;CAGhB"}
@@ -91,6 +91,9 @@ export class CardForm {
91
91
  specs;
92
92
  staged = new Map();
93
93
  listeners = new Set();
94
+ /** The scope subscription installed in the constructor; released by dispose(). */
95
+ disposeScope;
96
+ disposed = false;
94
97
  saving = false;
95
98
  failed = false;
96
99
  failedReason;
@@ -98,7 +101,18 @@ export class CardForm {
98
101
  constructor(scope, specs) {
99
102
  this.scope = scope;
100
103
  this.specs = new Map(specs.map(spec => [spec.field, spec]));
101
- scope.subscribe(() => { this.publish(); });
104
+ this.disposeScope = scope.subscribe(() => { this.publish(); });
105
+ }
106
+ /**
107
+ * Release the scope subscription and every bound store listener. The card
108
+ * must call this on teardown; later calls are no-ops.
109
+ */
110
+ dispose() {
111
+ if (this.disposed)
112
+ return;
113
+ this.disposed = true;
114
+ this.disposeScope();
115
+ this.listeners.clear();
102
116
  }
103
117
  /** Publish a projection of this form, rebuilt whenever the scope or a draft changes. */
104
118
  bind(project) {
@@ -13,18 +13,13 @@
13
13
  import type { IncomingMessage } from 'node:http';
14
14
  import type { PairingService } from './pairing.ts';
15
15
  /**
16
- * Whether a normalized URL hostname names the local loopback authority.
17
- * Semantics mirror the connection package's internal predicate (localhost,
18
- * IPv6 loopback, any IPv4 address in 127/8); it is reimplemented here because
19
- * the connection package no longer exports it — the fence now lives inside
20
- * the connection plugin, and external host plugins only need the
21
- * classification, not the whole trust decision.
16
+ * Loopback classification for the desktop client. The predicates now live in
17
+ * the shared synced copy (shared/host/loopback.ts, mirrored to ./loopback.ts
18
+ * by scripts/sync-shared.mjs): localhost, IPv6 loopback, and any IPv4 address
19
+ * in 127/8.
22
20
  * @param hostname - WHATWG URL hostname (IPv6 literals retain brackets).
23
21
  * @returns true for localhost, IPv6 loopback, or any IPv4 address in 127/8.
24
22
  */
25
- export declare function isLoopbackHostname(hostname: string): boolean;
26
- /** Whether a socket remote address names the loopback range (127/8, ::1, IPv4-mapped). */
27
- export declare function isLoopbackAddress(address: string | undefined): boolean;
28
23
  /**
29
24
  * Read one cookie value from a Cookie header.
30
25
  * @param header - the raw Cookie header value (or undefined).
@@ -1 +1 @@
1
- {"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../../src/gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAElD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAM5D;AAED,0FAA0F;AAC1F,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAMtE;AAUD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CASvF;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,GAAG,SAAS,CAQvE;AAED,oGAAoG;AACpG,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAKlE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,cAAc,EACvB,oBAAoB,GAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAQ,EACtD,OAAO,GAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAQ,GACxC,CAAC,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAW9H"}
1
+ {"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../../src/gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAGlD;;;;;;;GAOG;AAEH;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CASvF;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,GAAG,SAAS,CAQvE;AAED,oGAAoG;AACpG,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAKlE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,cAAc,EACvB,oBAAoB,GAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAQ,EACtD,OAAO,GAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAQ,GACxC,CAAC,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAW9H"}
package/lib/types/gate.js CHANGED
@@ -10,42 +10,15 @@
10
10
  * gives "停止" real teeth: revoked devices 403 on their next request,
11
11
  * including the mux/SSE stream (which then dies on reconnect).
12
12
  */
13
+ import { isLoopbackAddress, isLoopbackHostname } from "./loopback.js";
13
14
  /**
14
- * Whether a normalized URL hostname names the local loopback authority.
15
- * Semantics mirror the connection package's internal predicate (localhost,
16
- * IPv6 loopback, any IPv4 address in 127/8); it is reimplemented here because
17
- * the connection package no longer exports it — the fence now lives inside
18
- * the connection plugin, and external host plugins only need the
19
- * classification, not the whole trust decision.
15
+ * Loopback classification for the desktop client. The predicates now live in
16
+ * the shared synced copy (shared/host/loopback.ts, mirrored to ./loopback.ts
17
+ * by scripts/sync-shared.mjs): localhost, IPv6 loopback, and any IPv4 address
18
+ * in 127/8.
20
19
  * @param hostname - WHATWG URL hostname (IPv6 literals retain brackets).
21
20
  * @returns true for localhost, IPv6 loopback, or any IPv4 address in 127/8.
22
21
  */
23
- export function isLoopbackHostname(hostname) {
24
- if (hostname === 'localhost' || hostname === '[::1]')
25
- return true;
26
- const parts = hostname.split('.');
27
- return parts.length === 4
28
- && parts[0] === '127'
29
- && parts.every(part => /^\d{1,3}$/.test(part) && Number(part) <= 255);
30
- }
31
- /** Whether a socket remote address names the loopback range (127/8, ::1, IPv4-mapped). */
32
- export function isLoopbackAddress(address) {
33
- if (address === undefined)
34
- return false;
35
- const normalized = address.toLowerCase();
36
- if (normalized === '::1')
37
- return true;
38
- if (normalized.startsWith('::ffff:'))
39
- return isIPv4Loopback(normalized.slice('::ffff:'.length));
40
- return isIPv4Loopback(normalized);
41
- }
42
- /** IPv4 127/8 predicate (four decimal octets, first == 127). */
43
- function isIPv4Loopback(v4) {
44
- const parts = v4.split('.');
45
- return parts.length === 4
46
- && parts[0] === '127'
47
- && parts.every(part => /^\d{1,3}$/.test(part) && Number(part) <= 255);
48
- }
49
22
  /**
50
23
  * Read one cookie value from a Cookie header.
51
24
  * @param header - the raw Cookie header value (or undefined).
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Loopback trust fence shared by the host route families: socket address,
3
+ * Host header, and browser same-origin markers. Packages receive this file as
4
+ * a generated copy via scripts/sync-shared.mjs; edit the shared source and
5
+ * re-run the sync instead of editing a copy.
6
+ *
7
+ * Semantics: RFC 5735 IPv4 127/8, ::1, IPv4-mapped ::ffff:127/8 (matching the
8
+ * remote-web-ui gate), localhost hostnames, plus the browser same-origin
9
+ * markers (sec-fetch-site and Origin) for the request-level fence.
10
+ * @module dsh-web-ui-shared/host/loopback
11
+ */
12
+ import type { IncomingMessage } from 'node:http';
13
+ /** IPv4 127/8 predicate (four decimal octets, first == 127). */
14
+ export declare function isIPv4Loopback(v4: string): boolean;
15
+ /** Whether a socket remote address names the loopback range (127/8, ::1, IPv4-mapped). */
16
+ export declare function isLoopbackAddress(address: string | undefined): boolean;
17
+ /** Whether a normalized URL hostname names the loopback authority (localhost, [::1], 127/8). */
18
+ export declare function isLoopbackHostname(hostname: string): boolean;
19
+ /**
20
+ * Request-level trust fence: a loopback socket address AND a loopback Host
21
+ * header, plus browser same-origin markers. The socket address is
22
+ * authoritative; X-Forwarded-For is never trusted.
23
+ */
24
+ export declare function isLoopbackRequest(request: IncomingMessage): boolean;
25
+ //# sourceMappingURL=loopback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loopback.d.ts","sourceRoot":"","sources":["../../src/loopback.ts"],"names":[],"mappings":"AACA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAEhD,gEAAgE;AAChE,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAKlD;AAED,0FAA0F;AAC1F,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAMtE;AAED,gGAAgG;AAChG,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAG5D;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAmBnE"}
@@ -0,0 +1,68 @@
1
+ // Generated by scripts/sync-shared.mjs from shared/host/loopback.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
2
+ /**
3
+ * Loopback trust fence shared by the host route families: socket address,
4
+ * Host header, and browser same-origin markers. Packages receive this file as
5
+ * a generated copy via scripts/sync-shared.mjs; edit the shared source and
6
+ * re-run the sync instead of editing a copy.
7
+ *
8
+ * Semantics: RFC 5735 IPv4 127/8, ::1, IPv4-mapped ::ffff:127/8 (matching the
9
+ * remote-web-ui gate), localhost hostnames, plus the browser same-origin
10
+ * markers (sec-fetch-site and Origin) for the request-level fence.
11
+ * @module dsh-web-ui-shared/host/loopback
12
+ */
13
+ /** IPv4 127/8 predicate (four decimal octets, first == 127). */
14
+ export function isIPv4Loopback(v4) {
15
+ const parts = v4.split('.');
16
+ return parts.length === 4
17
+ && parts[0] === '127'
18
+ && parts.every(part => /^\d{1,3}$/.test(part) && Number(part) <= 255);
19
+ }
20
+ /** Whether a socket remote address names the loopback range (127/8, ::1, IPv4-mapped). */
21
+ export function isLoopbackAddress(address) {
22
+ if (address === undefined)
23
+ return false;
24
+ const normalized = address.toLowerCase();
25
+ if (normalized === '::1')
26
+ return true;
27
+ if (normalized.startsWith('::ffff:'))
28
+ return isIPv4Loopback(normalized.slice('::ffff:'.length));
29
+ return isIPv4Loopback(normalized);
30
+ }
31
+ /** Whether a normalized URL hostname names the loopback authority (localhost, [::1], 127/8). */
32
+ export function isLoopbackHostname(hostname) {
33
+ if (hostname === 'localhost' || hostname === '[::1]')
34
+ return true;
35
+ return isIPv4Loopback(hostname);
36
+ }
37
+ /**
38
+ * Request-level trust fence: a loopback socket address AND a loopback Host
39
+ * header, plus browser same-origin markers. The socket address is
40
+ * authoritative; X-Forwarded-For is never trusted.
41
+ */
42
+ export function isLoopbackRequest(request) {
43
+ if (!isLoopbackAddress(request.socket.remoteAddress))
44
+ return false;
45
+ const host = request.headers.host;
46
+ if (typeof host !== 'string')
47
+ return false;
48
+ let hostUrl;
49
+ try {
50
+ hostUrl = new URL('http://' + host);
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ if (!isLoopbackHostname(hostUrl.hostname))
56
+ return false;
57
+ if (request.headers['sec-fetch-site'] === 'cross-site')
58
+ return false;
59
+ const origin = request.headers.origin;
60
+ if (origin === undefined)
61
+ return true;
62
+ try {
63
+ return new URL(origin).host === hostUrl.host;
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-remote-web-ui",
3
3
  "description": "Mobile remote control for the dsh web GUI: scan-to-pair QR entry beside the settings button, one-time pairing tokens, live device status, and revocable mobile sessions",
4
- "version": "0.1.18",
4
+ "version": "0.1.19",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -104,6 +104,14 @@ export class RemoteSettingsCardController {
104
104
  inject(): RemoteSettingsCardFace {
105
105
  return { hooks: { remoteSettingsCard: this.store }, ...this.form.actions() }
106
106
  }
107
+
108
+ /**
109
+ * Release the card's scope subscription and bound stores; the slot
110
+ * disposer calls this on teardown.
111
+ */
112
+ dispose(): void {
113
+ this.form.dispose()
114
+ }
107
115
  }
108
116
 
109
117
  /** Props the renderer binds for the remote-control card. */
@@ -157,13 +157,19 @@ export function apply(ctx: ClientContext): void {
157
157
  // Plugin configuration card: one staged form over the `remote-web-ui`
158
158
  // settings namespace, contributed to the Web UI plugin group.
159
159
  const remoteSettings = new RemoteSettingsCardController(settingsScope)
160
- ctx.slots.inject('web-ui.plugin.item', () => ctx.slots.register({
161
- name: 'web-ui.plugin.item',
162
- id: 'remote-web-ui',
163
- order: 90,
164
- locale: NS,
165
- inject: () => remoteSettings.inject(),
166
- }, RemoteSettingsCard))
160
+ ctx.slots.inject('web-ui.plugin.item', () => {
161
+ const unregister = ctx.slots.register({
162
+ name: 'web-ui.plugin.item',
163
+ id: 'remote-web-ui',
164
+ order: 90,
165
+ locale: NS,
166
+ inject: () => remoteSettings.inject(),
167
+ }, RemoteSettingsCard)
168
+ return () => {
169
+ remoteSettings.dispose()
170
+ unregister()
171
+ }
172
+ })
167
173
 
168
174
  // Phone-side boot flow + heartbeats. Loopback pages (the desktop) never
169
175
  // heartbeat; the server ignores unpaired heartbeats anyway. Both run only
@@ -234,6 +234,9 @@ export class CardForm<T> {
234
234
  private readonly specs: Map<string, FieldSpec>
235
235
  private readonly staged = new Map<string, StagedEdit>()
236
236
  private readonly listeners = new Set<() => void>()
237
+ /** The scope subscription installed in the constructor; released by dispose(). */
238
+ private readonly disposeScope: () => void
239
+ private disposed = false
237
240
  private saving = false
238
241
  private failed = false
239
242
  private failedReason: string | undefined
@@ -244,7 +247,18 @@ export class CardForm<T> {
244
247
  specs: FieldSpec[],
245
248
  ) {
246
249
  this.specs = new Map(specs.map(spec => [spec.field, spec]))
247
- scope.subscribe(() => { this.publish() })
250
+ this.disposeScope = scope.subscribe(() => { this.publish() })
251
+ }
252
+
253
+ /**
254
+ * Release the scope subscription and every bound store listener. The card
255
+ * must call this on teardown; later calls are no-ops.
256
+ */
257
+ dispose(): void {
258
+ if (this.disposed) return
259
+ this.disposed = true
260
+ this.disposeScope()
261
+ this.listeners.clear()
248
262
  }
249
263
 
250
264
  /** Publish a projection of this form, rebuilt whenever the scope or a draft changes. */
package/src/gate.ts CHANGED
@@ -13,41 +13,16 @@
13
13
 
14
14
  import type { IncomingMessage } from 'node:http'
15
15
  import type { PairingService } from './pairing.ts'
16
+ import { isLoopbackAddress, isLoopbackHostname } from './loopback.ts'
16
17
 
17
18
  /**
18
- * Whether a normalized URL hostname names the local loopback authority.
19
- * Semantics mirror the connection package's internal predicate (localhost,
20
- * IPv6 loopback, any IPv4 address in 127/8); it is reimplemented here because
21
- * the connection package no longer exports it — the fence now lives inside
22
- * the connection plugin, and external host plugins only need the
23
- * classification, not the whole trust decision.
19
+ * Loopback classification for the desktop client. The predicates now live in
20
+ * the shared synced copy (shared/host/loopback.ts, mirrored to ./loopback.ts
21
+ * by scripts/sync-shared.mjs): localhost, IPv6 loopback, and any IPv4 address
22
+ * in 127/8.
24
23
  * @param hostname - WHATWG URL hostname (IPv6 literals retain brackets).
25
24
  * @returns true for localhost, IPv6 loopback, or any IPv4 address in 127/8.
26
25
  */
27
- export function isLoopbackHostname(hostname: string): boolean {
28
- if (hostname === 'localhost' || hostname === '[::1]') return true
29
- const parts = hostname.split('.')
30
- return parts.length === 4
31
- && parts[0] === '127'
32
- && parts.every(part => /^\d{1,3}$/.test(part) && Number(part) <= 255)
33
- }
34
-
35
- /** Whether a socket remote address names the loopback range (127/8, ::1, IPv4-mapped). */
36
- export function isLoopbackAddress(address: string | undefined): boolean {
37
- if (address === undefined) return false
38
- const normalized = address.toLowerCase()
39
- if (normalized === '::1') return true
40
- if (normalized.startsWith('::ffff:')) return isIPv4Loopback(normalized.slice('::ffff:'.length))
41
- return isIPv4Loopback(normalized)
42
- }
43
-
44
- /** IPv4 127/8 predicate (four decimal octets, first == 127). */
45
- function isIPv4Loopback(v4: string): boolean {
46
- const parts = v4.split('.')
47
- return parts.length === 4
48
- && parts[0] === '127'
49
- && parts.every(part => /^\d{1,3}$/.test(part) && Number(part) <= 255)
50
- }
51
26
 
52
27
  /**
53
28
  * Read one cookie value from a Cookie header.
@@ -0,0 +1,63 @@
1
+ // Generated by scripts/sync-shared.mjs from shared/host/loopback.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
2
+ /**
3
+ * Loopback trust fence shared by the host route families: socket address,
4
+ * Host header, and browser same-origin markers. Packages receive this file as
5
+ * a generated copy via scripts/sync-shared.mjs; edit the shared source and
6
+ * re-run the sync instead of editing a copy.
7
+ *
8
+ * Semantics: RFC 5735 IPv4 127/8, ::1, IPv4-mapped ::ffff:127/8 (matching the
9
+ * remote-web-ui gate), localhost hostnames, plus the browser same-origin
10
+ * markers (sec-fetch-site and Origin) for the request-level fence.
11
+ * @module dsh-web-ui-shared/host/loopback
12
+ */
13
+
14
+ import type { IncomingMessage } from 'node:http'
15
+
16
+ /** IPv4 127/8 predicate (four decimal octets, first == 127). */
17
+ export function isIPv4Loopback(v4: string): boolean {
18
+ const parts = v4.split('.')
19
+ return parts.length === 4
20
+ && parts[0] === '127'
21
+ && parts.every(part => /^\d{1,3}$/.test(part) && Number(part) <= 255)
22
+ }
23
+
24
+ /** Whether a socket remote address names the loopback range (127/8, ::1, IPv4-mapped). */
25
+ export function isLoopbackAddress(address: string | undefined): boolean {
26
+ if (address === undefined) return false
27
+ const normalized = address.toLowerCase()
28
+ if (normalized === '::1') return true
29
+ if (normalized.startsWith('::ffff:')) return isIPv4Loopback(normalized.slice('::ffff:'.length))
30
+ return isIPv4Loopback(normalized)
31
+ }
32
+
33
+ /** Whether a normalized URL hostname names the loopback authority (localhost, [::1], 127/8). */
34
+ export function isLoopbackHostname(hostname: string): boolean {
35
+ if (hostname === 'localhost' || hostname === '[::1]') return true
36
+ return isIPv4Loopback(hostname)
37
+ }
38
+
39
+ /**
40
+ * Request-level trust fence: a loopback socket address AND a loopback Host
41
+ * header, plus browser same-origin markers. The socket address is
42
+ * authoritative; X-Forwarded-For is never trusted.
43
+ */
44
+ export function isLoopbackRequest(request: IncomingMessage): boolean {
45
+ if (!isLoopbackAddress(request.socket.remoteAddress)) return false
46
+ const host = request.headers.host
47
+ if (typeof host !== 'string') return false
48
+ let hostUrl: URL
49
+ try {
50
+ hostUrl = new URL('http://' + host)
51
+ } catch {
52
+ return false
53
+ }
54
+ if (!isLoopbackHostname(hostUrl.hostname)) return false
55
+ if (request.headers['sec-fetch-site'] === 'cross-site') return false
56
+ const origin = request.headers.origin
57
+ if (origin === undefined) return true
58
+ try {
59
+ return new URL(origin).host === hostUrl.host
60
+ } catch {
61
+ return false
62
+ }
63
+ }