dsh-plugin-dshm 0.0.0-stage → 0.1.0

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 (80) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/LICENSE +21 -0
  3. package/README.md +228 -2
  4. package/README.zh-CN.md +190 -0
  5. package/cordis.patch.yml +7 -0
  6. package/lib/auth/signature.d.ts +4 -0
  7. package/lib/auth/signature.js +24 -0
  8. package/lib/cli/index.d.ts +4 -0
  9. package/lib/cli/index.js +41 -0
  10. package/lib/client.js +419 -0
  11. package/lib/config.d.ts +10 -0
  12. package/lib/config.js +27 -0
  13. package/lib/discovery/advertiser.d.ts +45 -0
  14. package/lib/discovery/advertiser.js +141 -0
  15. package/lib/discovery/interfaces.d.ts +5 -0
  16. package/lib/discovery/interfaces.js +24 -0
  17. package/lib/errors.d.ts +5 -0
  18. package/lib/errors.js +10 -0
  19. package/lib/host/local.d.ts +5 -0
  20. package/lib/host/local.js +122 -0
  21. package/lib/host/tailscale.d.ts +43 -0
  22. package/lib/host/tailscale.js +137 -0
  23. package/lib/host/types.d.ts +40 -0
  24. package/lib/host/types.js +1 -0
  25. package/lib/index.d.ts +30 -0
  26. package/lib/index.js +98 -0
  27. package/lib/methods/index.d.ts +9 -0
  28. package/lib/methods/index.js +206 -0
  29. package/lib/pairing/state.d.ts +26 -0
  30. package/lib/pairing/state.js +89 -0
  31. package/lib/pairing/window.d.ts +18 -0
  32. package/lib/pairing/window.js +57 -0
  33. package/lib/projection/client.d.mts +2 -0
  34. package/lib/projection/client.mjs +11 -0
  35. package/lib/projection/files.d.ts +33 -0
  36. package/lib/projection/files.js +278 -0
  37. package/lib/projection/fold-recording.d.mts +50 -0
  38. package/lib/projection/fold-recording.mjs +440 -0
  39. package/lib/projection/host.d.ts +6 -0
  40. package/lib/projection/host.js +86 -0
  41. package/lib/projection/ids.d.mts +1 -0
  42. package/lib/projection/ids.mjs +1 -0
  43. package/lib/projection/index.d.ts +2 -0
  44. package/lib/projection/index.js +2 -0
  45. package/lib/projection/interactions.d.ts +10 -0
  46. package/lib/projection/interactions.js +73 -0
  47. package/lib/projection/native.d.ts +114 -0
  48. package/lib/projection/native.js +903 -0
  49. package/lib/protocol/THIRD_PARTY_LICENSES.txt +135 -0
  50. package/lib/protocol/dmp.d.ts +402 -0
  51. package/lib/protocol/dmp.js +1 -0
  52. package/lib/protocol/schema/dmp-v1/common.schema.json +88 -0
  53. package/lib/protocol/schema/dmp-v1/events.schema.json +107 -0
  54. package/lib/protocol/schema/dmp-v1/frames.schema.json +280 -0
  55. package/lib/protocol/schema/dmp-v1/methods.schema.json +584 -0
  56. package/lib/protocol/schema/dmp-v1/models.schema.json +674 -0
  57. package/lib/protocol/scripts/contract.mjs +8106 -0
  58. package/lib/push/controller.d.ts +64 -0
  59. package/lib/push/controller.js +290 -0
  60. package/lib/push/observer.d.ts +51 -0
  61. package/lib/push/observer.js +266 -0
  62. package/lib/push/payload.d.ts +15 -0
  63. package/lib/push/payload.js +18 -0
  64. package/lib/push/store.d.ts +17 -0
  65. package/lib/push/store.js +75 -0
  66. package/lib/server/index.d.ts +30 -0
  67. package/lib/server/index.js +500 -0
  68. package/lib/server/rate-limit.d.ts +7 -0
  69. package/lib/server/rate-limit.js +19 -0
  70. package/lib/server/uploads.d.ts +21 -0
  71. package/lib/server/uploads.js +74 -0
  72. package/lib/types.d.ts +92 -0
  73. package/lib/types.js +1 -0
  74. package/lib/ui/card.d.mts +3 -0
  75. package/lib/ui/card.mjs +59 -0
  76. package/lib/ui/copy.d.mts +146 -0
  77. package/lib/ui/copy.mjs +106 -0
  78. package/lib/ui/remote-setup.d.mts +81 -0
  79. package/lib/ui/remote-setup.mjs +75 -0
  80. package/package.json +67 -4
package/lib/client.js ADDED
@@ -0,0 +1,419 @@
1
+ window.__ModuleLoader__.load({id:"dsh-plugin-dshm",factory(require){const module={exports:{}};const exports=module.exports;
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/projection/client.mjs
32
+ var client_exports = {};
33
+ __export(client_exports, {
34
+ apply: () => apply,
35
+ inject: () => inject
36
+ });
37
+ module.exports = __toCommonJS(client_exports);
38
+ var React = __toESM(require("react"), 1);
39
+ var primitives = __toESM(require("@deepseek-ai/dsh-client-ui-primitives"), 1);
40
+
41
+ // src/ui/copy.mjs
42
+ function languageFor(hostLanguage, browserLanguage) {
43
+ const language = typeof hostLanguage === "string" && hostLanguage.trim() ? hostLanguage : browserLanguage;
44
+ return /^zh(?:[-_]|$)/i.test(language ?? "") ? "zh-CN" : "en";
45
+ }
46
+ var readLanguage = () => languageFor(globalThis.document?.documentElement.lang, globalThis.navigator?.language);
47
+ function subscribeLanguage(listener) {
48
+ const element = globalThis.document?.documentElement;
49
+ const observer = element && typeof MutationObserver !== "undefined" ? new MutationObserver(listener) : void 0;
50
+ observer?.observe(element, { attributes: true, attributeFilter: ["lang"] });
51
+ globalThis.addEventListener?.("languagechange", listener);
52
+ return () => {
53
+ observer?.disconnect();
54
+ globalThis.removeEventListener?.("languagechange", listener);
55
+ };
56
+ }
57
+ function useCopy(React2) {
58
+ const language = React2.useSyncExternalStore(subscribeLanguage, readLanguage, () => "en");
59
+ return { language, copy: copies[language] };
60
+ }
61
+ var copies = {
62
+ en: {
63
+ intro: "Phone not on the same Wi-Fi as this computer? Use Tailscale. On the same Wi-Fi, nothing else is needed.",
64
+ step1: "Connect this computer",
65
+ step2: "Connect your iPhone or iPad",
66
+ "step2.detail": "Install Tailscale, sign in to the same account, and connect.",
67
+ step3: "Pair with a fresh code",
68
+ "step3.detail": "Connect Tailscale on both devices first. Paired on Wi-Fi before? Forget the computer in DSHM, revoke the old device below, then pair here.",
69
+ "step3.check": "Test with your phone on cellular or a different Wi-Fi.",
70
+ "button.pairRemote": "Pair for remote access",
71
+ "button.open": "Open Tailscale",
72
+ "button.check": "Check again",
73
+ away: "Not on the same Wi-Fi",
74
+ states: {
75
+ "not-installed": ["Install Tailscale on this computer", "Use the official installer, then check again."],
76
+ "needs-login": ["Sign in to Tailscale", "Open Tailscale and finish signing in."],
77
+ "needs-approval": ["Waiting for approval", "Ask your Tailscale admin to approve this computer."],
78
+ stopped: ["Tailscale is disconnected", "Open Tailscale and choose Connect."],
79
+ starting: ["Tailscale is starting", "Check again in a moment."],
80
+ offline: ["Tailscale is offline", "Check this computer's internet connection."],
81
+ unavailable: ["Couldn't check Tailscale", "Open Tailscale, then check again."],
82
+ unsupported: ["Set up your VPN manually", "Automatic checks support Windows and macOS. Same-network pairing still works."],
83
+ "bridge-not-ready": ["Tailscale connected, DSHM not ready yet", "Check again in a few seconds. If it persists, check the firewall."],
84
+ ready: ["Ready for remote pairing", "No phone has connected over Tailscale yet."],
85
+ connected: ["Phone connected over Tailscale", ""]
86
+ },
87
+ "409": "Connect Tailscale on this computer, check again, then make a new code.",
88
+ requestError: "DSHM could not complete this request. Try again.",
89
+ bridgeError: "The bridge is unavailable. Check the host, then retry.",
90
+ setupError: "Could not check remote setup. Check the host and retry. Local pairing is still available.",
91
+ openError: "Could not open Tailscale on the host. Open it from the Start menu or Applications, then check again.",
92
+ checkingTailscale: "Checking Tailscale\u2026",
93
+ unavailableStatus: "Status unavailable",
94
+ opening: "Opening\u2026",
95
+ checking: "Checking\u2026",
96
+ getComputer: "Get Tailscale for this computer",
97
+ getPhone: "Get Tailscale for iPhone or iPad",
98
+ installDetail: "Run the installer on the computer running DSH. Complete installation and sign-in yourself; canceling leaves local pairing available.",
99
+ opened: "Tailscale was opened on the host. Complete sign-in or choose Connect there, then return here.",
100
+ cardLabel: "DSHM remote access",
101
+ connectedCount: (count) => `${count} connected`,
102
+ listening: "Listening",
103
+ noNetwork: "No private network",
104
+ loading: "Loading bridge\u2026",
105
+ working: "Working\u2026",
106
+ pair: "Pair a device",
107
+ retry: "Retry",
108
+ qrLabel: "Pairing QR code",
109
+ scan: "Scan with your iPhone",
110
+ qrAlt: "DSHM one-use pairing QR code",
111
+ expires: (time) => `Expires in ${time}`,
112
+ cancelPair: "Cancel pairing",
113
+ guidanceMac: "Allow DeepSeek Harness under System Settings \u2192 Privacy & Security \u2192 Local Network. If you run dsh web in a terminal, allow that terminal too.",
114
+ guidanceWindows: "Allow DeepSeek Harness on Private networks when Windows Firewall asks. Keep Public networks disabled.",
115
+ bonjour: "Bonjour is unavailable on this network. Pairing still includes the host\u2019s direct addresses.",
116
+ closeSetup: "Close remote setup",
117
+ openSetup: "Set up remote access",
118
+ devices: "Paired devices",
119
+ viaTailscale: "Connected via Tailscale",
120
+ viaLAN: "Connected via local network",
121
+ connected: "Connected",
122
+ offline: "Offline",
123
+ lastSeen: (date) => `Last seen ${date}`,
124
+ notConnected: "Not connected yet",
125
+ revokeQuestion: (name) => `Revoke ${name}?`,
126
+ cancel: "Cancel",
127
+ revokeDevice: "Revoke device",
128
+ revoke: "Revoke",
129
+ noDevices: "No paired devices."
130
+ },
131
+ "zh-CN": {
132
+ intro: "\u624B\u673A\u548C\u8FD9\u53F0\u7535\u8111\u4E0D\u5728\u540C\u4E00 Wi-Fi\uFF1F\u4F7F\u7528 Tailscale\u3002\u5728\u540C\u4E00 Wi-Fi \u4E0B\u65E0\u9700\u5176\u4ED6\u8BBE\u7F6E\u3002",
133
+ step1: "\u8FDE\u63A5\u8FD9\u53F0\u7535\u8111",
134
+ step2: "\u8FDE\u63A5 iPhone \u6216 iPad",
135
+ "step2.detail": "\u5B89\u88C5 Tailscale\uFF0C\u767B\u5F55\u540C\u4E00\u8D26\u6237\u5E76\u8FDE\u63A5\u3002",
136
+ step3: "\u7528\u65B0\u7684\u914D\u5BF9\u7801\u914D\u5BF9",
137
+ "step3.detail": "\u8BF7\u5148\u5728\u4E24\u53F0\u8BBE\u5907\u4E0A\u8FDE\u63A5 Tailscale\u3002\u4E4B\u524D\u5728 Wi-Fi \u4E0B\u914D\u5BF9\u8FC7\uFF1F\u5728 DSHM \u4E2D\u79FB\u9664\u8FD9\u53F0\u7535\u8111\uFF0C\u5728\u4E0B\u65B9\u64A4\u9500\u65E7\u8BBE\u5907\uFF0C\u7136\u540E\u5728\u6B64\u91CD\u65B0\u914D\u5BF9\u3002",
138
+ "step3.check": "\u8BF7\u8BA9\u624B\u673A\u4F7F\u7528\u8702\u7A9D\u7F51\u7EDC\u6216\u5176\u4ED6 Wi-Fi \u8FDB\u884C\u6D4B\u8BD5\u3002",
139
+ "button.pairRemote": "\u914D\u5BF9\u4EE5\u8FDC\u7A0B\u8BBF\u95EE",
140
+ "button.open": "\u6253\u5F00 Tailscale",
141
+ "button.check": "\u518D\u6B21\u68C0\u67E5",
142
+ away: "\u4E0D\u5728\u540C\u4E00 Wi-Fi",
143
+ states: {
144
+ "not-installed": ["\u5728\u8FD9\u53F0\u7535\u8111\u4E0A\u5B89\u88C5 Tailscale", "\u4F7F\u7528\u5B98\u65B9\u5B89\u88C5\u7A0B\u5E8F\uFF0C\u7136\u540E\u518D\u6B21\u68C0\u67E5\u3002"],
145
+ "needs-login": ["\u767B\u5F55 Tailscale", "\u6253\u5F00 Tailscale \u5E76\u5B8C\u6210\u767B\u5F55\u3002"],
146
+ "needs-approval": ["\u7B49\u5F85\u6279\u51C6", "\u8BF7 Tailscale \u7BA1\u7406\u5458\u6279\u51C6\u8FD9\u53F0\u7535\u8111\u3002"],
147
+ stopped: ["Tailscale \u672A\u8FDE\u63A5", "\u6253\u5F00 Tailscale \u5E76\u9009\u62E9\u201C\u8FDE\u63A5\u201D\u3002"],
148
+ starting: ["Tailscale \u6B63\u5728\u542F\u52A8", "\u8BF7\u7A0D\u540E\u518D\u6B21\u68C0\u67E5\u3002"],
149
+ offline: ["Tailscale \u79BB\u7EBF", "\u8BF7\u68C0\u67E5\u8FD9\u53F0\u7535\u8111\u7684\u7F51\u7EDC\u8FDE\u63A5\u3002"],
150
+ unavailable: ["\u65E0\u6CD5\u68C0\u67E5 Tailscale", "\u6253\u5F00 Tailscale \u540E\u518D\u6B21\u68C0\u67E5\u3002"],
151
+ unsupported: ["\u8BF7\u624B\u52A8\u8BBE\u7F6E VPN", "\u81EA\u52A8\u68C0\u67E5\u4EC5\u652F\u6301 Windows \u548C macOS\u3002\u540C\u4E00\u7F51\u7EDC\u914D\u5BF9\u4ECD\u53EF\u4F7F\u7528\u3002"],
152
+ "bridge-not-ready": ["Tailscale \u5DF2\u8FDE\u63A5\uFF0CDSHM \u5C1A\u672A\u5C31\u7EEA", "\u8BF7\u51E0\u79D2\u540E\u518D\u6B21\u68C0\u67E5\u3002\u5982\u6301\u7EED\u51FA\u73B0\uFF0C\u8BF7\u68C0\u67E5\u9632\u706B\u5899\u3002"],
153
+ ready: ["\u53EF\u4EE5\u8FDC\u7A0B\u914D\u5BF9", "\u5C1A\u65E0\u624B\u673A\u901A\u8FC7 Tailscale \u8FDE\u63A5\u3002"],
154
+ connected: ["\u624B\u673A\u5DF2\u901A\u8FC7 Tailscale \u8FDE\u63A5", ""]
155
+ },
156
+ "409": "\u8BF7\u5728\u8FD9\u53F0\u7535\u8111\u4E0A\u8FDE\u63A5 Tailscale\uFF0C\u518D\u6B21\u68C0\u67E5\u540E\u751F\u6210\u65B0\u7684\u914D\u5BF9\u7801\u3002",
157
+ requestError: "DSHM \u65E0\u6CD5\u5B8C\u6210\u6B64\u8BF7\u6C42\uFF0C\u8BF7\u91CD\u8BD5\u3002",
158
+ bridgeError: "\u6865\u63A5\u4E0D\u53EF\u7528\uFF0C\u8BF7\u68C0\u67E5\u7535\u8111\u540E\u91CD\u8BD5\u3002",
159
+ setupError: "\u65E0\u6CD5\u68C0\u67E5\u8FDC\u7A0B\u8BBE\u7F6E\uFF0C\u8BF7\u68C0\u67E5\u7535\u8111\u540E\u91CD\u8BD5\u3002\u5C40\u57DF\u7F51\u914D\u5BF9\u4ECD\u53EF\u4F7F\u7528\u3002",
160
+ openError: "\u65E0\u6CD5\u5728\u7535\u8111\u4E0A\u6253\u5F00 Tailscale\u3002\u8BF7\u4ECE\u201C\u5F00\u59CB\u201D\u83DC\u5355\u6216\u201C\u5E94\u7528\u7A0B\u5E8F\u201D\u4E2D\u6253\u5F00\uFF0C\u7136\u540E\u518D\u6B21\u68C0\u67E5\u3002",
161
+ checkingTailscale: "\u6B63\u5728\u68C0\u67E5 Tailscale\u2026",
162
+ unavailableStatus: "\u72B6\u6001\u4E0D\u53EF\u7528",
163
+ opening: "\u6B63\u5728\u6253\u5F00\u2026",
164
+ checking: "\u6B63\u5728\u68C0\u67E5\u2026",
165
+ getComputer: "\u83B7\u53D6\u7535\u8111\u7248 Tailscale",
166
+ getPhone: "\u83B7\u53D6 iPhone \u6216 iPad \u7248 Tailscale",
167
+ installDetail: "\u8BF7\u5728\u8FD0\u884C DSH \u7684\u7535\u8111\u4E0A\u5B89\u88C5\uFF0C\u5E76\u4EB2\u81EA\u5B8C\u6210\u5B89\u88C5\u548C\u767B\u5F55\u3002\u53D6\u6D88\u540E\u4ECD\u53EF\u5728\u5C40\u57DF\u7F51\u914D\u5BF9\u3002",
168
+ opened: "\u5DF2\u5728\u7535\u8111\u4E0A\u6253\u5F00 Tailscale\u3002\u8BF7\u5B8C\u6210\u767B\u5F55\u6216\u9009\u62E9\u201C\u8FDE\u63A5\u201D\uFF0C\u7136\u540E\u8FD4\u56DE\u6B64\u5904\u3002",
169
+ cardLabel: "DSHM \u8FDC\u7A0B\u8BBF\u95EE",
170
+ connectedCount: (count) => `${count} \u53F0\u8BBE\u5907\u5DF2\u8FDE\u63A5`,
171
+ listening: "\u6B63\u5728\u76D1\u542C",
172
+ noNetwork: "\u6CA1\u6709\u79C1\u6709\u7F51\u7EDC",
173
+ loading: "\u6B63\u5728\u52A0\u8F7D\u6865\u63A5\u2026",
174
+ working: "\u6B63\u5728\u5904\u7406\u2026",
175
+ pair: "\u914D\u5BF9\u8BBE\u5907",
176
+ retry: "\u91CD\u8BD5",
177
+ qrLabel: "\u914D\u5BF9\u4E8C\u7EF4\u7801",
178
+ scan: "\u7528 iPhone \u626B\u63CF",
179
+ qrAlt: "DSHM \u4E00\u6B21\u6027\u914D\u5BF9\u4E8C\u7EF4\u7801",
180
+ expires: (time) => `${time} \u540E\u8FC7\u671F`,
181
+ cancelPair: "\u53D6\u6D88\u914D\u5BF9",
182
+ guidanceMac: "\u8BF7\u5728\u201C\u7CFB\u7EDF\u8BBE\u7F6E \u2192 \u9690\u79C1\u4E0E\u5B89\u5168\u6027 \u2192 \u672C\u5730\u7F51\u7EDC\u201D\u4E2D\u5141\u8BB8 DeepSeek Harness\u3002\u5982\u679C\u5728\u7EC8\u7AEF\u8FD0\u884C dsh web\uFF0C\u4E5F\u8BF7\u5141\u8BB8\u8BE5\u7EC8\u7AEF\u3002",
183
+ guidanceWindows: "Windows \u9632\u706B\u5899\u8BE2\u95EE\u65F6\uFF0C\u8BF7\u5141\u8BB8 DeepSeek Harness \u8BBF\u95EE\u4E13\u7528\u7F51\u7EDC\uFF0C\u5E76\u4FDD\u6301\u516C\u7528\u7F51\u7EDC\u7981\u7528\u3002",
184
+ bonjour: "\u6B64\u7F51\u7EDC\u4E0A\u7684 Bonjour \u4E0D\u53EF\u7528\uFF0C\u914D\u5BF9\u4ECD\u5305\u542B\u7535\u8111\u7684\u76F4\u63A5\u8FDE\u63A5\u5730\u5740\u3002",
185
+ closeSetup: "\u5173\u95ED\u8FDC\u7A0B\u8BBE\u7F6E",
186
+ openSetup: "\u8BBE\u7F6E\u8FDC\u7A0B\u8BBF\u95EE",
187
+ devices: "\u5DF2\u914D\u5BF9\u8BBE\u5907",
188
+ viaTailscale: "\u5DF2\u901A\u8FC7 Tailscale \u8FDE\u63A5",
189
+ viaLAN: "\u5DF2\u901A\u8FC7\u5C40\u57DF\u7F51\u8FDE\u63A5",
190
+ connected: "\u5DF2\u8FDE\u63A5",
191
+ offline: "\u79BB\u7EBF",
192
+ lastSeen: (date) => `\u4E0A\u6B21\u8FDE\u63A5\uFF1A${date}`,
193
+ notConnected: "\u5C1A\u672A\u8FDE\u63A5",
194
+ revokeQuestion: (name) => `\u64A4\u9500 ${name} \u7684\u8BBF\u95EE\u6743\u9650\uFF1F`,
195
+ cancel: "\u53D6\u6D88",
196
+ revokeDevice: "\u64A4\u9500\u8BBE\u5907",
197
+ revoke: "\u64A4\u9500",
198
+ noDevices: "\u6CA1\u6709\u5DF2\u914D\u5BF9\u8BBE\u5907\u3002"
199
+ }
200
+ };
201
+ function guidanceCopy(text, copy) {
202
+ for (const key of ["guidanceMac", "guidanceWindows"]) if (text === copies.en[key]) return copy[key];
203
+ return text;
204
+ }
205
+
206
+ // src/ui/remote-setup.mjs
207
+ var install = { macos: "https://tailscale.com/docs/install/mac", windows: "https://tailscale.com/docs/install/windows", other: "https://tailscale.com/docs/install" };
208
+ function setupPresentation(status, copy = copies.en) {
209
+ if (!status) return { title: copy.checkingTailscale, detail: "", ready: false };
210
+ const ready = status.state === "running" && Boolean(status.bridgeListening);
211
+ const state = status.state !== "running" ? status.state : !ready ? "bridge-not-ready" : status.connectedDevices > 0 ? "connected" : "ready";
212
+ const [title, detail] = Object.hasOwn(copy.states, state) ? copy.states[state] : copy.states.unavailable;
213
+ return { title, detail, ready };
214
+ }
215
+ function createRemoteSetup(React2, { Button }) {
216
+ const h = React2.createElement;
217
+ const link = (href, label) => h("a", { className: "dshm-link-button", href, target: "_blank", rel: "noopener noreferrer" }, label, " \u2197");
218
+ return function RemoteSetup({ api, onPair, pairingBusy, copy }) {
219
+ const [status, setStatus] = React2.useState(null), [error, setError] = React2.useState("");
220
+ const [busy, setBusy] = React2.useState(false), [opened, setOpened] = React2.useState(false), [checking, setChecking] = React2.useState(false);
221
+ const live = React2.useRef(true), pending = React2.useRef(false);
222
+ const check = async (manual = false) => {
223
+ if (pending.current) return;
224
+ pending.current = true;
225
+ if (manual) setChecking(true);
226
+ try {
227
+ const value = await api("remoteSetup");
228
+ if (live.current) {
229
+ setStatus(value);
230
+ setError("");
231
+ }
232
+ } catch {
233
+ if (live.current) {
234
+ setStatus(null);
235
+ setError("setupError");
236
+ }
237
+ } finally {
238
+ pending.current = false;
239
+ if (live.current) setChecking(false);
240
+ }
241
+ };
242
+ React2.useEffect(() => {
243
+ live.current = true;
244
+ void check();
245
+ const timer = setInterval(() => {
246
+ void check();
247
+ }, 5e3);
248
+ return () => {
249
+ live.current = false;
250
+ clearInterval(timer);
251
+ };
252
+ }, []);
253
+ const open = async () => {
254
+ if (busy) return;
255
+ setBusy(true);
256
+ setError("");
257
+ setOpened(false);
258
+ try {
259
+ await api("openTailscale", {});
260
+ if (live.current) setOpened(true);
261
+ } catch {
262
+ if (live.current) setError("openError");
263
+ } finally {
264
+ if (live.current) setBusy(false);
265
+ }
266
+ };
267
+ const view = setupPresentation(status, copy);
268
+ return h(
269
+ "div",
270
+ { className: "dshm-panel", id: "dshm-remote-setup" },
271
+ h("p", null, copy.intro),
272
+ h(
273
+ "ol",
274
+ { className: "dshm-setup-steps" },
275
+ h(
276
+ "li",
277
+ null,
278
+ h("h3", null, copy.step1),
279
+ h("p", { role: "status", "aria-live": "polite", "aria-atomic": true }, error && !status ? copy.unavailableStatus : view.title),
280
+ view.detail ? h("p", null, view.detail) : null,
281
+ h(
282
+ "div",
283
+ { className: "dshm-actions" },
284
+ status?.state === "not-installed" || status?.state === "unsupported" ? link(install[status.platform] ?? install.other, copy.getComputer) : null,
285
+ // Keep keyboard focus during short requests; handlers prevent repeats.
286
+ status?.canOpenApp ? h(Button, { variant: "outline", "aria-disabled": busy, "aria-busy": busy, onClick: open }, busy ? copy.opening : copy["button.open"]) : null,
287
+ h(Button, { variant: "outline", "aria-disabled": busy || checking, "aria-busy": checking, onClick: () => {
288
+ if (!busy) void check(true);
289
+ } }, checking ? copy.checking : copy["button.check"])
290
+ ),
291
+ status?.state === "not-installed" ? h("p", null, copy.installDetail) : null,
292
+ opened ? h("p", { role: "status" }, copy.opened) : null
293
+ ),
294
+ h(
295
+ "li",
296
+ null,
297
+ h("h3", null, copy.step2),
298
+ h("p", null, copy["step2.detail"]),
299
+ link("https://tailscale.com/docs/install/ios", copy.getPhone)
300
+ ),
301
+ h(
302
+ "li",
303
+ null,
304
+ h("h3", null, copy.step3),
305
+ h("p", null, copy["step3.detail"]),
306
+ h(Button, { variant: "primary", disabled: !view.ready || pairingBusy, onClick: onPair }, copy["button.pairRemote"]),
307
+ h("p", null, copy["step3.check"])
308
+ )
309
+ ),
310
+ error ? h("p", { className: "dshm-error", role: "alert" }, copy[error]) : null
311
+ );
312
+ };
313
+ }
314
+
315
+ // src/ui/card.mjs
316
+ function createSettingsCard(React2, { Button }, apiBase) {
317
+ const h = React2.createElement;
318
+ const RemoteSetup = createRemoteSetup(React2, { Button });
319
+ const icon = (kind) => h("svg", { width: 18, height: 18, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.8, "aria-hidden": true }, h("path", { d: kind === "pair" ? "M4 4h6v6H4zM14 4h6v6h-6zM4 14h6v6H4zM14 14h2v2h-2zM18 14h2v6h-6v-2" : "M5 7h14M9 7V4h6v3M7 7l1 13h8l1-13M10 10v7M14 10v7" }));
320
+ const style = `.dshm-card{color:var(--dsw-alias-label-primary);font:inherit;font-size:16px;line-height:1.5;display:grid;gap:24px}.dshm-card h2,.dshm-card h3,.dshm-card p{margin:0}.dshm-card h2{font-size:20px}.dshm-card h3{font-size:16px}.dshm-card button{min-height:44px;gap:8px}.dshm-card button:focus-visible,.dshm-link-button:focus-visible{outline:2px solid var(--dsw-alias-label-primary);outline-offset:3px}.dshm-row{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap}.dshm-muted{color:var(--dsw-alias-label-secondary);font-size:14px}.dshm-panel{padding:16px;border:1px solid var(--dsw-alias-border-primary,currentColor);border-radius:var(--dsw-radius-md,12px);display:grid;gap:12px}.dshm-qr{display:block;width:256px;max-width:100%;height:auto;background:white;border-radius:8px}.dshm-devices{list-style:none;margin:0;padding:0;display:grid;gap:12px}.dshm-device{padding:14px 0;border-bottom:1px solid var(--dsw-alias-border-primary,currentColor)}.dshm-actions{display:flex;gap:8px;flex-wrap:wrap}.dshm-setup-steps{margin:0;padding-left:24px;display:grid;gap:24px}.dshm-setup-steps li{display:grid;gap:12px;min-width:0}.dshm-link-button{display:inline-flex;align-items:center;min-height:44px;padding:8px 12px;border:1px solid var(--dsw-alias-border-primary,currentColor);border-radius:var(--dsw-radius-md,12px);color:inherit;text-decoration:underline;box-sizing:border-box}.dshm-error{color:var(--dsw-alias-danger-label,#b42318)}.dshm-guidance{padding:14px 16px;background:var(--dsw-alias-bg-layer-3);border-radius:var(--dsw-radius-md,12px)}@media(max-width:480px){.dshm-row{align-items:flex-start}.dshm-qr{width:224px}}`;
321
+ return function SettingsCard2() {
322
+ const { language, copy } = useCopy(React2);
323
+ const [status, setStatus] = React2.useState(null), [devices, setDevices] = React2.useState([]);
324
+ const [pairing, setPairing] = React2.useState(null), [busy, setBusy] = React2.useState(false);
325
+ const [error, setError] = React2.useState(""), [confirm, setConfirm] = React2.useState(null), [now, setNow] = React2.useState(Date.now());
326
+ const [remoteOpen, setRemoteOpen] = React2.useState(false);
327
+ const pairPanel = React2.useRef(null);
328
+ React2.useEffect(() => {
329
+ if (pairing) pairPanel.current?.focus();
330
+ }, [pairing?.expiresAt]);
331
+ const api = async (route, body) => {
332
+ const response = await fetch(`${apiBase}/${route}`, { method: body === void 0 ? "GET" : "POST", credentials: "same-origin", cache: "no-store", headers: body === void 0 ? {} : { "Content-Type": "application/json" }, body: body === void 0 ? void 0 : JSON.stringify(body), signal: AbortSignal.timeout(6e3) });
333
+ if (!response.ok) throw new Error(route === "pairRemote" && response.status === 409 ? "409" : "requestError");
334
+ return response.json();
335
+ };
336
+ const refresh = async () => {
337
+ const [next, list] = await Promise.all([api("status"), api("devices")]);
338
+ setStatus(next);
339
+ setDevices(list.devices);
340
+ if (!next.pairing.active) setPairing(null);
341
+ };
342
+ React2.useEffect(() => {
343
+ let live = true;
344
+ const poll = async () => {
345
+ try {
346
+ const [next, list] = await Promise.all([api("status"), api("devices")]);
347
+ if (live) {
348
+ setStatus(next);
349
+ setDevices(list.devices);
350
+ if (!next.pairing.active) setPairing(null);
351
+ }
352
+ } catch {
353
+ if (live) setError("bridgeError");
354
+ }
355
+ };
356
+ void poll();
357
+ const timer = setInterval(poll, 2e3), clock = setInterval(() => setNow(Date.now()), 1e3);
358
+ return () => {
359
+ live = false;
360
+ clearInterval(timer);
361
+ clearInterval(clock);
362
+ };
363
+ }, []);
364
+ const action = async (work) => {
365
+ setBusy(true);
366
+ setError("");
367
+ try {
368
+ await work();
369
+ await refresh();
370
+ } catch (failure) {
371
+ setError(failure.message);
372
+ } finally {
373
+ setBusy(false);
374
+ }
375
+ };
376
+ const seconds = pairing ? Math.max(0, Math.ceil((Date.parse(pairing.expiresAt) - now) / 1e3)) : 0;
377
+ const button = (label, onClick, variant = "outline", glyph) => h(Button, { variant, disabled: busy, onClick }, glyph ? icon(glyph) : null, label);
378
+ return h(
379
+ "section",
380
+ { className: "dshm-card", "aria-label": copy.cardLabel, lang: language },
381
+ h("style", null, style),
382
+ h("div", { className: "dshm-row" }, h("div", null, h("h2", null, "DSHM"), h("p", { className: "dshm-muted" }, status ? `${copy.connectedCount(status.connectedDevices)} \xB7 ${status.addresses.length ? copy.listening : copy.noNetwork}` : copy.loading)), button(busy ? copy.working : copy.pair, () => action(async () => {
383
+ const next = await api("pair", {});
384
+ setNow(Date.now());
385
+ setPairing(next);
386
+ }), "primary", "pair")),
387
+ error ? h("div", { className: "dshm-panel", role: "alert" }, h("p", { className: "dshm-error" }, copy[error] ?? copy.requestError), button(copy.retry, () => action(refresh))) : null,
388
+ pairing && seconds > 0 ? h("div", { className: "dshm-panel", ref: pairPanel, tabIndex: -1, "aria-label": copy.qrLabel }, h("h3", null, copy.scan), h("img", { className: "dshm-qr", src: `data:image/svg+xml;base64,${btoa(pairing.svg)}`, alt: copy.qrAlt }), h("p", { role: "timer", "aria-live": "off" }, copy.expires(`${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, "0")}`)), button(copy.cancelPair, () => action(async () => {
389
+ await api("cancelPair", {});
390
+ setPairing(null);
391
+ }))) : null,
392
+ ...(status?.guidance ?? []).map((text) => h("p", { className: "dshm-guidance", key: text }, guidanceCopy(text, copy))),
393
+ status?.discovery.state === "unavailable" ? h("p", { role: "status" }, copy.bonjour) : null,
394
+ h("div", { className: "dshm-row" }, h("h3", null, copy.away), h(Button, { variant: "outline", "aria-expanded": remoteOpen, "aria-controls": "dshm-remote-setup", onClick: () => setRemoteOpen(!remoteOpen) }, remoteOpen ? copy.closeSetup : copy.openSetup)),
395
+ remoteOpen ? h(RemoteSetup, { api, copy, pairingBusy: busy, onPair: () => action(async () => {
396
+ const next = await api("pairRemote", {});
397
+ setNow(Date.now());
398
+ setPairing(next);
399
+ }) }) : null,
400
+ h("div", null, h("h3", null, copy.devices), devices.length ? h("ul", { className: "dshm-devices" }, ...devices.map((device) => h("li", { className: "dshm-device", key: device.deviceId }, h("div", { className: "dshm-row" }, h("div", null, h("strong", null, device.name), h("p", { className: "dshm-muted" }, `${device.connected ? device.connectedVia === "tailscale" ? copy.viaTailscale : device.connectedVia === "lan" ? copy.viaLAN : copy.connected : copy.offline} \xB7 ${device.lastSeenAt ? copy.lastSeen(new Date(device.lastSeenAt).toLocaleString(language)) : copy.notConnected}`)), confirm === device.deviceId ? h("div", null, h("p", null, copy.revokeQuestion(device.name)), h("div", { className: "dshm-actions" }, button(copy.cancel, () => setConfirm(null)), button(copy.revokeDevice, () => action(async () => {
401
+ await api("revoke", { deviceId: device.deviceId });
402
+ setConfirm(null);
403
+ }), "outline", "revoke"))) : button(copy.revoke, () => setConfirm(device.deviceId), "outline", "revoke"))))) : h("p", { className: "dshm-muted" }, copy.noDevices))
404
+ );
405
+ };
406
+ }
407
+
408
+ // src/projection/client.mjs
409
+ var inject = ["slots"];
410
+ var SettingsCard = createSettingsCard(React, primitives, "api/dshm");
411
+ function apply(ctx) {
412
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
413
+ name: "settings.section",
414
+ id: "dshm",
415
+ order: 60,
416
+ label: () => "DSHM"
417
+ }, SettingsCard));
418
+ }
419
+ return module.exports;}});
@@ -0,0 +1,10 @@
1
+ export interface Config {
2
+ port: number;
3
+ bindMode: 'lan' | 'tailscale' | 'all-private';
4
+ addresses?: string[];
5
+ pairOnStart: boolean;
6
+ quiet: boolean;
7
+ stateDir: string;
8
+ pushProxyUrl?: string;
9
+ }
10
+ export declare function parseConfig(input: unknown): Config;
package/lib/config.js ADDED
@@ -0,0 +1,27 @@
1
+ import { homedir } from 'node:os';
2
+ import { resolve } from 'node:path';
3
+ import { proxyURL } from './push/controller.js';
4
+ export function parseConfig(input) {
5
+ if (input !== undefined && (input === null || typeof input !== 'object' || Array.isArray(input)))
6
+ throw new Error('DSHM configuration must be an object');
7
+ const value = (input ?? {});
8
+ const allowed = new Set(['port', 'bindMode', 'addresses', 'pairOnStart', 'quiet', 'pushProxyUrl']);
9
+ for (const key of Object.keys(value))
10
+ if (!allowed.has(key))
11
+ throw new Error(`Unknown DSHM configuration field: ${key}`);
12
+ const port = value.port ?? 47390;
13
+ if (typeof port !== 'number' || !Number.isInteger(port) || port < 0 || port > 65535)
14
+ throw new Error('DSHM port must be 0..65535');
15
+ const mode = value.bindMode ?? 'all-private';
16
+ if (!['lan', 'tailscale', 'all-private'].includes(String(mode)))
17
+ throw new Error('Invalid DSHM bindMode');
18
+ if (value.addresses !== undefined && (!Array.isArray(value.addresses) || !value.addresses.length || value.addresses.some(address => typeof address !== 'string')))
19
+ throw new Error('DSHM addresses must be a nonempty list of IP literals');
20
+ for (const field of ['pairOnStart', 'quiet'])
21
+ if (value[field] !== undefined && typeof value[field] !== 'boolean')
22
+ throw new Error(`DSHM ${field} must be boolean`);
23
+ return { port, bindMode: mode, addresses: value.addresses,
24
+ pairOnStart: value.pairOnStart === true, quiet: value.quiet === true,
25
+ pushProxyUrl: proxyURL(value.pushProxyUrl),
26
+ stateDir: resolve(process.env.DSH_HOME ?? resolve(homedir(), '.dsh'), 'dshm') };
27
+ }
@@ -0,0 +1,45 @@
1
+ import multicastDns from 'multicast-dns';
2
+ type Transport = ReturnType<typeof multicastDns>;
3
+ type TransportOptions = NonNullable<Parameters<typeof multicastDns>[0]>;
4
+ export interface Advertisement {
5
+ hostId: string;
6
+ name: string;
7
+ port: number;
8
+ addresses: string[];
9
+ }
10
+ export interface DiscoveryStatus {
11
+ state: 'advertising' | 'unavailable' | 'stopped';
12
+ serviceName: string;
13
+ error?: string;
14
+ }
15
+ type Record = {
16
+ name: string;
17
+ ttl: number;
18
+ flush?: boolean;
19
+ } & ({
20
+ type: 'PTR' | 'A' | 'AAAA';
21
+ data: string;
22
+ } | {
23
+ type: 'SRV';
24
+ data: {
25
+ port: number;
26
+ target: string;
27
+ priority: number;
28
+ weight: number;
29
+ };
30
+ } | {
31
+ type: 'TXT';
32
+ data: Buffer[];
33
+ });
34
+ export declare function advertisementRecords(values: Advertisement, ttl?: number): {
35
+ serviceName: string;
36
+ target: string;
37
+ records: Record[];
38
+ };
39
+ /** Publishes only bound listener endpoints; this is not an authenticated RPC transport. */
40
+ export declare function createAdvertiser(initial: Advertisement, changed?: (status: DiscoveryStatus) => void, factory?: (options: TransportOptions) => Transport): Promise<{
41
+ update: (next: Advertisement) => Promise<void>;
42
+ status: () => DiscoveryStatus;
43
+ close(): Promise<void>;
44
+ }>;
45
+ export {};
@@ -0,0 +1,141 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { isIP } from 'node:net';
3
+ import { networkInterfaces, platform } from 'node:os';
4
+ import multicastDns from 'multicast-dns';
5
+ import { isPrivateAddress } from './interfaces.js';
6
+ const serviceType = '_dshm._tcp.local';
7
+ const enumeration = '_services._dns-sd._udp.local';
8
+ const normalName = (name) => name.toLowerCase().replace(/\.$/, '');
9
+ function names(values) {
10
+ const suffix = createHash('sha256').update(values.hostId).digest('hex').slice(0, 24);
11
+ let label = `DSHM ${values.name}`.replace(/[.\\\x00-\x1f\x7f]/g, ' ').trim();
12
+ while (Buffer.byteLength(label) > 46)
13
+ label = [...label].slice(0, -1).join('');
14
+ const serviceName = `${label} ${suffix.slice(0, 8)}`;
15
+ return { serviceName, target: `dshm-${suffix}.local` };
16
+ }
17
+ export function advertisementRecords(values, ttl = 120) {
18
+ if (!Number.isInteger(values.port) || values.port < 1 || values.port > 65535)
19
+ throw new Error('Invalid discovery port');
20
+ if (!values.addresses.length || values.addresses.some(address => !isPrivateAddress(address, true)))
21
+ throw new Error('Discovery requires private listener addresses');
22
+ const { serviceName, target } = names(values), fullName = `${serviceName}.${serviceType}`;
23
+ const records = [
24
+ { name: enumeration, type: 'PTR', ttl, data: serviceType },
25
+ { name: serviceType, type: 'PTR', ttl, data: fullName },
26
+ { name: fullName, type: 'SRV', ttl, flush: true, data: { port: values.port, target, priority: 0, weight: 0 } },
27
+ { name: fullName, type: 'TXT', ttl, flush: true, data: [Buffer.from(`host=${values.hostId}`), Buffer.from('v=1')] },
28
+ ...[...new Set(values.addresses)].map((address) => ({ name: target, type: isIP(address) === 6 ? 'AAAA' : 'A', ttl, flush: true, data: address })),
29
+ ];
30
+ return { serviceName, target, records };
31
+ }
32
+ function interfaces(addresses) {
33
+ const entries = Object.entries(networkInterfaces());
34
+ return addresses.flatMap(address => {
35
+ if (isIP(address) === 4)
36
+ return [{ interface: address, bind: '0.0.0.0', type: 'udp4', ip: '224.0.0.251', reuseAddr: true }];
37
+ const match = entries.find(([, values]) => values?.some(value => value.address === address));
38
+ if (!match)
39
+ return [];
40
+ const info = match[1]?.find(value => value.address === address);
41
+ const scope = platform() === 'win32' && info?.family === 'IPv6' ? String(info.scopeid) : match[0];
42
+ return [{ interface: `::%${scope}`, bind: '::', type: 'udp6', ip: `ff02::fb%${scope}`, reuseAddr: true }];
43
+ });
44
+ }
45
+ /** Publishes only bound listener endpoints; this is not an authenticated RPC transport. */
46
+ export async function createAdvertiser(initial, changed = () => { }, factory = multicastDns) {
47
+ let values = initial, service = { ...names(initial), records: [] }, sockets = [], timer;
48
+ let closed = false, status = { state: 'stopped', serviceName: service.serviceName }, previous = '';
49
+ let sequence = Promise.resolve();
50
+ const setStatus = (state, error) => {
51
+ status = { state, serviceName: service.serviceName, ...(error ? { error } : {}) };
52
+ changed(status);
53
+ };
54
+ const send = (socket, records = service.records) => new Promise(resolve => {
55
+ const timeout = setTimeout(resolve, 1000);
56
+ timeout.unref();
57
+ try {
58
+ socket.respond({ answers: records }, () => { clearTimeout(timeout); resolve(); });
59
+ }
60
+ catch {
61
+ clearTimeout(timeout);
62
+ resolve();
63
+ }
64
+ });
65
+ const stop = async () => {
66
+ clearInterval(timer);
67
+ timer = undefined;
68
+ const old = sockets;
69
+ sockets = [];
70
+ await Promise.all(old.map(async (socket) => {
71
+ await send(socket, service.records.map(record => ({ ...record, ttl: 0 })));
72
+ await new Promise(resolve => {
73
+ const timeout = setTimeout(resolve, 1000);
74
+ timeout.unref();
75
+ try {
76
+ socket.destroy(() => { clearTimeout(timeout); resolve(); });
77
+ }
78
+ catch {
79
+ clearTimeout(timeout);
80
+ resolve();
81
+ }
82
+ });
83
+ }));
84
+ };
85
+ const update = (next) => {
86
+ sequence = sequence.then(async () => {
87
+ if (closed)
88
+ return;
89
+ const identity = JSON.stringify({ ...next, addresses: [...next.addresses].sort() });
90
+ if (identity === previous && status.state === 'advertising')
91
+ return;
92
+ await stop();
93
+ values = next;
94
+ previous = identity;
95
+ if (!values.addresses.length) {
96
+ setStatus('unavailable', 'No private listener is available');
97
+ return;
98
+ }
99
+ service = advertisementRecords(values);
100
+ let failed = false;
101
+ const fail = () => { failed = true; setStatus('unavailable', 'Bonjour could not use a listener interface'); };
102
+ for (const options of interfaces(values.addresses)) {
103
+ const socket = factory(options);
104
+ sockets.push(socket);
105
+ socket.on('error', fail);
106
+ socket.on('warning', error => { if (['EACCES', 'EPERM', 'EADDRNOTAVAIL', 'ENODEV', 'ENETUNREACH', 'EHOSTUNREACH'].includes(String(error.code)))
107
+ fail(); });
108
+ socket.on('query', (packet, remote) => {
109
+ if (!packet.questions?.some(question => [serviceType, enumeration, `${service.serviceName}.${serviceType}`, service.target].some(name => normalName(question.name) === normalName(name))))
110
+ return;
111
+ // Legacy DNS clients use a unicast reply; standard mDNS browsers use multicast.
112
+ if (remote.port !== 5353)
113
+ socket.respond({ id: packet.id, answers: service.records }, remote);
114
+ else
115
+ void send(socket);
116
+ });
117
+ await new Promise(resolve => {
118
+ const timeout = setTimeout(() => { fail(); resolve(); }, 2000);
119
+ const ready = () => { clearTimeout(timeout); resolve(); };
120
+ socket.once('ready', ready);
121
+ socket.once('error', ready);
122
+ });
123
+ await send(socket);
124
+ }
125
+ if (!sockets.length)
126
+ fail();
127
+ if (!failed)
128
+ setStatus('advertising');
129
+ timer = setInterval(() => { for (const socket of sockets)
130
+ void send(socket); }, 60000);
131
+ timer.unref();
132
+ });
133
+ return sequence;
134
+ };
135
+ await update(initial);
136
+ return {
137
+ update,
138
+ status: () => ({ ...status }),
139
+ async close() { closed = true; await sequence; await stop(); setStatus('stopped'); },
140
+ };
141
+ }
@@ -0,0 +1,5 @@
1
+ export type BindMode = 'lan' | 'tailscale' | 'all-private';
2
+ export declare function isTailscale(address: string): boolean;
3
+ export declare function isPrivateAddress(address: string, allowLoopback?: boolean): boolean;
4
+ export declare function privateInterfaces(mode?: BindMode): string[];
5
+ export declare const endpoint: (address: string, port: number) => string;