@linxin666/dsh-remote-web-ui 0.3.24 → 0.4.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.
Files changed (112) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +13 -10
  3. package/README.zh.md +13 -10
  4. package/icon.svg +3 -0
  5. package/lib/client.js +263 -129
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +565 -256
  8. package/lib/types/client/FenceNotice.d.ts.map +1 -1
  9. package/lib/types/client/FenceNotice.js +0 -3
  10. package/lib/types/client/PluginSettingsCard.d.ts +4 -0
  11. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  12. package/lib/types/client/PluginSettingsCard.js +3 -2
  13. package/lib/types/client/RemoteEntry.d.ts.map +1 -1
  14. package/lib/types/client/RemoteEntry.js +14 -9
  15. package/lib/types/client/RemotePanel.d.ts.map +1 -1
  16. package/lib/types/client/RemotePanel.js +13 -4
  17. package/lib/types/client/RemoteSettingsCard.d.ts +6 -6
  18. package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
  19. package/lib/types/client/RemoteSettingsCard.js +14 -8
  20. package/lib/types/client/UpdateEntry.js +2 -2
  21. package/lib/types/client/UpdatePanel.js +2 -2
  22. package/lib/types/client/deep-link.js +2 -2
  23. package/lib/types/client/index.d.ts +16 -8
  24. package/lib/types/client/index.d.ts.map +1 -1
  25. package/lib/types/client/index.js +47 -18
  26. package/lib/types/client/locales.d.ts +0 -2
  27. package/lib/types/client/locales.d.ts.map +1 -1
  28. package/lib/types/client/locales.js +6 -8
  29. package/lib/types/client/mobile-adapt.d.ts.map +1 -1
  30. package/lib/types/client/mobile-adapt.js +103 -22
  31. package/lib/types/client/pair-api.d.ts +32 -3
  32. package/lib/types/client/pair-api.d.ts.map +1 -1
  33. package/lib/types/client/pair-api.js +44 -6
  34. package/lib/types/client/plugin-card-seat.d.ts +23 -16
  35. package/lib/types/client/plugin-card-seat.d.ts.map +1 -1
  36. package/lib/types/client/plugin-card-seat.js +16 -14
  37. package/lib/types/client/remote-channel.d.ts.map +1 -1
  38. package/lib/types/client/remote-channel.js +18 -5
  39. package/lib/types/client/settings-form.d.ts +17 -17
  40. package/lib/types/client/settings-form.d.ts.map +1 -1
  41. package/lib/types/client/settings-form.js +30 -27
  42. package/lib/types/http.d.ts +18 -1
  43. package/lib/types/http.d.ts.map +1 -1
  44. package/lib/types/http.js +22 -1
  45. package/lib/types/index.d.ts +19 -11
  46. package/lib/types/index.d.ts.map +1 -1
  47. package/lib/types/index.js +91 -76
  48. package/lib/types/inner-auth.d.ts +3 -2
  49. package/lib/types/inner-auth.d.ts.map +1 -1
  50. package/lib/types/inner-auth.js +3 -2
  51. package/lib/types/pair-grant.d.ts +66 -0
  52. package/lib/types/pair-grant.d.ts.map +1 -0
  53. package/lib/types/pair-grant.js +95 -0
  54. package/lib/types/pairing.d.ts +44 -12
  55. package/lib/types/pairing.d.ts.map +1 -1
  56. package/lib/types/pairing.js +69 -20
  57. package/lib/types/pairing.persist.test.js +26 -0
  58. package/lib/types/posture.d.ts +11 -3
  59. package/lib/types/posture.d.ts.map +1 -1
  60. package/lib/types/posture.js +8 -3
  61. package/lib/types/remote-api.d.ts +10 -13
  62. package/lib/types/remote-api.d.ts.map +1 -1
  63. package/lib/types/remote-api.js +34 -34
  64. package/lib/types/remote-channel-boot.d.ts +6 -1
  65. package/lib/types/remote-channel-boot.d.ts.map +1 -1
  66. package/lib/types/remote-channel-boot.js +16 -8
  67. package/lib/types/remote-channel-rules.d.ts +14 -0
  68. package/lib/types/remote-channel-rules.d.ts.map +1 -1
  69. package/lib/types/remote-channel-rules.js +8 -0
  70. package/lib/types/remote-methods.d.ts +1 -1
  71. package/lib/types/remote-methods.d.ts.map +1 -1
  72. package/lib/types/remote-methods.js +1 -0
  73. package/lib/types/routes.d.ts +47 -5
  74. package/lib/types/routes.d.ts.map +1 -1
  75. package/lib/types/routes.js +160 -58
  76. package/lib/types/tunnel.d.ts +21 -6
  77. package/lib/types/tunnel.d.ts.map +1 -1
  78. package/lib/types/tunnel.js +58 -11
  79. package/lib/types/update.d.ts +4 -0
  80. package/lib/types/update.d.ts.map +1 -1
  81. package/lib/types/update.js +74 -26
  82. package/package.json +23 -20
  83. package/src/client/FenceNotice.tsx +0 -2
  84. package/src/client/PluginSettingsCard.tsx +7 -2
  85. package/src/client/RemoteEntry.tsx +14 -9
  86. package/src/client/RemotePanel.tsx +17 -7
  87. package/src/client/RemoteSettingsCard.tsx +16 -9
  88. package/src/client/UpdateEntry.tsx +2 -2
  89. package/src/client/UpdatePanel.tsx +3 -3
  90. package/src/client/deep-link.ts +2 -2
  91. package/src/client/index.ts +61 -27
  92. package/src/client/locales.ts +6 -8
  93. package/src/client/mobile-adapt.ts +97 -20
  94. package/src/client/pair-api.ts +47 -7
  95. package/src/client/plugin-card-seat.ts +24 -17
  96. package/src/client/remote-channel.ts +19 -4
  97. package/src/client/remote.module.css +38 -0
  98. package/src/client/settings-form.ts +34 -31
  99. package/src/http.ts +23 -1
  100. package/src/index.ts +102 -79
  101. package/src/inner-auth.ts +3 -2
  102. package/src/pair-grant.ts +131 -0
  103. package/src/pairing.persist.test.ts +36 -0
  104. package/src/pairing.ts +75 -24
  105. package/src/posture.ts +12 -4
  106. package/src/remote-api.ts +34 -41
  107. package/src/remote-channel-boot.ts +16 -8
  108. package/src/remote-channel-rules.ts +17 -0
  109. package/src/remote-methods.ts +1 -0
  110. package/src/routes.ts +165 -57
  111. package/src/tunnel.ts +56 -11
  112. package/src/update.ts +74 -24
@@ -26,6 +26,7 @@ import { isHttpUrl, tunnelPlanOf } from "./tunnel-plan.js";
26
26
  import { loadRelayIdentity, RelayRegistrar } from "./relay-registry.js";
27
27
  import { desiredBindHost, desiredBindPort, firewallActionNeeded, pendingRestartOf } from "./lan-bind-plan.js";
28
28
  import { createInnerAuth } from "./inner-auth.js";
29
+ import { withIdentityEncoding } from "./http.js";
29
30
  import { TunnelManager } from "./tunnel.js";
30
31
  import { PublicBaseKeeper } from "./public-base.js";
31
32
  import { checkUpdates, fetchGitHubReleaseNotes, fetchLatestVersion, RELEASE_NOTES_CACHE_TTL_MS, resolveAnchorManifest, resolveUpdateTarget, runUpdateVerified, } from "./update.js";
@@ -38,9 +39,12 @@ export const name = 'remote-web-ui';
38
39
  /** Services required before the pairing surfaces can mount. */
39
40
  export const inject = ['webServer', 'typertGateway', 'connection'];
40
41
  /**
41
- * Settings namespace of the remote-control capability — the section the web
42
- * settings surface edits. Spelled here rather than imported: the browser
43
- * half spells the same value and must not depend on a Host package.
42
+ * Settings namespace of the remote-control capability. Under the 0.1.7
43
+ * settings model the namespace IS the Host profile entry id, so this is also
44
+ * the id the browser half asks `ctx.configForms` for (the family binder
45
+ * resolves the same value to the owning entry). Spelled here rather than
46
+ * imported: the browser half spells the same value and must not depend on a
47
+ * Host package.
44
48
  */
45
49
  export const REMOTE_WEB_UI_SETTINGS_NAMESPACE = 'remote-web-ui';
46
50
  export const Config = z.object({
@@ -62,11 +66,16 @@ export const Config = z.object({
62
66
  });
63
67
  /** Presence sweep cadence (a stale device flips to disconnected within two sweeps). */
64
68
  const SWEEP_INTERVAL_MS = 10_000;
69
+ /**
70
+ * How long one update-status probe answer is reused. The probe fans out one
71
+ * registry GET per family package plus a GitHub call, and the sidebar update
72
+ * entry asks for the status on every GUI page load.
73
+ */
74
+ const UPDATE_STATUS_TTL_MS = 60_000;
65
75
  /**
66
76
  * The single mapping from resolved plugin config to the pairing service
67
- * config. Both the constructed service and every live settings sync reuse
68
- * it, so no field can be silently dropped when the web settings surface
69
- * pushes a new value into the running service.
77
+ * config. Both the constructed service and every later apply of this row
78
+ * reuse it, so no field can be silently dropped.
70
79
  */
71
80
  export function pairingConfigOf(resolved) {
72
81
  return {
@@ -125,30 +134,12 @@ function applyImpl(ctx, config) {
125
134
  profile: config?.profile ?? process.env.DSH_PROFILE ?? DEFAULTS.profile,
126
135
  enabled: config?.enabled ?? DEFAULTS.enabled,
127
136
  };
128
- // The live source the pairing service and the gate read: the settings
129
- // section once the web settings surface is served, the composition entry
130
- // otherwise (installSection swaps it when the namespace registers).
131
- let current = () => config ?? {};
132
- const resolve = () => {
133
- const value = current();
134
- return {
135
- tokenTtlMs: value.tokenTtlMs ?? DEFAULTS.tokenTtlMs,
136
- offlineAfterMs: value.offlineAfterMs ?? DEFAULTS.offlineAfterMs,
137
- maxDevices: value.maxDevices ?? DEFAULTS.maxDevices,
138
- idleExpireMs: value.idleExpireMs ?? DEFAULTS.idleExpireMs,
139
- cookieName: value.cookieName ?? DEFAULTS.cookieName,
140
- requirePairingForLan: value.requirePairingForLan ?? DEFAULTS.requirePairingForLan,
141
- publicBaseUrl: value.publicBaseUrl ?? envPublicBase,
142
- trustedHosts: value.trustedHosts,
143
- devicesFile: value.devicesFile ?? DEFAULTS.devicesFile,
144
- autoTunnel: value.autoTunnel ?? DEFAULTS.autoTunnel,
145
- tunnelToken: value.tunnelToken,
146
- relay: value.relay ?? DEFAULTS.relay,
147
- lanBind: value.lanBind,
148
- profile: value.profile ?? process.env.DSH_PROFILE ?? DEFAULTS.profile,
149
- enabled: value.enabled ?? DEFAULTS.enabled,
150
- };
151
- };
137
+ // The effective configuration this activation runs on: under the 0.1.7
138
+ // settings model the plugin's own Config IS its settings document, so the
139
+ // Host serves the settings page from this schema and reloads the row after a
140
+ // save — a live edit re-enters apply() with the new value instead of being
141
+ // pushed into a running instance.
142
+ const resolve = () => resolved;
152
143
  const service = new PairingService(pairingConfigOf(resolved));
153
144
  // ── auto tunnel ─────────────────────────────────────────────────────────
154
145
  // The minted public URL becomes the QR base (and the pairing fence's
@@ -271,13 +262,14 @@ function applyImpl(ctx, config) {
271
262
  : [];
272
263
  service.setLanBases(lanBases);
273
264
  }
274
- // Push a committed settings section into the service and gate. The service
265
+ // Push the effective configuration into the service and gate. The service
275
266
  // config object is read per operation (token mint, touch, sweep), and the
276
- // gate re-reads its fence flag per request, so a live edit takes effect
277
- // without a restart. When `enabled` turns off, the pairing routes and
278
- // sweep timer are dropped and all device/token state is revoked, but the
279
- // gate listener stays mounted so a LAN-exposed /api stays behind pairing
280
- // (now vetoing every non-loopback request) instead of opening the fence.
267
+ // gate re-reads its fence flag per request, so a saved edit takes effect
268
+ // without a restart (the Host reloads this row, and apply re-runs). When
269
+ // `enabled` is off, the pairing routes and sweep timer are dropped and all
270
+ // device/token state is revoked, but the gate listener stays mounted so a
271
+ // LAN-exposed /api stays behind pairing (now vetoing every non-loopback
272
+ // request) instead of opening the fence.
281
273
  let disposeRoutes;
282
274
  let disposeSweep;
283
275
  // ── remote update ────────────────────────────────────────────────────────
@@ -308,16 +300,34 @@ function applyImpl(ctx, config) {
308
300
  releaseNotesCache.set(version, { at: Date.now(), notes });
309
301
  return notes;
310
302
  };
311
- const updateRoutes = makeUpdateRoutes({
312
- // Control endpoints are host-surface only: a LAN/phone origin must never
313
- // trigger a real install on this machine.
314
- fence: request => isTrustedApiRequest(request, []),
315
- check: () => checkUpdates({
303
+ // The status probe fans out one registry GET per family package (plus a
304
+ // GitHub release-notes call), and the sidebar update entry asks for it on
305
+ // every GUI page load — so the answer is memoized briefly and concurrent
306
+ // callers share one probe. A completed update invalidates it.
307
+ let updateStatusCache;
308
+ let updateStatusInFlight;
309
+ const checkStatusCached = () => {
310
+ const cached = updateStatusCache;
311
+ if (cached !== undefined && Date.now() - cached.at < UPDATE_STATUS_TTL_MS)
312
+ return Promise.resolve(cached.value);
313
+ if (updateStatusInFlight !== undefined)
314
+ return updateStatusInFlight;
315
+ updateStatusInFlight = checkUpdates({
316
316
  anchorManifestPath: resolveAnchorPath(),
317
317
  resolve: hostResolve,
318
318
  fetchLatest: name => fetchLatestVersion(name, fetch),
319
319
  fetchReleaseNotes: fetchReleaseNotesCached,
320
- }),
320
+ }).then((value) => {
321
+ updateStatusCache = { at: Date.now(), value };
322
+ return value;
323
+ }).finally(() => { updateStatusInFlight = undefined; });
324
+ return updateStatusInFlight;
325
+ };
326
+ const updateRoutes = makeUpdateRoutes({
327
+ // Control endpoints are host-surface only: a LAN/phone origin must never
328
+ // trigger a real install on this machine.
329
+ fence: request => isTrustedApiRequest(request, []),
330
+ check: () => checkStatusCached(),
321
331
  run: async () => {
322
332
  const target = resolveUpdateTarget({ anchorManifestPath: resolveAnchorPath() });
323
333
  if ('error' in target) {
@@ -334,7 +344,7 @@ function applyImpl(ctx, config) {
334
344
  // 11 minimumReleaseAge gate can silently keep the installed versions
335
345
  // (same-day releases), which a plain exit-0 check would report as
336
346
  // success — the user then restarts and nothing changed.
337
- return runUpdateVerified({
347
+ const result = await runUpdateVerified({
338
348
  run: { profileDir: target.profileDir, packages: target.packages },
339
349
  check: {
340
350
  anchorManifestPath: resolveAnchorPath(),
@@ -343,6 +353,9 @@ function applyImpl(ctx, config) {
343
353
  fetchReleaseNotes: fetchReleaseNotesCached,
344
354
  },
345
355
  });
356
+ // The install moved (or failed) the versions the cached status reported.
357
+ updateStatusCache = undefined;
358
+ return result;
346
359
  },
347
360
  });
348
361
  // LAN-bind facts for the settings card, re-read per request so a hot
@@ -416,11 +429,17 @@ function applyImpl(ctx, config) {
416
429
  return appShellCache.html;
417
430
  const cookie = await innerAuth.ready();
418
431
  try {
419
- const response = await fetch(`http://127.0.0.1:${String(ctx.webServer.port)}/`, {
432
+ const response = await fetch(`http://127.0.0.1:${String(ctx.webServer.port)}/`, withIdentityEncoding({
420
433
  headers: cookie !== undefined ? { cookie } : undefined,
421
- });
422
- if (!response.ok)
434
+ }));
435
+ if (!response.ok) {
436
+ // A stale credential (secret rotation, 30-day TTL) must not wedge every
437
+ // later landing: drop it so the next attempt re-redeems, exactly as the
438
+ // proxy path does on an upstream 401.
439
+ if (response.status === 401 || response.status === 403)
440
+ innerAuth.invalidate();
423
441
  return undefined;
442
+ }
424
443
  const html = await response.text();
425
444
  appShellCache = { at: Date.now(), html };
426
445
  return html;
@@ -458,15 +477,15 @@ function applyImpl(ctx, config) {
458
477
  return list;
459
478
  },
460
479
  }),
461
- // The remote desktop channel: policy-gated `/remote` prefix that
462
- // re-issues fenced paths to loopback (see remote-api.ts). The live
463
- // requirePairingForLan is re-read per request, same as the gate listener
464
- // and routes above, so a stale client rewrite on an open-LAN deployment
465
- // proxies instead of 403ing.
480
+ // The remote desktop channel: a paired-credential-gated `/remote` prefix
481
+ // that re-issues fenced paths to loopback (see remote-api.ts). The gate is
482
+ // unconditional — requirePairingForLan governs the plain /api surface and
483
+ // the desktop's client-side rewrite only; it is never an authorization
484
+ // input here, because this channel attaches the process's own browser
485
+ // credential to everything it forwards (issue #1665).
466
486
  ...makeRemoteApiRoutes({
467
487
  service,
468
488
  port: ctx.webServer.port,
469
- requirePairingForLan: () => resolve().requirePairingForLan,
470
489
  auth: innerAuth,
471
490
  }),
472
491
  ...updateRoutes,
@@ -474,7 +493,6 @@ function applyImpl(ctx, config) {
474
493
  const upgrades = makeRemoteApiUpgradeRoutes({
475
494
  service,
476
495
  port: ctx.webServer.port,
477
- requirePairingForLan: () => resolve().requirePairingForLan,
478
496
  auth: innerAuth,
479
497
  });
480
498
  const gate = makeGateListener(service, () => resolve().requirePairingForLan, () => resolve().enabled);
@@ -559,6 +577,9 @@ function applyImpl(ctx, config) {
559
577
  if (ctx.webServer.host === '0.0.0.0') {
560
578
  console.warn('remote-web-ui: LAN-exposed bind — pairing gates the /remote channel; direct /api stays under the harness fence + browser auth (stop() does not revoke an already-redeemed browser credential)');
561
579
  }
580
+ // Apply the effective configuration to the running surfaces once per
581
+ // activation: the service tunables, the LAN-bind block/firewall, the tunnel
582
+ // plan, the pairing routes, the presence sweep, and the posture probe.
562
583
  const sync = () => {
563
584
  const value = resolve();
564
585
  service.config = pairingConfigOf(value);
@@ -655,6 +676,10 @@ function applyImpl(ctx, config) {
655
676
  : { kind: 'quick', targetUrl: plan.targetUrl, originHostHeader });
656
677
  }
657
678
  else if (plan.mode === 'named') {
679
+ // The hostname is fixed and known before the process runs, so publish it
680
+ // now: a named tunnel that fails to start must not leave the previous
681
+ // mode's ephemeral host as the QR and fence base.
682
+ publicBase.markRunning(plan.publicUrl);
658
683
  tunnel.start({ kind: 'named', token: plan.token, publicUrl: plan.publicUrl });
659
684
  }
660
685
  else {
@@ -676,8 +701,17 @@ function applyImpl(ctx, config) {
676
701
  }
677
702
  }
678
703
  const enabled = value.enabled;
679
- if (!enabled)
704
+ if (!enabled) {
680
705
  service.stop();
706
+ // The tunnel branch above follows the plan, not the master switch, so an
707
+ // explicit "off" must also drop the public ingress it started: otherwise
708
+ // the cloudflared child keeps a live public URL (and the relay keeps its
709
+ // stable row) while the panel says remote control is stopped. The relay
710
+ // row is deliberately kept so re-enabling reuses the same origin.
711
+ tunnel.stop();
712
+ disposeRelayRegistrar();
713
+ publicBase.reset();
714
+ }
681
715
  if (disposeRoutes === undefined && enabled) {
682
716
  disposeRoutes = ctx.effect(() => {
683
717
  const disposers = [
@@ -726,27 +760,8 @@ function applyImpl(ctx, config) {
726
760
  return;
727
761
  table.push({ kind: 'script', placement: 'head', text: REMOTE_CHANNEL_BOOT_SCRIPT });
728
762
  }), 'remote-web-ui: remote channel boot patch');
729
- ctx.inject(['settings'], (settingsCtx) => {
730
- try {
731
- if (typeof settingsCtx.settings?.installSection === 'function') {
732
- settingsCtx.settings.installSection(ctx, REMOTE_WEB_UI_SETTINGS_NAMESPACE, Config, config ?? {}, {
733
- setSource: (source) => {
734
- current = source;
735
- sync();
736
- },
737
- onChange: sync,
738
- });
739
- }
740
- else if (typeof settingsCtx.settings?.register === 'function') {
741
- const scope = settingsCtx.settings.register(REMOTE_WEB_UI_SETTINGS_NAMESPACE, Config, { base: config ?? {} });
742
- current = () => scope?.get?.() ?? (config ?? {});
743
- scope?.watch?.(() => { sync(); });
744
- sync();
745
- }
746
- }
747
- catch {
748
- // Defensive fallback against settings registration differences
749
- }
750
- });
763
+ // No settings registration: the Host derives this plugin's settings page
764
+ // from the exported `Config` schema and reloads the row after a save, so
765
+ // every write re-enters apply() with the new config (see `resolve`).
751
766
  sync();
752
767
  }
@@ -7,8 +7,9 @@
7
7
  * therefore redeems its own launch token (the same one-time exchange every
8
8
  * paired device's first navigation performs) and attaches the resulting
9
9
  * cookie to inner requests. The credential is only ever exercised behind the
10
- * pairing gate: while requirePairingForLan is on, every proxied call already
11
- * carried a live paired-device cookie before this module is consulted.
10
+ * pairing gate: the `/remote` channel always requires a live paired-device
11
+ * credential before this module is consulted, regardless of the
12
+ * requirePairingForLan LAN policy (issue #1665).
12
13
  */
13
14
  /** Browser-auth credential the proxy attaches to loopback-bound requests. */
14
15
  export interface InnerAuth {
@@ -1 +1 @@
1
- {"version":3,"file":"inner-auth.d.ts","sourceRoot":"","sources":["../../src/inner-auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,6EAA6E;AAC7E,MAAM,WAAW,SAAS;IACxB;;;;OAIG;IACH,KAAK,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;IACpC,+DAA+D;IAC/D,UAAU,IAAI,IAAI,CAAA;CACnB;AAKD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,MAAM,MAAM,GAAG,SAAS,EACnC,SAAS,GAAE,OAAO,KAAa,GAC9B,SAAS,CA8CX"}
1
+ {"version":3,"file":"inner-auth.d.ts","sourceRoot":"","sources":["../../src/inner-auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,6EAA6E;AAC7E,MAAM,WAAW,SAAS;IACxB;;;;OAIG;IACH,KAAK,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;IACpC,+DAA+D;IAC/D,UAAU,IAAI,IAAI,CAAA;CACnB;AAKD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,MAAM,MAAM,GAAG,SAAS,EACnC,SAAS,GAAE,OAAO,KAAa,GAC9B,SAAS,CA8CX"}
@@ -7,8 +7,9 @@
7
7
  * therefore redeems its own launch token (the same one-time exchange every
8
8
  * paired device's first navigation performs) and attaches the resulting
9
9
  * cookie to inner requests. The credential is only ever exercised behind the
10
- * pairing gate: while requirePairingForLan is on, every proxied call already
11
- * carried a live paired-device cookie before this module is consulted.
10
+ * pairing gate: the `/remote` channel always requires a live paired-device
11
+ * credential before this module is consulted, regardless of the
12
+ * requirePairingForLan LAN policy (issue #1665).
12
13
  */
13
14
  /** The harness browser-auth cookie name prefix (see dsh-client-connection). */
14
15
  const BROWSER_AUTH_COOKIE_PREFIX = 'dsh-auth-';
@@ -0,0 +1,66 @@
1
+ /**
2
+ * One-time pairing grants for the /pair-app landing.
3
+ *
4
+ * The QR entry (`/pair-accept?pair=<token>`) used to hand the freshly minted
5
+ * device id to the app landing in its own URL (`/pair-app?device=<id>`). A
6
+ * device id is a live session credential: the channel gate authorizes every
7
+ * gated request by it and the cookie it mirrors lives a year, so a URL carrying
8
+ * it leaks that credential into browser history, the address bar, and every log
9
+ * on the path. The landing is reached with a one-time grant instead — 256 bits
10
+ * of entropy, a short TTL, and single-consume semantics — and the device id
11
+ * reaches the client only inside the server-rendered shell (the capture script
12
+ * the landing patches in).
13
+ *
14
+ * Consume semantics follow the reference implementation's one-time host
15
+ * capability (zcode `packages/server/src/hostCapability.ts`): the record is
16
+ * deleted on EVERY consume attempt — success, expiry, or replay — so a captured
17
+ * grant is worthless after its first use and a replayed one can never resolve,
18
+ * and only a first use inside the TTL yields a device.
19
+ *
20
+ * @module @linxin666/dsh-remote-web-ui/pair-grant
21
+ */
22
+ /**
23
+ * Default grant lifetime. The grant only has to survive one redirect chain
24
+ * from the accept page to the app landing, tunnel round trips included, so it
25
+ * is far shorter than the device session it stands in for.
26
+ */
27
+ export declare const DEFAULT_PAIR_GRANT_TTL_MS = 60000;
28
+ /**
29
+ * Cap on live grants. The accept page is rate-limited per source, but the
30
+ * table is still caller-driven, so it stays bounded (oldest evicted first)
31
+ * exactly like the device table.
32
+ */
33
+ export declare const MAX_LIVE_GRANTS = 256;
34
+ /** Clock/entropy injection for tests. */
35
+ export interface PairGrantStoreOptions {
36
+ /** Grant lifetime in ms (default {@link DEFAULT_PAIR_GRANT_TTL_MS}). */
37
+ ttlMs?: number;
38
+ /** Clock seam (default Date.now). */
39
+ now?: () => number;
40
+ /** Entropy seam (default 32 random bytes, base64url). */
41
+ createGrant?: () => string;
42
+ }
43
+ /** One issued grant and its absolute expiry (both non-secret to the issuer). */
44
+ export interface PairGrantRecord {
45
+ /** The one-time secret carried in the /pair-app URL. */
46
+ grant: string;
47
+ /** Absolute expiry (ms epoch); a consume past this resolves to nothing. */
48
+ expiresAt: number;
49
+ }
50
+ /** The grant table surface the routes consume. */
51
+ export interface PairGrantStore {
52
+ /** Mint one grant bound to a device session. */
53
+ issue(deviceId: string): PairGrantRecord;
54
+ /** Redeem a grant once, returning its device id (undefined when unusable). */
55
+ consume(grant: string | undefined): string | undefined;
56
+ /** Liveness check that does not spend the grant (the fence fallback). */
57
+ peek(grant: string | undefined): string | undefined;
58
+ /** Live grant count (tests/diagnostics). */
59
+ readonly size: number;
60
+ }
61
+ /**
62
+ * Build the grant table. Pure TypeScript with injected clock/randomness so the
63
+ * whole one-time semantics are unit-testable without a server.
64
+ */
65
+ export declare function createPairGrantStore(options?: PairGrantStoreOptions): PairGrantStore;
66
+ //# sourceMappingURL=pair-grant.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pair-grant.d.ts","sourceRoot":"","sources":["../../src/pair-grant.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,QAAS,CAAA;AAE/C;;;;GAIG;AACH,eAAO,MAAM,eAAe,MAAM,CAAA;AAElC,yCAAyC;AACzC,MAAM,WAAW,qBAAqB;IACpC,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,qCAAqC;IACrC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,MAAM,CAAA;CAC3B;AAED,gFAAgF;AAChF,MAAM,WAAW,eAAe;IAC9B,wDAAwD;IACxD,KAAK,EAAE,MAAM,CAAA;IACb,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,kDAAkD;AAClD,MAAM,WAAW,cAAc;IAC7B,gDAAgD;IAChD,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,CAAA;IACxC,8EAA8E;IAC9E,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAAA;IACtD,yEAAyE;IACzE,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAAA;IACnD,4CAA4C;IAC5C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAQD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,qBAA0B,GAAG,cAAc,CAoDxF"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * One-time pairing grants for the /pair-app landing.
3
+ *
4
+ * The QR entry (`/pair-accept?pair=<token>`) used to hand the freshly minted
5
+ * device id to the app landing in its own URL (`/pair-app?device=<id>`). A
6
+ * device id is a live session credential: the channel gate authorizes every
7
+ * gated request by it and the cookie it mirrors lives a year, so a URL carrying
8
+ * it leaks that credential into browser history, the address bar, and every log
9
+ * on the path. The landing is reached with a one-time grant instead — 256 bits
10
+ * of entropy, a short TTL, and single-consume semantics — and the device id
11
+ * reaches the client only inside the server-rendered shell (the capture script
12
+ * the landing patches in).
13
+ *
14
+ * Consume semantics follow the reference implementation's one-time host
15
+ * capability (zcode `packages/server/src/hostCapability.ts`): the record is
16
+ * deleted on EVERY consume attempt — success, expiry, or replay — so a captured
17
+ * grant is worthless after its first use and a replayed one can never resolve,
18
+ * and only a first use inside the TTL yields a device.
19
+ *
20
+ * @module @linxin666/dsh-remote-web-ui/pair-grant
21
+ */
22
+ import { randomBytes } from 'node:crypto';
23
+ /**
24
+ * Default grant lifetime. The grant only has to survive one redirect chain
25
+ * from the accept page to the app landing, tunnel round trips included, so it
26
+ * is far shorter than the device session it stands in for.
27
+ */
28
+ export const DEFAULT_PAIR_GRANT_TTL_MS = 60_000;
29
+ /**
30
+ * Cap on live grants. The accept page is rate-limited per source, but the
31
+ * table is still caller-driven, so it stays bounded (oldest evicted first)
32
+ * exactly like the device table.
33
+ */
34
+ export const MAX_LIVE_GRANTS = 256;
35
+ /**
36
+ * Build the grant table. Pure TypeScript with injected clock/randomness so the
37
+ * whole one-time semantics are unit-testable without a server.
38
+ */
39
+ export function createPairGrantStore(options = {}) {
40
+ const ttlMs = options.ttlMs ?? DEFAULT_PAIR_GRANT_TTL_MS;
41
+ const now = options.now ?? (() => Date.now());
42
+ const createGrant = options.createGrant ?? (() => randomBytes(32).toString('base64url'));
43
+ const entries = new Map();
44
+ const purgeExpired = (at) => {
45
+ for (const [grant, entry] of entries) {
46
+ if (entry.expiresAt <= at)
47
+ entries.delete(grant);
48
+ }
49
+ };
50
+ return {
51
+ issue(deviceId) {
52
+ const issuedAt = now();
53
+ purgeExpired(issuedAt);
54
+ while (entries.size >= MAX_LIVE_GRANTS) {
55
+ const oldest = entries.keys().next().value;
56
+ if (oldest === undefined)
57
+ break;
58
+ entries.delete(oldest);
59
+ }
60
+ const grant = createGrant();
61
+ const expiresAt = issuedAt + ttlMs;
62
+ entries.set(grant, { deviceId, expiresAt });
63
+ return { grant, expiresAt };
64
+ },
65
+ consume(grant) {
66
+ if (grant === undefined || grant === '')
67
+ return undefined;
68
+ const consumedAt = now();
69
+ const entry = entries.get(grant);
70
+ // Delete before validating: a replayed grant must never resolve, and an
71
+ // expired one must not linger in the table.
72
+ entries.delete(grant);
73
+ purgeExpired(consumedAt);
74
+ if (entry === undefined || entry.expiresAt <= consumedAt)
75
+ return undefined;
76
+ return entry.deviceId;
77
+ },
78
+ peek(grant) {
79
+ if (grant === undefined || grant === '')
80
+ return undefined;
81
+ const at = now();
82
+ const entry = entries.get(grant);
83
+ if (entry === undefined)
84
+ return undefined;
85
+ if (entry.expiresAt <= at) {
86
+ entries.delete(grant);
87
+ return undefined;
88
+ }
89
+ return entry.deviceId;
90
+ },
91
+ get size() {
92
+ return entries.size;
93
+ },
94
+ };
95
+ }
@@ -7,9 +7,9 @@
7
7
  * Security invariants:
8
8
  * - One active token at a time; `issue()` replaces it, so a refreshed QR
9
9
  * immediately invalidates the previous link.
10
- * - A token stays re-usable until it expires or is replaced: the first
11
- * successful `accept()` marks it consumed, but the same link may pair
12
- * again within the window (each re-accept mints a fresh device session).
10
+ * - A token is a bearer credential for its whole window: it is refused only
11
+ * when unknown, expired, or after `stop()`, and the same link may pair
12
+ * repeatedly within that window (each accept mints a fresh device session).
13
13
  * Mobile flows routinely split across cookie contexts (camera preview to
14
14
  * in-app browser to the system browser), and the later context must be
15
15
  * able to complete its own pairing from the same link.
@@ -39,8 +39,6 @@ export interface TokenRecord {
39
39
  issuedAt: number;
40
40
  /** Absolute expiry (ms epoch); accept() past this is refused. */
41
41
  expiresAt: number;
42
- /** Consumed by the first successful accept(). */
43
- consumed: boolean;
44
42
  /** Opaque, non-secret identifier surfaced in snapshots (never the pairing secret). */
45
43
  id: string;
46
44
  /** Workspace the QR link should land the phone in (optional). */
@@ -150,13 +148,17 @@ export interface PairingConfig {
150
148
  */
151
149
  devicesFile?: string;
152
150
  }
153
- /** Result of one accept() attempt. */
151
+ /**
152
+ * Result of one accept() attempt. A token is a bearer credential for its whole
153
+ * window (see the module doc), so the only refusal is `invalid`: unknown,
154
+ * expired, or after stop(). There is no "already used" outcome.
155
+ */
154
156
  export type AcceptResult = {
155
157
  ok: true;
156
158
  deviceId: string;
157
159
  } | {
158
160
  ok: false;
159
- code: 'invalid' | 'used';
161
+ code: 'invalid';
160
162
  };
161
163
  /** Thrown by issue() for an address outside the sampled LAN literals. */
162
164
  export declare class UnknownLanAddressError extends Error {
@@ -170,6 +172,11 @@ export interface PairingClock {
170
172
  now(): number;
171
173
  randomToken(): string;
172
174
  }
175
+ /** File-operation seam for the revocation-durability step (tests). */
176
+ export interface PairingFsSeams {
177
+ /** Remove the stale device store (defaults to fs.rmSync with force). */
178
+ removeFile?(path: string): void;
179
+ }
173
180
  /** Real clock/entropy: 32 random hex chars per token. */
174
181
  export declare const defaultClock: PairingClock;
175
182
  /**
@@ -184,6 +191,7 @@ export declare const defaultClock: PairingClock;
184
191
  export declare class PairingService {
185
192
  config: PairingConfig;
186
193
  private readonly clock;
194
+ private readonly fs;
187
195
  private readonly tokens;
188
196
  private readonly devices;
189
197
  private readonly listeners;
@@ -206,7 +214,7 @@ export declare class PairingService {
206
214
  * the current one.
207
215
  * @param clock - clock/entropy source (injectable for tests).
208
216
  */
209
- constructor(config: PairingConfig, clock?: PairingClock);
217
+ constructor(config: PairingConfig, clock?: PairingClock, fs?: PairingFsSeams);
210
218
  /**
211
219
  * Restore device sessions persisted by a previous process run. A corrupt
212
220
  * or missing file is tolerated (an empty device table, never a throw) —
@@ -228,6 +236,16 @@ export declare class PairingService {
228
236
  * atomic rename; a crash mid-write can never leave a half-written store.
229
237
  */
230
238
  private persist;
239
+ /**
240
+ * Persist a table change that REVOKES access (stop, per-device revoke, idle
241
+ * eviction). A failed write would leave the revoked session on disk, and
242
+ * {@link loadPersisted} restores that file verbatim on the next start — the
243
+ * device's still-valid cookie would authorize again after an explicit
244
+ * revocation. When the write cannot be made durable, the stale store is
245
+ * removed instead (a missing file already loads as an empty table), so the
246
+ * failure costs a re-pair rather than silently undoing the revocation.
247
+ */
248
+ private persistRevocation;
231
249
  /** The default LAN base URL (the first interface; undefined when not LAN-reachable). */
232
250
  get lanBaseUrl(): string | undefined;
233
251
  /** The LAN base URL for one specific literal (undefined when not constructible). */
@@ -241,7 +259,12 @@ export declare class PairingService {
241
259
  }[]): void;
242
260
  /** The configured public (tunneled) base URL, when present. */
243
261
  get publicBaseUrl(): string | undefined;
244
- /** Set or clear the public base URL (a tunnel in front of this server). */
262
+ /**
263
+ * Set or clear the public base URL (a tunnel in front of this server). The
264
+ * value is canonicalized to its origin: a trailing slash (what a browser
265
+ * address-bar copy produces) or a path would mint a dead `//pair-accept`
266
+ * link, which WHATWG resolves as an authority rather than a path.
267
+ */
245
268
  setPublicBaseUrl(url: string | undefined): void;
246
269
  /** Set or clear the auto-tunnel status frame (undefined when the feature is off). */
247
270
  setTunnelStatus(status: TunnelStatus | undefined): void;
@@ -266,9 +289,11 @@ export declare class PairingService {
266
289
  expiresAt: number;
267
290
  };
268
291
  /**
269
- * Consume a token and bind a device session. One-time: the second
270
- * successful call for the same token is impossible because the first
271
- * consumes it.
292
+ * Redeem a token and bind a device session. The token is a bearer
293
+ * credential for its whole window, not a single-use nonce: it is refused
294
+ * only when unknown, past its expiry, or after stop(), and every successful
295
+ * call — including a repeat within the same window — mints a fresh device
296
+ * session (see the module doc for the mobile cookie-context rationale).
272
297
  * @param token - the token secret from the QR link.
273
298
  * @param userAgent - optional User-Agent header captured at accept.
274
299
  * @returns the new device id, or a refusal code.
@@ -327,6 +352,13 @@ export declare class PairingService {
327
352
  private isOnlineAt;
328
353
  private notify;
329
354
  }
355
+ /**
356
+ * Canonical origin of a public base URL. An unparsable value is returned
357
+ * verbatim so the caller's malformed-base warning stays the single owner of
358
+ * that case.
359
+ * @param url - the configured public base.
360
+ */
361
+ export declare function canonicalBaseUrl(url: string): string;
330
362
  /** Strip control characters and cap the User-Agent stored with a session. */
331
363
  export declare function sanitizeUserAgent(raw: string | undefined): string | undefined;
332
364
  //# sourceMappingURL=pairing.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pairing.d.ts","sourceRoot":"","sources":["../../src/pairing.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pairing.d.ts","sourceRoot":"","sources":["../../src/pairing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAEnD,uDAAuD;AACvD,MAAM,MAAM,YAAY;AACtB,mEAAmE;AACjE,cAAc;AAChB,oEAAoE;GAClE,SAAS;AACX,4DAA4D;GAC1D,SAAS;AACX,sDAAsD;GACpD,WAAW;AACb,6EAA6E;GAC3E,cAAc,CAAA;AAElB,sDAAsD;AACtD,MAAM,WAAW,WAAW;IAC1B,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAA;IAChB,iEAAiE;IACjE,SAAS,EAAE,MAAM,CAAA;IACjB,sFAAsF;IACtF,EAAE,EAAE,MAAM,CAAA;IACV,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,2EAA2E;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,QAA2B,CAAA;AAK9D,8EAA8E;AAC9E,MAAM,WAAW,aAAa;IAC5B,+BAA+B;IAC/B,SAAS,EAAE,MAAM,CAAA;IACjB,gEAAgE;IAChE,UAAU,EAAE,MAAM,CAAA;IAClB,gFAAgF;IAChF,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,2EAA2E;AAC3E,MAAM,WAAW,cAAc;IAC7B,wDAAwD;IACxD,EAAE,EAAE,MAAM,CAAA;IACV,+BAA+B;IAC/B,SAAS,EAAE,MAAM,CAAA;IACjB,kDAAkD;IAClD,UAAU,EAAE,MAAM,CAAA;IAClB,uDAAuD;IACvD,MAAM,EAAE,OAAO,CAAA;IACf,2DAA2D;IAC3D,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,2EAA2E;AAC3E,MAAM,WAAW,YAAY;IAC3B,gFAAgF;IAChF,KAAK,EAAE,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAA;IACxC,yDAAyD;IACzD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,qCAAqC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,0EAA0E;IAC1E,KAAK,EAAE,KAAK,GAAG,aAAa,GAAG,SAAS,GAAG,QAAQ,CAAA;IACnD,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,2DAA2D;AAC3D,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,YAAY,CAAA;IACnB,yEAAyE;IACzE,YAAY,EAAE,OAAO,CAAA;IACrB,oEAAoE;IACpE,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mEAAmE;IACnE,MAAM,CAAC,EAAE,YAAY,CAAA;IACrB,uEAAuE;IACvE,KAAK,CAAC,EAAE,WAAW,CAAA;IACnB,6EAA6E;IAC7E,OAAO,CAAC,EAAE,eAAe,CAAA;IACzB,wFAAwF;IACxF,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,2CAA2C;IAC3C,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,oCAAoC;IACpC,WAAW,EAAE,MAAM,CAAA;IACnB,yDAAyD;IACzD,WAAW,EAAE,MAAM,CAAA;IACnB,iFAAiF;IACjF,OAAO,EAAE,cAAc,EAAE,CAAA;CAC1B;AAED,wEAAwE;AACxE,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,UAAU,EAAE,MAAM,CAAA;IAClB,oEAAoE;IACpE,cAAc,EAAE,MAAM,CAAA;IACtB,qEAAqE;IACrE,UAAU,EAAE,MAAM,CAAA;IAClB,0CAA0C;IAC1C,UAAU,EAAE,MAAM,CAAA;IAClB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;;;GAIG;AACH,MAAM,MAAM,YAAY,GACpB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAE,CAAA;AAElC,yEAAyE;AACzE,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C;;OAEG;gBACS,OAAO,EAAE,MAAM;CAI5B;AAED,6CAA6C;AAC7C,MAAM,WAAW,YAAY;IAC3B,GAAG,IAAI,MAAM,CAAA;IACb,WAAW,IAAI,MAAM,CAAA;CACtB;AAED,sEAAsE;AACtE,MAAM,WAAW,cAAc;IAC7B,wEAAwE;IACxE,UAAU,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAChC;AAED,yDAAyD;AACzD,eAAO,MAAM,YAAY,EAAE,YAG1B,CAAA;AAED;;;;;;;;GAQG;AACH,qBAAa,cAAc;IAyBhB,MAAM,EAAE,aAAa;IAC5B,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,EAAE;IA1BrB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiC;IACxD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmC;IAC3D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiD;IAC3E,OAAO,CAAC,WAAW,CAA6B;IAChD,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,WAAW,CAAI;IACvB,0EAA0E;IAC1E,OAAO,CAAC,QAAQ,CAA4B;IAC5C,sEAAsE;IACtE,OAAO,CAAC,UAAU,CAAoB;IACtC,mEAAmE;IACnE,OAAO,CAAC,YAAY,CAA0B;IAC9C,OAAO,CAAC,WAAW,CAAyB;IAC5C,OAAO,CAAC,OAAO,CAA6B;IAC5C,8EAA8E;IAC9E,OAAO,CAAC,KAAK,CAAQ;IAErB;;;;;OAKG;gBAEM,MAAM,EAAE,aAAa,EACX,KAAK,GAAE,YAA2B,EAClC,EAAE,GAAE,cAAmB;IAK1C;;;;OAIG;IACH,OAAO,CAAC,aAAa;IA6BrB,8EAA8E;IAC9E,OAAO,CAAC,iBAAiB;IAOzB,iEAAiE;IACjE,OAAO,CAAC,SAAS;IAajB;;;;;;;;;OASG;IACH,OAAO,CAAC,OAAO;IAkBf;;;;;;;;OAQG;IACH,OAAO,CAAC,iBAAiB;IAazB,wFAAwF;IACxF,IAAI,UAAU,IAAI,MAAM,GAAG,SAAS,CAEnC;IAED,oFAAoF;IACpF,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIlD,wEAAwE;IACxE,IAAI,YAAY,IAAI,MAAM,EAAE,CAE3B;IAED,6EAA6E;IAC7E,WAAW,CAAC,OAAO,EAAE,SAAS;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,GAAG,IAAI;IAKxE,+DAA+D;IAC/D,IAAI,aAAa,IAAI,MAAM,GAAG,SAAS,CAEtC;IAED;;;;;OAKG;IACH,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAK/C,qFAAqF;IACrF,eAAe,CAAC,MAAM,EAAE,YAAY,GAAG,SAAS,GAAG,IAAI;IAKvD,iFAAiF;IACjF,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,IAAI;IAKrD,iEAAiE;IACjE,UAAU,CAAC,QAAQ,EAAE,eAAe,GAAG,SAAS,GAAG,IAAI;IAKvD;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,WAAW,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE;IAuBnF;;;;;;;;;OASG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,YAAY;IA0BvD;;;;OAIG;IACH,IAAI,IAAI,IAAI;IAQZ;;;;;OAKG;IACH,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAQjC;;;;;;;;;;;;OAYG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAQtC,oEAAoE;IACpE,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAIpC;;;;;OAKG;IACH,KAAK,IAAI,IAAI;IAOb,2EAA2E;IAC3E,QAAQ,IAAI,eAAe;IAsB3B,+EAA+E;IAC/E,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAIpC,yEAAyE;IACzE,OAAO,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,eAAe,KAAK,IAAI,GAAG,MAAM,IAAI;IAKlE,OAAO,CAAC,WAAW;IASnB;;;OAGG;IACH,OAAO,CAAC,WAAW;IAcnB,OAAO,CAAC,gBAAgB;IAUxB,OAAO,CAAC,WAAW;IASnB,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,MAAM;CAaf;AA+CD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAMpD;AAED,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAK7E"}